hashcodecs 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/.gitignore +5 -2
  2. hashcodecs-0.6.0/BENCHMARK.md +58 -0
  3. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/Cargo.lock +23 -3
  4. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/Cargo.toml +21 -6
  5. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/PKG-INFO +43 -66
  6. hashcodecs-0.6.0/README.md +147 -0
  7. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/benches/base64.rs +1 -1
  8. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/benches/murmur3.rs +1 -1
  9. hashcodecs-0.6.0/benches/xxhash.rs +119 -0
  10. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/build.rs +5 -0
  11. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/hatch_build.py +2 -2
  12. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/pyproject.toml +4 -3
  13. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/__init__.py +18 -1
  14. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/_hashcodecs.pyi +23 -0
  15. hashcodecs-0.6.0/python/hashcodecs/base64.py +101 -0
  16. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/base64.pyi +15 -0
  17. hashcodecs-0.6.0/python/hashcodecs/xxhash.py +5 -0
  18. hashcodecs-0.6.0/src/backend.rs +222 -0
  19. hashcodecs-0.4.0/src/base64/aarch64.rs → hashcodecs-0.6.0/src/base64/aarch64/decode.rs +51 -114
  20. hashcodecs-0.6.0/src/base64/aarch64/encode.rs +113 -0
  21. hashcodecs-0.6.0/src/base64/aarch64/tests.rs +578 -0
  22. hashcodecs-0.6.0/src/base64/aarch64.rs +11 -0
  23. hashcodecs-0.6.0/src/base64/backend.rs +87 -0
  24. hashcodecs-0.6.0/src/base64/decode.rs +445 -0
  25. hashcodecs-0.6.0/src/base64/dispatch.rs +320 -0
  26. hashcodecs-0.6.0/src/base64/encode.rs +209 -0
  27. hashcodecs-0.4.0/src/base64/x86_avx512.rs → hashcodecs-0.6.0/src/base64/x86/avx512.rs +37 -10
  28. hashcodecs-0.6.0/src/base64/x86/cache.rs +130 -0
  29. hashcodecs-0.4.0/src/base64/x86.rs → hashcodecs-0.6.0/src/base64/x86/decode.rs +13 -375
  30. hashcodecs-0.6.0/src/base64/x86/encode.rs +588 -0
  31. hashcodecs-0.6.0/src/base64/x86.rs +32 -0
  32. hashcodecs-0.6.0/src/base64.rs +259 -0
  33. hashcodecs-0.6.0/src/bindings/base64/decode.rs +982 -0
  34. {hashcodecs-0.4.0/src/python → hashcodecs-0.6.0/src/bindings}/base64/encode.rs +57 -51
  35. hashcodecs-0.6.0/src/bindings/base64/mod.rs +1086 -0
  36. hashcodecs-0.6.0/src/bindings/buffer.rs +222 -0
  37. hashcodecs-0.6.0/src/bindings/mod.rs +314 -0
  38. hashcodecs-0.4.0/src/python/murmur3.rs → hashcodecs-0.6.0/src/bindings/murmur3/mod.rs +118 -31
  39. hashcodecs-0.6.0/src/bindings/xxhash/batch.rs +82 -0
  40. hashcodecs-0.6.0/src/bindings/xxhash/mod.rs +197 -0
  41. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/src/lib.rs +8 -2
  42. hashcodecs-0.6.0/src/murmur3/dispatch.rs +51 -0
  43. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/src/murmur3/x86.rs +69 -3
  44. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/src/murmur3.rs +217 -165
  45. hashcodecs-0.6.0/src/xxhash/aarch64.rs +91 -0
  46. hashcodecs-0.6.0/src/xxhash/x86/avx2.rs +261 -0
  47. hashcodecs-0.6.0/src/xxhash/x86/avx512.rs +83 -0
  48. hashcodecs-0.6.0/src/xxhash/x86/sse.rs +88 -0
  49. hashcodecs-0.6.0/src/xxhash/x86.rs +50 -0
  50. hashcodecs-0.6.0/src/xxhash.rs +849 -0
  51. hashcodecs-0.4.0/BENCHMARK.md +0 -71
  52. hashcodecs-0.4.0/README.md +0 -170
  53. hashcodecs-0.4.0/python/hashcodecs/base64.py +0 -89
  54. hashcodecs-0.4.0/src/base64/dispatch.rs +0 -310
  55. hashcodecs-0.4.0/src/base64.rs +0 -555
  56. hashcodecs-0.4.0/src/murmur3/dispatch.rs +0 -47
  57. hashcodecs-0.4.0/src/python/base64.rs +0 -792
  58. hashcodecs-0.4.0/src/python/buffer.rs +0 -113
  59. hashcodecs-0.4.0/src/python.rs +0 -33
  60. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/LICENSE +0 -0
  61. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/LICENSE-MIT +0 -0
  62. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/benches/support/mod.rs +0 -0
  63. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/murmur3.py +0 -0
  64. {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/py.typed +0 -0
@@ -2,8 +2,11 @@
2
2
  # will have compiled files and executables
3
3
  debug
4
4
  target
5
- dist
6
- .venv
5
+ fuzz/artifacts
6
+ dist/
7
+ dist-*/
8
+ .venv/
9
+ .venv-*/
7
10
  .coverage
8
11
  coverage.xml
9
12
 
@@ -0,0 +1,58 @@
1
+ # Benchmark Details
2
+
3
+ Environment: Windows 10 x64 and Intel Core Ultra 7 265K.
4
+
5
+ Conditions: clean builds, one pinned logical CPU, single-threaded execution, and 15 Python samples. The upstream C baseline was compiled explicitly for AVX2, matching the backend selected by hashcodecs on this host. Higher is better.
6
+
7
+ Python hashcodecs measurements use a CPython 3.12 wheel built against the full C
8
+ API. Unchanged reference series are retained from their latest clean run.
9
+
10
+ The charts are generated by `uv run python benchmarks/render_charts.py`. Their exact measurements are available in
11
+ [CSV form](docs/benchmarks/results.csv).
12
+
13
+ ## XXH3
14
+
15
+ The Rust comparison calls xxHash 0.8.3 through `xxhash-c-sys`. Python compares with the upstream `xxhash` extension.
16
+ Batch measurements contain 32 equal-size inputs.
17
+
18
+ [![Rust XXH3 throughput](docs/benchmarks/xxh3-rust.svg)](docs/benchmarks/xxh3-rust.svg)
19
+
20
+ [![Python XXH3 throughput](docs/benchmarks/xxh3-python.svg)](docs/benchmarks/xxh3-python.svg)
21
+
22
+ ## Reusable Python Buffers
23
+
24
+ [![Reusable Python Base64 buffers](docs/benchmarks/base64-python-reusable.svg)](docs/benchmarks/base64-python-reusable.svg)
25
+
26
+ ## Python Memoryview Inputs
27
+
28
+ Full immutable memoryviews reuse their underlying bytes for inputs of at least 64 KiB.
29
+
30
+ [![Python Base64 memoryview inputs](docs/benchmarks/base64-python-memoryview.svg)](docs/benchmarks/base64-python-memoryview.svg)
31
+
32
+ ## Python Base64 Batches
33
+
34
+ The horizontal axis is the batch size and the vertical axis is total input throughput.
35
+
36
+ [![Python Base64 batch throughput](docs/benchmarks/base64-python-batch.svg)](docs/benchmarks/base64-python-batch.svg)
37
+
38
+ ## Reusable Python Base64 Batch Buffers
39
+
40
+ These measurements use the `*_batch_into` APIs with one reusable `bytearray` per item.
41
+
42
+ [![Reusable Python Base64 batch buffers](docs/benchmarks/base64-python-batch-reusable.svg)](docs/benchmarks/base64-python-batch-reusable.svg)
43
+
44
+ ## Large Python Base64 Batches
45
+
46
+ Each batch item is 1 MiB. The horizontal axis is the batch size.
47
+
48
+ [![Large Python Base64 batches](docs/benchmarks/base64-python-batch-large.svg)](docs/benchmarks/base64-python-batch-large.svg)
49
+
50
+ ## Mutable Python Inputs
51
+
52
+ ### Base64
53
+
54
+ [![Mutable Python Base64 inputs](docs/benchmarks/base64-python-mutable.svg)](docs/benchmarks/base64-python-mutable.svg)
55
+
56
+ ### MurmurHash3
57
+
58
+ [![Mutable Python MurmurHash3 inputs](docs/benchmarks/murmur3-python-mutable.svg)](docs/benchmarks/murmur3-python-mutable.svg)
@@ -46,9 +46,9 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
46
46
 
47
47
  [[package]]
48
48
  name = "base64-turbo"
49
- version = "0.2.0"
49
+ version = "0.3.0"
50
50
  source = "registry+https://github.com/rust-lang/crates.io-index"
51
- checksum = "341b5725644ff53e8384ba7c17b9abd9274aa79efc2eb9cf8629cb3cc860d4e3"
51
+ checksum = "8bb14240b770d247c8481cab7c78350daf08b4285885b916632dec270b7e6dfc"
52
52
 
53
53
  [[package]]
54
54
  name = "cast"
@@ -194,17 +194,21 @@ dependencies = [
194
194
 
195
195
  [[package]]
196
196
  name = "hashcodecs"
197
- version = "0.4.0"
197
+ version = "0.6.0"
198
198
  dependencies = [
199
199
  "base64",
200
200
  "base64-turbo",
201
201
  "criterion",
202
202
  "fastmurmur3",
203
+ "memchr",
203
204
  "mimalloc",
204
205
  "mm3h",
205
206
  "murmur3",
206
207
  "murmurs",
207
208
  "pyo3",
209
+ "pyo3-build-config",
210
+ "xxhash-c-sys",
211
+ "xxhash-rust",
208
212
  ]
209
213
 
210
214
  [[package]]
@@ -575,6 +579,22 @@ dependencies = [
575
579
  "windows-link",
576
580
  ]
577
581
 
582
+ [[package]]
583
+ name = "xxhash-c-sys"
584
+ version = "0.8.7"
585
+ source = "registry+https://github.com/rust-lang/crates.io-index"
586
+ checksum = "4d4a8210e2c9209f553817636cc6054d3b22424cc879c31b616e4cb4027be705"
587
+ dependencies = [
588
+ "cc",
589
+ "libc",
590
+ ]
591
+
592
+ [[package]]
593
+ name = "xxhash-rust"
594
+ version = "0.8.18"
595
+ source = "registry+https://github.com/rust-lang/crates.io-index"
596
+ checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6"
597
+
578
598
  [[package]]
579
599
  name = "zerocopy"
580
600
  version = "0.8.56"
@@ -1,13 +1,13 @@
1
1
  [package]
2
2
  name = "hashcodecs"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.89"
6
- description = "SIMD-accelerated Base64 codecs and fast MurmurHash3 implementations"
6
+ description = "SIMD-accelerated Base64 codecs and fast MurmurHash3 and xxHash implementations"
7
7
  license = "MIT OR Apache-2.0"
8
8
  repository = "https://github.com/kozistr/hashcodecs-rs"
9
9
  publish = false
10
- keywords = ["base64", "simd", "murmur3", "python"]
10
+ keywords = ["base64", "simd", "murmur3", "xxhash", "python"]
11
11
  categories = ["encoding", "algorithms"]
12
12
  readme = "README.md"
13
13
 
@@ -16,21 +16,27 @@ name = "hashcodecs"
16
16
  crate-type = ["rlib", "cdylib"]
17
17
 
18
18
  [features]
19
- python = ["dep:pyo3"]
19
+ python = ["dep:memchr", "dep:pyo3"]
20
20
  extension-module = ["python", "pyo3/extension-module"]
21
21
 
22
22
  [dependencies]
23
+ memchr = { version = "2.8.0", optional = true }
23
24
  mimalloc = "0.1.52"
24
- pyo3 = { version = "0.29.2", features = ["abi3-py310"], optional = true }
25
+ pyo3 = { version = "0.29.2", optional = true }
26
+
27
+ [build-dependencies]
28
+ pyo3-build-config = "0.29.2"
25
29
 
26
30
  [dev-dependencies]
27
31
  base64 = "0.23.1"
28
- base64-turbo = "0.2.0"
32
+ base64-turbo = "0.3.0"
29
33
  criterion = { version = "0.8.2", default-features = false, features = ["cargo_bench_support"] }
30
34
  fastmurmur3 = "0.2.0"
31
35
  mm3h = "0.1.3"
32
36
  murmur3 = "0.5"
33
37
  murmurs = "1.0.5"
38
+ xxhash-c-sys = "0.8.7"
39
+ xxhash-rust = { version = "0.8.18", features = ["xxh3"] }
34
40
 
35
41
  [[bench]]
36
42
  name = "base64"
@@ -40,6 +46,15 @@ harness = false
40
46
  name = "murmur3"
41
47
  harness = false
42
48
 
49
+ [[bench]]
50
+ name = "xxhash"
51
+ harness = false
52
+
53
+ [[test]]
54
+ name = "sanitizers"
55
+ path = "tests/sanitizers.rs"
56
+ harness = false
57
+
43
58
  [profile.release]
44
59
  codegen-units = 1
45
60
  lto = "fat"
@@ -1,7 +1,7 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hashcodecs
3
- Version: 0.4.0
4
- Summary: SIMD-accelerated Base64 and MurmurHash3 codecs
3
+ Version: 0.6.0
4
+ Summary: SIMD-accelerated Base64, MurmurHash3, and xxHash codecs
5
5
  License-Expression: MIT OR Apache-2.0
6
6
  License-File: LICENSE
7
7
  License-File: LICENSE-MIT
@@ -21,19 +21,22 @@ Description-Content-Type: text/markdown
21
21
  # hashcodecs
22
22
 
23
23
  [![CI](https://img.shields.io/github/actions/workflow/status/kozistr/hashcodecs-rs/ci.yml?branch=main&style=for-the-badge&logo=github)](https://github.com/kozistr/hashcodecs-rs/actions/workflows/ci.yml)
24
+ [![Codecov](https://codecov.io/gh/kozistr/hashcodecs-rs/graph/badge.svg)](https://app.codecov.io/gh/kozistr/hashcodecs-rs)
24
25
  [![PyPI](https://img.shields.io/pypi/v/hashcodecs?style=for-the-badge&logo=pypi)](https://pypi.org/project/hashcodecs/)
25
26
  [![Python](https://img.shields.io/pypi/pyversions/hashcodecs?style=for-the-badge&logo=python)](https://pypi.org/project/hashcodecs/)
26
27
  [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-brightgreen?style=for-the-badge)](https://github.com/kozistr/hashcodecs-rs#license)
27
28
  [![Downloads](https://img.shields.io/pypi/dm/hashcodecs?style=for-the-badge&label=downloads)](https://pypi.org/project/hashcodecs/)
28
29
 
29
- `hashcodecs` provides runtime-dispatched SIMD Base64 codecs and fast, reference-compatible MurmurHash3 functions for Rust and Python.
30
+ `hashcodecs` provides runtime-dispatched SIMD Base64 codecs and fast, reference-compatible MurmurHash3 and XXH3 functions for Rust and Python.
30
31
 
31
32
  ## Design
32
33
 
33
34
  - Runtime-dispatched SIMD Base64 with portable scalar fallbacks.
34
35
  - Reference-compatible MurmurHash3 with SIMD acceleration.
36
+ - Bit-for-bit compatible XXH3-64 and XXH3-128 with runtime SIMD dispatch and native batch APIs.
35
37
  - Rust and Python `*_into` APIs for caller-managed output buffers.
36
38
  - A familiar Python Base64 API, including native batch encode and decode operations.
39
+ - Unsafe paths verified with Kani, strict-provenance Miri, ASan/MSan, and differential fuzzing.
37
40
 
38
41
  ## Install
39
42
 
@@ -53,13 +56,14 @@ let mut output = [0_u8; 8];
53
56
  let written = hashcodecs::b64encode_into(b"hello", &mut output).unwrap();
54
57
  assert_eq!(&output[..written], b"aGVsbG8=");
55
58
  assert_eq!(hashcodecs::murmur3_x86_32(b"hello", 0), 0x248b_fa47);
59
+ assert_eq!(hashcodecs::xxh3_64(b"", 0), 0x2d06_8005_38d3_94c2);
56
60
  ```
57
61
 
58
62
  ### Python
59
63
 
60
64
  ```python
61
65
  import hashcodecs.base64 as base64
62
- from hashcodecs import murmur3_32, murmur3_x64_128
66
+ from hashcodecs import murmur3_32, murmur3_x64_128, xxh3_64, xxh3_128_batch
63
67
 
64
68
  assert base64.b64encode(b'hello') == b'aGVsbG8='
65
69
  assert base64.b64decode(b'aGVsbG8=') == b'hello'
@@ -68,6 +72,11 @@ assert base64.b64decode_batch([b'aGVsbG8=', 'd29ybGQ=']) == [b'hello', b'world']
68
72
  assert base64.b64encode(b'hello', padded=False) == b'aGVsbG8'
69
73
  assert base64.b64decode(b'aGVsbG8', padded=False, canonical=True) == b'hello'
70
74
  assert murmur3_32(b'hello') == 0x248BFA47
75
+ assert xxh3_64(b'') == 0x2D06800538D394C2
76
+ assert xxh3_128_batch([b'hello', b'world']) == [
77
+ 0xB5E9C1AD071B3E7FC779CFAA5E523818,
78
+ 0xFA0D38A9B38280D0891E4985BDB2583E,
79
+ ]
71
80
 
72
81
  payload = b'hello'
73
82
  encoded = bytearray(4 * ((len(payload) + 2) // 3))
@@ -101,89 +110,57 @@ Comparison crates are development-only dependencies and are not included in cons
101
110
  ```sh
102
111
  cargo bench --bench base64
103
112
  cargo bench --bench murmur3
113
+ cargo bench --bench xxhash
104
114
  uv sync --group benchmark --no-install-project
105
115
  uv run --no-project --with . python benchmarks/python_base64.py
106
116
  uv run --no-project --with . python benchmarks/python_base64.py --into
107
117
  uv run --no-project --with . python benchmarks/python_base64.py --bytearray-input
118
+ uv run --no-project --with . python benchmarks/python_base64.py --memoryview-input
108
119
  uv run --no-project --with . python benchmarks/python_base64_batch.py
120
+ uv run --no-project --with . python benchmarks/python_base64_batch.py --large
109
121
  uv run --no-project --with . python benchmarks/python_murmur3.py
110
122
  uv run --no-project --with . python benchmarks/python_murmur3.py --incremental
123
+ uv run --no-project --with . python benchmarks/python_xxhash.py
111
124
  ```
112
125
 
126
+ For regression checks that do not need fresh competitor measurements, pass
127
+ `--hashcodecs-only` to any Python benchmark command.
128
+
129
+ For the same-ISA Windows comparison reported below, rebuild the C baseline with
130
+ `$env:CFLAGS='/O2 /arch:AVX2'; cargo clean -p xxhash-c-sys; cargo bench --bench xxhash`.
131
+
113
132
  ## Base64: Rust
114
133
 
115
- | Alphabet | Input | Operation | hashcodecs | `base64` | `base64-turbo` |
116
- | --- | --- | --- | ---: | ---: | ---: |
117
- | Standard | 4 KiB | encode | **20.41 GiB/s** | 5.81 GiB/s | 18.29 GiB/s |
118
- | | 4 KiB | decode | **27.11 GiB/s** | 4.33 GiB/s | 16.95 GiB/s |
119
- | | 1 MiB | encode | **42.38 GiB/s** | 5.16 GiB/s | 19.60 GiB/s |
120
- | | 1 MiB | decode | **31.07 GiB/s** | 4.10 GiB/s | 18.17 GiB/s |
121
- | | 32 MiB | encode | **11.97 GiB/s** | 3.28 GiB/s | 10.94 GiB/s |
122
- | | 32 MiB | decode | **11.51 GiB/s** | 3.35 GiB/s | 10.45 GiB/s |
123
- | URL-safe | 4 KiB | encode | **20.46 GiB/s** | 5.81 GiB/s | 18.31 GiB/s |
124
- | | 4 KiB | decode | **25.65 GiB/s** | 4.35 GiB/s | 16.96 GiB/s |
125
- | | 1 MiB | encode | **42.56 GiB/s** | 5.18 GiB/s | 19.66 GiB/s |
126
- | | 1 MiB | decode | **29.42 GiB/s** | 4.08 GiB/s | 18.16 GiB/s |
127
- | | 32 MiB | encode | **11.86 GiB/s** | 3.29 GiB/s | 11.09 GiB/s |
128
- | | 32 MiB | decode | **11.67 GiB/s** | 3.36 GiB/s | 10.62 GiB/s |
134
+ [![Rust Base64 throughput](docs/benchmarks/base64-rust.svg)](docs/benchmarks/base64-rust.svg)
129
135
 
130
136
  ## MurmurHash3: Rust
131
137
 
132
- | Variant | Input | hashcodecs | `murmur3` | `murmurs` | `fastmurmur3` | `mm3h` |
133
- | --- | --- | ---: | ---: | ---: | ---: | ---: |
134
- | x86 32-bit | 4 KiB | **4.30 GiB/s** | 2.60 GiB/s | 4.09 GiB/s | n/a | **4.30 GiB/s** |
135
- | | 1 MiB | **4.26 GiB/s** | 2.59 GiB/s | 4.04 GiB/s | n/a | 4.25 GiB/s |
136
- | | 32 MiB | **4.21 GiB/s** | 2.54 GiB/s | 3.87 GiB/s | n/a | 4.03 GiB/s |
137
- | x86 128-bit | 4 KiB | **9.54 GiB/s** | 4.97 GiB/s | 8.64 GiB/s | n/a | n/a |
138
- | | 1 MiB | **9.84 GiB/s** | 5.06 GiB/s | 8.77 GiB/s | n/a | n/a |
139
- | | 32 MiB | **9.63 GiB/s** | 4.84 GiB/s | 6.05 GiB/s | n/a | n/a |
140
- | x64 128-bit | 4 KiB | **10.76 GiB/s** | 6.83 GiB/s | 9.45 GiB/s | 10.06 GiB/s | 9.54 GiB/s |
141
- | | 1 MiB | **10.84 GiB/s** | 6.89 GiB/s | 9.51 GiB/s | 10.04 GiB/s | 9.51 GiB/s |
142
- | | 32 MiB | **10.12 GiB/s** | 6.11 GiB/s | 6.64 GiB/s | 7.25 GiB/s | 6.70 GiB/s |
138
+ [![Rust MurmurHash3 throughput](docs/benchmarks/murmur3-rust.svg)](docs/benchmarks/murmur3-rust.svg)
139
+
140
+ ## XXH3: Rust
141
+
142
+ `upstream C` is xxHash 0.8.3 built through `xxhash-c-sys` with AVX2 enabled, matching the backend selected by hashcodecs on the benchmark host. Batch results hash 32 equal-size inputs and include result-vector allocation.
143
+
144
+ [![Rust XXH3 throughput](docs/benchmarks/xxh3-rust.svg)](docs/benchmarks/xxh3-rust.svg)
143
145
 
144
146
  ## Base64: Python
145
147
 
146
148
  Python decoding uses `validate=True`, and `hashcodecs` passes `bytes` directly into Rust without an input copy.
147
149
 
148
- | Alphabet | Input | Operation | hashcodecs | CPython `base64` | `pybase64` |
149
- | --- | --- | --- | ---: | ---: | ---: |
150
- | Standard | 4 KiB | encode | 13.57 GiB/s | 0.48 GiB/s | **14.06 GiB/s** |
151
- | | 4 KiB | decode | **16.70 GiB/s** | 1.13 GiB/s | 8.47 GiB/s |
152
- | | 1 MiB | encode | **3.28 GiB/s** | 0.44 GiB/s | 3.02 GiB/s |
153
- | | 1 MiB | decode | **4.02 GiB/s** | 0.95 GiB/s | 3.68 GiB/s |
154
- | | 32 MiB | encode | **2.92 GiB/s** | 0.43 GiB/s | 2.84 GiB/s |
155
- | | 32 MiB | decode | 3.40 GiB/s | 0.94 GiB/s | **3.69 GiB/s** |
156
- | URL-safe | 4 KiB | encode | **12.63 GiB/s** | 0.41 GiB/s | 1.19 GiB/s |
157
- | | 4 KiB | decode | **11.34 GiB/s** | 0.76 GiB/s | 1.56 GiB/s |
158
- | | 1 MiB | encode | **3.24 GiB/s** | 0.36 GiB/s | 0.91 GiB/s |
159
- | | 1 MiB | decode | **4.15 GiB/s** | 0.63 GiB/s | 1.45 GiB/s |
160
- | | 32 MiB | encode | **3.03 GiB/s** | 0.36 GiB/s | 0.90 GiB/s |
161
- | | 32 MiB | decode | **3.40 GiB/s** | 0.61 GiB/s | 1.48 GiB/s |
150
+ [![Python Base64 throughput](docs/benchmarks/base64-python.svg)](docs/benchmarks/base64-python.svg)
162
151
 
163
152
  ## MurmurHash3: Python
164
153
 
165
- | Variant | API | Input | hashcodecs | `mmh3` |
166
- | --- | --- | --- | ---: | ---: |
167
- | x86 32-bit | one-shot | 4 KiB | **3.79 GiB/s** | 3.71 GiB/s |
168
- | | | 1 MiB | **3.98 GiB/s** | 3.83 GiB/s |
169
- | | | 32 MiB | **3.97 GiB/s** | 3.66 GiB/s |
170
- | | incremental | 4 KiB | **3.58 GiB/s** | **3.58 GiB/s** |
171
- | | | 1 MiB | **3.96 GiB/s** | 3.81 GiB/s |
172
- | | | 32 MiB | **3.92 GiB/s** | 3.74 GiB/s |
173
- | x86 128-bit | one-shot | 4 KiB | 8.09 GiB/s | **8.21 GiB/s** |
174
- | | | 1 MiB | **9.22 GiB/s** | 8.87 GiB/s |
175
- | | | 32 MiB | **9.12 GiB/s** | 6.15 GiB/s |
176
- | | incremental | 4 KiB | **7.02 GiB/s** | 0.77 GiB/s |
177
- | | | 1 MiB | **9.17 GiB/s** | 0.80 GiB/s |
178
- | | | 32 MiB | **9.01 GiB/s** | 0.79 GiB/s |
179
- | x64 128-bit | one-shot | 4 KiB | 8.68 GiB/s | **9.46 GiB/s** |
180
- | | | 1 MiB | 10.10 GiB/s | **10.24 GiB/s** |
181
- | | | 32 MiB | **9.54 GiB/s** | 6.61 GiB/s |
182
- | | incremental | 4 KiB | 7.70 GiB/s | **7.99 GiB/s** |
183
- | | | 1 MiB | **10.01 GiB/s** | 9.28 GiB/s |
184
- | | | 32 MiB | **9.41 GiB/s** | 7.34 GiB/s |
185
-
186
- Reusable-buffer and mutable-input results are available in [BENCHMARK.md](https://github.com/kozistr/hashcodecs-rs/blob/main/BENCHMARK.md).
154
+ [![Python MurmurHash3 throughput](docs/benchmarks/murmur3-python.svg)](docs/benchmarks/murmur3-python.svg)
155
+
156
+ ## XXH3: Python
157
+
158
+ The batch comparison uses one native `hashcodecs` call versus a loop over the upstream `xxhash` extension.
159
+
160
+ [![Python XXH3 throughput](docs/benchmarks/xxh3-python.svg)](docs/benchmarks/xxh3-python.svg)
161
+
162
+ Reusable-buffer and mutable-input charts are available in [BENCHMARK.md](BENCHMARK.md). Exact chart values are
163
+ available as [CSV](docs/benchmarks/results.csv).
187
164
 
188
165
  ## SIMD References
189
166
 
@@ -0,0 +1,147 @@
1
+ # hashcodecs
2
+
3
+ [![CI](https://img.shields.io/github/actions/workflow/status/kozistr/hashcodecs-rs/ci.yml?branch=main&style=for-the-badge&logo=github)](https://github.com/kozistr/hashcodecs-rs/actions/workflows/ci.yml)
4
+ [![Codecov](https://codecov.io/gh/kozistr/hashcodecs-rs/graph/badge.svg)](https://app.codecov.io/gh/kozistr/hashcodecs-rs)
5
+ [![PyPI](https://img.shields.io/pypi/v/hashcodecs?style=for-the-badge&logo=pypi)](https://pypi.org/project/hashcodecs/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/hashcodecs?style=for-the-badge&logo=python)](https://pypi.org/project/hashcodecs/)
7
+ [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-brightgreen?style=for-the-badge)](https://github.com/kozistr/hashcodecs-rs#license)
8
+ [![Downloads](https://img.shields.io/pypi/dm/hashcodecs?style=for-the-badge&label=downloads)](https://pypi.org/project/hashcodecs/)
9
+
10
+ `hashcodecs` provides runtime-dispatched SIMD Base64 codecs and fast, reference-compatible MurmurHash3 and XXH3 functions for Rust and Python.
11
+
12
+ ## Design
13
+
14
+ - Runtime-dispatched SIMD Base64 with portable scalar fallbacks.
15
+ - Reference-compatible MurmurHash3 with SIMD acceleration.
16
+ - Bit-for-bit compatible XXH3-64 and XXH3-128 with runtime SIMD dispatch and native batch APIs.
17
+ - Rust and Python `*_into` APIs for caller-managed output buffers.
18
+ - A familiar Python Base64 API, including native batch encode and decode operations.
19
+ - Unsafe paths verified with Kani, strict-provenance Miri, ASan/MSan, and differential fuzzing.
20
+
21
+ ## Install
22
+
23
+ ```
24
+ pip3 install hashcodecs
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Rust
30
+
31
+ ```rust
32
+ let encoded = hashcodecs::b64encode(b"hello");
33
+ assert_eq!(encoded, "aGVsbG8=");
34
+
35
+ let mut output = [0_u8; 8];
36
+ let written = hashcodecs::b64encode_into(b"hello", &mut output).unwrap();
37
+ assert_eq!(&output[..written], b"aGVsbG8=");
38
+ assert_eq!(hashcodecs::murmur3_x86_32(b"hello", 0), 0x248b_fa47);
39
+ assert_eq!(hashcodecs::xxh3_64(b"", 0), 0x2d06_8005_38d3_94c2);
40
+ ```
41
+
42
+ ### Python
43
+
44
+ ```python
45
+ import hashcodecs.base64 as base64
46
+ from hashcodecs import murmur3_32, murmur3_x64_128, xxh3_64, xxh3_128_batch
47
+
48
+ assert base64.b64encode(b'hello') == b'aGVsbG8='
49
+ assert base64.b64decode(b'aGVsbG8=') == b'hello'
50
+ assert base64.b64encode_batch([b'hello', b'world']) == [b'aGVsbG8=', b'd29ybGQ=']
51
+ assert base64.b64decode_batch([b'aGVsbG8=', 'd29ybGQ=']) == [b'hello', b'world']
52
+ assert base64.b64encode(b'hello', padded=False) == b'aGVsbG8'
53
+ assert base64.b64decode(b'aGVsbG8', padded=False, canonical=True) == b'hello'
54
+ assert murmur3_32(b'hello') == 0x248BFA47
55
+ assert xxh3_64(b'') == 0x2D06800538D394C2
56
+ assert xxh3_128_batch([b'hello', b'world']) == [
57
+ 0xB5E9C1AD071B3E7FC779CFAA5E523818,
58
+ 0xFA0D38A9B38280D0891E4985BDB2583E,
59
+ ]
60
+
61
+ payload = b'hello'
62
+ encoded = bytearray(4 * ((len(payload) + 2) // 3))
63
+ encoded_len = base64.b64encode_into(payload, encoded)
64
+ decoded = bytearray(len(encoded))
65
+ decoded_len = base64.b64decode_into(encoded, decoded, validate=True)
66
+ assert encoded[:encoded_len] == b'aGVsbG8='
67
+ assert decoded[:decoded_len] == payload
68
+
69
+ hasher = murmur3_x64_128(seed=42)
70
+ hasher.update(b'hello')
71
+ assert hasher.hexdigest() == hasher.digest().hex()
72
+ ```
73
+
74
+ Build an installable wheel and source distribution with:
75
+
76
+ ```sh
77
+ uv build
78
+ ```
79
+
80
+ # Benchmark
81
+
82
+ Environment: Windows 10 x64 and Intel Core Ultra 7 265K.
83
+
84
+ Conditions: clean builds, one pinned logical CPU, single-threaded execution, 50 Rust samples, and 15 Python samples. Returned-output allocation is included except in the reusable-buffer table. Higher is better.
85
+
86
+ ## Run Locally
87
+
88
+ Comparison crates are development-only dependencies and are not included in consumer builds.
89
+
90
+ ```sh
91
+ cargo bench --bench base64
92
+ cargo bench --bench murmur3
93
+ cargo bench --bench xxhash
94
+ uv sync --group benchmark --no-install-project
95
+ uv run --no-project --with . python benchmarks/python_base64.py
96
+ uv run --no-project --with . python benchmarks/python_base64.py --into
97
+ uv run --no-project --with . python benchmarks/python_base64.py --bytearray-input
98
+ uv run --no-project --with . python benchmarks/python_base64.py --memoryview-input
99
+ uv run --no-project --with . python benchmarks/python_base64_batch.py
100
+ uv run --no-project --with . python benchmarks/python_base64_batch.py --large
101
+ uv run --no-project --with . python benchmarks/python_murmur3.py
102
+ uv run --no-project --with . python benchmarks/python_murmur3.py --incremental
103
+ uv run --no-project --with . python benchmarks/python_xxhash.py
104
+ ```
105
+
106
+ For regression checks that do not need fresh competitor measurements, pass
107
+ `--hashcodecs-only` to any Python benchmark command.
108
+
109
+ For the same-ISA Windows comparison reported below, rebuild the C baseline with
110
+ `$env:CFLAGS='/O2 /arch:AVX2'; cargo clean -p xxhash-c-sys; cargo bench --bench xxhash`.
111
+
112
+ ## Base64: Rust
113
+
114
+ [![Rust Base64 throughput](docs/benchmarks/base64-rust.svg)](docs/benchmarks/base64-rust.svg)
115
+
116
+ ## MurmurHash3: Rust
117
+
118
+ [![Rust MurmurHash3 throughput](docs/benchmarks/murmur3-rust.svg)](docs/benchmarks/murmur3-rust.svg)
119
+
120
+ ## XXH3: Rust
121
+
122
+ `upstream C` is xxHash 0.8.3 built through `xxhash-c-sys` with AVX2 enabled, matching the backend selected by hashcodecs on the benchmark host. Batch results hash 32 equal-size inputs and include result-vector allocation.
123
+
124
+ [![Rust XXH3 throughput](docs/benchmarks/xxh3-rust.svg)](docs/benchmarks/xxh3-rust.svg)
125
+
126
+ ## Base64: Python
127
+
128
+ Python decoding uses `validate=True`, and `hashcodecs` passes `bytes` directly into Rust without an input copy.
129
+
130
+ [![Python Base64 throughput](docs/benchmarks/base64-python.svg)](docs/benchmarks/base64-python.svg)
131
+
132
+ ## MurmurHash3: Python
133
+
134
+ [![Python MurmurHash3 throughput](docs/benchmarks/murmur3-python.svg)](docs/benchmarks/murmur3-python.svg)
135
+
136
+ ## XXH3: Python
137
+
138
+ The batch comparison uses one native `hashcodecs` call versus a loop over the upstream `xxhash` extension.
139
+
140
+ [![Python XXH3 throughput](docs/benchmarks/xxh3-python.svg)](docs/benchmarks/xxh3-python.svg)
141
+
142
+ Reusable-buffer and mutable-input charts are available in [BENCHMARK.md](BENCHMARK.md). Exact chart values are
143
+ available as [CSV](docs/benchmarks/results.csv).
144
+
145
+ ## SIMD References
146
+
147
+ The SIMD implementation follows the approach described in [Faster Base64 Encoding and Decoding using AVX2 Instructions](https://arxiv.org/abs/1704.00605), with AVX-512 VBMI and AArch64 NEON backends selected automatically when available.
@@ -6,7 +6,7 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m
6
6
 
7
7
  mod support;
8
8
 
9
- const SIZES: [usize; 3] = [4 * 1024, 1024 * 1024, 32 * 1024 * 1024];
9
+ const SIZES: [usize; 4] = [1024, 4 * 1024, 1024 * 1024, 8 * 1024 * 1024];
10
10
  const SAMPLE_SIZE: usize = 50;
11
11
 
12
12
  fn data(size: usize) -> Vec<u8> {
@@ -6,7 +6,7 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m
6
6
 
7
7
  mod support;
8
8
 
9
- const SIZES: [usize; 3] = [4 * 1024, 1024 * 1024, 32 * 1024 * 1024];
9
+ const SIZES: [usize; 4] = [1024, 4 * 1024, 1024 * 1024, 8 * 1024 * 1024];
10
10
  const SAMPLE_SIZE: usize = 50;
11
11
 
12
12
  fn data(size: usize) -> Vec<u8> {
@@ -0,0 +1,119 @@
1
+ use std::ffi::c_void;
2
+ use std::hint::black_box;
3
+ use std::time::Duration;
4
+
5
+ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
6
+
7
+ mod support;
8
+
9
+ const SIZES: [usize; 5] = [64, 1024, 4 * 1024, 1024 * 1024, 8 * 1024 * 1024];
10
+
11
+ fn data(size: usize, salt: u8) -> Vec<u8> {
12
+ (0..size)
13
+ .map(|index| (index as u8).wrapping_mul(31).wrapping_add(salt))
14
+ .collect()
15
+ }
16
+
17
+ fn c_xxh3_64(input: &[u8], seed: u64) -> u64 {
18
+ unsafe {
19
+ xxhash_c_sys::XXH3_64bits_withSeed(input.as_ptr().cast::<c_void>(), input.len(), seed)
20
+ }
21
+ }
22
+
23
+ fn c_xxh3_128(input: &[u8], seed: u64) -> [u64; 2] {
24
+ let hash = unsafe {
25
+ xxhash_c_sys::XXH3_128bits_withSeed(input.as_ptr().cast::<c_void>(), input.len(), seed)
26
+ };
27
+ [hash.low64, hash.high64]
28
+ }
29
+
30
+ fn one_shot(c: &mut Criterion) {
31
+ for size in SIZES {
32
+ let input = data(size, 17);
33
+ let mut group = c.benchmark_group(format!("xxh3_64/{size}"));
34
+ group.throughput(Throughput::Bytes(size as u64));
35
+ assert_eq!(hashcodecs::xxh3_64(&input, 42), c_xxh3_64(&input, 42));
36
+ group.bench_function("hashcodecs", |bench| {
37
+ bench.iter(|| hashcodecs::xxh3_64(black_box(&input), 42))
38
+ });
39
+ group.bench_function("upstream_c", |bench| {
40
+ bench.iter(|| c_xxh3_64(black_box(&input), 42))
41
+ });
42
+ group.finish();
43
+
44
+ let mut group = c.benchmark_group(format!("xxh3_128/{size}"));
45
+ group.throughput(Throughput::Bytes(size as u64));
46
+ assert_eq!(hashcodecs::xxh3_128(&input, 42), c_xxh3_128(&input, 42));
47
+ group.bench_function("hashcodecs", |bench| {
48
+ bench.iter(|| hashcodecs::xxh3_128(black_box(&input), 42))
49
+ });
50
+ group.bench_function("upstream_c", |bench| {
51
+ bench.iter(|| c_xxh3_128(black_box(&input), 42))
52
+ });
53
+ group.finish();
54
+ }
55
+ }
56
+
57
+ fn batch(c: &mut Criterion) {
58
+ const ITEMS: usize = 32;
59
+ for size in [64, 1024, 4 * 1024, 1024 * 1024] {
60
+ let owned = (0..ITEMS)
61
+ .map(|index| data(size, index as u8))
62
+ .collect::<Vec<_>>();
63
+ let inputs = owned.iter().map(Vec::as_slice).collect::<Vec<_>>();
64
+ let mut group = c.benchmark_group(format!("xxh3_batch/{size}"));
65
+ group.throughput(Throughput::Bytes((size * ITEMS) as u64));
66
+
67
+ group.bench_with_input(
68
+ BenchmarkId::new("hashcodecs_64", ITEMS),
69
+ &inputs,
70
+ |bench, inputs| bench.iter(|| hashcodecs::xxh3_64_batch(black_box(inputs), 42)),
71
+ );
72
+ group.bench_with_input(
73
+ BenchmarkId::new("upstream_c_64", ITEMS),
74
+ &inputs,
75
+ |bench, inputs| {
76
+ bench.iter(|| {
77
+ inputs
78
+ .iter()
79
+ .map(|input| c_xxh3_64(black_box(input), 42))
80
+ .collect::<Vec<_>>()
81
+ })
82
+ },
83
+ );
84
+ group.bench_with_input(
85
+ BenchmarkId::new("hashcodecs_128", ITEMS),
86
+ &inputs,
87
+ |bench, inputs| bench.iter(|| hashcodecs::xxh3_128_batch(black_box(inputs), 42)),
88
+ );
89
+ group.bench_with_input(
90
+ BenchmarkId::new("upstream_c_128", ITEMS),
91
+ &inputs,
92
+ |bench, inputs| {
93
+ bench.iter(|| {
94
+ inputs
95
+ .iter()
96
+ .map(|input| c_xxh3_128(black_box(input), 42))
97
+ .collect::<Vec<_>>()
98
+ })
99
+ },
100
+ );
101
+ group.finish();
102
+ }
103
+ }
104
+
105
+ fn xxhash(c: &mut Criterion) {
106
+ support::pin_to_one_cpu();
107
+ one_shot(c);
108
+ batch(c);
109
+ }
110
+
111
+ criterion_group! {
112
+ name = benches;
113
+ config = Criterion::default()
114
+ .measurement_time(Duration::from_secs(1))
115
+ .sample_size(30)
116
+ .warm_up_time(Duration::from_millis(300));
117
+ targets = xxhash
118
+ }
119
+ criterion_main!(benches);