为啥不直接使用 random_device?

Posted

技术标签:

【中文标题】为啥不直接使用 random_device?【英文标题】:Why not just use random_device?为什么不直接使用 random_device? 【发布时间】:2017-01-10 08:17:22 【问题描述】:

我对 c++11 随机库有点困惑。

我的理解:我们需要两个独立的概念:

随机引擎(可以是伪的(需要种子)或真实的) 分布:它将从引擎获得的数字映射到特定的区间,使用特定的分布。

我不明白为什么不直接使用这个:

std::random_device rd;
std::uniform_int_distribution<int> dist(1, 5);

// get random numbers with:
dist(rd);

据我所知,这很好用。

相反,这是我在大多数示例/网站/文章中发现的:

std::random_device rd;
std::mt19937 erd(); // or std::default_random_engine erd();
std::uniform_int_distribution<int> dist1, 5;

// get random numbers with:
dist(e);

我不是在谈论特殊用途,例如密码学,只是您的基本入门文章。

我的怀疑是因为std::mt19937(或std::default_random_engine)接受种子,在调试会话期间提供相同的种子可以更容易地进行调试。

另外,为什么不只是:

std::mt19937 estd::random_device();

【问题讨论】:

“一旦熵池耗尽,random_device 的许多实现的性能会急剧下降。在实际使用中,random_device 通常仅用于播种 PRNG,例如 mt19937” @987654321 @ @Michael 那应该是一个答案 嗯,答案可能需要更详细地说明(例如,为什么性能会下降)。因此,我将其发布为评论。 关于你的最后一个问题;这就是正是我经常为许多非加密授权目的启动prng的方式。 @Michael 可能,但肯定不是必须的。 【参考方案1】:

为什么不直接使用 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()ma​​x() 之间,包括在内。 实现定义这些值是如何生成的。

^^^ 因此,如果我们真的需要 - 我们可能会在某种程度上将应用程序的可移植性限制为仅产生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);

【讨论】:

【参考方案2】:

另外,为什么不只是:

std::mt19937 estd::random_device();

如果您只执行一次可能没问题,但如果您会执行多次,最好跟踪您的std::random_device,而不是不必要地创建/销毁它。

查看 libc++ 源代码以实现std::random_device 可能会有所帮助,这非常简单。它只是std::fopen("/dev/urandom") 的一个薄包装。因此,每次创建 std::random_device 时,您都会获得另一个文件系统句柄,并支付所有相关费用。

据我了解,在 Windows 上,std::random_device 代表对 Microsoft 加密 API 的一些调用,因此每次执行此操作时都将初始化和销毁​​一些加密库接口。

这取决于您的应用程序,但出于一般目的,我不会认为这种开销总是可以忽略不计。有时确实如此,然后这很棒。

我想这与您的第一个问题有关:

相反,这是我在大多数示例/网站/文章中发现的:

 std::random_device rd;
 std::mt19937 erd(); // or std::default_random_engine erd();
 std::uniform_int_distribution<int> dist1, 5;

至少我是这么想的:

std::mt19937 是一个非常简单可靠的随机生成器。它是独立的,将完全存在于您的进程中,而不需要调用操作系统或其他任何东西。该实现是标准强制,至少在 boost 中,它在任何地方都使用相同的代码,源自原始的mt19937 论文。这段代码非常稳定并且是跨平台的。您可以非常自信地对其进行初始化、查询等操作将在您编译它的任何平台上编译为类似的代码,并且您将获得类似的性能。

std::random_device 相比之下非常不透明。你并不确切知道它是什么,它会做什么,或者它的效率如何。你甚至不知道它是否真的可以被获取——当你尝试创建它时它可能会抛出一个异常。你知道它不需要种子。您通常不应该从中提取大量数据,只需使用它来生成种子。有时,它作为加密 API 的一个很好的接口,但实际上并不需要这样做,遗憾的是有时它不需要。它可能对应于 unix 上的/dev/random,它可能对应于/dev/urandom/。它可能对应于一些 MSVC 加密 API (visual studio),或者它可能只是一个固定常量 (mingw)。如果您为某些手机进行交叉编译,谁知道它会做什么。 (即使您确实得到了/dev/random,您仍然会遇到性能可能不一致的问题——它可能看起来工作得很好,直到熵池用完,然后运行缓慢作为一条狗。)

我的想法是,std::random_device 应该是 time(NULL) 播种的改进版本——这是一个低标准,因为从所有方面考虑,time(NULL) 都是一个非常糟糕的种子。我通常在过去使用time(NULL) 生成种子的地方使用它。除此之外,我真的认为它并没有什么用处。

【讨论】:

而不是具有潜在2^19937 - 1 状态的mt19937 PRNG(具有624 个32 位元素,即uint32_t state[624] b> 向量),您已将 mt19937 限制为具有此单个种子值的至多 2^32 唯一状态 - 因此,至多 2^32 随机“流”数据。这可能适用于某些应用程序,但mt19937 PRNG 的好处就失去了。 @BrettHale 这是一个很好的一般建议,但这不是问题所在。 OP 在问为什么不首先使用random_device。另外,我认为说 mt19937 PRNG 的所有好处都失去了是不合理的。在很多方面,即使播种了几个字节的 mt19937 也比 libc rand 函数好。他们将这种“引导”机制构建到标准库中是有原因的,这样您就可以用几个字节为 mt19937 播种,它会使用另一个 PRNG 将这几个字节拉伸到完整的 mt19937 状态——这有时是一种有用的技术跨度> 【参考方案3】:

This article 是一个很好的起点。

我将只综合几点:

成本未知。

从这个“设备”读取一个数字的成本是多少?那是未指定的。例如,它可能是从 Linux 系统上的 /dev/random 读取,这可能会长时间阻塞等待熵(由于各种原因,这本身就有问题)。

根据我的个人经验,我发现std::random_device 通常比简单的伪随机算法慢。一般来说,这可能不是真的,但通常确实如此。那是因为它可能涉及物理设备或简单 CPU 以外的其他硬件。

它实际上可能是确定性的。

C++11 的 std::random_device 不需要是不确定的!实现可以并且确实将其实现为具有固定种子的简单 RNG,因此它为程序的每次运行产生相同的输出。

【讨论】:

以上是关于为啥不直接使用 random_device?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用 glTranslatef?为啥不直接更改渲染坐标?

为啥使用闭包进行赋值而不是直接为键赋值?

为啥 Electron 页面不直接加载 JQuery? [复制]

为啥我不能直接分配数据源?

为啥 shared_ptr 不允许直接分配

c#里为啥有的使用时函数需要new一个对象而有的不用?为啥不直接调用就好?