Repository navigation
Description
Background and motivation
The WebAssembly relaxed-SIMD proposal is a finished proposal integrated into the Wasm 3.0 draft (WG approval 2024-07-10). It is implemented in the major engines we target: V8, SpiderMonkey, JavaScriptCore, and Wasmtime. Relaxed SIMD adds a small set of 128-bit SIMD instructions whose observable behavior may differ across engines — trading strict determinism for direct mapping to native SIMD (FMA, pshufb/vqrdmulh/vdot, hardware min/max). These are the same instructions cross-platform intrinsics like Vector128.MultiplyAddEstimate and Vector128.ConvertToInt32Native were designed to expose, and they're exactly the instructions our own numerics/ML/codec paths need for cross-platform speedups on wasm.
We already surface System.Runtime.Intrinsics.Wasm.PackedSimd (approved in #53730, refined in #88092). This proposal adds a sibling System.Runtime.Intrinsics.Wasm.RelaxedSimd class following the same shape: IsSupported gate + static methods that map 1:1 to spec instructions. Callers opt in explicitly by naming the class, mirroring how X86 splits Sse2 vs Ssse3 and Wasm splits PackedSimd vs relaxed. Cross-platform code should still prefer Vector128.* helpers where they exist — this class is the low-level surface for PackedSimd-style consumers.
Prior art:
System.Runtime.Intrinsics.Wasm.PackedSimd— sibling class for deterministic wasm SIMD.Vector128.MultiplyAddEstimate,Vector128.ConvertToInt32Native— cross-platform helpers whose semantics deliberately match relaxed-SIMD instructions.- All eight browsers/runtimes finalized support in 2023–2024; the spec is stable.
Workarounds today:
- Callers who need FMA on wasm currently write
a * b + cand hope the JIT contracts it (Mono LLVM AOT sometimes does; interp/JIT do not). No equivalent path exists forrelaxed_swizzle,relaxed_dot,relaxed_laneselect, orrelaxed_q15mulr— those regress to scalar loops. Vector128.MultiplyAddEstimateon wasm currently lowers to separatemul+add. This proposal is a prerequisite for wiringMultiplyAddEstimatetof32x4.relaxed_maddon wasm.
Related: This proposal has no known duplicates. #127665 (Wasm RyuJIT SIMD + Intrinsics tracking issue) references relaxed-SIMD as future work.
API Proposal
namespace System.Runtime.Intrinsics.Wasm;
[CLSCompliant(false)]
public abstract class RelaxedSimd
{
public static bool IsSupported { get; }
// Swizzle — i8x16.relaxed_swizzle. Out-of-range indices produce an
// implementation-defined value (PackedSimd.Swizzle zeroes them).
public static Vector128<sbyte> Swizzle(Vector128<sbyte> vector, Vector128<sbyte> indices);
public static Vector128<byte> Swizzle(Vector128<byte> vector, Vector128<byte> indices);
// Float-to-int truncation — i32x4.relaxed_trunc_f{32x4,64x2}_{s,u}[_zero].
// NaN and out-of-range inputs may produce any of the results the wasm
// relaxed-trunc semantics permit — closest existing .NET contract is
// Vector128.ConvertToInt32Native ("platform specific behavior on overflow"),
// with the additional caveat that on wasm the specific "engine choice" is
// per-embedding rather than per-ISA. Callers wanting portable behavior
// should use PackedSimd.ConvertToInt32Saturate.
public static Vector128<int> ConvertToInt32(Vector128<float> value);
public static Vector128<uint> ConvertToUInt32(Vector128<float> value);
public static Vector128<int> ConvertToInt32(Vector128<double> value);
public static Vector128<uint> ConvertToUInt32(Vector128<double> value);
// Multiply-add estimates — f{32x4,64x2}.relaxed_{madd,nmadd}.
// Implementation may or may not fuse; matches Vector128.MultiplyAddEstimate.
public static Vector128<float> MultiplyAddEstimate (Vector128<float> a, Vector128<float> b, Vector128<float> c);
public static Vector128<double> MultiplyAddEstimate (Vector128<double> a, Vector128<double> b, Vector128<double> c);
public static Vector128<float> MultiplyAddNegatedEstimate (Vector128<float> a, Vector128<float> b, Vector128<float> c);
public static Vector128<double> MultiplyAddNegatedEstimate (Vector128<double> a, Vector128<double> b, Vector128<double> c);
// Lane-select — i{8,16,32,64}x{16,8,4,2}.relaxed_laneselect.
// Behavior of non-high mask bits is implementation-defined
// (PackedSimd.BitwiseSelect examines every bit).
public static Vector128<sbyte> LaneSelect(Vector128<sbyte> left, Vector128<sbyte> right, Vector128<sbyte> mask);
public static Vector128<byte> LaneSelect(Vector128<byte> left, Vector128<byte> right, Vector128<byte> mask);
public static Vector128<short> LaneSelect(Vector128<short> left, Vector128<short> right, Vector128<short> mask);
public static Vector128<ushort> LaneSelect(Vector128<ushort> left, Vector128<ushort> right, Vector128<ushort> mask);
public static Vector128<int> LaneSelect(Vector128<int> left, Vector128<int> right, Vector128<int> mask);
public static Vector128<uint> LaneSelect(Vector128<uint> left, Vector128<uint> right, Vector128<uint> mask);
public static Vector128<long> LaneSelect(Vector128<long> left, Vector128<long> right, Vector128<long> mask);
public static Vector128<ulong> LaneSelect(Vector128<ulong> left, Vector128<ulong> right, Vector128<ulong> mask);
// Min/Max — f{32x4,64x2}.relaxed_{min,max}.
// NaN and signed-zero handling is implementation-defined
// (PackedSimd.Min/Max are IEEE-conformant).
public static Vector128<float> Min(Vector128<float> left, Vector128<float> right);
public static Vector128<float> Max(Vector128<float> left, Vector128<float> right);
public static Vector128<double> Min(Vector128<double> left, Vector128<double> right);
public static Vector128<double> Max(Vector128<double> left, Vector128<double> right);
// Q15 fixed-point multiply — i16x8.relaxed_q15mulr_s.
// Saturation on 0x8000 * 0x8000 is implementation-defined
// (PackedSimd.MultiplyRoundedSaturateQ15 always saturates).
public static Vector128<short> MultiplyRoundedQ15(Vector128<short> left, Vector128<short> right);
// Extended integer dot products — i16x8.relaxed_dot_i8x16_i7x16_s,
// i32x4.relaxed_dot_i8x16_i7x16_add_s.
// Per the finished spec: `a` is signed, `b` is unsigned 7-bit. When any lane
// of `b` has the high bit set, that lane's product is implementation-defined
// (may be interpreted as signed or unsigned). The sum reduction is also
// implementation-defined-saturating (may or may not saturate on overflow).
public static Vector128<short> DotProduct (Vector128<sbyte> left, Vector128<byte> right);
public static Vector128<int> DotProductAdd(Vector128<sbyte> left, Vector128<byte> right, Vector128<int> accumulator);
}Prototype: lewing@7f99067dc5e (branch wasm-relaxed-simd-api on lewing/runtime; earlier revision at bfebb64b7042e0f1095a4d87ff42a3504f62e122)
API Usage
Cross-platform saturating FMA in a hot vector loop (mirrors Vector128.MultiplyAddEstimate shape):
static Vector128<float> Blend(Vector128<float> a, Vector128<float> b, Vector128<float> t)
{
if (RelaxedSimd.IsSupported)
return RelaxedSimd.MultiplyAddEstimate(a, t, RelaxedSimd.MultiplyAddEstimate(-b, t, b));
// Cross-platform helper already exists; on wasm it currently lowers to
// separate mul+add. Wiring it to RelaxedSimd is the follow-up.
return Vector128.MultiplyAddEstimate(a, t, Vector128.MultiplyAddEstimate(-b, t, b));
}Byte permutation with a computed table (relaxed swizzle enables Ssse3.Shuffle-shaped code paths on wasm):
static Vector128<byte> Pshufb(Vector128<byte> data, Vector128<byte> indices)
{
if (RelaxedSimd.IsSupported)
return RelaxedSimd.Swizzle(data, indices); // may leave OOR lanes implementation-defined
if (PackedSimd.IsSupported)
return PackedSimd.Swizzle(data, indices); // OOR lanes are zeroed
return Vector128.Shuffle(data, indices);
}INT8 dot-product (ML inference / audio codecs):
static Vector128<int> DotAccum(Vector128<byte> left, Vector128<sbyte> weights7bit, Vector128<int> acc)
=> RelaxedSimd.IsSupported
? RelaxedSimd.DotProductAdd(left, weights7bit, acc)
: Fallback(left, weights7bit, acc);Alternative Designs
-
Name the class
Wasm.RelaxedSimdvs prefixing every method withRelaxedonPackedSimd. Chose a separate class soIsSupportedgates the whole set (relaxed-SIMD requires a distinct engine feature flag), matchingX86.Sse2vsX86.Ssse3and giving trimming a clean cut point. Not merging intoPackedSimdalso avoids expanding an already-large surface. -
Reuse
Vector128.MultiplyAddEstimateetc. rather than adding a class. Cross-platform helpers should stay the recommended API, but users ofPackedSimdalso need direct access (e.g., to reason about codegen, or when compiling withWasmEnableRelaxedSimd=false). Same argument the design has already accepted forPackedSimdliving alongsideVector128. -
SwizzlevsRelaxedSwizzle. PackedSimd already usesSwizzle; disambiguation happens at the class name. Same forMin/Max. -
MultiplyRoundedQ15vsMultiplyRoundedSaturateQ15. PackedSimd uses the latter (deterministic saturation on overflow); this class deliberately omitsSaturatebecause saturation on0x8000 * 0x8000is the exact bit spec leaves implementation-defined. The name reflects the contract.
Open design questions worth reviewer input:
ConvertToInt32vsConvertToInt32Native. Cross-platformVector128.ConvertToInt32Nativeuses theNativesuffix to mark implementation-defined OOR/NaN behavior — exactly whati32x4.relaxed_trunc_f32x4_sprovides. Renaming toConvertToInt32Native/ConvertToUInt32Nativewould make it obvious that this class supplies the wasm implementation of the cross-platform "Native" contract. Tentative: rename toConvertToInt32Native/ConvertToUInt32Native.DotProductoperand types.The spec defines the right operand as 7-bit signed; passing an 8-bit value is implementation-defined.Per the finished spec pseudocode, operandais signed and operandbis unsigned-7-bit — the current prototype had these reversed. The signatures above (sbyte left, byte right) match the spec. Reviewers should confirm the nameDotProduct/DotProductAddconveys enough about the implementation-defined summation behavior; adding aSaturatesuffix would be misleading (may or may not saturate).ConvertToInt32(Vector128<double>). The wasm instruction isi32x4.relaxed_trunc_f64x2_s_zero— it produces four int lanes with the upper two zeroed. Cross-platform helpers spell this asNarrow. ConsiderNarrowingConvertToInt32for thedoubleoverloads (matches PackedSimd'sConvertNarrowingSaturatenaming), or leave as an overload and document. Tentative: keep overloadedConvertToInt32and document.Min/MaxvsMinNative/MaxNative. Same argument as WIP: repo consolidation scouting kick-off - make clr build locally on Windows #1 —Vector128.MinNative/MaxNativealready document platform-specific NaN/±0 handling, which matchesf32x4/f64x2.relaxed_min/max. Reviewers may prefer the class use theNativesuffix for symmetry with WIP: repo consolidation scouting kick-off - make clr build locally on Windows #1 (and to reinforce that this is whereVector128.MinNativeon wasm gets its implementation). Tentative: keepMin/Maxand rely on class-name disambiguation, matchingPackedSimd.Min/Max.
Risks
- Non-deterministic results across engines. By design. Users opting into
RelaxedSimd.*accept implementation-defined behavior; documented per-method. - Overload resolution ambiguity. None expected: all methods are on a new class in a namespace already reserved for
Wasm.*intrinsics. - TFM compatibility.
System.Runtime.Intrinsics.Wasmtypes are net5.0+;RelaxedSimdis a new type in that namespace with no netstandard/netfx surface. No compatibility risk. - Analyzer coverage. [Analyzer] Upgrade from platform specific to cross platform intrinsics #82486 (cross-platform intrinsics analyzer) does not yet consider relaxed-SIMD lowerings. Not a blocker; a follow-up item.
- Binary/source breaking: none — this is pure addition.
Usage in dotnet/runtime
The *Native and *Estimate suffixes on cross-platform Vector128 helpers are already documented as "platform-specific" / "may or may not fuse". Lowering them to relaxed-SIMD on wasm does not stretch that contract — it satisfies it (compare: ConvertToInt32Native maps to cvttps2dq on x64 and fcvtzs on arm64, both platform-defined for OOR/NaN; i32x4.relaxed_trunc_f32x4_s is the wasm equivalent). Callers who need portable behavior already use Vector128.ConvertToInt32Saturate / PackedSimd.* instead.
Adoption in Vector128.* wasm lowerings
The most compelling in-tree consumers are Vector128.ShuffleNative and the Vector128.*Native/*Estimate helpers, whose current wasm lowerings emit portable-but-slower fallbacks even though their documented contracts already allow platform-specific behavior. Concrete examples:
| Cross-platform helper | Current wasm lowering | Relaxed target |
|---|---|---|
Vector128.ShuffleNative(byte) |
falls through to PackedSimd.Swizzle (Vector128.cs:3534-3536) — deterministic zero-OOR |
i8x16.relaxed_swizzle — engine-defined OOR, matches ShuffleNative's contract |
Vector128.MultiplyAddEstimate(float|double) |
FMUL+FADD (two ops) in src/mono/mono/mini/simd-intrinsics.c:2208-2244 |
f32x4/f64x2.relaxed_madd |
Vector128.MultiplyAddNegatedEstimate |
falls through to managed body | relaxed_nmadd |
Vector128.ConvertToInt32Native(float) / ConvertToUInt32Native(float) |
falls through to managed body | i32x4.relaxed_trunc_f32x4_{s,u} |
Vector128.MinNative / MaxNative(float|double) |
generic OP_XBINOP FMIN/FMAX |
f32x4/f64x2.relaxed_min/max |
All the target intrinsics (INTRINS_WASM_RELAXED_*) are already declared and used by the RelaxedSimd.* class-path lowering added in this prototype; the follow-up is teaching the generic Vector128.* intrinsic recognizer to prefer them on wasm when the engine reports relaxed-SIMD support. RyuJIT wasm work (tracked in #127665) will get the same lowering table.
For ShuffleNative specifically: adopting RelaxedSimd.Swizzle on wasm is a behavior change for callers that today implicitly rely on PackedSimd.Swizzle's zero-OOR fallback. That's within ShuffleNative's documented contract, but is worth a compatibility scan of internal call sites before the companion PR lands.
Adoption strategy
Nothing is updated in this prototype commit — following the PackedSimd precedent (#53730 shipped without in-tree adoption; #88092 refined based on real use). Companion PRs wiring the lowerings above are straightforward and can land alongside if reviewers prefer.
Note
This API proposal was drafted with the assistance of GitHub Copilot.
Activity
dotnet-policy-service commentedon Jul 5, 2026
Tagging subscribers to this area: @dotnet/area-system-runtime-intrinsics
See info in area-owners.md if you want to be subscribed.
[wasm] Address review feedback on RelaxedSimd surface
dotnet-policy-service commentedon Jul 5, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
lewing commentedon Jul 6, 2026
cc @tannergooding
tannergooding commentedon Jul 6, 2026
Looks good to me, marked ready-for-review.
LaneSelectcould possibly have its comment worded better. Logically it only looks at themost-significant-bitand uses that to mean "select entire element/lane", in contrast toBitwiseSelectwhich does it per operation.This is notably one of the only xplat APIs we don't have some "native version" around and it may be worth a separate proposal for that.
tannergooding commentedon Jul 7, 2026
This is too late to get in for 11.0 as a certainty, so marking for 12.0
If there is an explicit need and desire to push it through for 11, do let me know and I can get it marked blocking instead.
bartonjs commentedon Aug 25, 2026
Video
[wasm] Align RelaxedSimd with approved API
[wasm] Implement RelaxedSimd intrinsics api (#130050)