Using SIMD with WebAssembly

Emscripten supports the WebAssembly SIMD feature. There are five different ways to leverage WebAssembly SIMD in your C/C++ programs:

  1. Enable LLVM/Clang SIMD autovectorizer to automatically target WebAssembly SIMD, without requiring changes to C/C++ source code.

  2. Write SIMD code using the GCC/Clang SIMD Vector Extensions (__attribute__((vector_size(16))))

  3. Write SIMD code using the WebAssembly SIMD intrinsics (#include <wasm_simd128.h>)

  4. Compile existing SIMD code that uses the x86 SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, AVX2, or FMA intrinsics (#include <*mmintrin.h>)

  5. Compile existing SIMD code that uses the ARM NEON intrinsics (#include <arm_neon.h>)

These techniques can be freely combined in a single program.

To enable any of the five types of SIMD above, pass the WebAssembly-specific -msimd128 flag at compile time. This will also turn on LLVM’s autovectorization passes. If that is not desirable, additionally pass flags -fno-vectorize -fno-slp-vectorize to disable the autovectorizer. See Auto-Vectorization in LLVM for more information.

WebAssembly SIMD is supported by

  • Chrome ≥ 91 (May 2021),

  • Firefox ≥ 89 (June 2021),

  • Safari ≥ 16.4 (March 2023) and

  • Node.js ≥ 16.4 (June 2021).

See WebAssembly Roadmap for details about other VMs.

An upcoming Relaxed SIMD proposal will add more SIMD instructions to WebAssembly.

GCC/Clang SIMD Vector Extensions

At the source level, the GCC/Clang SIMD Vector Extensions can be used and will be lowered to WebAssembly SIMD instructions where possible.

This enables developers to create custom wide vector types via typedefs, and use arithmetic operators (+,-,*,/) on the vectorized types, as well as allow individual lane access via the vector[i] notation. However, the GCC vector built-in functions are not available. Instead, use the WebAssembly SIMD Intrinsics functions below.

WebAssembly SIMD Intrinsics

LLVM maintains a WebAssembly SIMD Intrinsics header file that is provided with Emscripten, and adds type definitions for the different supported vector types.

#include <wasm_simd128.h>
#include <stdio.h>

int main() {
#ifdef __wasm_simd128__
  v128_t v1 = wasm_f32x4_make(1.2f, 3.4f, 5.6f, 7.8f);
  v128_t v2 = wasm_f32x4_make(2.1f, 4.3f, 6.5f, 8.7f);
  v128_t v3 = wasm_f32x4_add(v1, v2);
  // Prints "v3: [3.3, 7.7, 12.1, 16.5]"
  printf("v3: [%.1f, %.1f, %.1f, %.1f]\n",
         wasm_f32x4_extract_lane(v3, 0),
         wasm_f32x4_extract_lane(v3, 1),
         wasm_f32x4_extract_lane(v3, 2),
         wasm_f32x4_extract_lane(v3, 3));
#endif
}

The Wasm SIMD header can be browsed online at wasm_simd128.h.

Pass flag -msimd128 at compile time to enable targeting WebAssembly SIMD Intrinsics. C/C++ code can use the built-in preprocessor define #ifdef __wasm_simd128__ to detect when building with WebAssembly SIMD enabled.

Pass -mrelaxed-simd to target WebAssembly Relaxed SIMD Intrinsics. C/C++ code can use the built-in preprocessor define #ifdef __wasm_relaxed_simd__ to detect when this target is active.

Limitations and behavioral differences

When porting native SIMD code, it should be noted that because of portability concerns, the WebAssembly SIMD specification does not expose access to all of the native x86/ARM SIMD instructions. In particular the following changes exist:

  • Emscripten does not support x86 or any other native inline SIMD assembly or building .s assembly files, so all code should be written to use SIMD intrinsic functions or compiler vector extensions.

  • WebAssembly SIMD does not have control over managing floating point rounding modes or handling denormals.

  • Cache line prefetch instructions are not available, and calls to these functions will compile, but are treated as no-ops.

  • Asymmetric memory fence operations are not available, but will be implemented as fully synchronous memory fences when SharedArrayBuffer is enabled (-pthread) or as no-ops when multithreading is not enabled (the default).

SIMD-related bug reports are tracked in the Emscripten bug tracker with the label SIMD.

Optimization considerations

When developing SIMD code to use WebAssembly SIMD, implementors should be aware of semantic differences between the host hardware and WebAssembly semantics; as acknowledged in the WebAssembly design documentation, “this sometimes will lead to poor performance.” The following list outlines some WebAssembly SIMD instructions to look out for when performance tuning:

WebAssembly SIMD instructions with performance implications

WebAssembly SIMD instruction

Arch

Considerations

[i8x16|i16x8|i32x4|i64x2].[shl|shr_s|shr_u]

x86, arm

Use a constant shift amount to avoid extra instructions checking that it is in bounds.

i8x16.[shl|shr_s|shr_u]

x86

Included for orthogonality, these instructions have no equivalent x86 instruction and are emulated with 5-11 x86 instructions in v8 (i.e. using 16x8 shifts).

i64x2.shr_s

x86

Included for orthogonality, this instruction has no equivalent x86 instruction and is emulated with 6-12 x86 instructions in v8.

i8x16.swizzle

x86

The zeroing behavior does not match x86 (i.e. this instruction zeroes when an index is out-of-range instead of when the most significant bit is 1); use a constant swizzle amount (or i8x16.shuffle) to avoid 3 extra x86 instructions in some runtimes.

[f32x4|f64x2].[min|max]

x86

As with the scalar versions, the NaN propagation semantics force runtimes to emulate with 7-10 x86 instructions (e.g., see v8’s emulation; if possible, use [f32x4|f64x2].[pmin|pmax] instead (1 x86 instruction).

i32x4.trunc_sat_f32x4_[u|s]

x86

No equivalent x86 semantics; emulated with 8-14 x86 instructions in v8.

i32x4.trunc_sat_f64x2_[u|s]_zero

x86

No equivalent x86 semantics; emulated with 5-6 x86 instructions in v8.

f32x4.convert_f32x4_u

x86

No equivalent x86 semantics; emulated with 8 x86 instructions in v8.

[i8x16|i64x2].mul

x86

Included for orthogonality, these instructions have no equivalent x86 instruction and are emulated with 10 x86 instructions in v8.

Compiling SIMD code targeting x86 SSE* instruction sets

Emscripten supports compiling existing codebases that use x86 SSE instructions by passing the -msimd128 flag, and additionally one of the following:

  • SSE: pass -msse and #include <xmmintrin.h>. Use #ifdef __SSE__ to gate code.

  • SSE2: pass -msse2 and #include <emmintrin.h>. Use #ifdef __SSE2__ to gate code.

  • SSE3: pass -msse3 and #include <pmmintrin.h>. Use #ifdef __SSE3__ to gate code.

  • SSSE3: pass -mssse3 and #include <tmmintrin.h>. Use #ifdef __SSSE3__ to gate code.

  • SSE4.1: pass -msse4.1 and #include <smmintrin.h>. Use #ifdef __SSE4_1__ to gate code.

  • SSE4.2: pass -msse4.2 and #include <nmmintrin.h>. Use #ifdef __SSE4_2__ to gate code.

  • AVX: pass -mavx and #include <immintrin.h>. Use #ifdef __AVX__ to gate code.

  • AVX2: pass -mavx2 and #include <immintrin.h>. Use #ifdef __AVX2__ to gate code.

  • FMA: pass -mfma and #include <immintrin.h>. Use #ifdef __FMA__ to gate code. Also pass -mrelaxed-simd to enable Wasm relaxed SIMD FMA.

Currently the SSE1, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, AVX2, and FMA instruction sets are supported. Each of these instruction sets add on top of the previous ones, so e.g. when targeting SSE3, the instruction sets SSE1 and SSE2 are also available.

The following tables highlight the availability and expected performance of different SSE* intrinsics. This can be useful for understanding the performance limitations that the Wasm SIMD specification has when running on x86 hardware.

For detailed information on each SSE intrinsic function, visit the excellent Intel Intrinsics Guide on SSE1.

The following legend is used to highlight the expected performance of various instructions:
  • ✅ Wasm SIMD has a native opcode that matches the x86 SSE instruction, should yield native performance

  • 💡 while the Wasm SIMD spec does not provide a proper performance guarantee, given a suitably smart enough compiler and a runtime VM path, this intrinsic should be able to generate the identical native SSE instruction.

  • 🟡 there is some information missing (e.g. type or alignment information) for a Wasm VM to be guaranteed to be able to reconstruct the intended x86 SSE opcode. This might cause a penalty depending on the target CPU hardware family, especially on older CPU generations.

  • ⚠️ the underlying x86 SSE instruction is not available, but it is emulated via at most few other Wasm SIMD instructions, causing a small penalty.

  • ❌ the underlying x86 SSE instruction is not exposed by the Wasm SIMD specification, so it must be emulated via a slow path, e.g. a sequence of several slower SIMD instructions, or a scalar implementation.

  • 💣 the underlying x86 SSE opcode is not available in Wasm SIMD, and the implementation must resort to such a slow emulated path, that a workaround rethinking the algorithm at a higher level is advised.

  • 💭 the given SSE intrinsic is available to let applications compile, but does nothing.

  • ⚫ the given SSE intrinsic is not available. Referencing the intrinsic will cause a compiler error.

Certain intrinsics in the table below are marked “virtual”. This means that there does not actually exist a native x86 SSE instruction set opcode to implement them, but native compilers offer the function as a convenience. Different compilers might generate a different instruction sequence for these.

In addition to consulting the tables below, you can turn on diagnostics for slow, emulated functions by defining the macro #define WASM_SIMD_COMPAT_SLOW. This will print out warnings if you attempt to use any of the slow paths (corresponding to ❌ or 💣 in the legend).

x86 SSE intrinsics available via #include <xmmintrin.h> and -msse

Intrinsic name

WebAssembly SIMD support

_mm_set_ps

✅ wasm_f32x4_make

_mm_setr_ps

✅ wasm_f32x4_make

_mm_set_ss

💡 emulated with wasm_f32x4_make

_mm_set_ps1 (_mm_set1_ps)

✅ wasm_f32x4_splat

_mm_setzero_ps

💡 emulated with wasm_f32x4_const(0)

_mm_load_ps

🟡 wasm_v128_load. VM must guess type.
Unaligned load on x86 CPUs.

_mm_loadl_pi

❌ No Wasm SIMD support.
Emulated with scalar loads + shuffle.

_mm_loadh_pi

❌ No Wasm SIMD support.
Emulated with scalar loads + shuffle.

_mm_loadr_ps

💡 Virtual. Simd load + shuffle.

_mm_loadu_ps

🟡 wasm_v128_load. VM must guess type.

_mm_load_ps1 (_mm_load1_ps)

🟡 Virtual. Simd load + shuffle.

_mm_load_ss

❌ emulated with wasm_f32x4_make

_mm_storel_pi

❌ scalar stores

_mm_storeh_pi

❌ shuffle + scalar stores

_mm_store_ps

🟡 wasm_v128_store. VM must guess type.
Unaligned store on x86 CPUs.

_mm_stream_ps

🟡 wasm_v128_store. VM must guess type.
No cache control in Wasm SIMD.

_mm_prefetch

💭 No-op.

_mm_sfence

⚠️ A full barrier in multithreaded builds.

_mm_shuffle_ps

🟡 wasm_i32x4_shuffle. VM must guess type.

_mm_storer_ps

💡 Virtual. Shuffle + Simd store.

_mm_store_ps1 (_mm_store1_ps)

🟡 Virtual. Emulated with shuffle.
Unaligned store on x86 CPUs.

_mm_store_ss

💡 emulated with scalar store

_mm_storeu_ps

🟡 wasm_v128_store. VM must guess type.

_mm_storeu_si16

💡 emulated with scalar store

_mm_storeu_si64

💡 emulated with scalar store

_mm_movemask_ps

✅ wasm_i32x4_bitmask

_mm_move_ss

💡 emulated with a shuffle. VM must guess type.

_mm_add_ps

✅ wasm_f32x4_add

_mm_add_ss

⚠️ emulated with a shuffle

_mm_sub_ps

✅ wasm_f32x4_sub

_mm_sub_ss

⚠️ emulated with a shuffle

_mm_mul_ps

✅ wasm_f32x4_mul

_mm_mul_ss

⚠️ emulated with a shuffle

_mm_div_ps

✅ wasm_f32x4_div

_mm_div_ss

⚠️ emulated with a shuffle

_mm_min_ps

TODO: pmin once it works

_mm_min_ss

⚠️ emulated with a shuffle

_mm_max_ps

TODO: pmax once it works

_mm_max_ss

⚠️ emulated with a shuffle

_mm_rcp_ps

❌ No Wasm SIMD support.
Emulated with full precision div. simd/#3

_mm_rcp_ss

❌ No Wasm SIMD support.
Emulated with full precision div+shuffle simd/#3

_mm_sqrt_ps

✅ wasm_f32x4_sqrt

_mm_sqrt_ss

⚠️ emulated with a shuffle

_mm_rsqrt_ps

❌ No Wasm SIMD support.
Emulated with full precision div+sqrt. simd/#3

_mm_rsqrt_ss

❌ No Wasm SIMD support.
Emulated with full precision div+sqrt+shuffle. simd/#3

_mm_unpackhi_ps

💡 emulated with a shuffle

_mm_unpacklo_ps

💡 emulated with a shuffle

_mm_movehl_ps

💡 emulated with a shuffle

_mm_movelh_ps

💡 emulated with a shuffle

_MM_TRANSPOSE4_PS

💡 emulated with a shuffle

_mm_cmplt_ps

✅ wasm_f32x4_lt

_mm_cmplt_ss

⚠️ emulated with a shuffle

_mm_cmple_ps

✅ wasm_f32x4_le

_mm_cmple_ss

⚠️ emulated with a shuffle

_mm_cmpeq_ps

✅ wasm_f32x4_eq

_mm_cmpeq_ss

⚠️ emulated with a shuffle

_mm_cmpge_ps

✅ wasm_f32x4_ge

_mm_cmpge_ss

⚠️ emulated with a shuffle

_mm_cmpgt_ps

✅ wasm_f32x4_gt

_mm_cmpgt_ss

⚠️ emulated with a shuffle

_mm_cmpord_ps

❌ emulated with 2xcmp+and

_mm_cmpord_ss

❌ emulated with 2xcmp+and+shuffle

_mm_cmpunord_ps

❌ emulated with 2xcmp+or

_mm_cmpunord_ss

❌ emulated with 2xcmp+or+shuffle

_mm_and_ps

🟡 wasm_v128_and. VM must guess type.

_mm_andnot_ps

🟡 wasm_v128_andnot. VM must guess type.

_mm_or_ps

🟡 wasm_v128_or. VM must guess type.

_mm_xor_ps

🟡 wasm_v128_xor. VM must guess type.

_mm_cmpneq_ps

✅ wasm_f32x4_ne

_mm_cmpneq_ss

⚠️ emulated with a shuffle

_mm_cmpnge_ps

⚠️ emulated with not+ge

_mm_cmpnge_ss

⚠️ emulated with not+ge+shuffle

_mm_cmpngt_ps

⚠️ emulated with not+gt