Skip to content

Commit 136aebd

Browse files
feat(simd): add AVX2 backend (v0.2.0)
Specializes pack 1/8/16/32 and unpack 8/16/32 with 256-bit kernels; other widths delegate to SseBackend with byte-identical output. Bench shows 6-8x pack speedup at widths 8 and 16. CI extended: release-mode tests, miri (scalar via cfg(miri)), macOS/Windows matrix, fuzz no longer masks crashes (300s/target).
1 parent 05ad2b5 commit 136aebd

12 files changed

Lines changed: 628 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ on:
88

99
jobs:
1010
test:
11-
runs-on: ubuntu-latest
11+
runs-on: ${{ matrix.os }}
1212
strategy:
1313
fail-fast: false
1414
matrix:
15+
os:
16+
- ubuntu-latest
17+
- macos-latest
18+
- windows-latest
1519
rust:
1620
- stable
1721
- beta
@@ -26,6 +30,18 @@ jobs:
2630
- run: cargo test --test edge_cases
2731
- run: cargo test --tests --all-features
2832

33+
test-release:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- uses: actions/checkout@v4
37+
- uses: dtolnay/rust-toolchain@master
38+
with:
39+
toolchain: stable
40+
- run: cargo test --release --lib
41+
- run: cargo test --release --test correctness
42+
- run: cargo test --release --test edge_cases
43+
- run: cargo test --release --doc
44+
2945
clippy:
3046
runs-on: ubuntu-latest
3147
steps:
@@ -55,6 +71,23 @@ jobs:
5571
toolchain: stable
5672
- run: cargo test --doc
5773

74+
miri:
75+
runs-on: ubuntu-latest
76+
steps:
77+
- uses: actions/checkout@v4
78+
- uses: dtolnay/rust-toolchain@master
79+
with:
80+
toolchain: nightly
81+
components: miri
82+
# Miri cannot interpret x86 SIMD intrinsics, so the dispatch layer
83+
# forces the scalar backend under cfg(miri). This run validates the
84+
# public API and the scalar reference under interpretation; it skips
85+
# the SSE/AVX2 module-internal tests, which would still attempt to
86+
# call intrinsics directly.
87+
- run: cargo miri test --lib -- --skip simd::sse::tests --skip simd::avx2::tests
88+
- run: cargo miri test --test correctness
89+
- run: cargo miri test --test edge_cases
90+
5891
fuzz:
5992
runs-on: ubuntu-latest
6093
steps:
@@ -65,8 +98,8 @@ jobs:
6598
- name: Install cargo-fuzz
6699
run: cargo install cargo-fuzz
67100
- name: Run fuzz_roundtrip
68-
run: cargo fuzz run roundtrip -- -max_total_time=60s -timeout=10s || true
101+
run: cargo fuzz run roundtrip -- -max_total_time=300s -timeout=10s
69102
- name: Run fuzz_compress_only
70-
run: cargo fuzz run compress_only -- -max_total_time=60s -timeout=10s || true
103+
run: cargo fuzz run compress_only -- -max_total_time=300s -timeout=10s
71104
- name: Run fuzz_decompress_only
72-
run: cargo fuzz run decompress_only -- -max_total_time=60s -timeout=10s || true
105+
run: cargo fuzz run decompress_only -- -max_total_time=300s -timeout=10s

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.0] - 2026-04-13
9+
10+
### Added
11+
- AVX2 backend (`Avx2Backend` in `src/simd/avx2.rs`) with native 256-bit kernels for widths 1 (pack), 4 (unpack), 8, 16, and 32. All other widths delegate to `SseBackend`, preserving byte-identical output across backends.
12+
- Dispatch routing: `BackendType::Avx2` now resolves to `Avx2Backend` (was previously falling through to SSE). `BackendType::Avx512` falls through to `Avx2Backend` until a dedicated AVX-512 kernel ships.
13+
- AVX2-internal tests: byte-for-byte SSE compatibility, scalar parity, roundtrip, boundary patterns, and invalid-input handling, all gated on `is_x86_feature_detected!("avx2")`.
14+
- AVX2 column added to `benches/throughput_comparison.rs` for direct scalar/SSE/AVX2 comparison.
15+
- `cfg(miri)` override in `dispatch::detect_best_backend` so `cargo miri test` exercises the scalar backend without choking on x86 SIMD intrinsics.
16+
- CI matrix expanded to test on Linux, macOS, and Windows; release-mode test job added.
17+
- CI now fails on fuzz crashes (previously masked with `|| true`); fuzz duration bumped from 60s to 300s per target.
18+
19+
### Changed
20+
- Lane-crossing handling: every `_mm256_packus_epi*` is followed by `_mm256_permute4x64_epi64::<0xD8>` to restore linear in-memory order, ensuring AVX2 output is byte-identical to SSE/scalar.
21+
- Doc comments in `compress`, `decompress`, and `lib.rs` now mention AVX2 alongside SSE4.1.
22+
- `internal::SseBackend` re-export is now gated on `target_arch = "x86_64"` so the crate compiles on non-x86_64 targets (aarch64, wasm32, etc.).
23+
24+
[0.2.0]: https://github.com/themankindproject/simd-bp128-rs/releases/tag/v0.2.0
25+
826
## [0.1.1] - 2026-04-03
927

1028
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "packsimd"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
edition = "2021"
55
authors = ["Ashutosh Kumar <kumarashutosh34169@gmail.com>"]
66
description = "SIMD-accelerated bit packing compression for u32 arrays"

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
[![Build Status](https://img.shields.io/github/actions/workflow/status/themankindproject/simd-bp128-rs/ci.yml)](https://github.com/themankindproject/packsimd/actions)
77
![Rust Version](https://img.shields.io/badge/rust-1.70%2B-blue)
88

9-
> **Note:** This release includes the **Scalar** and **SSE4.1** backends only. AVX2 and AVX-512 implementations are planned for a future release. On x86_64 CPUs with SSE4.1, the SSE4.1 backend is used automatically.
10-
11-
High-performance BP128 compression for `u32` integer arrays with **SIMD acceleration**, **zero-allocation APIs**, and **deterministic encoding**.
9+
High-performance BP128 compression for `u32` integer arrays with **SIMD acceleration**, **zero-allocation APIs**, and **deterministic encoding**. The crate ships **scalar**, **SSE4.1**, and **AVX2** backends and selects the best one at runtime. AVX-512 is planned for a future release.
1210

1311
## Overview
1412

@@ -25,7 +23,7 @@ High-performance BP128 compression for `u32` integer arrays with **SIMD accelera
2523
## Features
2624

2725
- **BP128 Algorithm** — Variable bit-width packing, 128 values per block
28-
- **SIMD Acceleration** — SSE4.1 on x86_64 with automatic runtime detection
26+
- **SIMD Acceleration**AVX2 and SSE4.1 on x86_64 with automatic runtime detection
2927
- **Scalar Fallback** — Reference implementation for non-SIMD targets
3028
- **Zero-Allocation API**`compress_into` / `decompress_into` with pre-allocated buffers
3129
- **Fast Header Inspection**`decompressed_len` reads size without decompressing
@@ -38,7 +36,7 @@ High-performance BP128 compression for `u32` integer arrays with **SIMD accelera
3836

3937
```toml
4038
[dependencies]
41-
packsimd = "0.1"
39+
packsimd = "0.2"
4240
```
4341

4442
## Quick Start
@@ -99,7 +97,7 @@ For complete API reference and usage examples, see [USAGE.md](USAGE.md).
9997
│ │ │
10098
┌────┴────┐ ┌─────┴─────┐ ┌────┴────┐
10199
│ Scalar │ │ SSE4.1 │ │ AVX2 │
102-
│Backend │ │ Backend │ │(planned)
100+
│Backend │ │ Backend │ │ Backend
103101
│ │ │ │ │ │
104102
│Reference│ │ 128-bit │ │ 256-bit │
105103
│ impl │ │ SIMD │ │ SIMD │
@@ -114,6 +112,7 @@ For complete API reference and usage examples, see [USAGE.md](USAGE.md).
114112
| **dispatch** | Runtime SIMD backend selection and caching |
115113
| **simd/scalar** | Reference scalar implementation (all bit widths) |
116114
| **simd/sse** | SSE4.1-accelerated kernels (x86_64 only) |
115+
| **simd/avx2** | AVX2-accelerated kernels for byte-aligned widths and 1-bit pack (x86_64 only) |
117116

118117
## Performance
119118

@@ -193,7 +192,8 @@ cargo doc --no-deps --open
193192
|:--------|:-------|
194193
| Scalar implementation | Done |
195194
| SSE4.1 backend | Done |
196-
| AVX2 backend | Planned |
195+
| AVX2 backend | Done |
196+
| BMI2 PDEP/PEXT for irregular widths | Planned |
197197
| AVX-512 backend | Planned |
198198

199199
## License

USAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Add the dependency to your `Cargo.toml`:
3232

3333
```toml
3434
[dependencies]
35-
packsimd = "0.1"
35+
packsimd = "0.2"
3636
```
3737

3838
### Basic Example

benches/throughput_comparison.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Scalar vs SSE block-level kernel comparison.
1+
//! Scalar vs SSE vs AVX2 block-level kernel comparison.
22
//!
33
//! Run with: cargo bench --bench throughput_comparison
44
//!
@@ -13,7 +13,7 @@ use std::hint::black_box;
1313
use std::time::Duration;
1414

1515
#[cfg(target_arch = "x86_64")]
16-
use packsimd::internal::SseBackend;
16+
use packsimd::internal::{Avx2Backend, SseBackend};
1717

1818
fn generate_block(bits: u32) -> [u32; 128] {
1919
let mut rng = StdRng::seed_from_u64(42);
@@ -34,8 +34,8 @@ fn packed_bytes(bits: u32) -> usize {
3434
}
3535

3636
fn benchmark_scalar_vs_sse(c: &mut Criterion) {
37-
let mut group = c.benchmark_group("scalar_vs_sse");
38-
group.measurement_time(Duration::from_secs(5));
37+
let mut group = c.benchmark_group("scalar_vs_sse_vs_avx2");
38+
group.measurement_time(Duration::from_secs(3));
3939
group.sample_size(100);
4040

4141
for bits in [1u8, 2, 4, 8, 16, 24, 32] {
@@ -47,10 +47,16 @@ fn benchmark_scalar_vs_sse(c: &mut Criterion) {
4747
let mut scalar_unpacked = [0u32; 128];
4848
let mut sse_packed = vec![0u8; bytes_needed];
4949
let mut sse_unpacked = [0u32; 128];
50+
#[cfg(target_arch = "x86_64")]
51+
let mut avx2_packed = vec![0u8; bytes_needed];
52+
#[cfg(target_arch = "x86_64")]
53+
let mut avx2_unpacked = [0u32; 128];
5054

5155
ScalarBackend::pack_block(&block, bits, &mut scalar_packed).unwrap();
5256
#[cfg(target_arch = "x86_64")]
5357
SseBackend::pack_block(&block, bits, &mut sse_packed).unwrap();
58+
#[cfg(target_arch = "x86_64")]
59+
Avx2Backend::pack_block(&block, bits, &mut avx2_packed).unwrap();
5460

5561
group.throughput(Throughput::Bytes(input_bytes as u64));
5662
group.bench_function(format!("scalar_pack_{}bit", bits), |b| {
@@ -77,6 +83,18 @@ fn benchmark_scalar_vs_sse(c: &mut Criterion) {
7783
.unwrap();
7884
});
7985
});
86+
87+
group.throughput(Throughput::Bytes(input_bytes as u64));
88+
group.bench_function(format!("avx2_pack_{}bit", bits), |b| {
89+
b.iter(|| {
90+
Avx2Backend::pack_block(
91+
black_box(&block),
92+
black_box(bits),
93+
black_box(&mut avx2_packed),
94+
)
95+
.unwrap();
96+
});
97+
});
8098
}
8199

82100
group.throughput(Throughput::Bytes(bytes_needed as u64));
@@ -104,6 +122,18 @@ fn benchmark_scalar_vs_sse(c: &mut Criterion) {
104122
.unwrap();
105123
});
106124
});
125+
126+
group.throughput(Throughput::Bytes(bytes_needed as u64));
127+
group.bench_function(format!("avx2_unpack_{}bit", bits), |b| {
128+
b.iter(|| {
129+
Avx2Backend::unpack_block(
130+
black_box(&avx2_packed),
131+
black_box(bits),
132+
black_box(&mut avx2_unpacked),
133+
)
134+
.unwrap();
135+
});
136+
});
107137
}
108138
}
109139

src/compress.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub fn max_compressed_size(input_len: usize) -> usize {
8080
///
8181
/// - Time complexity: O(n) where n = `input.len()`
8282
/// - Space complexity: O(1) — no heap allocation
83-
/// - Automatically selects the best available SIMD backend (SSE4.1 on x86_64, scalar fallback otherwise)
83+
/// - Automatically selects the best available SIMD backend (AVX2 / SSE4.1 on x86_64, scalar fallback otherwise)
8484
///
8585
/// # Errors
8686
///

src/decompress.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ fn parse_header(input: &[u8]) -> Result<ParsedHeader, DecompressionError> {
165165
///
166166
/// - Time complexity: O(n) where n = number of decompressed values
167167
/// - Space complexity: O(1) — no heap allocation
168-
/// - Automatically selects the best available SIMD backend (SSE4.1 on x86_64, scalar fallback otherwise)
168+
/// - Automatically selects the best available SIMD backend (AVX2 / SSE4.1 on x86_64, scalar fallback otherwise)
169169
///
170170
/// # Errors
171171
///

src/dispatch.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use crate::error::Error;
22
use crate::simd::scalar::ScalarBackend;
33
use crate::simd::SimdBackend;
44

5+
#[cfg(target_arch = "x86_64")]
6+
use crate::simd::avx2::Avx2Backend;
57
#[cfg(target_arch = "x86_64")]
68
use crate::simd::sse::SseBackend;
79

@@ -16,15 +18,24 @@ pub(crate) enum BackendType {
1618
Scalar,
1719
#[cfg(target_arch = "x86_64")]
1820
Sse,
19-
// AVX2 and AVX512 are detected but currently fall back to SSE
20-
// because the SSE implementation is correct and feature-complete.
21-
// Dedicated AVX2/AVX512 kernels are planned.
21+
// AVX2 has a dedicated kernel (specializes byte-aligned widths and 1-bit
22+
// pack, delegates the rest to SseBackend). AVX-512 detection currently
23+
// falls through to the AVX2 backend until a dedicated kernel lands.
2224
#[cfg(target_arch = "x86_64")]
2325
Avx2,
2426
#[cfg(target_arch = "x86_64")]
2527
Avx512,
2628
}
2729

30+
// Miri cannot interpret x86 SIMD intrinsics, so force the scalar
31+
// reference under miri. This lets `cargo miri test` validate the
32+
// public API and the scalar backend without choking on intrinsics.
33+
#[cfg(miri)]
34+
pub(crate) fn detect_best_backend() -> BackendType {
35+
BackendType::Scalar
36+
}
37+
38+
#[cfg(not(miri))]
2839
pub(crate) fn detect_best_backend() -> BackendType {
2940
#[cfg(target_arch = "x86_64")]
3041
{
@@ -58,7 +69,9 @@ pub(crate) fn get_pack_fn() -> PackFn {
5869
match get_backend() {
5970
BackendType::Scalar => ScalarBackend::pack_block,
6071
#[cfg(target_arch = "x86_64")]
61-
BackendType::Sse | BackendType::Avx2 | BackendType::Avx512 => SseBackend::pack_block,
72+
BackendType::Sse => SseBackend::pack_block,
73+
#[cfg(target_arch = "x86_64")]
74+
BackendType::Avx2 | BackendType::Avx512 => Avx2Backend::pack_block,
6275
}
6376
}
6477

@@ -68,7 +81,9 @@ pub(crate) fn get_unpack_fn() -> UnpackFn {
6881
match get_backend() {
6982
BackendType::Scalar => ScalarBackend::unpack_block,
7083
#[cfg(target_arch = "x86_64")]
71-
BackendType::Sse | BackendType::Avx2 | BackendType::Avx512 => SseBackend::unpack_block,
84+
BackendType::Sse => SseBackend::unpack_block,
85+
#[cfg(target_arch = "x86_64")]
86+
BackendType::Avx2 | BackendType::Avx512 => Avx2Backend::unpack_block,
7287
}
7388
}
7489

0 commit comments

Comments
 (0)