为什么不直接使用 random_device?
这个问题其实很好。
答案是 - 当然,您可以完全按照您在示例中所写的方式使用 std::random_device。 std::random_device 的使用是完全合法和正确的 - 任何发行版都可以在它之上使用,就像任何其他随机引擎一样。如果你不需要 Pseudo-Random Number Generator (PRNG) 像 std::mt19937 或任何其他 - 只是不要使用它。就是这样。
许多人重复的口头禅 - “std::random_device 只是用于播种 blah-blah-blah”是一个随机的 BS(双关语),与 std::random_device 的含义和目的无关。当然std::random_device 可以用作 PRNG 种子 - 就像任何其他随机信息来源一样。
话虽如此 - 您是否真的应该只使用 std::random_device 而不是好的 PRNG 完全取决于您的应用程序需求 - 下面写了一些详细信息。
您应该将任何 PRNG 视为一个数学函数,它采用有限大小的位输入序列并产生具有某种(通常是均匀的)分布的非常长的数字输出序列。如果您将相同的输入位传递给相同的 PRNG 两次 - 您将获得相同的输出序列。就像您使用相同的 x 值计算两次 std::sin(x) 一样 - 您将得到完全相同的正弦值返回。这就是为什么如果您需要避免每次都重复相同的 PRNG 输出数字序列 - 它的输入位(种子)每次都必须不同。显然,由于 PRNG 操作只需要一些计算 - 它通常是本地的且快速的 - 没有系统调用,不涉及外部设备,在等待某事时没有阻塞,没有抛出异常 - 即时结果和高生成率的数字很容易扩展CPU 性能提高。
另一方面,std::random_device 是第一次尝试在 C++ 标准库中引入实际的随机数生成器。
引用自 C++ 标准 (ISO/IEC 14882-2017):
29.6.6 类 random_device
- random_device 统一随机位生成器生成不确定的随机数。
- 如果实施限制阻止生成不确定的随机数,则实施可能会使用随机数引擎。
^^^ 这句话很有趣,因为上面的(1)和(2)完全相互矛盾。 std::random_device 要么产生不确定的随机数,要么其实现阻止它 - 两者不可能同时为真。但是“if”和“may”这个词只出现在(2)中——所以对上面引用的唯一可能的非矛盾理解是(2)中的 “if” 永远不会实现,并且每个实现只会产生不确定的随机数 - 即符合 (1)。
让我们假设符合标准的std::random_device 简单地产生随机和独立位的均匀分布序列。如果我们非常乐观,我们甚至可以希望获得加密安全的随机数——即使 C++ 标准不保证甚至承诺这样的事情。好消息是现代实现实际上提供了它——典型的/dev/urandom UNIX 实现和 Win32 Crypto API 实现都应该足够安全。如果没有加密安全,std::random_device 无论如何都不是很有用的工具。
特别是因为根据 C++ 标准:
result_type operator()();
6 返回: 一个不确定的随机值,均匀分布在 min() 和 max() 之间,包括在内。
实现定义这些值是如何生成的。
^^^ 因此,如果我们真的需要 - 我们可能会在某种程度上将应用程序的可移植性限制为仅产生std::random_device::operator()() 的加密安全输出的那些实现 - 因为这是针对每个特定实现单独定义和记录的(即implementation-defined 意味着 BTW)。当然,如果我们不需要像安全随机数这样的严格要求,我们不应该限制可移植性。
如果没有外部信息源(外部随机性源),就无法产生均匀分布且独立的随机位(AKA 真随机数)的非确定性序列——比如一些传感器信号噪声或一些外部事件的精确测量时间——任何外部并且本质上是不规则的。 (外部是指信息来自外部媒体 - 但传感器本身可能集成到 CPU/SoC)。然后通常过滤任何此类外部随机性源以去除可检测的规律性,以确保符合均匀分布和独立位序列输出的要求。 所有这些都极大地限制了产生的数据速率,并在等待新的外部数据时产生故障和/或阻塞的可能性。
现在让我们权衡一下 PRNG 序列与真随机数在不同类型应用中的优缺点。
-
如果应用程序需要出于信息安全目的生成随机数 - 密码、加密密钥、盐值、安全令牌等,那么毫无疑问 - C++ 标准功能只有 std::random_device 而不是任何兼容的功能(更不用说非- 兼容的),但仅限于提供加密安全实现的那些。一些 PRNG 也可以用于信息安全目的,但只有特殊类别的安全 PRNG,并且只有在它们仔细地播种有足够大小(足够熵)的安全随机种子时 - 所以无论如何你都需要真正的随机数来产生种子。截至目前 - C++ 标准库中没有任何 PRNG 引擎是加密安全的。如果您不相信std::random_device 是安全的(例如 - 您不想仅将可移植性限制为合适的实现,或者您想避免在每次更新后检查每个受支持平台的实现适用性),那么只有非- 可以安全使用标准的可信第三方解决方案 - 例如Win32 加密 API 直接或 UNIX /dev/random 或 /dev/urandom 直接或其他一些非标准解决方案 - 任何对您来说足够值得信赖的解决方案。
-
其他对随机数的不可预测性非常重要的敏感应用程序(如在线赌场、在线投注、股票市场交易等)也可能需要加密安全的随机数——因此 (1) 中的所有注意事项也适用于此.
-
对于大多数其他应用程序 - 例如通常的游戏或科学模拟或任何不涉及金钱或安全且因此随机数序列的潜在可预测性不会受到损害的应用程序 - 典型的优质 PRNG 就足够了。虽然对于许多这些应用程序仍然使用std::random_device 可能没问题,但前提是性能(生成速率和延迟)并不重要。在许多情况下,性能实际上是非常重要的——例如用于科学模拟或实时噪声模拟(用于计算机图形或声音效果等) - 因此,出于性能原因,有时真随机数不适合。
-
还有一些应用程序从根本上需要 PRNG - 例如。某些游戏可能会使用具有固定种子值的 PRNG 即时生成地图/世界/关卡,以避免存储它们以节省磁盘空间(这在早期的 RAM 和存储空间非常少的计算机上是一个流行的技巧,但仍在一些现代游戏中使用也)。另一个例子是音频/视频压缩算法的噪声替代阶段——实际背景噪声被 PRNG 生成的与原始噪声具有相同幅度和频谱特征的伪噪声替换,以仅将种子存储到压缩比特流中,而不是大量存储实际不可压缩的随机信息。
最后一点:
如果您不需要安全随机数并且不想依赖 std::random_device 实现的质量甚至标准合规性,那么单独使用它来生成 PRNG 种子也是一个坏主意。你应该在混合中加入更多的随机性——例如将std::random_device 输出与最大可用精度的当前时间(微秒/纳秒/任何可用)相结合,如果可用,还可以添加一些其他外部传感器的读数(例如原始陀螺仪传感器读数或音频麦克风读数或原始图像传感器读数 - 任何外部和嘈杂)。
例如。
而不是使用这个:
std::mt19937::result_type seed = std::random_device()();
std::mt19937 gen(seed);
最好使用这样的东西:
std::mt19937::result_type seed = std::random_device()()
^ std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count()
^ std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count()
/* ^ more_external_random_stuff */ ;
std::mt19937 gen(seed);
您还可以初始化std::mt19937::state_size (=624) 32 位数字的完整状态种子序列:
std::random_device rd;
std::array< std::uint32_t, std::mt19937::state_size > seed_array;
for( auto it = seed_array.begin(); it != seed_array.end(); ++it )
{
// read from std::random_device
*it = rd();
// mix with a C++ equivalent of time(NULL) - UNIX time in seconds
*it ^= std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
// mix with a high precision time in microseconds
*it ^= std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count();
//*it ^= more_external_random_stuff;
}
std::seed_seq sseq( seed_array.cbegin(), seed_array.cend() );
std::mt19937 gen(sseq);