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

but5z9lq  于 2022-11-13  发布在  其他
关注(0)|答案(3)|浏览(650)

英特尔C++编译器和/或GCC是否像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*);

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

tkclm6bt

tkclm6bt1#

GCC和英特尔编译器都支持它们。GCC支持于2010年底推出。它们需要标头<immintrin.h>
GCC支持至少从4.6版本开始就存在了,但是似乎没有任何特定的编译时常量--您可以只检查__GNUC_MAJOR__ > 4 || (__GNUC_MAJOR__ == 4 && __GNUC_MINOR__ >= 6)

nzrxty8p

nzrxty8p2#

所有主要的编译器都通过<immintrin.h>支持英特尔的rdrandrdseed内部函数。
rdseed需要一些较新版本的编译器,例如GCC 9(2019)或铿锵7(2018),尽管这些函数已经稳定了很长一段时间。如果你宁愿使用旧的编译器,或者不启用ISA扩展选项,比如-march=skylake,那么使用library 1 Package 函数而不是内部函数是一个不错的选择。(内联asm不是必需的,除非您想使用它,否则我不推荐使用它。)

#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__. GCC/clang,因为它们完全支持内部函数,但只有在更晚的ICC(19.0)版本中才支持。对于ICC,-march=ivybridge直到2021.1才隐含-mrdrnd或定义__RDRND__
ICX是基于LLVM的,其行为类似于clang。
MSVC不定义任何宏;其对内部函数的处理仅围绕运行时特征检测而设计,unlike gcc/clang where the easy way is compile-time CPU feature options
为什么要用do{}while()而不是while(){}呢?事实证明,ICC使用do{}while()编译成一个不那么愚蠢的循环,而不是无用地剥离第一次迭代。其他编译器不会从这种控制中受益,而且这对ICC来说也不是一个正确性问题。
为什么是unsigned long long而不是uint64_t?类型必须与内部函数所期望的指针类型一致,否则C,尤其是C编译器将抱怨,而不管对象表示是否相同例如,在Linux上,uint64_tunsigned long,但是GCC/clang的immintrin.h定义了int _rdrand64_step(unsigned long long*),和Windows上一样。所以你总是需要unsigned long long ret和GCC/clang。MSVC是没有问题的,因为它(AFAIK)只能针对Windows,其中unsigned long long是唯一的64位无符号类型。
但是根据我在https://godbolt.org/上的测试,ICC在为GNU/Linux编译时将内部函数定义为采用unsigned long*。即使在C
中,我也不知道如何使用auto或其他类型推导来声明与之匹配变量。

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

在Godbolt上测试;它最早的MSVC版本是2015,ICC 2013,所以我不能再往前追溯了。对_rdrand16_step/ 32 / 64的支持都是在任何给定的编译器中同时引入的。64需要64位模式。
| | 中央处理器|通用计算机|铿锵声|MSVC语言|国际商会|
| - -|- -|- -|- -|- -|- -|
| rdrand个|Ivy桥/挖掘机|四点六|三、二|2015年之前(19.10)|13.0.1之前的版本,但为-mrdrnd定义__RDRND__的版本为19.0。为-march=ivybridge启用-mrdrnd的版本为2021.1|
| rdseed个|布罗德韦尔/禅宗1|第9.1节|7.0版本|2015年之前(19.10)|13.0.1之前的版本,但19.0还添加了-mrdrnd-mrdseed选项)|
最早的GCC和clang版本不能识别-march=ivybridge,只能识别-mrdrnd。(GCC 4.9和clang 3.6用于IvyBridge,如果现代CPU更适合,并不是说你特别想使用IvyBridge。所以使用一个非古老的编译器,并为你真正关心的CPU设置一个合适的CPU选项,或者至少为一个更新的CPU设置一个-mtune=。)

英特尔新推出的oneAPI / ICX编译器都支持rdrand/rdseed,并且基于LLVM内部,因此它们的工作方式与CPU选项的clang类似。(它没有定义__INTEL_COMPILER,这很好,因为它与ICC不同。)

GCC和clang只允许你对目标编译器支持的指令使用intrinsics。如果在你自己的机器上编译,使用-march=native,或者使用-march=skylake或其他方法来为你的目标CPU启用所有伊萨扩展。但是如果你需要你的程序在旧的CPU上运行,并且在运行时检测之后只使用RDRAND或RDSEED,只有那些函数需要__attribute__((target("rdrnd")))rdseed,并且不能内嵌到具有不同目标选项的函数中。或者使用单独编译的库会更容易1。

  • -mrdrnd:由-march=ivybridge-march=znver1(或bdver4探测器APU)及更新版本启用
  • -mrdseed:由-march=broadwell-march=znver1或更高版本启用

通常,如果您要启用一个CPU特性,那么启用该代CPU将具有的其他特性,并设置调优选项是有意义的。(与BMI 2 shlx不同的是,BMI 2 shlx的可变计数移位更高效,因此,全局启用-mrdrnd可能不会使程序在IvyBridge之前的CPU上崩溃,如果您检查CPU特性,并且实际上没有在没有该特性的CPU上运行使用_rdrand64_step的代码。
但是,如果您只打算在某种特定类型的CPU或更高版本上运行代码,则gcc -O3 -march=haswell是一个不错的选择。(-march也暗示着-mtune=haswell,专门针对Ivy Bridge进行的调优是not what you want for modern CPUs。您可以使用-march=ivybridge -mtune=skylake来设置较旧的CPU特性基准,但仍然针对较新的CPU进行调优。)

可在任何地方编译的 Package 函数

这是有效的C++和C。对于C,您可能需要static inline而不是inline,这样您就不需要在.c中手动示例化extern inline版本,以防调试构建决定不内联。(或者在GNU C中使用__attribute__((always_inline))。)
64位版本仅针对x86-64目标定义,因为asm指令在64位模式下只能使用64位操作数大小。(-64)构建,不会使ifdef过于混乱。它 * 确实 * 只定义了rdseed Package 器(如果在编译时启用了它),或者对于无法启用或检测它们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

支持英特尔的内部函数API这一事实意味着unsigned int是一种32位类型,无论uint32_t是定义为unsigned int还是unsigned long(如果有编译器这样做的话)。
Godbolt编译器资源管理器上,我们可以看到这些编译器是如何编译的。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

只要我们使用do{}while()重试循环,ICC就会生成相同的asm;对于while() {},情况更糟,在第一次进入循环之前执行一个rdrand并进行检查。

脚注1:rdrand/rdseed库 Package 程序

librdrandIntel's libdrng具有带重试循环的 Package 函数,如我所示,以及填充uint32_t*uint64_t*的字节或数组缓冲区的 Package 函数。(在某些目标上,始终采用uint64_t*,而不采用unsigned long long*)。
如果你要做运行时CPU特性检测,库也是一个很好的选择,这样你就不必在__attribute__((target))的东西上浪费时间了。不管你怎么做,这都会限制使用内部函数的内联,所以一个小的静态库是等价的。
libdrng还提供了RdRand_isSupported()RdSeed_isSupported(),因此您不需要执行自己的CPUID检查。
但是,如果您打算使用-march=构建比Ivy Bridge / Broadwell或Excavator /Zen 1更新的代码,内联一个2指令重试循环(如clang编译它)的代码大小与函数调用站点大致相同,但不会损坏任何寄存器。rdrand非常慢,因此这可能不是什么大问题,但也意味着没有额外的库依赖性。

rdrand/rdseed的性能/内部组件

关于Intel(不是AMD的版本)的硬件内部的更多细节,请参见Intel's docs。对于实际的TRNG逻辑,请参见Understanding Intel's Ivy Bridge Random Number Generator--这是一个亚稳态锁存器,由于热噪声而稳定到0或1。或者至少Intel是这么说的;基本上不可能真正 * 验证 * rdrand位实际上来自于您购买的CPU中的什么地方。最坏的情况是,如果您将其与其他熵源混合,就像Linux对/dev/random所做的那样,仍然比没有要好得多。
有关内核从缓冲区中提取数据的更多信息,请参阅设计硬件并编写librdrand(如thisthis)的工程师的一些SO回答,这些回答是关于其在Ivy Bridge上的耗尽/性能特性的,Ivy Bridge是第一代提供该特性的产品。

无限重试次数?

asm指令在成功时将FLAGS中的进位标志(CF)设置为1,当它将一个随机数放入目标寄存器时。否则CF=0,输出寄存器= 0。您打算在重试循环中调用它,这就是(我假设)为什么内在函数的名称中有单词step;它是生成一个随机数的一个步骤。
理论上,微代码更新可以改变事情,因此它总是指示故障,例如,如果在某些CPU模型中发现使RNG不可信的问题硬件RNG也有一些自我诊断功能,所以理论上CPU可以判断RNG坏了,不产生任何输出。我还没有听说过任何CPU会这样做,但我还没有去看。而且未来的微码更新总是可能的。
这两种情况都可能导致无限的重试循环。这并不好,但除非你想写一堆代码来报告这种情况,否则这至少是一种可观察的行为,用户可以在不太可能发生的情况下处理。

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

如果缓冲区中没有准备好随机数,CPU可以报告故障,而不是让该内核延迟更长时间。这种设计选择可能与中断延迟有关,或者只是为了使其更简单,而不必在微码中构建重试。

根据设计者的说法,Ivy Bridge从DRNG提取数据的速度无法超过它的速度,即使所有内核都在循环rdrand,但后来的CPU可以。
@jww有一些在libcrypto++中部署rdrand的经验,他发现如果重试次数设置得太低,偶尔会有虚假失败的报告。他从无限次重试中得到了很好的结果,这就是为什么我在这个答案中选择了这个。(我怀疑他会听到用户报告说CPU坏了,总是失败,如果这是一件事的话。)
英特尔的库函数包含一个重试循环,它需要一个重试计数。这很可能是为了处理永久性故障的情况,正如我所说的,我认为这种情况还没有在任何真实的的CPU中发生 *。如果没有一个有限的重试计数,你将永远循环下去。
无限重试计数允许一个简单的API按值返回数值,而没有像OpenSSL的函数那样使用0作为错误返回的愚蠢限制:它们不能随机生成0
如果您确实需要有限的重试次数,我建议您重试次数非常高,比如100万次,因此可能需要一秒钟或一秒钟的旋转时间来给予一个损坏的CPU,如果一个线程在争用内部队列时一再不幸,那么它挨饿的可能性微乎其微。
https://uops.info/在Skylake上测得的吞吐量为:Skylake每3554个周期一次,桤木Lake P内核每1352个周期一次,E内核每1230个周期一次。Zen 2每1809个周期一次。Skylake版本运行了数千个微操作,其他微操作都是低两位数。Ivy Bridge的吞吐量为110个周期,而Haswell的吞吐量已经高达2436个周期。但仍然是两位数的微操作数。
在最新的Intel CPU上,这些糟糕的性能数字可能是由于微码更新,以解决硬件设计时没有预料到的问题。Agner Fog measuredrdrandrdseed在Skylake上每460个周期一个吞吐量,当它是新的时,每一个都要花费16个微操作。2数千个微操作可能是最近更新的那些指令在微码中挂接的额外缓冲区刷新。3 Agner测量Haswell为17个微操作,320个周期。请参阅Phoronix上的RdRand Performance As Bad As ~3% Original Speed With CrossTalk/SRBDS Mitigation
如前一篇文章所述,**缓解串扰涉及到在更新暂存缓冲区之前锁定整个内存总线,**并在内容被清除后解锁。这些指令现在涉及的锁定和序列化对性能非常不利,但幸运的是,大多数实际工作负载不应过多使用这些指令。
如果锁定内存总线就像lock艾德指令的缓存线分割一样,那么它听起来甚至可能会损害其他内核的性能。
(那些周期数是核心时钟周期计数;如果DRNG与内核运行在不同的时钟上,则可能会因CPU型号而异。我想知道uops.info的测试是否在相同硬件的多个内核上运行rdrand,因为Coffee Lake的微操作数是Skylake的两倍,每个随机数的周期数是Skylake的1.4倍。除非更高的时钟导致更多的微码重试?)

oiopk7p5

oiopk7p53#

Microsoft编译器不支持RDSEED和RDRAND指令的内部函数。
但是,您可以使用NASM或MASM实现这些指令。汇编代码可从以下网址获得:
https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide
对于“英特尔编译器,”您可以使用标头来确定版本.您可以使用以下宏来确定版本与子版本:

__INTEL_COMPILER //Major Version
__INTEL_COMPILER_UPDATE // Minor Update.

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

__INTEL_COMPILER  = 1500
__INTEL_COMPILER_UPDATE = 3

有关预定义宏的详细信息,请访问:https://software.intel.com/en-us/node/524490

相关问题