为什么随机设备创建成本昂贵?

Dai*_*ner 31 c++ random performance c++11

C++11 通过该库支持伪随机数生成<random>

我看过多本书籍,其中提到持续构建和销毁std::random_device, std::uniform_int_distribution<>,std::uniform_real_distribution<>对象的成本非常高,并且他们建议在应用程序中保留这些对象的单个副本。

为什么创建/销毁这些对象的成本很高?这里的贵到底是什么意思呢?就执行速度可执行文件大小或其他方面而言,它是否昂贵?

有人可以提供一些解释吗?

Ted*_*gmo 32

  • std::random_device创建起来可能很昂贵。它可能会打开一个向其提供数据的设备,这需要一些时间。调用起来也可能很昂贵,因为它需要熵来提供真正的随机数,而不是随机数。
  • uniform_int_distribution创造从来都不是昂贵的。它的设计目的是为了便宜。但它确实有内部状态,因此在极端情况下,如果可以的话,请在调用之间保持相同的分布。
  • 伪随机数生成器 (PRNG),如default_random_enginemt19937,通常创建成本较低,但播种成本较高。这有多昂贵很大程度上取决于它们内部保存的状态的大小以及播种程序是什么。例如,std::mersenne_twister_engine标准库中的 保持“大”(倾向于“巨大”)状态,这使得创建成本非常昂贵。

优点:

  • 您通常只使用一个临时变量random_device并调用它一次来播种一个PRNG。因此,需要进行一项昂贵的创建和调用,random_device以及一项昂贵的 PRNG 播种。

  • @TedLyngmo——保留同一对象的另一个原因正是因为它可以具有内部状态,旨在提供适当的分布。丢弃该状态意味着先前和后续生成的值之间没有联系,因此不能保证结果满足分布。 (6认同)
  • @阿米尔是的。**1)** 实际上使用“random_device”作为随机数源的情况非常罕见(除了用于快速 PRNG 的播种)。**2)** 如果您_确实_使用它(不仅仅是种子 PRNG:s),只需创建一个实例。这适用于所有随机数生成器。只需创建一个实例。 (4认同)

Hom*_*512 23

简短的答案是:这取决于系统和库的实现

标准库的类型

  • 在低质量的实现中,例如 9.2 之前的 MinGWrandom_device只不过是一个多余的包装器std::rand()。我不知道这样的实现仍然存在,但有人可能会纠正我

  • 在裸机系统上,例如嵌入式微控制器,random_device可以直接与硬件随机数生成器或相应的 CPU 功能交互。这可能需要也可能不需要昂贵的设置,例如配置硬件、打开通信通道或丢弃前 N 个样本

  • 您很可能位于托管平台上,这意味着具有硬件抽象级别的现代操作系统。让我们在本文的其余部分考虑这种情况

种类random_device

您的系统可能有一个真正的硬件随机数生成器,例如 TPM 模块可以充当其中之一。请参阅可信平台模块如何生成其真正的随机数?对此硬件的任何访问都必须通过操作系统,例如在 Windows 上,这可能是加密服务提供商 (CSP)

或者您的 CPU 可能内置一些指令,例如 Intel 的rdrandrdseed指令。在这种情况下,random_device直接映射到这些的 a 只需要发现它们是否可用并检查它们是否可操作。rdrand例如可以检测硬件故障,此时实现可以提供后备。请参阅Ivy Bridge 上 RDRAND 的耗尽特性是什么?

然而,由于这些功能可能不可用,因此操作系统通常提供熵池来生成随机数。如果这些硬件功能可用,您的操作系统可能会使用它们来为该池提供数据,或者在池耗尽时提供后备。您的标准库很可能只是通过特定于操作系统的 API 访问该池。

这就是random_device目前所有主流库实现中的内容:用于随机数生成的操作系统设施的访问点。那么这些的设置开销是多少?

系统API

  • 传统的 POSIX (UNIX) 操作系统通过伪设备/dev/random和提供随机数/dev/urandom。因此设置成本与打开和关闭该文件相同。我想这就是你的书所指的

  • 由于这个 API 有一些缺点,新的 API 已经出现,例如 Linux 的getrandom. 这个不会有任何设置成本,但如果内核不支持它,它可能会失败,此时一个好的库可能会/dev/urandom再次尝试

  • Windows 库可能会通过其加密 API。因此,无论是旧的 CSP APICryptGenRandom还是新的BCryptGenRandom. 两者都需要服务或算法提供者的句柄。所以这可能类似于/dev/urandom方法

结果

在所有这些情况下,您将需要至少一个系统调用来访问 RNG,并且这些调用比正常的函数调用要慢得多。请参阅系统调用开销,甚至rdrand每条指令的指令大约有 150 个周期。请参阅Ivy Bridge 上 RDRAND 指令的延迟和吞吐量是多少?或者更糟糕的是,查看各种编译器上的 RDRAND 和 RDSEED 内在函数?

库(或用户)可能会试图通过缓冲大量随机字节(例如使用缓冲文件 I/O)来减少系统调用的数量。random_device假设这会丢弃缓冲区,这又会让打开和关闭变得不明智。

此外,操作系统熵池的大小有限,可能会耗尽,这可能会导致整个系统受到影响(通过使用低于标准的随机数或阻塞直到熵再次可用)。这和缓慢的性能意味着您通常不应该random_device直接将其输入uniform_int_distribution或类似的东西。相反,使用它来初始化伪随机数生成器。

当然这也有例外。例如,如果您的程序在整个运行过程中只需要 64 个随机字节,那么绘制 2.5 kiB 随机状态来初始化梅森扭曲器将是愚蠢的。或者,如果您需要尽可能好的熵,例如生成加密密钥,那么请务必使用它(或者更好的是,为此使用库;永远不要重新发明加密!

  • 需要明确的是,主流的“std::random_device”实现*不*直接使用“rdrand”,并且出于信任原因以及缺乏有保证的支持,我不希望将来出现这种情况。尽管它们*可能*在某些时候使用 Linux `getrandom` (相当于从 /dev/urandom 读取),因此没有任何构建开销,仅用于实际使用。(或者也许像“fd”这样的状态被初始化为“-1”,以指示它应该在回退到打开“/dev/urandom”之前尝试“getrandom”。) (5认同)
  • @Homer512 - 这里的问题是“random_device”并不正式要求使用系统特定的随机数源。它可以是任何东西。你是对的,它**应该**从操作系统中获取一些东西,据推测,操作系统提供了某种硬件支持。但实际上并不需要这样做;调用“rand()”的实现符合标准。+1。 (4认同)
  • @Homer512:x86 `rdrand` 和 [`rdseed`](https://www.felixcloutier.com/x86/rdseed) 正是:从片上熵源中提取“真正”随机性的非特权指令。但大多数人认为,由于可能存在后门/无法验证的黑匣子原因,在密码敏感的情况下直接使用是不明智的。因此,即使你使用 `g++ -march=skylake` 或其他东西(具有 rdrand 和 rdseed 的 CPU)进行编译,主流 C++ 实现也不会直接使用它。 (3认同)
  • 是的,很好的编辑。请注意,在某些具有微代码更新的 CPU 上,“rdrand”速度[慢得多](/sf/answers/5058613871/),这些微代码更新可以解决潜在的信息暴露问题,例如数千个周期。(https://www.phoronix.com/scan.php?page=news_item&amp;px=RdRand-3-Percent / https://arstechnica.com/gadgets/2019/10/how-a-months-old-amd-microcode -bug-destroyed-my-weekend/) 但是如果 C++ 实现*直接*使用它,那么构造一个 `std::random_device` 应该没有开销,就像它乐观地期望能够使用 `getrandom` 一样。 (2认同)