为什么不使用random_device?

时间:2016-09-02 09:11:00

标签: c++ c++11 random

我对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 e{rd()}; // or std::default_random_engine e{rd()};
std::uniform_int_distribution<int> dist{1, 5};

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

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

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

另外,为什么不呢:

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

4 个答案:

答案 0 :(得分:16)

  

另外,为什么不呢:

     

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

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

查看用于实现std::random_device的libc ++源代码可能会有所帮助,这非常简单。它只是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};

至少我想到的方式是:

  • 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)来生成种子。除此之外,我并不认为这有用。

答案 1 :(得分:7)

This article是一个很好的起点。

我将综合几点:

  • 成本未知。
      

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

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

  • 它可能确实是确定性的。
      

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

答案 2 :(得分:1)

<块引用>

为什么不直接使用 random_device?

这个问题其实很好。

答案是 - 当然,您可以使用 std::random_device,就像您在示例中所写的那样。 std::random_device 的使用是完全合法和正确的 - 任何分布都可以在它之上使用,就像任何其他随机引擎一样。如果您不需要 Pseudo-Random Number Generator (PRNG) 之类的 std::mt19937 或任何其他 - 只是不要使用它。就是这样。

Mantra 被很多人重复一遍——“std::random_device is just for seeding 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

  1. random_device 统一随机位生成器生成非确定性随机数。
  2. 如果实施限制阻止生成不确定性随机数,则实施可能会使用随机数引擎。

^^^ 这句话很有趣,因为上面的(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()() 的加密安全输出的那些实现 - 因为对于每个特定实现,这是明确定义和单独记录的(这就是实现定义 BTW 的意思)。当然,如果我们不需要一些严格的要求,比如安全随机数,我们不应该限制可移植性。

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

现在让我们权衡 PRNG 序列 VS 真随机数对于不同类型应用的利弊。

  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);

最好使用这样的东西:

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);

答案 3 :(得分:0)

Okidoki,让我们消除一些困惑!

std::random_device 是等效于 C 中的 time(NULL) 的标准 C++。由于以下原因,该标准通常建议您仅将其用于播种:

<块引用>

一旦熵池耗尽,random_device 的许多实现的性能会急剧下降。对于实际使用,random_device 通常只用于播种一个 PRNG,例如 mt19937。

(访问 https://en.cppreference.com/w/cpp/numeric/random/random_device 查看他们的措辞)。

然而,这种推理并不完全准确。该标准还声明(来自 C++11 N4296 draft),如果依赖于系统的 CSPRNG 不可用,则允许 std::random_device 的实现是确定性的:

<块引用>

26.5.6 类 random_device

...

2 如果实现限制阻止生成非确定性随机数,则实现可能会使用随机数引擎。

因此,std::random_device 可能永远不会与熵池交互,以至于 entropy 成员函数可以返回零:

<块引用>

26.5.6 类 random_device

...

double entropy() const noexcept;

5 返回:如果实现使用随机数引擎,则返回 0.0...

这个事实使得 std::random_device 作为生成器不可靠,而且它也可能不可靠用于播种,C++ 标准显然打算将其主要用途用于(是的,有 track record 的糟糕实现).

至少对于 std::mt19937,您知道无论平台如何,您都会获得确定性伪随机值,而 std::random_device 可能会给您确定性、非确定性或简单常数,这些常数因平台而异处于不同的性能水平。

最后,你可以这样写std::random_device

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

但这只会为您提供一个 std::random_device 的实例,其范围仅限于 e。如果您计划为 e 播种一次,这很好,但如果您希望重新播种或使用 std::seed_seq,至少为了性能,则确实需要一个可重用的实例。

但是,您永远无法实现以下目标:

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

这是因为 C++11 Seed-Sequence Concept 的限制性规则。 std::random_device 缺少一大堆在 C++11 眼中被认为是合适的种子排序器的东西。但是,以下规则几乎可以保证 std::random_device 不能成为种子排序器:

<块引用>

26.5.1.2

表 115

r.param(ob) - 将一个 32 位单元序列复制到给定的目的地,这些单元可以提供给第二个 S 类型对象的构造函数,并且会在第二个对象中再现与当前对象无法区分的状态第一个对象的状态。

花哨的措辞只是说明标准将种子序列视为一组固定的种子数据,因此排除了种子数据的不确定来源。 std::random_device 的不变量违反了这条规则,因为不确定性确保 std::random_device 不能创建“不可区分”的对象副本。即使尝试制作涉及 std::random_device 的种子序列包装器也非常困难。

总而言之,std::random_device 的角色被标准明确定义为播种者。它简单、易读且有缺陷,但只要你看起来不太难,它就可以工作,就像 time(NULL) 一样。