为什么不使用random_device?

bol*_*lov 18 c++ random c++11

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

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

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

我不明白的是为什么不使用这个:

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

// get random numbers with:
dist(rd);
Run Code Online (Sandbox Code Playgroud)

据我所知,这很有效.

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

std::random_device rd;
std::mt19937 e{rd()}; // or std::default_random_engine e{rd()};
std::uniform_int_distribution<int> dist{1, 5};

// get random numbers with:
dist(e);
Run Code Online (Sandbox Code Playgroud)

我不是在谈论特殊用途,例如加密,只是你的基本入门文章.

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

另外,为什么不呢:

std::mt19937 e{std::random_device{}()};
Run Code Online (Sandbox Code Playgroud)

Chr*_*eck 21

另外,为什么不呢:

std::mt19937 e{std::random_device{}()};

如果你只这样做一次可能没问题,但如果你多次这样做,最好跟踪你的情况,std::random_device而不是不必要地创建/销毁它.

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

在Windows上,据我所知,std::random_device代表了对微软加密API的一些调用,因此每次执行此操作时,您将初始化并销毁某些加密库接口.

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

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

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

 std::random_device rd;
 std::mt19937 e{rd()}; // or std::default_random_engine e{rd()};
 std::uniform_int_distribution<int> dist{1, 5};
Run Code Online (Sandbox Code Playgroud)

至少我的想法是:

  • std::mt19937是一个非常简单和可靠的随机发生器.该实现是由标准强制执行的,至少在boost中,它在原始mt19937论文中使用了相同的代码.这段代码非常稳定,而且是跨平台的.您可以非常自信地初始化它,查询它等等将在您编译它的任何平台上编译为类似的代码,并且您将获得类似的性能.

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

我认为它的方式,std::random_device应该是一个改进的播种版本time(NULL)- 这是一个低杠,因为time(NULL)是一个非常糟糕的种子考虑所有事情.我通常time(NULL)在白天用它生成种子的地方使用它.我并不认为除此之外有用.

  • @BrettHale 这是很好的一般建议,但这不是问题所问的。OP 询问为什么不首先使用 `random_device`。此外,我认为说 mt19937 PRNG 的所有好处都丢失了是不合理的。即使 mt19937 以几个字节作为种子,在很多方面也比 libc rand 函数更好。他们将这种“引导”机制构建到标准库中是有原因的,这样您就可以用几个字节为 mt19937 设置种子,然后使用另一个 PRNG 将这几个字节拉伸到完整的 mt19937 状态——这有时是一种有用的技术 (3认同)
  • 而不是具有潜在的“2^19937-1”状态的“mt19937”PRNG(具有**624个32位元素**,即**`uint32_t状态[624]`**向量),您已将 `mt19937` 限制为最多具有带有此单个种子值的 `2^32` 唯一状态 - 因此,最多具有 `2^32` 随机数据“流”。这*可能*适用于某些应用程序,但是`mt19937` PRNG 的优势就消失了。 (2认同)

Ser*_*ich 10

为什么不直接使用 random_device 呢?

这个问题其实是一个非常好的问题。

答案是 - 当然,您可以像在示例中编写的那样使用std::random_device 。它是完全合法和正确的使用std::random_device- 并且任何发行版都可以在它之上使用,就像任何其他随机引擎一样。如果您不需要像std::mt19937或任何其他那样的伪随机数生成器 (PRNG) - 只是不要使用它。就是这样。

很多人重复的咒语是“std::random_device 只是为了播种等等”是一个随机的 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

  1. random_device 均匀随机位生成器生成不确定的随机数。
  2. 如果实现限制阻止生成不确定性随机数,则实现可以采用随机数引擎。

^^^ 这句话很有趣,因为上面的(1)和(2)完全矛盾。std::random_device要么产生不确定的随机数,要么其实现阻止它 - 两者不可能同时为真。但是单词“if”和“may”仅出现在(2)中 - 因此对上面引用的唯一可能的非矛盾理解是(2)中的“if”永远不会实现,并且每个实现都只会产生不确定的随机数 -即符合(1)。

我们假设符合标准std::random_device只是产生均匀分布的随机和独立位序列。如果我们超级乐观,我们甚至可以希望获得加密安全的随机数 - 尽管 C++ 标准不保证甚至承诺类似的事情。好消息是现代实现实际上提供了它 - 典型的/dev/urandomUNIX 实现和 Win32 Crypto API 实现都应该足够安全。无论如何,如果没有加密安全,std::random_device就不是非常有用的工具。

特别是根据 C++ 标准:

结果类型运算符()();

6 返回:一个不确定的随机值,均匀分布在min()max()之间(含)。 这些值的生成方式是由实现定义的

^^^ 因此,如果我们确实需要 - 我们可能会在某种程度上将应用程序的可移植性限制为仅产生加密安全输出的那些实现std::random_device::operator()()- 因为这是针对每个特定实现单独定义和记录的(这就是实现定义的意思顺便说一句)。当然,如果我们不需要一些严格的要求,例如安全随机数,我们不应该限制可移植性。

显然,如果没有外部信息源(外部随机源),就无法生成均匀分布且独立的随​​机位(又称真随机数)的非确定性序列 - 例如某些传感器信号噪声或某些外部事件的精确测量时间 - 任何外部且本质上不规则的东西。(我所说的外部是指信息来自外部媒体 - 但传感器本身可能集成到 CPU/SoC)。然后,通常对任何此类外部随机性源进行过滤,以去除可检测到的规律性,以确保符合均匀分布且独立的比特序列输出的要求。所有这些都极大地限制了产生的数据速率,并在等待新的外部数据时产生失败和/或阻塞的可能性。

现在让我们针对不同类型的应用权衡 PRNG 序列与真随机数的优缺点。

  1. 如果应用程序出于信息安全目的需要生成随机数 - 密码、加密密钥、盐值、安全令牌等,那么毫无疑问 - 仅是 C++ 标准功能,std::random_device而不是任何兼容的功能(更不用说不兼容的功能),而只是那些提供加密安全实现的。一些 PRNG 也可以用于信息安全目的,但仅限特殊类别的安全 PRNG,并且只有当它们仔细播种足够大小(足够熵)的安全随机种子时 - 因此无论如何您都需要真正的随机数来生成种子。截至目前 - C++ 标准库中没有任何 PRNG 引擎是加密安全的。如果您不相信std::random_device安全(例如,您不想将可移植性限制在合适的实现上,或者您想避免在每次更新后检查每个受支持平台的实现适用性),那么只有非标准可信可以安全地使用第三方解决方案 - 例如直接 Win32 加密 API 或直接 UNIX /dev/random 或 /dev/urandom 或其他一些非标准解决方案 - 只要对您来说足够值得信赖。

  2. 随机数的不可预测性非常重要的其他敏感应用程序 - 例如在线赌场、在线投注、股票市场交易等 - 也可能需要加密安全的随机数 - 因此 (1) 中的所有考虑因素也适用于此。

  3. 对于大多数其他应用程序(例如通常的游戏或科学模拟或任何不涉及金钱或安全的应用程序,因此随机数序列的潜在可预测性不会造成损害),典型的高质量 PRNG 就足够了。虽然仍然可以用于std::random_device许多此类应用程序,但前提是性能(生成速率和延迟)并不重要。在许多情况下,性能实际上非常重要 - 例如对于科学模拟或实时噪声模拟(对于计算机图形或声音效果等) - 因此有时真正的随机数出于性能原因并不适合。

  4. 此外,还有一些应用程序从根本上需要 PRNG - 例如,某些游戏可能会使用具有固定种子值的 PRNG 动态生成地图/世界/关卡,以避免存储它们以节省磁盘空间(这是早期计算机上的一种流行技巧,几乎没有RAM 和存储空间,但仍在一些现代游戏中使用)。另一个例子是音频/视频压缩算法的噪声替换阶段 - 其中实际背景噪声被替换为与原始噪声具有相同幅度和频谱特性的 PRNG 生成的伪噪声,以便仅将种子存储到压缩比特流中,而不是存储大量种子。实际不可压缩的随机信息。

最后一点:

如果您不需要安全的随机数,并且不想依赖std::random_device实施的质量甚至标准合规性,那么单独使用它来生成 PRNG 种子也是一个坏主意。您应该在混合中加入更多的随机性 - 例如,将std::random_device输出与最大可用精度的当前时间(微秒/纳秒/任何可用的)相结合,如果可用,也添加一些其他外部传感器的读数(例如原始陀螺仪传感器读数或音频麦克风读数或原始图像传感器读数 - 任何外部和噪音)。

例如。而不是使用这个:

std::mt19937::result_type seed = std::random_device()();

std::mt19937 gen(seed);
Run Code Online (Sandbox Code Playgroud)

最好使用这样的东西:

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);
Run Code Online (Sandbox Code Playgroud)

您还可以初始化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);
Run Code Online (Sandbox Code Playgroud)


Bia*_*sta 9

这篇文章是一个很好的开始.

我要综合几点:

  • 它的成本未知.

    从这个"设备"读取数字的代价是多少?这是未指明的.例如,它可以在Linux系统上读取/ dev/random,这可能会阻塞等待熵的长时间(由于各种原因,这本身就存在问题).

根据我的个人经验,我已经通知它std::random_device通常比简单的伪随机算法慢.一般情况下这可能不是真的,但通常情况确实如此.那是因为它可能涉及物理设备或其他硬件而不是简单的CPU.

  • 它实际上可能是确定性的.

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