SEJ*_*JPM 3 c++ gcc icc intrinsics rdrand
英特尔C++编译器和/或GCC是否支持以下内在函数,例如自2012/2013年以来MSVC的内在函数?
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*);
Run Code Online (Sandbox Code Playgroud)
如果支持这些内在函数,那么它们支持哪个版本(请使用编译时常量)?
所有主要编译器都支持 Intel 的内在函数forrdrand和rdseedvia <immintrin.h>。
需要某些编译器的最新版本rdseed,例如 GCC9 (2019) 或 clang7 (2018),尽管这些编译器到目前为止已经稳定了很长一段时间。如果您宁愿使用较旧的编译器,或者不启用 ISA 扩展选项(例如 )-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.
Run Code Online (Sandbox Code Playgroud)
一些编译器定义__RDRND__何时在编译时启用指令。GCC/clang 因为它们完全支持内在函数,但只是在很晚的时候支持 ICC (19.0)。对于 ICC,直到 2021.1 才-march=ivybridge暗示-mrdrnd或定义__RDRND__。
ICX 基于 LLVM,行为类似于 clang。
MSVC 没有定义任何宏;它对内在函数的处理仅围绕运行时功能检测而设计,与 gcc/clang 不同,gcc/clang 的简单方法是编译时 CPU 功能选项。
为什么do{}while()而不是while(){}?结果 ICC 编译为一个不太愚蠢的循环do{}while(),而不是无用地剥离第一次迭代。其他编译器无法从这种控制中受益,并且这对于 ICC 来说不是正确性问题。
为什么unsigned long long而不是uint64_t?该类型必须与内在函数期望的指针类型一致,否则 C(尤其是 C++)编译器会抱怨,无论对象表示形式是否相同(64 位无符号)。例如,在 Linux 上uint64_t是unsigned long,但 GCC/clang 的immintrin.hDefineint _rdrand64_step(unsigned long long*)与 Windows 上相同。所以你总是需要unsigned long long retGCC/clang。MSVC 不是问题,因为它(据我所知)只能针对 Windows,其中unsigned long long是唯一的 64 位无符号类型。但根据我在https://godbolt.org/
上的测试,
ICC 将内在定义为编译 GNU/Linux 时所采用的。因此,要移植到 ICC,您实际上需要;即使在 C++ 中,我也不知道如何使用或其他类型推导来声明与其匹配的变量。unsigned long*#ifdef __INTEL_COMPILERauto
在 Godbolt 上测试;MSVC 的最早版本是 2015 年,ICC 是 2013 年,所以我不能再回溯了。_rdrand16_step在任何给定的编译器中都同时引入了对 / 32 / 64 的支持。64 需要 64 位模式。
| 中央处理器 | 海湾合作委员会 | 铛 | MSVC | 国际商会 | |
|---|---|---|---|---|---|
rdrand |
常春藤桥/挖掘机 | 4.6 | 3.2 | 2015 年之前 (19.10) | 13.0.1 之前,但 19.0 用于-mrdrnd定义__RDRND__. 2021.1 为 -march=ivybridge 启用-mrdrnd |
rdseed |
布罗德韦尔 / 禅宗 1 | 9.1 | 7.0 | 2015 年之前 (19.10) | 在(?)13.0.1之前,但19.0还添加了-mrdrnd和-mrdseed选项) |
最早的 GCC 和 clang 版本只能-march=ivybridge识别-mrdrnd. (Ivy Bridge 的 GCC 4.9 和 clang 3.6,并不是说您特别想在现代 CPU 更相关的情况下使用 IvyBridge。因此,使用非古代编译器并设置适合您实际关心的 CPU 的 CPU 选项,或者至少-mtune=使用更新的 CPU。)
英特尔的新 oneAPI / ICX 编译器都支持rdrand/rdseed,并且基于 LLVM 内部结构,因此它们的工作方式与 CPU 选项的 clang 类似。(它没有定义__INTEL_COMPILER,这很好,因为它与 ICC 不同。)
GCC 和 clang 只允许您使用内在函数来执行您告诉编译器目标支持的指令。-march=native如果为您自己的机器进行编译,请使用它,或者使用-march=skylake其他方法来为您的目标 CPU 启用所有 ISA 扩展。但是,如果您需要程序在旧 CPU 上运行,并且在运行时检测后仅使用 RDRAND 或 RDSEED,则只有这些函数需要__attribute__((target("rdrnd")))或rdseed,并且无法内联到具有不同目标选项的函数中。或者使用单独编译的库会更容易1。
-mrdrnd:由-march=ivybridge或-march=znver1(或bdver4Exavator APU)及更高版本启用-mrdseed:由-march=broadwell或-march=znver1或更晚启用通常,如果您要启用一项 CPU 功能,那么启用该代 CPU 所具有的其他功能并设置调整选项是有意义的。但这rdrand不是编译器自己使用的东西(与用于shlx更有效的变量计数移位的 BMI2 或用于自动向量化和数组/结构复制和初始化的 AVX/SSE 不同)。因此,-mrdrnd如果您检查 CPU 功能并且实际上没有运行在没有该功能的 CPU 上使用的代码,则全局启用可能不会使您的程序在 Ivy Bridge 之前的 CPU 上崩溃_rdrand64_step。
但如果您只想在某种特定类型的 CPU 或更高版本上运行代码,那么gcc -O3 -march=haswell这是一个不错的选择。(-march也意味着-mtune=haswell,针对 Ivy Bridge 的调整并不是您想要的现代 CPU。您可以-march=ivybridge -mtune=skylake设置较旧的 CPU 功能基线,但仍然针对较新的 CPU 进行调整。)
这是有效的 C++ 和 C。对于 C,您可能需要static inline代替,inline这样您就不需要extern inline在 a 中手动实例化版本.c,以防调试版本决定不内联。__attribute__((always_inline))(或在 GNU C 中使用。)
64 位版本仅针对 x86-64 目标定义,因为 asm 指令只能在 64 位模式下使用 64 位操作数大小。我没有#ifdef __RDRND__或#if defined(__i386__)||defined(__x86_64__),假设您只在 x86(-64) 构建中包含此内容,不会使 ifdef 混乱超过必要的程度。它仅定义rdseed包装器(如果在编译时启用),或者对于 MSVC(无法启用或检测它们)。
__attribute__((target("rdseed")))如果您想这样做而不是编译器选项,则可以取消注释 一些带注释的示例。rdrand16/rdseed16被故意省略,因为通常没有用。 rdrand对于不同的操作数大小,运行相同的速度,甚至从 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
Run Code Online (Sandbox Code Playgroud)
事实上,英特尔的内在函数 API 完全受支持,这意味着它unsigned int是 32 位类型,无论是否uint32_t定义为unsigned int或unsigned long是否有编译器这样做。
在Godbolt 编译器浏览器上我们可以看到它们是如何编译的。Clang 和 MSVC 执行我们期望的操作,只是一个 2 指令循环,直到rdrandCF=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.
Run Code Online (Sandbox Code Playgroud)
不幸的是,GCC 并不是那么好,即使是当前的 GCC12.1 也会产生奇怪的汇编:
# 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
Run Code Online (Sandbox Code Playgroud)
只要我们使用do{}while()重试循环,ICC 就会生成相同的 asm;使用 awhile() {}则更糟糕,在第一次进入循环之前执行 rdrand 并进行检查。
rdrand/rdseed库包装器librdrand或者英特尔的libdrng包装函数具有像我所示的重试循环,以及填充字节缓冲区或uint32_t*or数组的包装函数uint64_t*。(始终采取uint64_t*,unsigned long long*某些目标则不采取)。
如果您正在进行运行时 CPU 功能检测,那么库也是一个不错的选择,这样您就不必浪费__attribute__((target))时间。不管你怎么做,无论如何都会限制使用内在函数的函数内联,所以一个小的静态库是等效的。
libdrng还提供RdRand_isSupported()和RdSeed_isSupported(),因此您不需要自己进行 CPUID 检查。
但是,如果您打算使用比 Ivy Bridge / Broadwell 或 Excavator / Zen1 更新的东西进行构建-march=,那么内联 2 条指令重试循环(如 clang 编译它)与函数调用站点的代码大小大致相同,但不会破坏任何寄存器。 rdrand速度很慢,所以这可能不是什么大问题,但这也意味着没有额外的库依赖。
rdrand/rdseed有关 Intel(非 AMD 版本)硬件内部结构的更多详细信息,请参阅Intel 文档。对于实际的 TRNG 逻辑,请参阅了解 Intel 的 Ivy Bridge 随机数生成器- 它是一个亚稳态锁存器,由于热噪声而稳定为 0 或 1。或者至少英特尔是这么说的;基本上不可能真正验证rdrand您购买的 CPU 中的位实际来自何处。最坏的情况,如果你将它与其他熵源混合,仍然比没有好得多,就像 Linux 所做的那样/dev/random。
有关内核从中提取缓冲区这一事实的更多信息,请参阅设计硬件并编写的工程师的一些 SO 答案librdrand,例如这个和这个关于 Ivy Bridge(第一代具有它的功能)上的耗尽/性能特征的内容。
当成功将随机数放入目标寄存器时,asm 指令会在 FLAGS 中设置进位标志 (CF) = 1。否则 CF=0 且输出寄存器 = 0。您打算在重试循环中调用它,这就是(我假设)内在函数step名称中包含该单词的原因;这是生成单个随机数的一个步骤。
理论上,微代码更新可能会改变一些事情,因此它总是表明失败,例如,如果在某些 CPU 模型中发现问题,导致 RNG 不可信(根据 CPU 供应商的标准)。硬件 RNG 还具有一些自诊断功能,因此理论上 CPU 可以确定 RNG 已损坏并且不产生任何输出。我没有听说过任何 CPU 这样做过,但我也没有去寻找过。并且未来的微代码更新始终是可能的。
其中任何一个都可能导致无限重试循环。这不太好,但除非您想编写一堆代码来报告这种情况,否则这至少是一种可观察的行为,用户可以在万一发生这种情况时可能会处理该行为。
但偶尔的临时故障是正常的,也是预料之中的,必须进行处理。 最好在不告诉用户的情况下重试。
如果缓冲区中没有准备好随机数,CPU 可以报告故障,而不是使该核心停滞更长时间。该设计选择可能与中断延迟有关,或者只是使其更简单,而不必在微代码中构建重试。
根据设计者的说法,即使所有核心都在循环,Ivy Bridge 从 DRNG 中提取数据的速度也无法快于其所能跟上的速度rdrand,但后来的 CPU 可以。因此,实际重试很重要。
@jww 有一些rdrand在 libcrypto++ 中部署的经验,并发现由于重试次数设置得太低,有报告称偶尔会出现虚假故障。他通过无限次重试获得了良好的结果,这就是我选择这个答案的原因。(我怀疑他会听到 CPU 损坏的用户的报告,这些 CPU 总是会失败,如果这是真的的话。)
包含重试循环的英特尔库函数会计算重试计数。这可能会处理永久性故障的情况,正如我所说,我认为这种情况在任何真正的 CPU 中都不会发生。如果没有有限的重试次数,您将永远循环。
无限重试计数允许简单的 API 按值返回数字,而没有像 OpenSSL 用作错误返回的函数那样愚蠢的限制0:它们不能随机生成0!
如果您确实想要有限的重试次数,我建议非常高。大概有 100 万个,所以可能需要一秒或一秒的旋转才能放弃损坏的 CPU,如果一个线程在竞争内部队列的访问权方面屡屡不走运,那么一个线程饿死这么长时间的可能性可以忽略不计。
https://uops.info/测得 Skylake 上的吞吐量为:在 Skylake 上每 3554 个周期 1 个,在 Alder Lake P 核上每 1352 个周期 1 个,在 E 核上每 1230 个周期 1 个。Zen2 上每 1809 个周期 1 个。Skylake 版本运行了数千个微指令,其他版本的运行速度只有两位数。Ivy Bridge 具有 110 个周期吞吐量,但在 Haswell 中已经达到 2436 个周期,但仍然是两位数的微指令数。
最近的英特尔 CPU 上这些糟糕的性能数据可能是由于微代码更新来解决设计硬件时没有预料到的问题。Agner Fog在 Skylake 刚推出时测量了每 460 个周期的吞吐量,每个成本为 16 uops。数千个 uop 可能是通过最近的更新连接到这些指令的微代码中的额外缓冲区刷新。Agner 在新推出时测量 Haswell 的速度为 17 uops,320 个周期。通过Phoronix 上的CrossTalk/SRBDS 缓解,RdRand 性能仅为原始速度的约 3%:rdrandrdseed
正如前面的文章中所解释的,缓解串扰涉及在更新暂存缓冲区之前锁定整个内存总线,并在清除内容后解锁它。这些指令现在涉及的锁定和序列化对性能来说非常残酷,但值得庆幸的是,大多数现实世界的工作负载不应该过多使用这些指令。
锁定内存总线听起来甚至会损害其他内核的性能,如果它就像locked 指令的缓存行分割一样。
rdrand(这些周期数是核心时钟周期数;如果 DRNG 不在与核心相同的时钟上运行,这些周期数可能会因 CPU 型号而异。我想知道 uops.info 的测试是否在同一硬件的多个核心上运行,因为Coffee Lake 的 uop 是 Skylake 的两倍,每个随机数的周期数是 Skylake 的 1.4 倍。除非这只是更高的时钟导致更多的微代码重试?)
GCC和英特尔编译器都支持它们.GCC支持于2010年底推出.它们需要标题<immintrin.h>.
至少从版本4.6开始就存在GCC支持,但似乎没有任何特定的编译时常量 - 你可以检查一下__GNUC_MAJOR__ > 4 || (__GNUC_MAJOR__ == 4 && __GNUC_MINOR__ >= 6).
| 归档时间: |
|
| 查看次数: |
2653 次 |
| 最近记录: |