hashcodecs 0.5.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 (60) hide show
  1. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/.gitignore +5 -2
  2. hashcodecs-0.6.0/BENCHMARK.md +58 -0
  3. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/Cargo.lock +22 -3
  4. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/Cargo.toml +19 -5
  5. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/PKG-INFO +39 -65
  6. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/README.md +37 -63
  7. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/benches/base64.rs +1 -1
  8. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/benches/murmur3.rs +1 -1
  9. hashcodecs-0.6.0/benches/xxhash.rs +119 -0
  10. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/build.rs +5 -0
  11. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/hatch_build.py +2 -2
  12. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/pyproject.toml +4 -3
  13. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/python/hashcodecs/__init__.py +6 -1
  14. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/python/hashcodecs/_hashcodecs.pyi +4 -0
  15. hashcodecs-0.6.0/python/hashcodecs/xxhash.py +5 -0
  16. hashcodecs-0.6.0/src/backend.rs +222 -0
  17. hashcodecs-0.6.0/src/base64/aarch64/decode.rs +229 -0
  18. hashcodecs-0.6.0/src/base64/aarch64/encode.rs +113 -0
  19. hashcodecs-0.6.0/src/base64/aarch64/tests.rs +578 -0
  20. hashcodecs-0.6.0/src/base64/aarch64.rs +11 -0
  21. hashcodecs-0.6.0/src/base64/backend.rs +87 -0
  22. hashcodecs-0.5.0/src/base64.rs → hashcodecs-0.6.0/src/base64/decode.rs +39 -310
  23. hashcodecs-0.6.0/src/base64/dispatch.rs +320 -0
  24. hashcodecs-0.6.0/src/base64/encode.rs +209 -0
  25. hashcodecs-0.5.0/src/base64/x86_avx512.rs → hashcodecs-0.6.0/src/base64/x86/avx512.rs +37 -10
  26. hashcodecs-0.6.0/src/base64/x86/cache.rs +130 -0
  27. hashcodecs-0.5.0/src/base64/x86.rs → hashcodecs-0.6.0/src/base64/x86/decode.rs +13 -375
  28. hashcodecs-0.6.0/src/base64/x86/encode.rs +588 -0
  29. hashcodecs-0.6.0/src/base64/x86.rs +32 -0
  30. hashcodecs-0.6.0/src/base64.rs +259 -0
  31. hashcodecs-0.5.0/src/python/base64.rs → hashcodecs-0.6.0/src/bindings/base64/decode.rs +151 -479
  32. {hashcodecs-0.5.0/src/python → hashcodecs-0.6.0/src/bindings}/base64/encode.rs +20 -18
  33. hashcodecs-0.6.0/src/bindings/base64/mod.rs +1086 -0
  34. {hashcodecs-0.5.0/src/python → hashcodecs-0.6.0/src/bindings}/buffer.rs +57 -18
  35. hashcodecs-0.6.0/src/bindings/mod.rs +314 -0
  36. hashcodecs-0.5.0/src/python/murmur3.rs → hashcodecs-0.6.0/src/bindings/murmur3/mod.rs +118 -31
  37. hashcodecs-0.6.0/src/bindings/xxhash/batch.rs +82 -0
  38. hashcodecs-0.6.0/src/bindings/xxhash/mod.rs +197 -0
  39. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/src/lib.rs +8 -2
  40. hashcodecs-0.6.0/src/murmur3/dispatch.rs +51 -0
  41. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/src/murmur3/x86.rs +69 -3
  42. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/src/murmur3.rs +217 -165
  43. hashcodecs-0.6.0/src/xxhash/aarch64.rs +91 -0
  44. hashcodecs-0.6.0/src/xxhash/x86/avx2.rs +261 -0
  45. hashcodecs-0.6.0/src/xxhash/x86/avx512.rs +83 -0
  46. hashcodecs-0.6.0/src/xxhash/x86/sse.rs +88 -0
  47. hashcodecs-0.6.0/src/xxhash/x86.rs +50 -0
  48. hashcodecs-0.6.0/src/xxhash.rs +849 -0
  49. hashcodecs-0.5.0/BENCHMARK.md +0 -113
  50. hashcodecs-0.5.0/src/base64/aarch64.rs +0 -927
  51. hashcodecs-0.5.0/src/base64/dispatch.rs +0 -373
  52. hashcodecs-0.5.0/src/murmur3/dispatch.rs +0 -47
  53. hashcodecs-0.5.0/src/python.rs +0 -52
  54. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/LICENSE +0 -0
  55. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/LICENSE-MIT +0 -0
  56. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/benches/support/mod.rs +0 -0
  57. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/python/hashcodecs/base64.py +0 -0
  58. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/python/hashcodecs/base64.pyi +0 -0
  59. {hashcodecs-0.5.0 → hashcodecs-0.6.0}/python/hashcodecs/murmur3.py +0 -0
  60. {hashcodecs-0.5.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,7 +194,7 @@ dependencies = [
194
194
 
195
195
  [[package]]
196
196
  name = "hashcodecs"
197
- version = "0.5.0"
197
+ version = "0.6.0"
198
198
  dependencies = [
199
199
  "base64",
200
200
  "base64-turbo",
@@ -206,6 +206,9 @@ dependencies = [
206
206
  "murmur3",
207
207
  "murmurs",
208
208
  "pyo3",
209
+ "pyo3-build-config",
210
+ "xxhash-c-sys",
211
+ "xxhash-rust",
209
212
  ]
210
213
 
211
214
  [[package]]
@@ -576,6 +579,22 @@ dependencies = [
576
579
  "windows-link",
577
580
  ]
578
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
+
579
598
  [[package]]
580
599
  name = "zerocopy"
581
600
  version = "0.8.56"
@@ -1,13 +1,13 @@
1
1
  [package]
2
2
  name = "hashcodecs"
3
- version = "0.5.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
 
@@ -22,16 +22,21 @@ extension-module = ["python", "pyo3/extension-module"]
22
22
  [dependencies]
23
23
  memchr = { version = "2.8.0", optional = true }
24
24
  mimalloc = "0.1.52"
25
- 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"
26
29
 
27
30
  [dev-dependencies]
28
31
  base64 = "0.23.1"
29
- base64-turbo = "0.2.0"
32
+ base64-turbo = "0.3.0"
30
33
  criterion = { version = "0.8.2", default-features = false, features = ["cargo_bench_support"] }
31
34
  fastmurmur3 = "0.2.0"
32
35
  mm3h = "0.1.3"
33
36
  murmur3 = "0.5"
34
37
  murmurs = "1.0.5"
38
+ xxhash-c-sys = "0.8.7"
39
+ xxhash-rust = { version = "0.8.18", features = ["xxh3"] }
35
40
 
36
41
  [[bench]]
37
42
  name = "base64"
@@ -41,6 +46,15 @@ harness = false
41
46
  name = "murmur3"
42
47
  harness = false
43
48
 
49
+ [[bench]]
50
+ name = "xxhash"
51
+ harness = false
52
+
53
+ [[test]]
54
+ name = "sanitizers"
55
+ path = "tests/sanitizers.rs"
56
+ harness = false
57
+
44
58
  [profile.release]
45
59
  codegen-units = 1
46
60
  lto = "fat"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hashcodecs
3
- Version: 0.5.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
@@ -27,14 +27,16 @@ Description-Content-Type: text/markdown
27
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)
28
28
  [![Downloads](https://img.shields.io/pypi/dm/hashcodecs?style=for-the-badge&label=downloads)](https://pypi.org/project/hashcodecs/)
29
29
 
30
- `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.
31
31
 
32
32
  ## Design
33
33
 
34
34
  - Runtime-dispatched SIMD Base64 with portable scalar fallbacks.
35
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.
36
37
  - Rust and Python `*_into` APIs for caller-managed output buffers.
37
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.
38
40
 
39
41
  ## Install
40
42
 
@@ -54,13 +56,14 @@ let mut output = [0_u8; 8];
54
56
  let written = hashcodecs::b64encode_into(b"hello", &mut output).unwrap();
55
57
  assert_eq!(&output[..written], b"aGVsbG8=");
56
58
  assert_eq!(hashcodecs::murmur3_x86_32(b"hello", 0), 0x248b_fa47);
59
+ assert_eq!(hashcodecs::xxh3_64(b"", 0), 0x2d06_8005_38d3_94c2);
57
60
  ```
58
61
 
59
62
  ### Python
60
63
 
61
64
  ```python
62
65
  import hashcodecs.base64 as base64
63
- from hashcodecs import murmur3_32, murmur3_x64_128
66
+ from hashcodecs import murmur3_32, murmur3_x64_128, xxh3_64, xxh3_128_batch
64
67
 
65
68
  assert base64.b64encode(b'hello') == b'aGVsbG8='
66
69
  assert base64.b64decode(b'aGVsbG8=') == b'hello'
@@ -69,6 +72,11 @@ assert base64.b64decode_batch([b'aGVsbG8=', 'd29ybGQ=']) == [b'hello', b'world']
69
72
  assert base64.b64encode(b'hello', padded=False) == b'aGVsbG8'
70
73
  assert base64.b64decode(b'aGVsbG8', padded=False, canonical=True) == b'hello'
71
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
+ ]
72
80
 
73
81
  payload = b'hello'
74
82
  encoded = bytearray(4 * ((len(payload) + 2) // 3))
@@ -102,6 +110,7 @@ Comparison crates are development-only dependencies and are not included in cons
102
110
  ```sh
103
111
  cargo bench --bench base64
104
112
  cargo bench --bench murmur3
113
+ cargo bench --bench xxhash
105
114
  uv sync --group benchmark --no-install-project
106
115
  uv run --no-project --with . python benchmarks/python_base64.py
107
116
  uv run --no-project --with . python benchmarks/python_base64.py --into
@@ -111,82 +120,47 @@ uv run --no-project --with . python benchmarks/python_base64_batch.py
111
120
  uv run --no-project --with . python benchmarks/python_base64_batch.py --large
112
121
  uv run --no-project --with . python benchmarks/python_murmur3.py
113
122
  uv run --no-project --with . python benchmarks/python_murmur3.py --incremental
123
+ uv run --no-project --with . python benchmarks/python_xxhash.py
114
124
  ```
115
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
+
116
132
  ## Base64: Rust
117
133
 
118
- | Alphabet | Input | Operation | hashcodecs | `base64` | `base64-turbo` |
119
- | --- | --- | --- | ---: | ---: | ---: |
120
- | Standard | 4 KiB | encode | **20.41 GiB/s** | 5.81 GiB/s | 18.29 GiB/s |
121
- | | 4 KiB | decode | **27.11 GiB/s** | 4.33 GiB/s | 16.95 GiB/s |
122
- | | 1 MiB | encode | **42.38 GiB/s** | 5.16 GiB/s | 19.60 GiB/s |
123
- | | 1 MiB | decode | **31.07 GiB/s** | 4.10 GiB/s | 18.17 GiB/s |
124
- | | 32 MiB | encode | **11.97 GiB/s** | 3.28 GiB/s | 10.94 GiB/s |
125
- | | 32 MiB | decode | **11.51 GiB/s** | 3.35 GiB/s | 10.45 GiB/s |
126
- | URL-safe | 4 KiB | encode | **20.46 GiB/s** | 5.81 GiB/s | 18.31 GiB/s |
127
- | | 4 KiB | decode | **25.65 GiB/s** | 4.35 GiB/s | 16.96 GiB/s |
128
- | | 1 MiB | encode | **42.56 GiB/s** | 5.18 GiB/s | 19.66 GiB/s |
129
- | | 1 MiB | decode | **29.42 GiB/s** | 4.08 GiB/s | 18.16 GiB/s |
130
- | | 32 MiB | encode | **11.86 GiB/s** | 3.29 GiB/s | 11.09 GiB/s |
131
- | | 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)
132
135
 
133
136
  ## MurmurHash3: Rust
134
137
 
135
- | Variant | Input | hashcodecs | `murmur3` | `murmurs` | `fastmurmur3` | `mm3h` |
136
- | --- | --- | ---: | ---: | ---: | ---: | ---: |
137
- | x86 32-bit | 4 KiB | **4.30 GiB/s** | 2.60 GiB/s | 4.09 GiB/s | n/a | **4.30 GiB/s** |
138
- | | 1 MiB | **4.26 GiB/s** | 2.59 GiB/s | 4.04 GiB/s | n/a | 4.25 GiB/s |
139
- | | 32 MiB | **4.21 GiB/s** | 2.54 GiB/s | 3.87 GiB/s | n/a | 4.03 GiB/s |
140
- | x86 128-bit | 4 KiB | **9.54 GiB/s** | 4.97 GiB/s | 8.64 GiB/s | n/a | n/a |
141
- | | 1 MiB | **9.84 GiB/s** | 5.06 GiB/s | 8.77 GiB/s | n/a | n/a |
142
- | | 32 MiB | **9.63 GiB/s** | 4.84 GiB/s | 6.05 GiB/s | n/a | n/a |
143
- | 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 |
144
- | | 1 MiB | **10.84 GiB/s** | 6.89 GiB/s | 9.51 GiB/s | 10.04 GiB/s | 9.51 GiB/s |
145
- | | 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)
146
145
 
147
146
  ## Base64: Python
148
147
 
149
148
  Python decoding uses `validate=True`, and `hashcodecs` passes `bytes` directly into Rust without an input copy.
150
149
 
151
- | Alphabet | Input | Operation | hashcodecs | CPython `base64` | `pybase64` |
152
- | --- | --- | --- | ---: | ---: | ---: |
153
- | Standard | 4 KiB | encode | **14.42 GiB/s** | 0.49 GiB/s | 14.36 GiB/s |
154
- | | 4 KiB | decode | **16.72 GiB/s** | 1.12 GiB/s | 8.35 GiB/s |
155
- | | 1 MiB | encode | **3.69 GiB/s** | 0.43 GiB/s | 3.51 GiB/s |
156
- | | 1 MiB | decode | **4.90 GiB/s** | 0.97 GiB/s | 4.60 GiB/s |
157
- | | 32 MiB | encode | **2.81 GiB/s** | 0.44 GiB/s | 2.60 GiB/s |
158
- | | 32 MiB | decode | 3.15 GiB/s | 0.93 GiB/s | **3.49 GiB/s** |
159
- | URL-safe | 4 KiB | encode | **14.48 GiB/s** | 0.40 GiB/s | 1.19 GiB/s |
160
- | | 4 KiB | decode | **13.45 GiB/s** | 0.75 GiB/s | 1.56 GiB/s |
161
- | | 1 MiB | encode | **3.96 GiB/s** | 0.36 GiB/s | 0.92 GiB/s |
162
- | | 1 MiB | decode | **4.53 GiB/s** | 0.61 GiB/s | 1.58 GiB/s |
163
- | | 32 MiB | encode | **2.64 GiB/s** | 0.34 GiB/s | 0.83 GiB/s |
164
- | | 32 MiB | decode | **3.15 GiB/s** | 0.60 GiB/s | 1.49 GiB/s |
150
+ [![Python Base64 throughput](docs/benchmarks/base64-python.svg)](docs/benchmarks/base64-python.svg)
165
151
 
166
152
  ## MurmurHash3: Python
167
153
 
168
- | Variant | API | Input | hashcodecs | `mmh3` |
169
- | --- | --- | --- | ---: | ---: |
170
- | x86 32-bit | one-shot | 4 KiB | **3.79 GiB/s** | 3.71 GiB/s |
171
- | | | 1 MiB | **3.98 GiB/s** | 3.83 GiB/s |
172
- | | | 32 MiB | **3.97 GiB/s** | 3.66 GiB/s |
173
- | | incremental | 4 KiB | **3.58 GiB/s** | **3.58 GiB/s** |
174
- | | | 1 MiB | **3.96 GiB/s** | 3.81 GiB/s |
175
- | | | 32 MiB | **3.92 GiB/s** | 3.74 GiB/s |
176
- | x86 128-bit | one-shot | 4 KiB | 8.09 GiB/s | **8.21 GiB/s** |
177
- | | | 1 MiB | **9.22 GiB/s** | 8.87 GiB/s |
178
- | | | 32 MiB | **9.12 GiB/s** | 6.15 GiB/s |
179
- | | incremental | 4 KiB | **7.02 GiB/s** | 0.77 GiB/s |
180
- | | | 1 MiB | **9.17 GiB/s** | 0.80 GiB/s |
181
- | | | 32 MiB | **9.01 GiB/s** | 0.79 GiB/s |
182
- | x64 128-bit | one-shot | 4 KiB | 8.68 GiB/s | **9.46 GiB/s** |
183
- | | | 1 MiB | 10.10 GiB/s | **10.24 GiB/s** |
184
- | | | 32 MiB | **9.54 GiB/s** | 6.61 GiB/s |
185
- | | incremental | 4 KiB | 7.70 GiB/s | **7.99 GiB/s** |
186
- | | | 1 MiB | **10.01 GiB/s** | 9.28 GiB/s |
187
- | | | 32 MiB | **9.41 GiB/s** | 7.34 GiB/s |
188
-
189
- 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).
190
164
 
191
165
  ## SIMD References
192
166
 
@@ -7,14 +7,16 @@
7
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
8
  [![Downloads](https://img.shields.io/pypi/dm/hashcodecs?style=for-the-badge&label=downloads)](https://pypi.org/project/hashcodecs/)
9
9
 
10
- `hashcodecs` provides runtime-dispatched SIMD Base64 codecs and fast, reference-compatible MurmurHash3 functions for Rust and Python.
10
+ `hashcodecs` provides runtime-dispatched SIMD Base64 codecs and fast, reference-compatible MurmurHash3 and XXH3 functions for Rust and Python.
11
11
 
12
12
  ## Design
13
13
 
14
14
  - Runtime-dispatched SIMD Base64 with portable scalar fallbacks.
15
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.
16
17
  - Rust and Python `*_into` APIs for caller-managed output buffers.
17
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.
18
20
 
19
21
  ## Install
20
22
 
@@ -34,13 +36,14 @@ let mut output = [0_u8; 8];
34
36
  let written = hashcodecs::b64encode_into(b"hello", &mut output).unwrap();
35
37
  assert_eq!(&output[..written], b"aGVsbG8=");
36
38
  assert_eq!(hashcodecs::murmur3_x86_32(b"hello", 0), 0x248b_fa47);
39
+ assert_eq!(hashcodecs::xxh3_64(b"", 0), 0x2d06_8005_38d3_94c2);
37
40
  ```
38
41
 
39
42
  ### Python
40
43
 
41
44
  ```python
42
45
  import hashcodecs.base64 as base64
43
- from hashcodecs import murmur3_32, murmur3_x64_128
46
+ from hashcodecs import murmur3_32, murmur3_x64_128, xxh3_64, xxh3_128_batch
44
47
 
45
48
  assert base64.b64encode(b'hello') == b'aGVsbG8='
46
49
  assert base64.b64decode(b'aGVsbG8=') == b'hello'
@@ -49,6 +52,11 @@ assert base64.b64decode_batch([b'aGVsbG8=', 'd29ybGQ=']) == [b'hello', b'world']
49
52
  assert base64.b64encode(b'hello', padded=False) == b'aGVsbG8'
50
53
  assert base64.b64decode(b'aGVsbG8', padded=False, canonical=True) == b'hello'
51
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
+ ]
52
60
 
53
61
  payload = b'hello'
54
62
  encoded = bytearray(4 * ((len(payload) + 2) // 3))
@@ -82,6 +90,7 @@ Comparison crates are development-only dependencies and are not included in cons
82
90
  ```sh
83
91
  cargo bench --bench base64
84
92
  cargo bench --bench murmur3
93
+ cargo bench --bench xxhash
85
94
  uv sync --group benchmark --no-install-project
86
95
  uv run --no-project --with . python benchmarks/python_base64.py
87
96
  uv run --no-project --with . python benchmarks/python_base64.py --into
@@ -91,82 +100,47 @@ uv run --no-project --with . python benchmarks/python_base64_batch.py
91
100
  uv run --no-project --with . python benchmarks/python_base64_batch.py --large
92
101
  uv run --no-project --with . python benchmarks/python_murmur3.py
93
102
  uv run --no-project --with . python benchmarks/python_murmur3.py --incremental
103
+ uv run --no-project --with . python benchmarks/python_xxhash.py
94
104
  ```
95
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
+
96
112
  ## Base64: Rust
97
113
 
98
- | Alphabet | Input | Operation | hashcodecs | `base64` | `base64-turbo` |
99
- | --- | --- | --- | ---: | ---: | ---: |
100
- | Standard | 4 KiB | encode | **20.41 GiB/s** | 5.81 GiB/s | 18.29 GiB/s |
101
- | | 4 KiB | decode | **27.11 GiB/s** | 4.33 GiB/s | 16.95 GiB/s |
102
- | | 1 MiB | encode | **42.38 GiB/s** | 5.16 GiB/s | 19.60 GiB/s |
103
- | | 1 MiB | decode | **31.07 GiB/s** | 4.10 GiB/s | 18.17 GiB/s |
104
- | | 32 MiB | encode | **11.97 GiB/s** | 3.28 GiB/s | 10.94 GiB/s |
105
- | | 32 MiB | decode | **11.51 GiB/s** | 3.35 GiB/s | 10.45 GiB/s |
106
- | URL-safe | 4 KiB | encode | **20.46 GiB/s** | 5.81 GiB/s | 18.31 GiB/s |
107
- | | 4 KiB | decode | **25.65 GiB/s** | 4.35 GiB/s | 16.96 GiB/s |
108
- | | 1 MiB | encode | **42.56 GiB/s** | 5.18 GiB/s | 19.66 GiB/s |
109
- | | 1 MiB | decode | **29.42 GiB/s** | 4.08 GiB/s | 18.16 GiB/s |
110
- | | 32 MiB | encode | **11.86 GiB/s** | 3.29 GiB/s | 11.09 GiB/s |
111
- | | 32 MiB | decode | **11.67 GiB/s** | 3.36 GiB/s | 10.62 GiB/s |
114
+ [![Rust Base64 throughput](docs/benchmarks/base64-rust.svg)](docs/benchmarks/base64-rust.svg)
112
115
 
113
116
  ## MurmurHash3: Rust
114
117
 
115
- | Variant | Input | hashcodecs | `murmur3` | `murmurs` | `fastmurmur3` | `mm3h` |
116
- | --- | --- | ---: | ---: | ---: | ---: | ---: |
117
- | x86 32-bit | 4 KiB | **4.30 GiB/s** | 2.60 GiB/s | 4.09 GiB/s | n/a | **4.30 GiB/s** |
118
- | | 1 MiB | **4.26 GiB/s** | 2.59 GiB/s | 4.04 GiB/s | n/a | 4.25 GiB/s |
119
- | | 32 MiB | **4.21 GiB/s** | 2.54 GiB/s | 3.87 GiB/s | n/a | 4.03 GiB/s |
120
- | x86 128-bit | 4 KiB | **9.54 GiB/s** | 4.97 GiB/s | 8.64 GiB/s | n/a | n/a |
121
- | | 1 MiB | **9.84 GiB/s** | 5.06 GiB/s | 8.77 GiB/s | n/a | n/a |
122
- | | 32 MiB | **9.63 GiB/s** | 4.84 GiB/s | 6.05 GiB/s | n/a | n/a |
123
- | 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 |
124
- | | 1 MiB | **10.84 GiB/s** | 6.89 GiB/s | 9.51 GiB/s | 10.04 GiB/s | 9.51 GiB/s |
125
- | | 32 MiB | **10.12 GiB/s** | 6.11 GiB/s | 6.64 GiB/s | 7.25 GiB/s | 6.70 GiB/s |
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)
126
125
 
127
126
  ## Base64: Python
128
127
 
129
128
  Python decoding uses `validate=True`, and `hashcodecs` passes `bytes` directly into Rust without an input copy.
130
129
 
131
- | Alphabet | Input | Operation | hashcodecs | CPython `base64` | `pybase64` |
132
- | --- | --- | --- | ---: | ---: | ---: |
133
- | Standard | 4 KiB | encode | **14.42 GiB/s** | 0.49 GiB/s | 14.36 GiB/s |
134
- | | 4 KiB | decode | **16.72 GiB/s** | 1.12 GiB/s | 8.35 GiB/s |
135
- | | 1 MiB | encode | **3.69 GiB/s** | 0.43 GiB/s | 3.51 GiB/s |
136
- | | 1 MiB | decode | **4.90 GiB/s** | 0.97 GiB/s | 4.60 GiB/s |
137
- | | 32 MiB | encode | **2.81 GiB/s** | 0.44 GiB/s | 2.60 GiB/s |
138
- | | 32 MiB | decode | 3.15 GiB/s | 0.93 GiB/s | **3.49 GiB/s** |
139
- | URL-safe | 4 KiB | encode | **14.48 GiB/s** | 0.40 GiB/s | 1.19 GiB/s |
140
- | | 4 KiB | decode | **13.45 GiB/s** | 0.75 GiB/s | 1.56 GiB/s |
141
- | | 1 MiB | encode | **3.96 GiB/s** | 0.36 GiB/s | 0.92 GiB/s |
142
- | | 1 MiB | decode | **4.53 GiB/s** | 0.61 GiB/s | 1.58 GiB/s |
143
- | | 32 MiB | encode | **2.64 GiB/s** | 0.34 GiB/s | 0.83 GiB/s |
144
- | | 32 MiB | decode | **3.15 GiB/s** | 0.60 GiB/s | 1.49 GiB/s |
130
+ [![Python Base64 throughput](docs/benchmarks/base64-python.svg)](docs/benchmarks/base64-python.svg)
145
131
 
146
132
  ## MurmurHash3: Python
147
133
 
148
- | Variant | API | Input | hashcodecs | `mmh3` |
149
- | --- | --- | --- | ---: | ---: |
150
- | x86 32-bit | one-shot | 4 KiB | **3.79 GiB/s** | 3.71 GiB/s |
151
- | | | 1 MiB | **3.98 GiB/s** | 3.83 GiB/s |
152
- | | | 32 MiB | **3.97 GiB/s** | 3.66 GiB/s |
153
- | | incremental | 4 KiB | **3.58 GiB/s** | **3.58 GiB/s** |
154
- | | | 1 MiB | **3.96 GiB/s** | 3.81 GiB/s |
155
- | | | 32 MiB | **3.92 GiB/s** | 3.74 GiB/s |
156
- | x86 128-bit | one-shot | 4 KiB | 8.09 GiB/s | **8.21 GiB/s** |
157
- | | | 1 MiB | **9.22 GiB/s** | 8.87 GiB/s |
158
- | | | 32 MiB | **9.12 GiB/s** | 6.15 GiB/s |
159
- | | incremental | 4 KiB | **7.02 GiB/s** | 0.77 GiB/s |
160
- | | | 1 MiB | **9.17 GiB/s** | 0.80 GiB/s |
161
- | | | 32 MiB | **9.01 GiB/s** | 0.79 GiB/s |
162
- | x64 128-bit | one-shot | 4 KiB | 8.68 GiB/s | **9.46 GiB/s** |
163
- | | | 1 MiB | 10.10 GiB/s | **10.24 GiB/s** |
164
- | | | 32 MiB | **9.54 GiB/s** | 6.61 GiB/s |
165
- | | incremental | 4 KiB | 7.70 GiB/s | **7.99 GiB/s** |
166
- | | | 1 MiB | **10.01 GiB/s** | 9.28 GiB/s |
167
- | | | 32 MiB | **9.41 GiB/s** | 7.34 GiB/s |
168
-
169
- Reusable-buffer and mutable-input results are available in [BENCHMARK.md](https://github.com/kozistr/hashcodecs-rs/blob/main/BENCHMARK.md).
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).
170
144
 
171
145
  ## SIMD References
172
146
 
@@ -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);