183 lines
6.8 KiB
Python
183 lines
6.8 KiB
Python
# Copyright 2026 Umbra Tools (umbratools.com)
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""The C probe, kept in a string so the module has no build step.
|
|
|
|
Design notes that are not obvious from the code, and which matter:
|
|
|
|
**One translation unit per feature.** A single source containing every intrinsic cannot
|
|
be built with `-mavx2` alone: the FMA, AVX-512 and AES intrinsics then fail to inline
|
|
(`always_inline` against a target lacking the flag is a hard error). That was the first
|
|
version and it reported every feature unverified on a machine that has most of them. So
|
|
the source below is compiled once per feature with `__FEATURE__` substituted.
|
|
|
|
**Intrinsics, not inline assembly.** The first attempt used inline asm and every operand
|
|
count was wrong. Intrinsics let the compiler get the encoding right, and the result is
|
|
verified by disassembly anyway.
|
|
|
|
**Volatile inputs.** At `-O2` with constant arrays, GCC folded `a*b` at compile time and
|
|
emitted no instruction, so a probe that "passed" would have proved nothing and reported
|
|
every CPU as supporting AVX. The arrays are `volatile` and seeded from `argc`, so the
|
|
load must happen at run time.
|
|
|
|
**Wrong answers are unverified, not absent.** If the program runs and computes something
|
|
wrong, the CPU executed the instruction. Reporting `absent` would be a lie about the
|
|
hardware; it is a bug in the probe.
|
|
"""
|
|
|
|
_C_SOURCE = r"""
|
|
#include <immintrin.h>
|
|
#include <emmintrin.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
|
|
/* volatile: these must be loaded at run time so the compiler cannot fold. */
|
|
static volatile double vin[8];
|
|
static volatile double vcoef[8];
|
|
static volatile int iin[8];
|
|
static volatile int icoef[8];
|
|
|
|
static double elapsed(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
double d[8];
|
|
int iv[8];
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
vin[i] = 1.5; vcoef[i] = 2.0;
|
|
iin[i] = i + 1; icoef[i] = i + 1;
|
|
}
|
|
/* Seed from argc so the values are unknowable at compile time. */
|
|
if (argc > 99) { vin[0] = 3.0; iin[0] = 7; }
|
|
|
|
/* The volatile arrays are cast away once, explicitly: the load intrinsics take a
|
|
* non-volatile pointer, and casting here keeps the qualifier from silently
|
|
* disabling the optimisation we depend on. */
|
|
const double *vinp = (const double *)vin;
|
|
const double *vcoefp = (const double *)vcoef;
|
|
const int *iinp = (const int *)iin;
|
|
const int *icoefp = (const int *)icoef;
|
|
|
|
#if __FEATURE_INDEX__ == 0
|
|
{
|
|
__m128d a = _mm_loadu_pd(vinp), b = _mm_loadu_pd(vcoefp);
|
|
_mm_storeu_pd(d, _mm_mul_pd(a, b));
|
|
if (fabs(d[0] - 3.0) > 1e-9) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 1
|
|
{
|
|
/* CMPNEQPD is SSE4.2. On an older target this is rejected at build time,
|
|
* which is the correct outcome: the toolchain cannot express the feature.
|
|
*
|
|
* The result is a *mask* - all ones in the 64-bit lane where the operands
|
|
* differ, zero where they match - not a double. Storing it through
|
|
* _mm_storeu_pd and reading a double back gives NaN, which reads as a wrong
|
|
* answer and made this probe report unverified on every machine. The first
|
|
* version did exactly that. */
|
|
unsigned long long mask[2];
|
|
__m128d a = _mm_loadu_pd(vinp), b = _mm_loadu_pd(vcoefp);
|
|
_mm_storeu_si64(mask, _mm_castpd_si128(_mm_cmpneq_pd(a, b)));
|
|
/* 1.5 != 2.0 -> mask, 2.5 != 2.0 -> no mask. Seeded from argc, so both
|
|
* lanes are unknowable at compile time. */
|
|
if (mask[0] != 0xFFFFFFFFFFFFFFFFULL) return 3;
|
|
if (mask[1] != 0) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 2
|
|
{
|
|
__m256d a = _mm256_loadu_pd(vinp), b = _mm256_loadu_pd(vcoefp);
|
|
_mm256_storeu_pd(d, _mm256_mul_pd(a, b));
|
|
if (fabs(d[0] - 3.0) > 1e-9) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 3
|
|
{
|
|
__m256i a = _mm256_loadu_si256((const __m256i *)iinp);
|
|
__m256i b = _mm256_loadu_si256((const __m256i *)icoefp);
|
|
_mm256_storeu_si256((__m256i *)iv, _mm256_add_epi32(a, b));
|
|
if (iv[0] != 2 || iv[7] != 16) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 4
|
|
{
|
|
/* a*b + a: the result differs from either input, so a compiler that split
|
|
* this into a multiply and an add, or folded it to a multiply, is caught. */
|
|
__m256d a = _mm256_loadu_pd(vinp), b = _mm256_loadu_pd(vcoefp);
|
|
_mm256_storeu_pd(d, _mm256_fmadd_pd(a, b, a));
|
|
if (fabs(d[0] - 4.5) > 1e-9) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 5
|
|
{
|
|
__m512d a = _mm512_loadu_pd(vinp), b = _mm512_loadu_pd(vcoefp);
|
|
_mm512_storeu_pd(d, _mm512_mul_pd(a, b));
|
|
if (fabs(d[0] - 3.0) > 1e-9) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 6
|
|
{
|
|
unsigned long long a = 0x9e3779b97f4a7c15ULL, r = 0;
|
|
/* RORX is BMI2. On an older target the assembler rejects it. */
|
|
__asm__ volatile("rorx $13, %1, %0" : "=r"(r) : "r"(a));
|
|
if (r == a) return 3;
|
|
}
|
|
#elif __FEATURE_INDEX__ == 7
|
|
{
|
|
unsigned char state[16], plain[16];
|
|
__m128i k, block;
|
|
memcpy(plain, "0123456789abcdef", 16);
|
|
memcpy(state, plain, 16);
|
|
block = _mm_loadu_si128((const __m128i *)state);
|
|
k = _mm_setzero_si128();
|
|
k = _mm_aesenc_si128(k, block);
|
|
k = _mm_aesenclast_si128(k, block);
|
|
_mm_storeu_si128((__m128i *)state, k);
|
|
/* No reference AES here, so the assertion is that the bytes changed: the
|
|
* instruction ran and did AES work rather than being a no-op copy. */
|
|
if (!memcmp(state, plain, 16)) return 3;
|
|
}
|
|
#else
|
|
#error "unknown feature index"
|
|
#endif
|
|
|
|
{
|
|
double t0, t1, acc = 0.0;
|
|
t0 = elapsed();
|
|
for (int i = 0; i < 300000; i++) acc += (double)i * 0.5;
|
|
t1 = elapsed();
|
|
(void)acc;
|
|
printf("%.6f\n", t1 - t0);
|
|
}
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
#: The mnemonic each feature must produce, checked in the disassembly. If the compiler
|
|
#: folded the probe away, none of these appear and the feature is reported unverified.
|
|
#: Lists, because different compilers choose different encodings.
|
|
_EMITTED = {
|
|
"sse2": ("mulpd", "addpd"),
|
|
"sse4_2": ("cmpneqpd", "cmpltpd", "cmpneqpd"),
|
|
"avx": ("vmulpd", "vaddpd"),
|
|
"avx2": ("vpaddd", "vpaddq"),
|
|
"fma": ("vfmadd", "vfnmadd"),
|
|
"avx512f": ("vmulpd", "vaddpd"),
|
|
"bmi2": ("rorx",),
|
|
"aes_ni": ("aesenc",),
|
|
}
|
|
|
|
#: Compiler flag per feature. Without the explicit flag the compiler targets its own
|
|
#: build machine and emits baseline instructions, so the probe would pass everywhere.
|
|
_FEATURE_FLAGS = {
|
|
"sse2": ["-msse2"],
|
|
"sse4_2": ["-msse4.2"],
|
|
"avx": ["-mavx"],
|
|
"avx2": ["-mavx2"],
|
|
"fma": ["-mfma"],
|
|
"avx512f": ["-mavx512f"],
|
|
"bmi2": ["-mbmi2"],
|
|
"aes_ni": ["-maes"],
|
|
}
|