Compile-Time Target CPU Feature Detection in C
The goal is to detect whether the compilation target (instruction set architecture or ISA to be precise) supports certain features that the compiler is allowed to use, such as instructions or registers. Typically this is done to use the appropriate, often more performant, implementation, or to verify if the program is compatible with the target at all. This doesn't actually involve detecting CPU features directly but rather exposes properties of the compiler's current target, either as specified or detected based on the CPU the compiler is run on, when built with -march=native or equivalent.
This contrasts with runtime detection, when the information about the CPU capabilities is inspected during execution to select a different code path. Without going into the details, on x86 runtime detection is done using the CPUID instruction1, e.g. via __builtin_cpu_supports2 compiler builtin in GCC/Clang and __check_isa_support3 in MSVC. On Arm, the CPU capabilities are described by the instruction set attribute (ISAR4) and other registers; the operating system typically exposes this information indirectly (e.g. via getauxval(AT_HWCAP)56 or register emulation7 on Linux).
There's a technique which allows compiling different functions for different ISA feature sets by marking them with corresponding target attributes8 (e.g. __attribute__((target("arch=armv8-a+crc")))), but it still requires runtime dispatch between different implementations and is not discussed here.
Depending on the capabilities the target supports, a different set of preprocessor macros will be defined by the C compiler. The macros can be used to conditionally include a different implementation or fail compilation when the platform is not supported. For example, hardware support for CRC-32C9 is commonly available in modern CPUs. On Arm, the feature (optional in v8.0, required from v8.110) is represented by the __ARM_FEATURE_CRC32 macro. On x86-64, the feature is part of SSE4.211, which is represented by __SSE4_2__.
Here is an example of selecting the appropriate intrinsic function that computes one round of CRC32-C checksum on 8-byte input with GCC/Clang, or failing if the target ISA (-march) doesn't support it:
#if defined(__x86_64__) && defined(__SSE4_2__)
#include <nmmintrin.h>
static inline uint32_t crc32c_u64(uint32_t crc, uint64_t v)
{
return (uint32_t) _mm_crc32_u64(crc, v);
}
#elif defined(__ARM_FEATURE_CRC32)
#include <arm_acle.h>
static inline uint32_t crc32c_u64(uint32_t crc, uint64_t v)
{
return __crc32cd(crc, v);
}
#else
#error "No CRC32C implementation for the target architecture"
// This is where the fallback software implementation would be defined.
#endif
Note that while the feature macros for Arm are specified by the Arm C Language Extensions (ACLE)12, the x86-64 definitions are conventional. The exact list for a given architecture is compiler-dependent and might vary across platforms and compiler versions. For example, MSVC replaces __x86_64__ with _M_X64 and doesn't include a macro definition for SSE4.2 - instead the __check_arch_support(__IA_SUPPORT_SSE42, 0) intrinsic (evaluated at compile time) has to be used.
The way to discover the macros defined for a given target architecture depends on the compiler. With GCC (Red Hat version Red Hat 16.2.1-2) and Clang (Fedora version 22.1.8-4.fc44) one can list all #define directives with -E -dM (stop after preprocessor stage and dump macro definitions):
$ gcc -march=armv8.1-a -E -dM - </dev/null | grep -E 'ARM_(ARCH|FEATURE_CRC)'
#define __ARM_ARCH_PROFILE 65
#define __ARM_ARCH 8
#define __ARM_ARCH_8A 1
#define __ARM_FEATURE_CRC32 1
#define __ARM_ARCH_ISA_A64 1
$ clang -march=armv8.1-a -E -dM - </dev/null | grep -E 'ARM_(ARCH|FEATURE_CRC)'
#define __ARM_ARCH 8
#define __ARM_ARCH_ISA_A64 1
#define __ARM_ARCH_PROFILE 'A'
#define __ARM_FEATURE_CRC32 1
The MSVC compiler command to list macro definitions is /PD, a full list is available in the documentation13.
Interestingly, both compilers diverge from ACLE, which requires 801 for Armv8.1-A. This highlights the need to test the specific build configuration and is another reason to prefer feature-specific macros.