Skip to content

Commit ad84da9

Browse files
nyurikCopilot
andauthored
feat: SIMD compilation mode - CPU portable vs native mode selection (#67)
This pull request introduces a new SIMD mode configuration system for the FastPFOR crate, allowing users to select between portable, native, and runtime SIMD instruction sets for the C++ backend. The changes improve build flexibility, CI reliability, and documentation clarity, while updating the crate version and standardizing feature usage in development scripts. ### SIMD Mode Configuration and Build System * Added support for selecting SIMD mode (`portable`, `native`, or `runtime`) via Cargo features and the `FASTPFOR_SIMD_MODE` environment variable; the build script now warns if multiple SIMD features are enabled and passes the selected mode to CMake. (`build.rs`, `Cargo.toml`) [[1]](diffhunk://#diff-d0d98998092552a1d3259338c2c71e118a5b8343dd4703c0c7f552ada7f9cb42L16-R59) [[2]](diffhunk://#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R28-R35) * Updated GitHub Actions CI workflow to test all combinations of OS and SIMD mode, and to cache builds separately for each SIMD mode. (`.github/workflows/ci.yml`) ### Documentation Improvements * Expanded `README.md` with a clear explanation of SIMD modes, their use cases, and configuration recommendations for end users. ### Development Scripts and Testing * Standardized the use of the `_all_compatible` feature group in the `justfile` for building, testing, linting, and coverage, and added recipes to test all SIMD modes. [[1]](diffhunk://#diff-deb9bb56fb122db0b605aa5b63f95a4665c905b18dd670e1fa6c877576a94ff1L4-R30) [[2]](diffhunk://#diff-deb9bb56fb122db0b605aa5b63f95a4665c905b18dd670e1fa6c877576a94ff1L51-R65) [[3]](diffhunk://#diff-deb9bb56fb122db0b605aa5b63f95a4665c905b18dd670e1fa6c877576a94ff1L95-R130) [[4]](diffhunk://#diff-deb9bb56fb122db0b605aa5b63f95a4665c905b18dd670e1fa6c877576a94ff1L130-R145) * Improved installation logic in the `justfile` for development tools, with clearer output and better CI compatibility. ### Version Update * Bumped the crate version from `0.7.0` to `0.8.0` to reflect these breaking and feature enhancements. (`Cargo.toml`) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f60bbe4 commit ad84da9

5 files changed

Lines changed: 125 additions & 42 deletions

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,22 @@ defaults:
1313

1414
jobs:
1515
test:
16-
name: Test on ${{ matrix.os }}
16+
name: Test ${{ matrix.os }} (${{ matrix.simd_mode }})
1717
runs-on: ${{ matrix.os }}
1818
strategy:
19-
fail-fast: true
19+
fail-fast: false
2020
matrix:
21-
include:
22-
- os: ubuntu-latest
23-
- os: macos-latest
21+
os: [ubuntu-latest, macos-latest]
22+
simd_mode: [portable, native]
23+
env:
24+
FASTPFOR_SIMD_MODE: ${{ matrix.simd_mode }}
2425
steps:
2526
- uses: actions/checkout@v6
2627
with: { submodules: recursive }
2728
- if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
2829
uses: Swatinem/rust-cache@v2
30+
with:
31+
prefix-key: "v0-${{ matrix.simd_mode }}"
2932
- uses: taiki-e/install-action@v2
3033
with: { tool: 'just,cargo-binstall' }
3134
- run: just ci-test
@@ -60,42 +63,35 @@ jobs:
6063
uses: dtolnay/rust-toolchain@stable
6164
with:
6265
toolchain: ${{ steps.msrv.outputs.value }}
63-
components: rustfmt
6466
- run: just ci_mode=0 ci-test-msrv # Ignore warnings in MSRV
67+
6568
fuzz:
6669
name: Fuzz
6770
runs-on: ubuntu-latest
68-
6971
env:
7072
# The number of seconds to run the fuzz target.
7173
FUZZ_TIME: 60
72-
7374
strategy:
7475
matrix:
7576
include:
7677
- fuzz_target: cpp_roundtrip
7778
- fuzz_target: rust_compress_oracle
7879
- fuzz_target: rust_decompress_oracle
79-
8080
steps:
8181
- uses: actions/checkout@v6
8282
with: {persist-credentials: false, submodules: recursive}
83-
8483
# Install the nightly Rust channel.
8584
- run: rustup toolchain install nightly
8685
- run: rustup default nightly
87-
8886
# Install and cache `cargo-fuzz`.
8987
- uses: taiki-e/install-action@v2
9088
with:
9189
tool: cargo-binstall
9290
- run: cargo binstall -y cargo-fuzz@0.13.1 # Pinned to avoid breakage.
93-
9491
# Build and then run the fuzz target.
9592
# --target x86_64-unknown-linux-gnu is necessary to not default to musl, which is not supported by cargo-fuzz.
9693
- run: cargo fuzz build --target x86_64-unknown-linux-gnu ${{ matrix.fuzz_target }}
9794
- run: cargo fuzz run --target x86_64-unknown-linux-gnu ${{ matrix.fuzz_target }} -- -max_total_time=${{ env.FUZZ_TIME }}
98-
9995
# Upload fuzzing artifacts on failure for post-mortem debugging.
10096
- uses: actions/upload-artifact@v7
10197
if: failure()
@@ -130,7 +126,7 @@ jobs:
130126
steps:
131127
- name: Result of the needed steps
132128
run: echo "${{ toJSON(needs) }}"
133-
- if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
129+
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
134130
run: exit 1
135131

136132
release:

Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fastpfor"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
description = "FastPFOR lib with C++ Rust wrapper and pure Rust implementation"
55
authors = [
66
"Francisco Jimenez <jjcfrank@gmail.com>",
@@ -25,6 +25,14 @@ harness = false
2525
# Eventually we may want to build without the C++ bindings by default.
2626
# Keeping it on for now to simplify development.
2727
default = ["cpp"]
28+
# Used internally for testing and benchmarking. Not intended for public use.
29+
_all_compatible = ["cpp_portable", "rust"]
30+
# Use portable C++ code that will not rely on the latest CPU features. This is the default for the C++ bindings.
31+
cpp_portable = ["cpp"]
32+
# Optimize FastPFOR for the current CPU.
33+
cpp_native = ["cpp"]
34+
# Experimental mode for dynamic dispatch of FastPFOR implementations.
35+
cpp_runtime = ["cpp"]
2836
cpp = ["dep:cmake", "dep:cxx", "dep:cxx-build"]
2937
rust = ["dep:thiserror", "dep:bytes"]
3038

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,22 @@ Unless otherwise specified, all codecs support `&[u32]` only.
4848
## Usage
4949

5050
### Crate Features
51-
* `cpp` - C++ implementation (default)
51+
* `cpp` - C++ implementation (default, uses portable SIMD mode)
5252
* `rust` - Rust implementation (work in progress, opt-in)
5353

54+
#### SIMD Mode Configuration
55+
56+
The C++ backend can be compiled with different SIMD instruction sets. Control this by enabling one of these features:
57+
| Mode | Description |
58+
|------|-------------|
59+
| `cpp_portable` | **Default.** Uses SSE4.2 baseline only. Binaries run on any x86-64 CPU from ~2008+. Best for distributable libraries. |
60+
| `cpp_native` | Uses `-march=native` to enable all SIMD instructions supported by the build machine (AVX, AVX2, etc.). Maximum performance but may crash on CPUs lacking those instructions. |
61+
| `cpp_runtime` | Experimental. Reserved for future runtime CPU dispatch. |
62+
63+
Feature selection can be overridden with the `FASTPFOR_SIMD_MODE` environment variable set to "portable", "native", or "runtime".
64+
65+
**Recommendation:** Use `portable` (default) for libraries and distributed binaries. Use `native` only when building for a specific machine where you need maximum performance.
66+
5467
### Using C++ Wrapper
5568

5669
```rust

build.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,57 @@ fn build_fastpfor() {
1313

1414
// Compile FastPFOR using CMake
1515
println!("cargo:rerun-if-changed=cpp");
16-
let cmake_out = cmake::Config::new("cpp").define("WITH_TEST", "OFF").build();
16+
17+
// Warn if more than one feature is enabled. The order is important, must match the if else block below.
18+
let simd_features = [
19+
(cfg!(feature = "cpp_portable"), "'cpp_portable'"),
20+
(cfg!(feature = "cpp_runtime"), "'cpp_runtime'"),
21+
(cfg!(feature = "cpp_native"), "'cpp_native'"),
22+
];
23+
let enabled_simd_features: Vec<_> = simd_features
24+
.into_iter()
25+
.filter_map(|(enabled, name)| enabled.then_some(name))
26+
.collect();
27+
28+
// SIMD mode configuration via environment variable:
29+
// - native: Use -march=native for maximum performance (not portable across CPUs)
30+
// - portable: Use baseline SSE4.2 only for maximum compatibility (default)
31+
// - runtime: Use function multi-versioning for runtime CPU dispatch (experimental)
32+
let simd_mode = env::var("FASTPFOR_SIMD_MODE");
33+
if enabled_simd_features.len() > 1 {
34+
let feats = enabled_simd_features.join(", ");
35+
if let Ok(simd_mode) = &simd_mode {
36+
println!(
37+
"cargo::warning=Multiple SIMD mode features are enabled: {feats}, but FASTPFOR_SIMD_MODE overrides it with {simd_mode}."
38+
);
39+
} else {
40+
println!(
41+
"cargo::warning=Multiple SIMD mode features enabled: {feats}. Defaulting to {}.",
42+
enabled_simd_features[0]
43+
);
44+
}
45+
}
46+
47+
let simd_mode = simd_mode.as_deref().unwrap_or({
48+
{
49+
// The order is important, must match the list above.
50+
if cfg!(feature = "cpp_portable") {
51+
"portable"
52+
} else if cfg!(feature = "cpp_runtime") {
53+
"runtime"
54+
} else if cfg!(feature = "cpp_native") {
55+
"native"
56+
} else {
57+
"portable" // fallback
58+
}
59+
}
60+
});
61+
println!("cargo:rerun-if-env-changed=FASTPFOR_SIMD_MODE");
62+
63+
let cmake_out = cmake::Config::new("cpp")
64+
.define("FASTPFOR_WITH_TEST", "OFF")
65+
.define("FASTPFOR_SIMD_MODE", simd_mode)
66+
.build();
1767
let lib_path = cmake_out.join("lib");
1868
let lib_path = lib_path.to_str().unwrap();
1969

justfile

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
#!/usr/bin/env just --justfile
22

33
main_crate := 'fastpfor'
4-
features_flag := '--all-features'
4+
# How to call the current just executable. Note that just_executable() may have `\` in Windows paths, so we need to quote it.
5+
just := quote(just_executable())
6+
# cargo-binstall needs a workaround due to caching when used in CI
7+
binstall_args := if env('CI', '') != '' {'--no-confirm --no-track --disable-telemetry'} else {''}
58

69
# if running in CI, treat warnings as errors by setting RUSTFLAGS and RUSTDOCFLAGS to '-D warnings' unless they are already set
710
# Use `CI=true just ci-test` to run the same tests as in GitHub CI.
811
# Use `just env-info` to see the current values of RUSTFLAGS and RUSTDOCFLAGS
912
ci_mode := if env('CI', '') != '' {'1'} else {''}
10-
# cargo-binstall needs a workaround due to caching
11-
# ci_mode might be manually set by user, so re-check the env var
12-
binstall_args := if env('CI', '') != '' {'--no-track'} else {''}
1313
export RUSTFLAGS := env('RUSTFLAGS', if ci_mode == '1' {'-D warnings'} else {''})
1414
export RUSTDOCFLAGS := env('RUSTDOCFLAGS', if ci_mode == '1' {'-D warnings'} else {''})
15-
export RUST_BACKTRACE := env('RUST_BACKTRACE', if ci_mode == '1' {'1'} else {''})
15+
export RUST_BACKTRACE := env('RUST_BACKTRACE', if ci_mode == '1' {'1'} else {'0'})
1616

1717
@_default:
18-
{{just_executable()}} --list
18+
{{just}} --list
1919

2020
# Run integration tests and save its output as the new expected output
2121
bless *args: (cargo-install 'cargo-insta')
22-
cargo insta test --accept --unreferenced=delete {{features_flag}} {{args}}
22+
cargo insta test --accept --unreferenced=delete --features _all_compatible {{args}}
2323

2424
# Build the project
2525
build:
26-
cargo build --workspace --all-targets {{features_flag}}
26+
cargo build --workspace --all-targets --features _all_compatible
2727

2828
# Quick compile without building a binary
2929
check:
30-
cargo check --workspace --all-targets {{features_flag}}
30+
cargo check --workspace --all-targets --features _all_compatible
3131

3232
# Generate code coverage report to upload to codecov.io
3333
ci-coverage: env-info && \
@@ -48,21 +48,21 @@ clean:
4848

4949
# Run cargo clippy to lint the code
5050
clippy *args:
51-
cargo clippy --workspace --all-targets {{features_flag}} {{args}}
51+
cargo clippy --workspace --all-targets --features _all_compatible {{args}}
5252

5353
# Generate code coverage report. Will install `cargo llvm-cov` if missing.
5454
coverage *args='--no-clean --open': (cargo-install 'cargo-llvm-cov')
55-
cargo llvm-cov --workspace --all-targets {{features_flag}} --include-build-script {{args}}
55+
cargo llvm-cov --workspace --all-targets --features _all_compatible --include-build-script {{args}}
5656

5757
# Build and open code documentation
5858
docs *args='--open':
59-
DOCS_RS=1 cargo doc --no-deps {{args}} --workspace {{features_flag}}
59+
DOCS_RS=1 cargo doc --no-deps {{args}} --workspace --features _all_compatible
6060

6161
# Print environment info
6262
env-info:
63-
@echo "Running {{if ci_mode == '1' {'in CI mode'} else {'in dev mode'} }} on {{os()}} / {{arch()}}"
64-
@echo "PWD $(pwd)"
65-
{{just_executable()}} --version
63+
@echo "Running for '{{main_crate}}' crate {{if ci_mode == '1' {'in CI mode'} else {'in dev mode'} }} on {{os()}} / {{arch()}}"
64+
@echo "PWD {{justfile_directory()}}"
65+
{{just}} --version
6666
rustc --version
6767
cargo --version
6868
rustup --version
@@ -92,29 +92,42 @@ fmt:
9292
fmt-toml *args: (cargo-install 'cargo-sort')
9393
cargo sort --workspace --grouped {{args}}
9494

95-
# Get any package's field from the metadata
95+
# Get a package field from the metadata
9696
get-crate-field field package=main_crate: (assert-cmd 'jq')
97-
cargo metadata --format-version 1 | jq -e -r '.packages | map(select(.name == "{{package}}")) | first | .{{field}} | select(. != null)'
97+
cargo metadata --format-version 1 | jq -e -r '.packages | map(select(.name == "{{package}}")) | first | .{{field}} // error("Field \"{{field}}\" is missing in Cargo.toml for package {{package}}")'
9898

9999
# Get the minimum supported Rust version (MSRV) for the crate
100100
get-msrv package=main_crate: (get-crate-field 'rust_version' package)
101101

102102
# Find the minimum supported Rust version (MSRV) using cargo-msrv extension, and update Cargo.toml
103103
msrv: (cargo-install 'cargo-msrv')
104-
cargo msrv find --write-msrv --component rustfmt {{features_flag}} --ignore-lockfile -- {{just_executable()}} ci-test-msrv
104+
cargo msrv find --write-msrv --features _all_compatible --ignore-lockfile -- {{just}} ci-test-msrv
105105

106106
# Run cargo-release
107107
release *args='': (cargo-install 'release-plz')
108108
release-plz {{args}}
109109

110110
# Check semver compatibility with prior published version. Install it with `cargo install cargo-semver-checks`
111111
semver *args: (cargo-install 'cargo-semver-checks')
112-
cargo semver-checks {{features_flag}} {{args}}
112+
cargo semver-checks --features _all_compatible {{args}}
113113

114114
# Run all unit and integration tests
115115
test:
116-
cargo test --workspace --all-targets {{features_flag}}
117-
cargo test --workspace --doc {{features_flag}}
116+
cargo test --workspace --all-targets --features _all_compatible
117+
cargo test --workspace --doc --features _all_compatible
118+
119+
# Test with a specific SIMD mode (portable, native, or runtime)
120+
test-simd mode='portable':
121+
cargo test --workspace --all-targets --features cpp_{{mode}}
122+
123+
# Test all SIMD modes
124+
test-all-simd-modes:
125+
cargo clean -p fastpfor
126+
{{just}} test-simd portable
127+
cargo clean -p fastpfor
128+
{{just}} test-simd native
129+
cargo clean -p fastpfor
130+
{{just}} test-simd runtime
118131

119132
# Test documentation generation
120133
test-doc: (docs '')
@@ -127,9 +140,9 @@ test-fmt: && (fmt-toml '--check' '--check-format')
127140
test-publish:
128141
cargo +nightly -Z package-workspace publish --dry-run
129142

130-
# Find unused dependencies. Install it with `cargo install cargo-udeps`
143+
# Find unused dependencies. Uses `cargo-udeps`
131144
udeps: (cargo-install 'cargo-udeps')
132-
cargo +nightly udeps --workspace --all-targets {{features_flag}}
145+
cargo +nightly udeps --workspace --all-targets --features _all_compatible
133146

134147
# Update all dependencies, including breaking changes. Requires nightly toolchain (install with `rustup install nightly`)
135148
update:
@@ -161,11 +174,14 @@ cargo-install $COMMAND $INSTALL_CMD='' *args='':
161174
#!/usr/bin/env bash
162175
set -euo pipefail
163176
if ! command -v $COMMAND > /dev/null; then
177+
echo "$COMMAND could not be found. Installing..."
164178
if ! command -v cargo-binstall > /dev/null; then
165-
echo "$COMMAND could not be found. Installing it with cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}}"
179+
set -x
166180
cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}}
181+
{ set +x; } 2>/dev/null
167182
else
168-
echo "$COMMAND could not be found. Installing it with cargo binstall ${INSTALL_CMD:-$COMMAND} {{binstall_args}} --locked {{args}}"
183+
set -x
169184
cargo binstall ${INSTALL_CMD:-$COMMAND} {{binstall_args}} --locked {{args}}
185+
{ set +x; } 2>/dev/null
170186
fi
171187
fi

0 commit comments

Comments
 (0)