具有任意边界的快速,无偏,整数伪随机生成器

时间:2014-08-30 15:37:15

标签: c++ performance algorithm random random-sample

对于monte carlo集成过程,我需要从中抽取批次随机样本 具有N个桶的直方图,其中N是任意的(即不是2的幂)但是 在计算过程中根本不会改变。

通过 lot ,我的意思是大约10 ^ 10,数十亿,所以几乎任何 面对绝对的数字,一种冗长的预计算可能是值得的 样本)。

我有一个非常快速的统一伪随机数发生器 通常产生无符号的64位整数(讨论中的所有内容) 以下是未签名的。)

抽取样本的天真方式:histogram[ prng() % histogram.size() ]

天真的方式非常慢:模运算使用整数除法(IDIV) 这是非常昂贵的编译器,不知道histogram.size()的价值 在编译时,不能达到其通常的魔法(即http://www.azillionmonkeys.com/qed/adiv.html

事实上,我的大部分计算时间都花在提取模型上。

稍微不那么天真的方式:我使用有能力的libdivide(http://libdivide.com/) 通过在编译时不知道的常数来快速“划分”。

这给了我一个非常好的胜利(25%左右),但我有一种唠叨的感觉,我能做到 更好,这就是原因:

  • 第一直觉:libdivide计算分裂。我需要的是模数,并达到目的 我必须做一个额外的mult和sub:mod = dividend - divisor*(uint64_t)(dividend/divisor)。我怀疑使用libdivide-type可能会有一个小胜利 直接生成模数的技术。

  • 第二种直觉:我实际上对模数本身并不感兴趣。我真正想要的是 有效地产生一个均匀分布的整数值,保证严格小于N.

模数是一种相当标准的方法,因为它有两个属性:

  • A)如果mod(prng(), N)

  • ,保证prng()均匀分布
  • B)mod(prgn(), N)保证属于[0,N [

但模数是/更多只是满足上面的两个约束,事实上 它可能做得太多了。

所有需要的是一个函数,任何函数,它遵守约束条件A)和B)并且快速

所以,长篇介绍,但这里有两个问题:

  • 有没有相当于libdivide的东西直接计算整数模

  • 是否有一些整数X和N的函数F(X,N)遵循以下两个约束条件:

    • 如果X是一个均匀分布的随机变量,那么F(X,N)分布不均
    • F(X,N)保证为[0,N [

(PS:我知道如果N很小,我不需要输出所有64位 PRNG。事实上,我已经这样做了。但就像我说的那样,即使是那种优化 与必须计算模数的大量减肥相比,这是一个小小的胜利。

编辑:prng() % N确实不是完全均匀分布的。但是对于N足够大,我认为这不是问题(或者是它?)

编辑2:prng() % N确实可能分布很差。我从来没有意识到它会变得多么糟糕。哎哟。我发现了一篇很好的文章:http://ericlippert.com/2013/12/16/how-much-bias-is-introduced-by-the-remainder-technique

9 个答案:

答案 0 :(得分:3)

在这种情况下,最简单的方法可能效果最好。如果你的PRNG足够快,一个非常简单的方法可能是预先计算一个小于下一个比你的N更大的2的功率用作掩码。也就是说,给出一些在二进制文件中看起来像0001xxxxxxxx的数字(其中x表示我们不关心它是1还是0)我们想要像{{1}这样的掩码}。

从那里,我们生成数字如下:

  1. 生成一个数字
  2. 000111111111带着你的面具
  3. 如果结果> n,转到1
  4. 这的确切有效性将取决于N与2的幂的接近程度.2的每个连续功率(显然足够)是其前身的两倍。因此,在最好的情况下,N恰好比2的幂小1,并且我们在步骤3中的测试总是通过。我们只添加了一个掩码,并与PRNG本身的时间进行了比较。

    在最坏的情况下,N恰好等于2的幂。在这种情况下,我们希望丢弃大约一半的数字。

    平均而言,N最终大约在2的幂之间。这意味着,平均而言,我们扔掉了大约四分之一的输入。我们几乎可以忽略面具和比较本身,所以我们的速度损失与" raw"生成器基本上等于我们丢弃的输出数量,平均为25%。

答案 1 :(得分:2)

如果您可以快速访问所需指令,则可以将prng()N进行64位乘法运算,并返回128位结果的高64位。这有点像将[0,1]中的统一实数乘以N并截断,对模数版本的顺序进行偏差(即,几乎可以忽略不计;这个答案的32位版本会很小但也许是明显的偏见。)

探索的另一种可能性是在单个位上运行的无分支模运算上使用单词并行,以便批量获取随机数。

答案 2 :(得分:1)

Libdivide或其他任何优化模数的复杂方法都只是矫枉过正。在你的情况下,唯一合理的方法是

  1. 确保您的表格大小为2的幂(如果必须,请添加填充!)

  2. 使用位掩码操作替换模运算。像这样:

    size_t tableSize = 1 << 16;
    size_t tableMask = tableSize - 1;
    
    ...
    
    histogram[prng() & tableMask]
    
  3. 位掩码操作是任何值钱的CPU上的单个循环,你不能超过它的速度。

    -

    注意:
    我不知道随机数生成器的质量,但使用随机数的最后几位可能不是一个好主意。一些RNG在最后比特中产生差的随机性并且在较高比特中产生更好的随机性。如果您的RNG就是这种情况,请使用bitshift来获取最重要的位:

    size_t bitCount = 16;
    
    ...
    
    histogram[prng() >> (64 - bitCount)]
    

    这与位掩码一样快,但它使用不同的位。

答案 3 :(得分:1)

您可以将histogram扩展为&#34;大&#34;通过循环它的两个幂,用一些虚拟值填充尾随空格(保证永远不会出现在真实数据中)。例如。给出直方图

[10, 5, 6]

将它扩展到16,如此(假设-1是一个合适的哨兵):

[10, 5, 6, 10, 5, 6, 10, 5, 6, 10, 5, 6, 10, 5, 6, -1]

然后可以通过二进制掩码histogram[prng() & mask]进行采样mask = (1 << new_length) - 1,并检查要重试的标记值,即

int value;
do {
    value = histogram[prng() & mask];
} while (value == SENTINEL);

// use `value` here

通过确保绝大多数元素是有效的(例如,在上面的示例中,只有1/16查找将&#34;失败&#34;,并且此速率可以是通过将其扩展到例如64)进一步减少。你甚至可以使用&#34;分支预测&#34;检查时提示(例如__builtin_expect in GCC),以便编译器将代码命令为value != SENTINEL时的最佳情况,这有希望是常见的情况。

这非常关注记忆与速度的关系。

答案 4 :(得分:1)

只有一些想法可以补充其他好的答案:

  1. 模数操作花费了多少时间,你怎么知道这个百分比是多少?我只是问,因为有时人们会说某些东西非常慢,实际上它只有不到10%的时间而且他们认为它很大,因为他们使用的是一个愚蠢的自我时间分析器。 (与随机数生成器相比,我很难设想模运算需要花费大量时间。)

  2. 什么时候知道水桶的数量?如果它没有太频繁地改变,你可以编写一个程序生成器。当桶的数量发生变化时,会自动打印出一个新程序,编译,链接并将其用于大规模执行。 这样,编译器就会知道桶的数量。

  3. 您是否考虑使用quasi-random number generator而不是伪随机生成器?它可以在更少的样品中为您提供更高的集成精度。

  4. 可以减少铲斗的数量而不会过多地损害整合的准确性吗?

答案 5 :(得分:1)

非均匀性dbaupp警告可以通过拒绝和重新绘制不小于M*(2^64/M)的值(在取模数之前)来进行侧面步骤。
如果M可以表示为不超过32位,则可以通过重复乘法(参见David Eisenstat的答案)或divmod来获得小于M的多个值;或者,您可以使用位操作将位模式单独输出M,再次拒绝不小于M的值。
(我会惊讶于模数在随机数生成的时间/周期/能耗方面没有相形见绌。)

答案 6 :(得分:0)

要为水桶提供食物,您可以使用std:: binomial_distribution直接喂食每个水桶,而不是将一个样品一个样品喂入水桶:

以下可能会有所帮助:

int nrolls = 60; // number of experiments
const std::size_t N = 6;
unsigned int bucket[N] = {};

std::mt19937 generator(time(nullptr));

for (int i = 0; i != N; ++i) {
    double proba = 1. / static_cast<double>(N - i);
    std::binomial_distribution<int> distribution (nrolls, proba);
    bucket[i] = distribution(generator);
    nrolls -= bucket[i];
}

Live example

答案 7 :(得分:0)

除了整数除法,你可以使用定点数学,即整数乘法&amp;位位移。假如你的prng()返回0-65535范围内的值并且你希望这个量化范围为0-99,那么你做(prng()* 100)&gt;&gt; 16。只需确保乘法不会溢出整数类型,因此您可能必须将prng()的结果右移。请注意,此映射优于模数,因为它保留了均匀分布。

答案 8 :(得分:0)

谢谢大家的建议。

首先,我现在完全相信模数真的是邪恶的 在大多数情况下,它非常慢会产生不正确的结果。

在实施和测试了不少建议后,有什么内容 似乎是提出解决方案的最佳速度/质量妥协 @Gene:

  1. normalizer预先计算为:

    auto normalizer = histogram.size() / (1.0+urng.max());

  2. 用以下方式绘制样本

    return histogram[ (uint32_t)floor(urng() * normalizer);

  3. 这是迄今为止我尝试过的所有方法中最快的,据我所知,
    它会产生更好的分布,即使它可能不是那么完美 作为拒绝方法。

    编辑:我实施了David Eisenstat的方法,该方法与Jarkkol的建议大致相同:index = (rng() * N) >> 32。它的工作原理与浮点归一化相同,速度更快(事实上快了9%)。所以这是我现在的首选方式。