fix(build): drop compile-time x86 target-features — runtime AES dispatch
Users on CPUs without AES-NI (Nehalem i7-860, pre-2010 Intel Macs) or without SSSE3 (AMD Phenom II) crashed at launch with SIGILL. The global `-C target-feature=+aes,+ssse3` was kept after the #353/#343 x86-64-v3 incident on the belief that the aes crate's cpufeatures runtime check would protect CPUs without AES-NI — but a compile-time target feature makes cpufeatures compile its check to a constant, so the AES-NI intrinsics ran unconditionally (credentials AES-GCM decrypt at startup), and +ssse3 let LLVM autovectorize anywhere, breaking Phenom II. Measured on the qbz-cmaf hot path (Ctr128BE<Aes128> apply_keystream, 256 MB, release): 2145 MB/s compiled-in vs 2162 MB/s runtime-dispatched, identical output — the flags bought nothing on capable CPUs. Removed from .cargo/config.toml, all dev/build scripts and the release/build workflows; old CPUs now get the software AES fallback instead of a crash. Fixes #549 cc
This commit is contained in:
9 files changed
+104
-68
No files matched your search
+22
-39
@@ -1,42 +1,25 @@
|
|||||||
# Enable CPU intrinsics for AES-NI + SSSE3 on x86_64 targets only.
|
# NO global x86_64 target-feature flags — baseline x86-64 on purpose.
|
||||||
#
|
#
|
||||||
# The qbz-cmaf crate does AES-128-CTR decrypt on every offline-cached
|
# History (#353 -> #343 -> #549): the binary once shipped with
|
||||||
# audio frame. Without AES-NI the decrypt falls back to a software
|
# `target-cpu=x86-64-v3`, which SIGILLed every pre-Haswell CPU at launch.
|
||||||
# implementation that hits ~10 MB/s on a modern CPU; with AES-NI it
|
# That was reduced to `target-feature=+aes,+ssse3` on the belief that the
|
||||||
# clears 400+ MB/s. For HiRes (200 MB+ tracks) this is the difference
|
# `aes` crate's cpufeatures runtime check would keep CPUs without AES-NI on
|
||||||
# between a 20-second gap and a sub-second one.
|
# the software path. That belief was WRONG: enabling a target feature at
|
||||||
|
# COMPILE time makes cpufeatures compile its check to a constant `true`, so
|
||||||
|
# the AES-NI intrinsics run unconditionally — every CPU without AES-NI
|
||||||
|
# (Nehalem/Lynnfield i7-860, pre-2010 Intel Macs, AMD pre-Bulldozer) died
|
||||||
|
# with SIGILL the moment AES ran (at LAUNCH: qbz-credentials decrypts with
|
||||||
|
# AES-GCM during startup). `+ssse3` additionally let LLVM autovectorize
|
||||||
|
# SSSE3 anywhere in the binary, killing AMD Phenom II (no SSSE3 until
|
||||||
|
# Bulldozer, 2011). That is issue #549.
|
||||||
#
|
#
|
||||||
# What we need is ONLY the AES-NI instruction set compiled in. The
|
# The flags buy NOTHING on capable CPUs: with no compile-time forcing, the
|
||||||
# `aes` crate uses `cpufeatures::new!` to do a runtime CPUID check
|
# `aes` crate does per-function `#[target_feature]` dispatch after a runtime
|
||||||
# before dispatching to the hardware path, so as long as the AES-NI
|
# CPUID check. Measured on the qbz-cmaf hot path (ctr::Ctr128BE<Aes128>
|
||||||
# code is present in the binary, CPUs without AES-NI fall back to the
|
# apply_keystream, 256 MB, release): 2145 MB/s compiled-in vs 2162 MB/s
|
||||||
# software implementation gracefully — no crash, just slower decrypt.
|
# runtime-dispatched — identical. Old CPUs get the software fallback
|
||||||
|
# (slower offline-cache decrypt) instead of a crash.
|
||||||
#
|
#
|
||||||
# `target-feature=+aes,+ssse3` is what enables that compile-in.
|
# If a future hot loop ever warrants AVX2/BMI2/etc., use per-function
|
||||||
# SSSE3 has been in every x86_64 CPU since 2006 (Core 2).
|
# `#[target_feature]` + `is_x86_feature_detected!` dispatch — never a
|
||||||
#
|
# global feature/microarch bump.
|
||||||
# We deliberately do NOT set `target-cpu=x86-64-v3` (Haswell baseline,
|
|
||||||
# 2013). v3 includes AVX2/BMI2/FMA/LZCNT/MOVBE and LLVM will freely
|
|
||||||
# emit those instructions anywhere in the binary — including the
|
|
||||||
# startup path, long before CMAF runs. On pre-Haswell CPUs (Sandy
|
|
||||||
# Bridge 2011, Ivy Bridge 2012, AMD Bulldozer/Piledriver) the process
|
|
||||||
# dies with SIGILL at launch. See issue #353 (Linux, Sandy Bridge) and
|
|
||||||
# discussion #343 (macOS, 2012 MacBookPro9,1 / Ivy Bridge). Raising
|
|
||||||
# the baseline gave no CMAF speedup — the perf win came entirely from
|
|
||||||
# `+aes` — so we keep the AES-NI path and drop the incompatible
|
|
||||||
# baseline.
|
|
||||||
#
|
|
||||||
# If a future hot loop ever warrants AVX2/BMI2, the correct pattern is
|
|
||||||
# per-function `#[target_feature]` with runtime dispatch (as the `aes`
|
|
||||||
# crate already does via `cpufeatures`), NOT a global microarch bump
|
|
||||||
# that silently breaks older hardware.
|
|
||||||
#
|
|
||||||
# IMPORTANT: these flags MUST be scoped to x86_64 targets. Using
|
|
||||||
# `[build] rustflags = ...` applies them to every target, which on
|
|
||||||
# aarch64-apple-darwin (and any other ARM target) corrupts the CPU
|
|
||||||
# feature inference inside the `ring` crate and fails the build with
|
|
||||||
# `assertion failed: CAPS_STATIC == MIN_STATIC_FEATURES`. ARM targets
|
|
||||||
# get their own AES/crypto extensions automatically via `aes` crate's
|
|
||||||
# armv8 path — we don't need to force anything there.
|
|
||||||
[target.'cfg(target_arch = "x86_64")']
|
|
||||||
rustflags = ["-C", "target-feature=+aes,+ssse3"]
|
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
CARGO_BUILD_JOBS: '1'
|
CARGO_BUILD_JOBS: '1'
|
||||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256'
|
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256'
|
||||||
CARGO_PROFILE_RELEASE_OPT_LEVEL: '2'
|
CARGO_PROFILE_RELEASE_OPT_LEVEL: '2'
|
||||||
RUSTFLAGS: '-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=1'
|
RUSTFLAGS: '-C link-arg=-fuse-ld=mold -Z threads=1'
|
||||||
run: |
|
run: |
|
||||||
cargo build --release --manifest-path crates/Cargo.toml -p qbz
|
cargo build --release --manifest-path crates/Cargo.toml -p qbz
|
||||||
echo "::group::result"; ls -lh crates/target/release/qbz; df -h /; free -h; echo "::endgroup::"
|
echo "::group::result"; ls -lh crates/target/release/qbz; df -h /; free -h; echo "::endgroup::"
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
# Keep symbols in this build: the unstripped binary is archived for
|
# Keep symbols in this build: the unstripped binary is archived for
|
||||||
# crash symbolication, then stripped for every shipped artifact.
|
# crash symbolication, then stripped for every shipped artifact.
|
||||||
CARGO_PROFILE_RELEASE_STRIP: "none"
|
CARGO_PROFILE_RELEASE_STRIP: "none"
|
||||||
RUSTFLAGS: "-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=1"
|
RUSTFLAGS: "-C link-arg=-fuse-ld=mold -Z threads=1"
|
||||||
run: |
|
run: |
|
||||||
cargo build --release --manifest-path crates/Cargo.toml -p qbz
|
cargo build --release --manifest-path crates/Cargo.toml -p qbz
|
||||||
cp crates/target/release/qbz qbz-unstripped
|
cp crates/target/release/qbz qbz-unstripped
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
CARGO_BUILD_JOBS: '1'
|
CARGO_BUILD_JOBS: '1'
|
||||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256'
|
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256'
|
||||||
CARGO_PROFILE_RELEASE_OPT_LEVEL: '2'
|
CARGO_PROFILE_RELEASE_OPT_LEVEL: '2'
|
||||||
RUSTFLAGS: '-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=1'
|
RUSTFLAGS: '-C link-arg=-fuse-ld=mold -Z threads=1'
|
||||||
run: |
|
run: |
|
||||||
cargo build --release --manifest-path crates/Cargo.toml -p qbz
|
cargo build --release --manifest-path crates/Cargo.toml -p qbz
|
||||||
strip crates/target/release/qbz || true
|
strip crates/target/release/qbz || true
|
||||||
|
|||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# QBZ — prune stale incremental-compilation caches under crates/target.
|
||||||
|
#
|
||||||
|
# WHY: `cargo check` writes an incremental cache dir per crate, keyed by a hash
|
||||||
|
# of the build config (RUSTFLAGS / codegen-units / opt-level). Because
|
||||||
|
# slint-run.sh flips those knobs per RAM tier, EVERY distinct config spawns a
|
||||||
|
# NEW `<crate>-<hash>/` dir and Cargo NEVER deletes the old ones. Over months
|
||||||
|
# they pile up (1000+ dirs, hundreds of GB of pure garbage).
|
||||||
|
#
|
||||||
|
# This keeps only the MOST-RECENTLY-MODIFIED incremental dir per crate (that's
|
||||||
|
# the one that actually speeds up your next check) and deletes the rest.
|
||||||
|
#
|
||||||
|
# NOTE: release builds (slint-run.sh / slint-dev.sh) do NOT use incremental at
|
||||||
|
# all, so this never slows them down. It only affects `cargo check` speed, and
|
||||||
|
# only prunes stale snapshots you're not using.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/prune-incremental.sh # prune, keep newest per crate
|
||||||
|
# DRY=1 ./scripts/prune-incremental.sh # show what WOULD be deleted
|
||||||
|
# KEEP=2 ./scripts/prune-incremental.sh # keep the 2 newest per crate
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
|
||||||
|
|
||||||
|
TARGET="crates/target"
|
||||||
|
KEEP="${KEEP:-1}"
|
||||||
|
DRY="${DRY:-0}"
|
||||||
|
|
||||||
|
freed=0
|
||||||
|
for incdir in "$TARGET"/*/incremental; do
|
||||||
|
[[ -d "$incdir" ]] || continue
|
||||||
|
|
||||||
|
declare -A seen=() # reset per profile (debug/, release/, etc.)
|
||||||
|
|
||||||
|
# Feed "<mtime>\t<crate>\t<path>" sorted by crate asc, then mtime desc, so the
|
||||||
|
# newest dir of each crate arrives first.
|
||||||
|
while IFS=$'\t' read -r _mtime crate path; do
|
||||||
|
[[ -n "$path" ]] || continue
|
||||||
|
seen["$crate"]=$(( ${seen["$crate"]:-0} + 1 ))
|
||||||
|
(( seen["$crate"] <= KEEP )) && continue # keep the newest KEEP per crate
|
||||||
|
sz=$(du -sm "$path" 2>/dev/null | cut -f1)
|
||||||
|
freed=$(( freed + ${sz:-0} ))
|
||||||
|
if [[ "$DRY" == 1 ]]; then
|
||||||
|
echo "would delete: $path (${sz}M)"
|
||||||
|
else
|
||||||
|
rm -rf "$path"
|
||||||
|
echo "deleted: $path (${sz}M)"
|
||||||
|
fi
|
||||||
|
done < <(
|
||||||
|
for d in "$incdir"/*/; do
|
||||||
|
[[ -d "$d" ]] || continue
|
||||||
|
name=$(basename "$d")
|
||||||
|
printf '%s\t%s\t%s\n' "$(stat -c %Y "$d")" "${name%-*}" "${d%/}"
|
||||||
|
done | sort -t$'\t' -k2,2 -k1,1rn
|
||||||
|
)
|
||||||
|
|
||||||
|
unset seen
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$DRY" == 1 ]]; then
|
||||||
|
echo "--- DRY run: would free ~${freed} MB (KEEP=$KEEP) ---"
|
||||||
|
else
|
||||||
|
echo "--- freed ~${freed} MB (KEEP=$KEEP) ---"
|
||||||
|
fi
|
||||||
@@ -15,12 +15,9 @@
|
|||||||
# DO NOT use this to judge performance — the binary is unoptimised. Always do a
|
# DO NOT use this to judge performance — the binary is unoptimised. Always do a
|
||||||
# final pass with ./slint-dev.sh (release) before trusting runtime behaviour.
|
# final pass with ./slint-dev.sh (release) before trusting runtime behaviour.
|
||||||
#
|
#
|
||||||
# RUSTFLAGS overrides .cargo/config.toml, so the AES-NI/SSSE3 features are
|
|
||||||
# re-listed here (keep in sync with slint-dev.sh / .cargo/config.toml).
|
|
||||||
#
|
|
||||||
# Usage: ./scripts/slint-dev-fast.sh [extra cargo args]
|
# Usage: ./scripts/slint-dev-fast.sh [extra cargo args]
|
||||||
# THREADS=22 ./scripts/slint-dev-fast.sh # override frontend threads
|
# THREADS=22 ./scripts/slint-dev-fast.sh # override frontend threads
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
|
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
|
||||||
export RUSTFLAGS="-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -C debuginfo=0 -Z threads=${THREADS:-16}"
|
export RUSTFLAGS="-C link-arg=-fuse-ld=mold -C debuginfo=0 -Z threads=${THREADS:-16}"
|
||||||
exec cargo +nightly run --manifest-path crates/Cargo.toml -p qbz "$@"
|
exec cargo +nightly run --manifest-path crates/Cargo.toml -p qbz "$@"
|
||||||
@@ -11,11 +11,10 @@
|
|||||||
# link (libc's build script) fails. Apple's default linker (ld-prime) is
|
# link (libc's build script) fails. Apple's default linker (ld-prime) is
|
||||||
# already fast, so we simply don't override the linker.
|
# already fast, so we simply don't override the linker.
|
||||||
#
|
#
|
||||||
# 2. AES-NI/SSSE3 features are applied ONLY on x86_64. On Intel Macs they keep
|
# 2. NO x86 target-features anywhere (#549): the aes crate runtime-dispatches
|
||||||
# the offline CMAF decrypt fast (matches .cargo/config.toml); on aarch64
|
# to AES-NI at identical speed, and compile-time features SIGILL CPUs
|
||||||
# they are meaningless (rustc warns "not a recognized feature" and ignores
|
# without them (pre-2010 Intel Macs). aarch64 gets its AES from the armv8
|
||||||
# them — AES there comes from the armv8 crypto extensions automatically),
|
# crypto extensions automatically.
|
||||||
# so we omit them to keep the build output clean.
|
|
||||||
#
|
#
|
||||||
# 3. The nightly parallel frontend (`-Z threads`) is kept — it works on macOS
|
# 3. The nightly parallel frontend (`-Z threads`) is kept — it works on macOS
|
||||||
# and only cuts COMPILE time; the produced binary is identical to a plain
|
# and only cuts COMPILE time; the produced binary is identical to a plain
|
||||||
@@ -41,19 +40,13 @@ if [[ "${1:-}" == "--fast" ]]; then
|
|||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Re-list AES-NI/SSSE3 ONLY on x86_64. Setting RUSTFLAGS *overrides* (does not
|
|
||||||
# merge) .cargo/config.toml's [target.'cfg(target_arch = "x86_64")'] rustflags,
|
|
||||||
# so on an Intel Mac we must restate them here to keep the AES-NI decrypt path.
|
|
||||||
AES_FLAGS=""
|
|
||||||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
|
||||||
AES_FLAGS="-C target-feature=+aes,+ssse3"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parallel rustc frontend across cores. Defaults to the machine's logical CPU
|
# Parallel rustc frontend across cores. Defaults to the machine's logical CPU
|
||||||
# count; override with THREADS=N. This only speeds compilation.
|
# count; override with THREADS=N. This only speeds compilation.
|
||||||
THREADS="${THREADS:-$(sysctl -n hw.ncpu)}"
|
THREADS="${THREADS:-$(sysctl -n hw.ncpu)}"
|
||||||
|
|
||||||
export RUSTFLAGS="${AES_FLAGS} -Z threads=${THREADS}"
|
# No x86 target-features (#549): the aes crate runtime-dispatches to AES-NI
|
||||||
|
# at identical speed, and compile-time features SIGILL pre-2010 Intel Macs.
|
||||||
|
export RUSTFLAGS="-Z threads=${THREADS}"
|
||||||
|
|
||||||
if [[ "$FAST" == true ]]; then
|
if [[ "$FAST" == true ]]; then
|
||||||
# DEBUG: opt-level 0 skips the heavy LLVM passes; -C debuginfo=0 means less to
|
# DEBUG: opt-level 0 skips the heavy LLVM passes; -C debuginfo=0 means less to
|
||||||
|
|||||||
@@ -10,10 +10,9 @@
|
|||||||
# only compile time improves. We deliberately do NOT use cranelift — it
|
# only compile time improves. We deliberately do NOT use cranelift — it
|
||||||
# would lower runtime performance.
|
# would lower runtime performance.
|
||||||
#
|
#
|
||||||
# RUSTFLAGS must re-list the AES-NI/SSSE3 features from .cargo/config.toml,
|
# No x86 target-features here or in .cargo/config.toml (#549): the aes crate
|
||||||
# because setting RUSTFLAGS *overrides* (does not merge) the config's
|
# runtime-dispatches to AES-NI at identical speed, and compile-time features
|
||||||
# [target.'cfg(target_arch = "x86_64")'] rustflags. Keep these in sync with
|
# SIGILL older CPUs.
|
||||||
# .cargo/config.toml (the AES-NI path keeps offline CMAF decrypt fast).
|
|
||||||
#
|
#
|
||||||
# Usage: ./scripts/slint-dev.sh [extra cargo args]
|
# Usage: ./scripts/slint-dev.sh [extra cargo args]
|
||||||
# THREADS=16 ./scripts/slint-dev.sh # override frontend threads
|
# THREADS=16 ./scripts/slint-dev.sh # override frontend threads
|
||||||
@@ -21,5 +20,5 @@ set -euo pipefail
|
|||||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
|
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
|
||||||
# `mold` (faster linker) + more frontend threads only cut COMPILE time — they
|
# `mold` (faster linker) + more frontend threads only cut COMPILE time — they
|
||||||
# don't change the optimised binary, so release perf measurements stay valid.
|
# don't change the optimised binary, so release perf measurements stay valid.
|
||||||
export RUSTFLAGS="-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=${THREADS:-16}"
|
export RUSTFLAGS="-C link-arg=-fuse-ld=mold -Z threads=${THREADS:-16}"
|
||||||
exec cargo +nightly run --release --manifest-path crates/Cargo.toml -p qbz "$@"
|
exec cargo +nightly run --release --manifest-path crates/Cargo.toml -p qbz "$@"
|
||||||
@@ -51,9 +51,10 @@ else
|
|||||||
echo "[slint-run] WARNING: only ${avail_mb} MB free — lowest-memory tier (slow). Close apps / drop to a TTY for a faster build." >&2
|
echo "[slint-run] WARNING: only ${avail_mb} MB free — lowest-memory tier (slow). Close apps / drop to a TTY for a faster build." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# RUSTFLAGS OVERRIDES (does not merge) the .cargo/config.toml rustflags, so the
|
# No x86 target-features here or in .cargo/config.toml (#549): the aes crate
|
||||||
# AES-NI/SSSE3 features must be re-listed (keep in sync with slint-dev.sh).
|
# runtime-dispatches to AES-NI at identical speed, and compile-time features
|
||||||
export RUSTFLAGS="-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=${THREADS}"
|
# SIGILL older CPUs.
|
||||||
|
export RUSTFLAGS="-C link-arg=-fuse-ld=mold -Z threads=${THREADS}"
|
||||||
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="${CODEGEN_UNITS}"
|
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="${CODEGEN_UNITS}"
|
||||||
export CARGO_PROFILE_RELEASE_OPT_LEVEL="${OPT}"
|
export CARGO_PROFILE_RELEASE_OPT_LEVEL="${OPT}"
|
||||||
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" # one rustc at a time (memory)
|
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" # one rustc at a time (memory)
|
||||||
|
|||||||
Reference in new issue
Block a user