在循环中使用normal_distribution

using normal_distribution in a loop

本文关键字:normal distribution 循环      更新时间:2023-10-16

我想知道将normal_distribution放入循环中是否存在问题。

以下是以这种奇怪方式使用normal_distribution的代码:

std::default_random_engine generator;
//std::normal_distribution<double> distribution(5.0,2.0);
for (int i=0; i<nrolls; ++i) {
std::normal_distribution<double> distribution(5.0,2.0);
float x = distribution(generator);
}

normal_distribution对象放在循环之外可能比将其放在循环中更有效。当它位于循环内时,normal_distribution对象每次都可以重新构造,而如果它在循环外部,则只构造一次。

组件的比较。

根据对程序集的分析,在循环外声明distribution更有效。

让我们看一下两个不同的函数,以及相应的程序集。其中一个在循环内声明distribution,另一个在循环外声明。为了简化分析,它们在这两种情况下都声明为 sst,因此我们(和编译器)知道发行版不会被修改。

您可以在此处查看完整的程序集。

// This function is here to prevent the compiler from optimizing out the
// loop entirely
void doSomething(std::normal_distribution<double> const& d) noexcept;
void inside_loop(double mean, double sd, int n) {
for(int i = 0; i < n; i++) {
const std::normal_distribution<double> d(mean, sd); 
doSomething(d); 
}
}
void outside_loop(double mean, double sd, int n) {
const std::normal_distribution<double> d(mean, sd);
for(int i = 0; i < n; i++) {
doSomething(d); 
}
}

inside_loop组装

循环的程序集如下所示(在 O3 优化时使用 gcc 8.3 编译)。

.L3:
movapd  xmm2, XMMWORD PTR [rsp]
lea     rdi, [rsp+16]
add     ebx, 1
mov     BYTE PTR [rsp+40], 0
movaps  XMMWORD PTR [rsp+16], xmm2
call    foo(std::normal_distribution<double> const&)
cmp     ebp, ebx
jne     .L3

基本上,它 - 构造分布 - 调用foo与发行版 - 测试它是否应该退出循环

outside_loop组装

使用相同的编译选项,outside_loop只是重复调用foo,而无需重新构造发行版。指令更少,所有内容都保留在寄存器中(因此无需访问堆栈)。

.L12:
mov     rdi, rsp
add     ebx, 1
call    foo(std::normal_distribution<double> const&)
cmp     ebp, ebx
jne     .L12

有什么理由在循环中声明变量吗?

是的。在循环中声明变量肯定是好时机。如果您在循环中以某种方式修改distribution,那么每次都通过再次构造来重置它是有意义的。

此外,如果您从未在循环之外使用变量,那么仅出于可读性的目的而在循环中声明它是有意义的。

适合 CPU 寄存器的类型(因此浮点数、整数、双精度型和小型用户定义类型)通常没有与其构造相关的开销,并且在循环中声明它们实际上可以通过简化编译器对寄存器分配的分析来实现更好的组装

查看正态分布的接口,有一个名为reset的成员,他:

重置分配的内部状态

这意味着分布可能具有内部状态。如果是这样,那么在每次迭代时重新创建对象时,您肯定会重置它。不按预期使用它可能会产生不正常的分布,或者可能只是效率低下。

会是什么状态?这当然是执行定义的。查看LLVM的一个实现,正态分布在这里定义。更具体地说,operator()在这里。查看代码,在后续调用之间肯定共享了一些状态。更具体地说,在每次后续调用时,布尔变量_V_hot_的状态都会被翻转。如果为 true,则执行的计算明显减少,并且使用存储_V_的值。如果为 false,则从头开始计算_V_

我没有深入探讨他们为什么选择这样做。但是,仅查看执行的计算,依赖内部状态应该要快得多。虽然这只是一些实现,但它表明该标准允许使用内部状态,并且在某些情况下它是有益的。

后期编辑:

std::normal_distribution的GCC libstdc++实现可以在这里找到。请注意,operator()调用另一个函数__generate_impl,该函数在此处的单独文件中定义。虽然不同,但此实现具有相同的标志,此处名为_M_saved_available,可加快每隔一次调用的速度。