Guide to SIMD AVX and NEON Intrinsics in C
Single Instruction, Multiple Data (SIMD) extensions accelerate compute-heavy workloads by processing multiple data points simultaneously within dedicated CPU vector registers. In the C programming language, developers directly harness hardware extensions like Intel/AMD AVX or ARM NEON without writing assembly by including architecture-specific vector headers. These headers expose compiler intrinsics—special C functions and datatypes that map directly to underlying SIMD machine instructions, offering high-performance execution while allowing the compiler to manage register allocation and instruction scheduling.
Including the Dedicated Vector Headers
To access SIMD capabilities, you must include the specific header file matching your target CPU architecture:
- x86/x86-64 (SSE, AVX, AVX-512): Include
<immintrin.h>. This master header exposes all Intel and AMD SIMD instruction sets, including legacy SSE, AVX, AVX2, and AVX-512. - ARM/AArch64 (NEON): Include
<arm_neon.h>. This header exposes 64-bit and 128-bit vector types and functions for ARM Cortex processors and Apple Silicon.
Vector Data Types
SIMD headers introduce custom data types representing the processor's wide registers:
- AVX Types: Represent 256-bit wide registers. Common
types include
__m256(eight 32-bit single-precision floats),__m256d(four 64-bit double-precision floats), and__m256i(packed integers ranging from 8-bit to 64-bit). - NEON Types: Explicitly defined by element type, bit
width, and lane count. Common 128-bit types include
float32x4_t(four 32-bit floats),int32x4_t(four 32-bit signed integers), anduint8x16_t(sixteen 8-bit unsigned integers).
The Core SIMD Execution Pattern
Utilizing SIMD through vector headers follows a four-step pipeline: loading data from memory into registers, executing operations, storing results back into standard memory arrays, and processing any remaining elements that do not fit into a full vector lane.
AVX Example (256-bit Vector Addition)
#include <immintrin.h>
void add_floats_avx(const float* a, const float* b, float* c, int n) {
int i = 0;
// Process 8 single-precision floats at a time
for (; i <= n - 8; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]); // Load unaligned memory
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vres = _mm256_add_ps(va, vb); // Execute SIMD add
_mm256_storeu_ps(&c[i], vres); // Store back to memory
}
// Tail loop for remaining elements
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}ARM NEON Example (128-bit Vector Addition)
#include <arm_neon.h>
void add_floats_neon(const float* a, const float* b, float* c, int n) {
int i = 0;
// Process 4 single-precision floats at a time
for (; i <= n - 4; i += 4) {
float32x4_t va = vld1q_f32(&a[i]); // Load 4 floats
float32x4_t vb = vld1q_f32(&b[i]);
float32x4_t vres = vaddq_f32(va, vb); // Execute NEON add
vst1q_f32(&c[i], vres); // Store back to memory
}
// Tail loop for remaining elements
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}Compiler Configuration
Compilers require specific target flags to enable these intrinsics during compilation:
- For AVX/AVX2 on GCC or Clang: Pass
-mavxor-mavx2. To automatically enable all SIMD extensions supported by your host machine, pass-march=native. - For NEON on ARM: On 64-bit ARM (AArch64), NEON is
enabled by default. On 32-bit ARM (ARMv7), specify flags such as
-mfpu=neonand-mfloat-abi=hard.