SIMD Dispatch
Demonstrates CPU-feature dispatch across all supported architectures (x86_64, aarch64, ppc64le) and two SIMD strategies: auto-vectorization (polysimd example) and intrinsics (simd_fill test case).
Simd Dispatch
Interface
# Python dict format equivalent of polysimd.c2py
{
"module": "_polysimd",
"source": [
"poly_f32_avx512.o",
"poly_f32_avx2.o",
"poly_f32_scalar.o",
],
"headers": ["c2py_amd64.h"],
"timing": True,
"functions": [
{
"py_sig": "poly(a: buffer, b: buffer, out: buffer) -> void",
"doc": "Polynomial computation with CPU-feature dispatch (AVX-512/AVX2/scalar).",
"checks": [
"a.n == b.n",
"a.n == out.n",
],
"c_overloads": [
{
"when": "a.format == 'f' and b.format == 'f' and out.format == 'f'",
"map": {
"a": "a.ptr",
"b": "b.ptr",
"out": "out.ptr",
"n": "a.n",
},
"group": "float",
"variants": [
{
"sig": "void poly_f32_avx512(const float *a, const float *b, float *out, int n)",
"when": "c2py_amd64_avx512f",
},
{
"sig": "void poly_f32_avx2(const float *a, const float *b, float *out, int n)",
"when": "c2py_amd64_avx2",
},
{
"sig": "void poly_f32_scalar(const float *a, const float *b, float *out, int n)",
},
],
},
],
},
],
}
C Source
/* poly_kernel.c -- compute-bound polynomial: out[i] = f(a[i], b[i])
*
* Each element does many arithmetic operations per memory load to
* make the kernel compute-bound rather than memory-bound. This
* ensures SIMD width differences (SSE 4-wide, AVX2 8-wide,
* AVX-512 16-wide) translate to visible throughput differences.
*
* This file is compiled multiple times with different -m flags and
* -DKERNEL_FN=<name> to produce ISA-specific variants. The kernel
* itself is plain C99; the compiler auto-vectorizes based on the
* -m flags supplied at compile time.
*
* Build (see Makefile):
* gcc -c -O3 -ffast-math -mavx512f -DKERNEL_FN=poly_f32_avx512 poly_kernel.c -o ...
* gcc -c -O3 -ffast-math -mavx2 -DKERNEL_FN=poly_f32_avx2 poly_kernel.c -o ...
* gcc -c -O3 -ffast-math -DKERNEL_FN=poly_f32_scalar poly_kernel.c -o ...
*/
#include <stddef.h>
#ifndef KERNEL_FN
#define KERNEL_FN poly_f32
#endif
/* Horner-like repeated squaring: x = a[i], then x = x*x + b[i] many times.
* Each inner iteration: 1 mul + 1 add, 0 extra memory accesses.
* Arithmetic intensity: ~16:1 (16 mul+add per 2 loads + 1 store). */
#define POLY_DEPTH 16
void KERNEL_FN(const float *a, const float *b, float *out, int n)
{
int i;
for (i = 0; i < n; i++) {
float x = a[i];
float y = b[i];
int k;
for (k = 0; k < POLY_DEPTH; k++)
x = x * x + y;
out[i] = x;
}
}
Build
Compile:
$ cc -shared -fPIC c2py23/runtime/c2py_runtime.c _polysimd_wrapper.c poly_kernel.c -I c2py23/runtime -o _polysimd.so -ldl -lm
See docs/building for cmake, meson, and setuptools options.
Run
"""Test the poly SIMD dispatch module.
Demonstrates:
- Automatic variant selection (CPU features)
- Manual rebinding via _rebind_poly()
- Timing via built-in c2py23 perf counters
- Compute-bound kernel where SIMD width directly matters
"""
from __future__ import print_function
import ctypes
import sys
import os
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import polysimd
try:
from c2py23.perf import read_perf, read_enabled, set_enabled
HAVE_PERF = True
except ImportError:
HAVE_PERF = False
print("(c2py23.perf not available)")
# --- Test data ---
N = 100000
a = (ctypes.c_float * N)(*[float(i % 100) / 100.0 for i in range(N)])
b = (ctypes.c_float * N)(*[float(i % 100) / 100.0 for i in range(N)])
out = (ctypes.c_float * N)()
print("=== Poly SIMD dispatch ===")
print("Array size: %d" % N)
print("Docstring:")
print(polysimd.poly.__doc__)
print()
# --- Correctness check ---
variants = ["poly_f32_scalar", "poly_f32_avx2", "poly_f32_avx512"]
results = {}
for v in variants:
polysimd._rebind_poly(v)
polysimd.poly(a, b, out)
results[v] = list(out[:3])
print("%-20s out[:3] = %s" % (v, results[v]))
# All should match
ref = results["poly_f32_scalar"]
for v in ["poly_f32_avx2", "poly_f32_avx512"]:
if results[v] != ref:
print("MISMATCH: %s != poly_f32_scalar!" % v)
else:
print("%-20s matches poly_f32_scalar" % v)
# --- Timing via wall clock (multiple runs, show variance) ---
print()
print("=== Wall-clock timing (100 iterations, %d elements) ===" % N)
N_ITER = 100
N_WARM = 5
for v in variants:
polysimd._rebind_poly(v)
for _ in range(N_WARM):
polysimd.poly(a, b, out)
runs = []
for run in range(5):
t0 = time.time()
for _ in range(N_ITER):
polysimd.poly(a, b, out)
dt = (time.time() - t0) / N_ITER * 1e6
runs.append(dt)
mean = sum(runs) / len(runs)
std = (sum((r - mean)**2 for r in runs) / len(runs)) ** 0.5
print(" %-8s %8.1f us/call (+/- %.1f us, cv=%.1f%%)" % (v, mean, std, 100*std/mean if mean else 0))
# --- Built-in perf ---
if HAVE_PERF:
freq_hz = polysimd._c2py_tick_frequency()
using_cycles = (freq_hz != 0 and freq_hz != 1000000000)
unit = "cycles" if using_cycles else "ns"
print()
print("=== c2py23 built-in perf (%s, 100 iterations) ===" % unit)
variant_short = {
"poly_f32_scalar": "poly_f32_scalar",
"poly_f32_avx2": "poly_f32_avx2",
"poly_f32_avx512": "poly_f32_avx512",
}
for v in variants:
polysimd._rebind_poly(v)
for _ in range(N_WARM):
polysimd.poly(a, b, out)
for _ in range(N_ITER):
polysimd.poly(a, b, out)
stats = read_perf(polysimd.poly, variant=variant_short[v],
freq_hz=freq_hz)
if using_cycles:
val = stats.get('c_mean_cycles', 0)
else:
val = stats.get('c_mean_ns', 0)
print(" %-8s %8.0f %s/call" % (v, val, unit))
# --- Speedup ratios ---
print()
print("=== Speedup ratios (wall clock, vs scalar) ===")
# Re-measure fresh
scalar_dt = None
for v in variants:
polysimd._rebind_poly(v)
for _ in range(N_WARM):
polysimd.poly(a, b, out)
t0 = time.time()
for _ in range(N_ITER):
polysimd.poly(a, b, out)
dt = time.time() - t0
if v == "scalar":
scalar_dt = dt
speedup = scalar_dt / dt if scalar_dt else 0
print(" %-8s %5.2fx" % (v, speedup))
polysimd._rebind_poly(None)
print()
print("Done.")
How It Works
Architecture-Independent Design
c2py23 has no "special" CPU. Every architecture declares its own feature flags in a runtime header:
| Architecture | Header | Feature Detection |
|---|---|---|
| x86_64 / amd64 | c2py_amd64.h |
cpuid instruction (GCC, Clang, MSVC) |
| aarch64 / arm64 | c2py_arm64.h |
getauxval(AT_HWCAP) on Linux; mrs on macOS/iOS |
| ppc64le | c2py_ppc64.h |
getauxval(AT_HWCAP) on Linux |
All feature flags are zero-initialized booleans, set to 1 at module init
time by c2py_runtime_init(). The generated wrapper dispatches based on
these flags using either:
- Grouped variants (
variants:key):switchstatement resolved once at init, with_rebind_<name>()for manual override. Used by the polysimd example. - Flat overloads:
if/else if/elsechain evaluated at each call. Used by the simd_fill test case.
Two SIMD Strategies
Auto-vectorization (poly_kernel.c): plain C99 compiled multiple times
with different -m flags (gcc/clang) or /arch: flags (MSVC). The
compiler auto-vectorizes based on the target ISA. No intrinsics needed.
# Linux / macOS (gcc/clang)
gcc -c -O3 -mavx512f -DKERNEL_FN=poly_f32_avx512 poly_kernel.c -o ...o
gcc -c -O3 -mavx2 -DKERNEL_FN=poly_f32_avx2 poly_kernel.c -o ...o
gcc -c -O3 -DKERNEL_FN=poly_f32_scalar poly_kernel.c -o ...o
# Windows MSVC
cl /c /O2 /arch:AVX512 /DKERNEL_FN=poly_f32_avx512 poly_kernel.c
cl /c /O2 /arch:AVX2 /DKERNEL_FN=poly_f32_avx2 poly_kernel.c
cl /c /O2 /DKERNEL_FN=poly_f32_scalar poly_kernel.c
Intrinsics (simd_fill.c): explicit SSE2/NEON/Altivec intrinsics
guarded by #ifdef on the target architecture. Non-matching platforms
get scalar fallback stubs. This guarantees the .so compiles on ALL
platforms even if intrinsics are unavailable for a given target.
#ifdef __x86_64__
#include <emmintrin.h>
void fill_sse2(float *buf, int n, float val) { /* SSE2 intrinsics */ }
#else
void fill_sse2(float *buf, int n, float val) { fill_scalar(buf, n, val); }
#endif
#ifdef __aarch64__
#include <arm_neon.h>
void fill_neon(float *buf, int n, float val) { /* NEON intrinsics */ }
#else
void fill_neon(float *buf, int n, float val) { fill_scalar(buf, n, val); }
#endif
Runtime Dispatch
At call time, the wrapper checks feature flags and routes to the best
available function. On x86_64: SSE2 -> scalar. On aarch64: NEON ->
scalar. On ppc64le: AltiVec -> scalar. The scalar fallback is
always last and has no when: condition.
Platform Support
All three architectures are tested in CI:
| Platform | Runner | SIMD Tested |
|---|---|---|
| Linux x86_64 | ubuntu-24.04 |
SSE2, AVX2, AVX-512 (via gcc) |
| Linux aarch64 | ubuntu-24.04-arm |
NEON (via getauxval) |
| Windows x86_64 | windows-latest |
SSE2 (via __cpuidex / MSVC) |
| macOS x86_64 | macos-13 |
SSE2, AVX2 (via clang) |
| macOS aarch64 | macos-latest |
NEON (via mrs / clang) |
ppc64le is validated via container emulation; no CI runner is available yet.