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.
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/.gitignore +5 -2
- hashcodecs-0.6.0/BENCHMARK.md +58 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/Cargo.lock +23 -3
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/Cargo.toml +21 -6
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/PKG-INFO +43 -66
- hashcodecs-0.6.0/README.md +147 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/benches/base64.rs +1 -1
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/benches/murmur3.rs +1 -1
- hashcodecs-0.6.0/benches/xxhash.rs +119 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/build.rs +5 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/hatch_build.py +2 -2
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/pyproject.toml +4 -3
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/__init__.py +18 -1
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/_hashcodecs.pyi +23 -0
- hashcodecs-0.6.0/python/hashcodecs/base64.py +101 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/base64.pyi +15 -0
- hashcodecs-0.6.0/python/hashcodecs/xxhash.py +5 -0
- hashcodecs-0.6.0/src/backend.rs +222 -0
- hashcodecs-0.4.0/src/base64/aarch64.rs → hashcodecs-0.6.0/src/base64/aarch64/decode.rs +51 -114
- hashcodecs-0.6.0/src/base64/aarch64/encode.rs +113 -0
- hashcodecs-0.6.0/src/base64/aarch64/tests.rs +578 -0
- hashcodecs-0.6.0/src/base64/aarch64.rs +11 -0
- hashcodecs-0.6.0/src/base64/backend.rs +87 -0
- hashcodecs-0.6.0/src/base64/decode.rs +445 -0
- hashcodecs-0.6.0/src/base64/dispatch.rs +320 -0
- hashcodecs-0.6.0/src/base64/encode.rs +209 -0
- hashcodecs-0.4.0/src/base64/x86_avx512.rs → hashcodecs-0.6.0/src/base64/x86/avx512.rs +37 -10
- hashcodecs-0.6.0/src/base64/x86/cache.rs +130 -0
- hashcodecs-0.4.0/src/base64/x86.rs → hashcodecs-0.6.0/src/base64/x86/decode.rs +13 -375
- hashcodecs-0.6.0/src/base64/x86/encode.rs +588 -0
- hashcodecs-0.6.0/src/base64/x86.rs +32 -0
- hashcodecs-0.6.0/src/base64.rs +259 -0
- hashcodecs-0.6.0/src/bindings/base64/decode.rs +982 -0
- {hashcodecs-0.4.0/src/python → hashcodecs-0.6.0/src/bindings}/base64/encode.rs +57 -51
- hashcodecs-0.6.0/src/bindings/base64/mod.rs +1086 -0
- hashcodecs-0.6.0/src/bindings/buffer.rs +222 -0
- hashcodecs-0.6.0/src/bindings/mod.rs +314 -0
- hashcodecs-0.4.0/src/python/murmur3.rs → hashcodecs-0.6.0/src/bindings/murmur3/mod.rs +118 -31
- hashcodecs-0.6.0/src/bindings/xxhash/batch.rs +82 -0
- hashcodecs-0.6.0/src/bindings/xxhash/mod.rs +197 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/src/lib.rs +8 -2
- hashcodecs-0.6.0/src/murmur3/dispatch.rs +51 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/src/murmur3/x86.rs +69 -3
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/src/murmur3.rs +217 -165
- hashcodecs-0.6.0/src/xxhash/aarch64.rs +91 -0
- hashcodecs-0.6.0/src/xxhash/x86/avx2.rs +261 -0
- hashcodecs-0.6.0/src/xxhash/x86/avx512.rs +83 -0
- hashcodecs-0.6.0/src/xxhash/x86/sse.rs +88 -0
- hashcodecs-0.6.0/src/xxhash/x86.rs +50 -0
- hashcodecs-0.6.0/src/xxhash.rs +849 -0
- hashcodecs-0.4.0/BENCHMARK.md +0 -71
- hashcodecs-0.4.0/README.md +0 -170
- hashcodecs-0.4.0/python/hashcodecs/base64.py +0 -89
- hashcodecs-0.4.0/src/base64/dispatch.rs +0 -310
- hashcodecs-0.4.0/src/base64.rs +0 -555
- hashcodecs-0.4.0/src/murmur3/dispatch.rs +0 -47
- hashcodecs-0.4.0/src/python/base64.rs +0 -792
- hashcodecs-0.4.0/src/python/buffer.rs +0 -113
- hashcodecs-0.4.0/src/python.rs +0 -33
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/LICENSE +0 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/LICENSE-MIT +0 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/benches/support/mod.rs +0 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/murmur3.py +0 -0
- {hashcodecs-0.4.0 → hashcodecs-0.6.0}/python/hashcodecs/py.typed +0 -0
|
@@ -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
|
+
[](docs/benchmarks/xxh3-rust.svg)
|
|
19
|
+
|
|
20
|
+
[](docs/benchmarks/xxh3-python.svg)
|
|
21
|
+
|
|
22
|
+
## Reusable Python Buffers
|
|
23
|
+
|
|
24
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](docs/benchmarks/base64-python-batch-large.svg)
|
|
49
|
+
|
|
50
|
+
## Mutable Python Inputs
|
|
51
|
+
|
|
52
|
+
### Base64
|
|
53
|
+
|
|
54
|
+
[](docs/benchmarks/base64-python-mutable.svg)
|
|
55
|
+
|
|
56
|
+
### MurmurHash3
|
|
57
|
+
|
|
58
|
+
[](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.
|
|
49
|
+
version = "0.3.0"
|
|
50
50
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
51
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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",
|
|
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.
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hashcodecs
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: SIMD-accelerated Base64 and
|
|
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
|
[](https://github.com/kozistr/hashcodecs-rs/actions/workflows/ci.yml)
|
|
24
|
+
[](https://app.codecov.io/gh/kozistr/hashcodecs-rs)
|
|
24
25
|
[](https://pypi.org/project/hashcodecs/)
|
|
25
26
|
[](https://pypi.org/project/hashcodecs/)
|
|
26
27
|
[](https://github.com/kozistr/hashcodecs-rs#license)
|
|
27
28
|
[](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
|
-
|
|
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
|
+
[](docs/benchmarks/base64-rust.svg)
|
|
129
135
|
|
|
130
136
|
## MurmurHash3: Rust
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
[](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
|
+
[](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
|
-
|
|
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
|
+
[](docs/benchmarks/base64-python.svg)
|
|
162
151
|
|
|
163
152
|
## MurmurHash3: Python
|
|
164
153
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
[](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
|
+
[](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
|
+
[](https://github.com/kozistr/hashcodecs-rs/actions/workflows/ci.yml)
|
|
4
|
+
[](https://app.codecov.io/gh/kozistr/hashcodecs-rs)
|
|
5
|
+
[](https://pypi.org/project/hashcodecs/)
|
|
6
|
+
[](https://pypi.org/project/hashcodecs/)
|
|
7
|
+
[](https://github.com/kozistr/hashcodecs-rs#license)
|
|
8
|
+
[](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
|
+
[](docs/benchmarks/base64-rust.svg)
|
|
115
|
+
|
|
116
|
+
## MurmurHash3: Rust
|
|
117
|
+
|
|
118
|
+
[](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
|
+
[](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
|
+
[](docs/benchmarks/base64-python.svg)
|
|
131
|
+
|
|
132
|
+
## MurmurHash3: Python
|
|
133
|
+
|
|
134
|
+
[](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
|
+
[](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;
|
|
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;
|
|
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);
|