Performance

March 29, 2026 · View on GitHub

Archmage generates identical assembly to hand-written #[target_feature] + unsafe code. The safety abstractions compile away. At runtime, you get raw SIMD instructions.

The only thing that costs you performance is calling #[arcane] from the wrong place.

Zero overhead: archmage = bare #[target_feature]

Every benchmark pattern shows archmage and bare #[target_feature] producing the same timings. There is no wrapper cost, no token cost, no abstraction tax.

What you writeTime (1000 x 8-float add)What LLVM sees
#[arcane] calling #[arcane] (matching features)547 nsFeatures match — LLVM inlines wrapper
Bare #[target_feature] (no archmage)544 nsSame
#[arcane] per loop iteration from non-SIMD code2209 ns (4x)Boundary crossing per call
Bare #[target_feature] per loop iteration2222 ns (4x)Same boundary, same cost

The 4x penalty comes from LLVM, not archmage. Read on.

The target-feature boundary

#[arcane] reads the token type from your function signature to decide which #[target_feature] to emit. A function taking X64V3Token gets #[target_feature(enable = "avx2,fma,...")].

#[arcane] generates a wrapper: an outer function that calls an inner #[target_feature] function via unsafe. This is how you cross into SIMD code without writing unsafe yourself. When an #[arcane] function calls another #[arcane] function with matching features, LLVM inlines the wrapper away — no boundary. The boundary only exists when the caller has fewer features than the callee (e.g., non-SIMD code calling an #[arcane] function).

LLVM won't inline across mismatched #[target_feature] attributes: no load hoisting, no store sinking, no cross-iteration vectorization. But matching features = no mismatch = full inlining.

The boundary has nothing to do with archmage. A bare #[target_feature] function has the same cost. #[arcane] just makes the wrapper safe; the boundary is LLVM's.

The fix: enter #[arcane] once from non-SIMD code, put the loop inside. From within #[arcane], call other #[arcane] functions freely — matching features means LLVM inlines the wrapper away. #[rite] is also available (adds #[target_feature] + #[inline] directly, no wrapper) but isn't necessary when features match.

// WRONG: boundary every iteration (4x slower)
fn process_all(points: &[[f32; 8]]) {
    let token = X64V3Token::summon().unwrap();
    for p in points {
        process_one(token, p);  // #[arcane] — boundary crossing
    }
}

// RIGHT: one boundary, loop inside
fn process_all(points: &[[f32; 8]]) {
    if let Some(token) = X64V3Token::summon() {
        process_all_simd(token, points);  // one #[arcane] entry
    }
}

#[arcane(import_intrinsics)]
fn process_all_simd(token: X64V3Token, points: &[[f32; 8]]) {
    for p in points {
        process_one(token, p);  // #[arcane] — features match, LLVM inlines
    }
}

#[arcane(import_intrinsics)]
fn process_one(_token: X64V3Token, p: &[f32; 8]) {
    // ...
}

Benchmark results

All benchmarks from cargo bench --bench asm_inspection --features "std avx512", run on x86-64 with AVX-512 support. Source: benches/asm_inspection.rs.

Simple vector add (1000 iterations, 8-float add)

Seven patterns isolating the target-feature boundary effect:

#PatternTimeRatioBoundary?
1#[arcane] per iteration2209 ns4.1xyes — baseline caller, AVX2 callee
2#[rite] in #[arcane]547 ns1.0xno — features match, LLVM inlines
3Manual inline in #[arcane]544 ns1.0xno — same function body
4#[rite] called directly (unsafe, no wrapper)2227 ns4.1xyes — proves it's not the wrapper
5Scalar via wrapper fn542 ns1.0xno — no #[target_feature] at all
6Scalar inline537 ns1.0xno — baseline
7Bare #[target_feature] (no archmage)2222 ns4.1xyes — same boundary, archmage not involved

Patterns 1, 4, and 7 all cross the boundary per iteration and land at the same ~2.2 us. Pattern 4 has no wrapper at all (calls #[rite] directly with unsafe), proving the overhead is the boundary, not the wrapper. Patterns 2, 3, 5, and 6 avoid the boundary and land at ~544 ns.

DCT-8 (100 rows, 8 dot products per row)

A realistic signal-processing workload. Each row computes 8 coefficient dot products using _mm256_mul_ps + horizontal sum. Higher computational density amplifies the boundary effect.

PatternTimeRatio
#[rite] in #[arcane]61 ns1.0x
#[arcane] per row376 ns6.2x
Bare #[target_feature] per row374 ns6.1x

Archmage and bare #[target_feature] produce identical numbers here too (376 vs 374 ns — noise). The boundary costs 6.2x instead of 4x because DCT-8 has more optimization potential per call: FMA fusion, register reuse across coefficient loads, instruction scheduling. When LLVM can inline, it exploits all of that. When the boundary forces a separate call, it can't. The multiplier depends on how much work LLVM loses at the boundary, not on which mechanism creates it.

Cross-token nesting (1000 iterations, 8-float add)

What happens when #[arcane] functions call other #[arcane] functions at different feature levels. When both functions take the same token type, their #[target_feature] strings match and LLVM inlines freely — no boundary. When the token types differ, direction matters.

PatternTimeRatioWhy
V3 #[arcane] calling V3 #[arcane]547 ns1.0xCaller has V3 features; callee needs V3. LLVM inlines.
V3 #[arcane] calling V3 #[rite]544 ns1.0xControl — #[rite] always inlines.
V4 entry calling V3 #[arcane] (downgrade)547 ns1.0xCaller has V4 superset; V3 callee inlines freely.
V3 down to V2 (AVX2 calling SSE)544 ns1.0xCaller has V3 superset; V2 callee inlines.
V2 up to V3 (SSE calling AVX2)2209 ns4.1xCaller lacks AVX2; boundary per call.
V3 up to V4 (AVX2 calling AVX-512)2222 ns4.1xCaller lacks AVX-512; boundary per call.

Every downgrade pattern matched its bare #[target_feature] equivalent exactly. Every upgrade pattern hit the same ~4x boundary as calling from baseline code.

The rule: downgrades are free (caller's superset features enable inlining), upgrades hit the boundary (callee needs features the caller doesn't have).

Generic magetypes types: zero overhead inside #[arcane] (with #[inline(always)])

A common concern: does using f32x8::<T> with a generic T: F32x8Backend produce worse code than concrete f32x8::<x64v3>? No — but only if the generic function can inline into the #[arcane] caller.

The backend trait methods are all #[inline(always)], but that's not enough on its own. Your generic helper function must also inline into the #[arcane] caller so LLVM compiles it within the #[target_feature] region. The generic function itself has no #[target_feature] — it gets the right features only by being inlined into a function that does.

Source: benches/generic_vs_concrete.rs.

PatternTimeAssembly
f32x8::<T> generic #[inline(always)] inside #[arcane]1.35 nsvmovups + vaddps + horizontal sum
f32x8::<T> generic (no annotation) inside #[arcane]1.37 nsidentical — LLVM chose to inline (not guaranteed)
f32x8::<x64v3> concrete inside #[arcane]1.16 nsidentical instructions
Concrete via #[rite] in #[arcane]1.40 nsidentical
f32x8::<T> generic #[inline(never)] inside #[arcane]23.7 ns (18x)call _mm256_add_ps — forced no-inline proves it
f32x8::<T> generic without #[target_feature]24.7 ns (18x)call _mm256_add_ps (function calls!)

The #[inline(never)] row is the smoking gun: even inside #[arcane], a generic function that can't inline is just as slow as having no #[target_feature] at all. The generic function body is compiled without target features — it only gets them by being inlined into the #[arcane] caller's #[target_feature] region.

Mark generic SIMD helpers #[inline(always)]. For small same-crate functions, LLVM usually inlines without annotation (the "no annotation" row above). But this is an LLVM heuristic, not a guarantee — LLVM can decline to inline any function without #[inline(always)]. Cross-crate, without at least #[inline], the function body isn't even available to the caller's compilation unit. #[inline(always)] removes all ambiguity: the function will always inline, and the generic code will always get the caller's target features.

// CORRECT: #[inline(always)] guarantees the generic body inlines into the caller
#[inline(always)]
fn generic_sum<T: F32x8Backend>(token: T, data: &[f32; 8]) -> f32 {
    let v = f32x8::<T>::from_array(token, *data);
    (v + v).reduce_add()  // ~1.35 ns from #[arcane(import_intrinsics)]
}

// The #[arcane] caller provides #[target_feature] — generic_sum inlines into it
#[arcane(import_intrinsics)]
fn entry(token: X64V3Token, data: &[f32; 8]) -> f32 {
    generic_sum(token, data)  // Inlines → full AVX2 codegen
}
; #[inline(always)] generic inside #[arcane] — identical to concrete:
vmovups ymm0, [rdi]          ; load 8 floats
vaddps  ymm0, ymm0, ymm0    ; v + v
vextractf128 xmm1, ymm0, 1  ; horizontal sum...
vaddps  xmm0, xmm1, xmm0
vhaddps xmm0, xmm0, xmm0
vmovshdup xmm1, xmm0
vaddss  xmm0, xmm0, xmm1

; #[inline(never)] generic inside #[arcane] — still catastrophic:
movups  xmm0, [rdi]          ; SSE2 load, not AVX!
call    _mm256_add_ps         ; FUNCTION CALL
call    _mm256_extractf128_ps ; FUNCTION CALL
call    _mm_hadd_ps           ; FUNCTION CALL

The rule: generic magetypes code is zero-cost when it inlines into an #[arcane] or #[rite] caller. Mark generic SIMD helpers #[inline(always)] to guarantee this. The generic function has no #[target_feature] of its own — it inherits the caller's features through inlining.

Rules

These are distilled from the benchmark data above.

  1. Enter #[arcane] once. Put loops inside it. Each call from non-SIMD code crosses the boundary.

  2. Use #[arcane] for helpers too. When an #[arcane] function calls another with matching features, LLVM inlines the wrapper — no boundary. #[rite] (adds #[target_feature] + #[inline] directly) and plain #[inline(always)] functions also work.

  3. Don't cross feature boundaries in hot loops. Calling #[arcane] from #[arcane] with matching features is free — LLVM inlines the wrapper (benchmark: V3→V3 = 1.0x). The boundary only hurts when the caller has fewer features than the callee (V3→V4 = 4x).

  4. Downcasting is free. A V4 function calling a V3 helper inlines because V4 is a superset of V3. Same for V3 calling V2.

  5. Upcasting hits the boundary. A V3 function calling a V4 helper can't inline because the caller lacks AVX-512 features. Dispatch at the entry point, not deep in hot code.

  6. Generic magetypes types are zero-cost inside #[arcane] — if they inline. f32x8::<T> and f32x8::<x64v3> produce identical assembly when the generic function inlines into the #[arcane] caller. Mark generic SIMD helpers #[inline(always)] to ensure this. The generic function has no #[target_feature] of its own — if it doesn't inline, intrinsics become function calls (18x slower). The backend methods are #[inline(always)], but that only helps once the generic body is inside the #[target_feature] region.

Reproducing

# Simple vector add + DCT-8
cargo bench --bench asm_inspection --features "std"

# Cross-token nesting (needs avx512 feature + AVX-512 hardware)
cargo bench --bench asm_inspection --features "std avx512"

Results will vary by CPU. The ratios between patterns are stable: archmage always matches bare #[target_feature] on the same workload. The boundary multiplier itself (4x on simple adds, 6.2x on DCT-8) depends on how much optimization LLVM loses when it can't inline — denser workloads lose more.

summon() overhead is separate: ~1.3 ns cached, 0 ns with -Ctarget-cpu=haswell (compiles away). See benches/summon_overhead.rs.