各种编译器上的 RDRAND 和 RDSEED 内在函数?

RDRAND and RDSEED intrinsics on various compilers?

英特尔 C++ 编译器 and/or GCC 是否支持以下 Intel intrinsics,就像 MSVC 自 2012 / 2013 以来所做的那样?

#include <immintrin.h>  // for the following intrinsics
int _rdrand16_step(uint16_t*);
int _rdrand32_step(uint32_t*);
int _rdrand64_step(uint64_t*);
int _rdseed16_step(uint16_t*);
int _rdseed32_step(uint32_t*);
int _rdseed64_step(uint64_t*);

如果支持这些内部函数,它们支持哪个版本(请使用编译时常量)?

GCC 和 Intel 编译器都支持它们。 GCC 支持于 2010 年底推出。它们需要 header <immintrin.h>.

至少从 4.6 版开始就支持 GCC,但似乎没有任何特定的 compile-time 常量 - 您可以检查 __GNUC_MAJOR__ > 4 || (__GNUC_MAJOR__ == 4 && __GNUC_MINOR__ >= 6).

Microsoft 编译器不支持 RDSEED 和 RDRAND 指令。

但是,您可以使用 NASM 或 MASM 来实现这些指令。汇编代码位于:

https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide

对于英特尔编译器,您可以使用header来确定版本。您可以使用以下宏来确定版本和 sub-version:

__INTEL_COMPILER //Major Version
__INTEL_COMPILER_UPDATE // Minor Update.

例如,如果您使用 ICC15.0 Update 3 编译器,它将显示您有

__INTEL_COMPILER  = 1500
__INTEL_COMPILER_UPDATE = 3

有关 pre-defined 宏的更多详细信息,您可以访问:https://software.intel.com/en-us/node/524490

所有主要编译器都通过 <immintrin.h> 支持 Intel 的 intrinsics for rdrand and rdseed
rdseed 需要某些编译器的最新版本,例如GCC9 (2019) 或 clang7 (2018),尽管它们已经稳定了很长一段时间。如果您宁愿使用较旧的编译器,或者不启用 ISA-extension 选项,如 -march=skylake,库 1 包装函数而不是内部函数是一个不错的选择。 (内联汇编不是必需的,除非你想玩,否则我不推荐它。)

#include <immintrin.h>
#include <stdint.h>

// gcc -march=native or haswell or znver1 or whatever, or manually enable -mrdrnd
uint64_t rdrand64(){
    unsigned long long ret;   // not uint64_t, GCC/clang wouldn't compile.
    do{}while( !_rdrand64_step(&ret) );  // retry until success.
    return ret;
}

// and equivalent for _rdseed64_step
// and 32 and 16-bit sizes with unsigned and unsigned short.

一些编译器定义 __RDRND__ 指令在 compile-time 处启用。 GCC/clang 因为他们完全支持内在函数,但只是在很久以后的 ICC (19.0) 中。对于 ICC,-march=ivybridge 并不意味着 -mrdrnd 或定义 __RDRND__ 直到 2021.1。
ICX 是 LLVM-based 并且表现得像 clang。
MSVC 没有定义任何宏;它对内在函数的处理仅围绕 运行 时间特征检测而设计,

为什么 do{}while() 而不是 while(){}?事实证明,ICC 编译为 less-dumb 循环 do{}while(),而不是无用地剥离第一次迭代。其他编译器不会从中受益 hand-holding,这不是 ICC 的正确性问题。

为什么 unsigned long long 而不是 uint64_t?该类型必须与内在函数期望的指针类型一致,否则 C 尤其是 C++ 编译器会报错,无论 object-representations 是否相同(64 位无符号)。例如,在 Linux 上,uint64_tunsigned long,但 GCC/clang 的 immintrin.h 定义 int _rdrand64_step(unsigned long long*),与 Windows 相同。所以你总是需要 unsigned long long ret 和 GCC/clang。 MSVC 是一个 non-problem,因为它(AFAIK)只能以 Windows 为目标,其中 unsigned long long 是唯一的 64 位无符号类型。
但根据我在 https://godbolt.org/ 上的测试,ICC 将内在函数定义为在为 GNU/Linux 编译时采用 unsigned long*。所以要移植到 ICC,你实际上需要 #ifdef __INTEL_COMPILER;即使在 C++ 中,我也不知道如何使用 auto 或其他 type-deduction 来声明与其匹配的变量。


支持内在函数的编译器版本

在 Godbolt 上测试;它最早的 MSVC 版本是 2015 年,ICC 是 2013 年,所以我不能再回头了。在任何给定的编译器中同时引入了对 _rdrand16_step / 32 / 64 的支持。 64 需要 64 位模式。

CPU gcc clang MSVC ICC
rdrand Ivy Bridge / Excavator 4.6 3.2 before 2015 (19.10) before 13.0.1, but 19.0 for -mrdrnd defining __RDRND__. 2021.1 for -march=ivybridge to enable -mrdrnd
rdseed Broadwell / Zen 1 9.1 7.0 before 2015 (19.10) before(?) 13.0.1, but 19.0 also added -mrdrnd and -mrdseed options)

最早的 GCC 和 clang 版本不能识别 -march=ivybridge,只能识别 -mrdrnd。 (Ivy Bridge 的 GCC 4.9 和 clang 3.6,如果现代 CPUs 更相关,并不是说你特别想使用 IvyBridge。所以使用 non-ancient 编译器并设置适合的 CPU 选项CPU你真正关心的,或者至少是 -mtune= 和最近的 CPU。)

Intel 的新编译器API / ICX 编译器都支持rdrand/rdseed,并且基于 LLVM 内部结构,因此它们的工作方式相似为 CPU 选项发出声音。 (它没有定义__INTEL_COMPILER,这很好,因为它不同于ICC。)

GCC 和 clang 只允许您将内部函数用于您告诉编译器目标支持的指令。如果为您自己的机器编译,请使用 -march=native,或者使用 -march=skylake 或其他东西为您的目标 CPU 启用所有 ISA 扩展。但是如果你需要你的程序在旧的 CPU 上 运行 并且只在 运行 时间检测后使用 RDRAND 或 RDSEED,那么只有那些函数需要 __attribute__((target("rdrnd")))rdseed ,并且将无法内联到具有不同目标选项的函数中。或者使用 separately-compiled 库会更容易1.

  • -mrdrnd:由 -march=ivybridge-march=znver1(或 bdver4 挖掘机 APU)及更高版本
  • 启用
  • -mrdseed:由-march=broadwell-march=znver1或更高版本
  • 启用

通常情况下,如果您要启用一项 CPU 功能,启用那一代 CPU 将拥有的其他功能并设置调整选项是有意义的。但是 rdrand 不是编译器自己使用的东西(不像 BMI2 shlx 更有效的 variable-count 移位,或 AVX/SSE 用于 auto-vectorization 和 array/struct复制和初始化)。因此,如果您检查 CPU 功能并且实际上没有 运行 代码,那么全局启用 -mrdrnd 可能不会使您的程序在 pre-Ivy Bridge CPU 上崩溃在 CPU 上使用 _rdrand64_step 而没有该功能。

但是,如果您只想 运行 某些特定类型的 CPU 或更高版本的代码,gcc -O3 -march=haswell 是一个不错的选择。 (-march 也意味着 -mtune=haswell,针对 Ivy Bridge 的具体调优是 。您可以 -march=ivybridge -mtune=skylake 设置较旧的 CPU 功能基线,但仍然调优对于较新的 CPUs。)

到处编译的包装器

这是有效的 C++ 和 C。对于 C,您可能需要 static inline 而不是 inline,因此您不需要在 [= 中手动实例化 extern inline 版本73=] 以防调试版本决定不内联。 (或者在 GNU C 中使用 __attribute__((always_inline))。)

64 位版本仅为 x86-64 目标定义,因为 asm 指令只能在 64 位模式下使用 64 位operand-size。我没有 #ifdef __RDRND__#if defined(__i386__)||defined(__x86_64__),假设你只在 x86(-64) 构建中包含它,而不是不必要地使 ifdef 混乱。它 母鹿 仅在编译时启用时定义 rdseed 包装器,或者对于无法启用或检测它的 MSVC。

有一些带注释的 __attribute__((target("rdseed"))) 示例,如果您想这样做而不是编译器选项,您可以取消注释。 rdrand16 / rdseed16 被故意省略,因为通常没有用。 rdrand 运行s 对于不同的 operand-sizes 相同的速度,甚至从 CPU 的内部 RNG 缓冲区中提取相同数量的数据,可选择丢弃其中的一部分给你。

#include <immintrin.h>
#include <stdint.h>

#if defined(__x86_64__) || defined (_M_X64)
// Figure out which 64-bit type the output arg uses
#ifdef __INTEL_COMPILER       // Intel declares the output arg type differently from everyone(?) else
// ICC for Linux declares rdrand's output as unsigned long, but must be long long for a Windows ABI
typedef uint64_t intrin_u64;
#else
// GCC/clang headers declare it as unsigned long long even for Linux where long is 64-bit, but uint64_t is unsigned long and not compatible
typedef unsigned long long intrin_u64;
#endif

//#if defined(__RDRND__) || defined(_MSC_VER)  // conditional definition if you want
inline
uint64_t rdrand64(){
    intrin_u64 ret;
    do{}while( !_rdrand64_step(&ret) );  // retry until success.
    return ret;
}
//#endif

#if defined(__RDSEED__) || defined(_MSC_VER)
inline
uint64_t rdseed64(){
    intrin_u64 ret;
    do{}while( !_rdseed64_step(&ret) );   // retry until success.
    return ret;
}
#endif  // RDSEED
#endif  // x86-64

//__attribute__((target("rdrnd")))
inline
uint32_t rdrand32(){
    unsigned ret;      // Intel documents this as unsigned int, not necessarily uint32_t
    do{}while( !_rdrand32_step(&ret) );   // retry until success.
    return ret;
}

#if defined(__RDSEED__) || defined(_MSC_VER)
//__attribute__((target("rdseed")))
inline
uint32_t rdseed32(){
    unsigned ret;      // Intel documents this as unsigned int, not necessarily uint32_t
    do{}while( !_rdseed32_step(&ret) );   // retry until success.
    return ret;
}
#endif

完全支持 Intel 的内在函数 API 的事实意味着 unsigned int 是 32 位类型,无论 uint32_t 是否定义为 unsigned intunsigned long 如果有编译器这样做的话。

Godbolt compiler explorer我们可以看看这些是怎么编译的。 Clang 和 MSVC 做我们所期望的,只是一个 2 指令循环,直到 rdrand 离开 CF=1

# clang 7.0 -O3 -march=broadwell    MSVC -O2 does the same.
rdrand64():
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        rdrand  rax
        jae     .LBB0_1      # synonym for jnc - jump if Not Carry
        ret

# same for other functions.

不幸的是,GCC 不是很好,即使是当前的 GCC12.1 也会产生奇怪的 asm:

# gcc 12.1 -O3 -march=broadwell
rdrand64():
        mov     edx, 1
.L2:
        rdrand  rax
        mov     QWORD PTR [rsp-8], rax    # store into the red-zone where retval is allocated
        cmovc   eax, edx                  # materialize a 0 or 1  from CF. (rdrand zeros EAX when it clears CF=0, otherwise copy the 1)
        test    eax, eax                  # then test+branch on it
        je      .L2                       # could have just been jnc after rdrand
        mov     rax, QWORD PTR [rsp-8]     # reload retval
        ret

rdseed64():
.L7:
        rdseed  rax
        mov     QWORD PTR [rsp-8], rax   # dead store into the red-zone
        jnc     .L7
        ret

ICC只要我们使用do{}while()重试循环就可以生成相同的asm;使用 while() {} 更糟,执行 rd运行d 并在第一次进入循环之前进行检查。


脚注 1:rdrand/rdseed 库包装器

librdrandIntel's libdrng 具有像我展示的那样带有重试循环的包装函数,以及填充字节缓冲区或 uint32_t*uint64_t* 数组的函数。 (在某些目标上持续 uint64_t*,没有 unsigned long long*)。

如果您要进行 运行time CPU 特征检测,库也是一个不错的选择,这样您就不必乱搞 __attribute__((target)) 东西。不管你怎么做,无论如何都会限制使用内部函数内联函数,所以一个小的静态库是等价的。

libdrng还提供了RdRand_isSupported()RdSeed_isSupported(),所以你不需要自己做CPU身份验证。

但是,如果您打算使用 -march= 比 Ivy Bridge / Broadwell 或 Excavator / Zen1 更新的东西进行构建,则内联一个 2 指令重试循环(就像 clang 将其编译成)大致相同code-size 作为函数 call-site,但不会破坏任何寄存器。 rdrand 很慢,所以这可能没什么大不了的,但这也意味着没有额外的库依赖。


rdrand/rdseed

的性能/内部构造

有关 Intel(非​​ AMD 版本)硬件内部结构的更多详细信息,请参阅 Intel's docs. Also some SO answers from the engineer who designed the hardware and wrote librdrand, such as this and this 关于其在 Ivy Bridge 上的耗尽/性能特征,第一代具有它。

无限重试次数?

asm 指令成功时在 FLAGS 中设置进位标志 (CF) = 1,当它在目标寄存器中放置一个 运行dom 数字时。否则 CF=0 并且输出寄存器 = 0。您打算在重试循环中调用它,这就是(我假设)内在名称中包含单词 step 的原因;这是生成单个 运行dom 编号的一个步骤。

理论上,微码更新可以改变一些事情,所以它总是指示失败,例如如果在某些 CPU 模型中发现问题使 RNG 不可信任(根据 CPU 供应商的标准)。硬件 RNG 也有一些 self-diagnostics,因此理论上 CPU 可以确定 RNG 损坏并且不产生任何输出。我还没有听说过任何 CPU 曾经这样做过,但我没有去寻找。未来的微码更新总是可能的。

其中任何一个都可能导致无限重试循环。这不是很好,但除非您想编写一堆代码来报告这种情况,否则它至少是一种可观察到的行为,用户可能会在这种不太可能发生的事件中处理。

但偶尔出现的临时故障是正常的和意料之中的,必须处理。最好在不告知用户的情况下重试。

如果在其缓冲区中没有准备好 运行dom 编号,CPU 可以报告失败,而不是使该核心停滞可能更长时间。该设计选择可能与中断延迟有关,或者只是使其更简单而不必在微代码中构建重试。

Ivy Bridge 从 DRNG 中提取数据的速度无法跟上,according to the designer,即使所有内核都在循环 rdrand,但稍后 CPU 可以。因此,实际重试很重要。

@jww 有一些在 libcrypto++ 中部署 rdrand 的经验,并且 found that 的重试次数设置得太低,有报告说偶尔会出现虚假失败。他从无限次重试中获得了很好的结果,这就是为什么我选择它作为这个答案的原因。 (我怀疑他会听到 CPU 损坏的用户报告总是失败,如果那是一回事的话。)

包含重试循环的英特尔库函数采用重试计数。这可能会处理 permanent-failure 案例,正如我所说,我认为在任何真实的 CPUs yet 中都不会发生。如果没有有限的重试次数,您将永远循环。

无限重试次数允许一个简单的API return 按值计算数字,没有 silly limitations 像使用 0 作为错误的 OpenSSL 函数 return:它们不能 运行 domly 生成一个 0!

如果您确实想要有限的重试次数,我建议非常高。可能有 100 万,所以可能需要一秒或一秒的旋转才能放弃损坏的 CPU,如果一个线程在争夺内部访问权时反复倒霉,那么长时间饿死的可能性微乎其微排队。

https://uops.info/ 在 Skylake 上测得的吞吐量为每 3554 个周期一个,在 Alder Lake P-cores 上每 1352 个周期一个,在 E-cores 上为 1230 个周期。在 Zen2 上每 1809 个周期一个。 Skylake 版本 运行 数千 uops,其他版本处于低两位数。 Ivy Bridge 有 110 个周期的吞吐量,但在 Haswell 中它已经高达 2436 个周期,但仍然是 double-digit 个 uops。

最近 Intel CPUs 上这些糟糕的性能数据可能是由于微代码更新解决了设计硬件时未预料到的问题。 Phoronix 上的阿格纳雾 measured one per 460 cycle throughput for rdrand and rdseed on Skylake when it was new, each costing 16 uops. The thousands of uops are probably extra buffer flushing hooked into the microcode for those instructions by recent updates. Agner measured Haswell at 17 uops, 320 cycles when it was new. See RdRand Performance As Bad As ~3% Original Speed With CrossTalk/SRBDS Mitigation

As explained in the earlier article, mitigating CrossTalk involves locking the entire memory bus before updating the staging buffer and unlocking it after the contents have been cleared. This locking and serialization now involved for those instructions is very brutal on the performance, but thankfully most real-world workloads shouldn't be making too much use of these instructions.

锁定内存总线听起来可能会损害其他内核的性能,如果它像 cache-line 拆分 locked 指令。

(那些周期数是核心时钟周期数;如果 DRNG 没有 运行 与核心在同一时钟上,这些可能因 CPU 型号而异。我想知道是否 uops.info 的测试是在同一硬件的多个内核上 运行ning rdrand,因为 Coffee Lake 的微处理器是 Skylake 的两倍,每个 运行dom 数的周期数是 Skylake 的 1.4 倍. 除非那只是更高的时钟导致更多的微代码重试?)