未初始化的局部变量是最快的随机数生成器吗?

我知道未初始化的局部变量是未定义的行为(乌兰巴托),而且该值可能有陷阱表示,这可能会影响进一步的操作,但有时我只想将随机数用于视觉表示,而不会在程序的其他部分进一步使用它们,例如,在视觉效果中设置带有随机颜色的东西,例如:

void updateEffect(){
for(int i=0;i<1000;i++){
int r;
int g;
int b;
star[i].setColor(r%255,g%255,b%255);
bool isVisible;
star[i].setVisible(isVisible);
}
}

比那么快吗

void updateEffect(){
for(int i=0;i<1000;i++){
star[i].setColor(rand()%255,rand()%255,rand()%255);
star[i].setVisible(rand()%2==0?true:false);
}
}

也比其他随机数生成器快吗?

21916 次浏览

不,太糟糕了。

使用未初始化变量的行为在C和c++中都是未定义的,而且这样的方案不太可能具有理想的统计属性。

如果你想要一个“快速而肮脏”的随机数生成器,那么rand()是你最好的选择。在它的实现中,它所做的只是一个乘法、一个加法和一个模数。

我所知道的最快的生成器要求你使用uint32_t作为伪随机变量I的类型,并使用

I = 1664525 * I + 1013904223

生成连续的值。你可以选择任何你喜欢的I的初始值(称为种子)。显然你可以内联编码。无符号类型的标准保证包装充当模数。(数字常数是由杰出的科学程序员Donald Knuth精心挑选的。)

未定义的行为意味着编译器的作者可以自由地忽略这个问题,因为无论发生什么,程序员都没有权利抱怨。

虽然在理论上,当进入UB土地任何事情都可能发生(包括恶魔从你鼻子上飞走了)时,通常意味着编译器作者只是不会关心,对于局部变量,其值将是此时堆栈内存中的任何值。

这也意味着内容通常是“奇怪的”,但是是固定的,或者是稍微随机的,或者是可变的,但是有一个清晰的模式(例如,在每次迭代中增加值)。

当然,你不能期望它是一个体面的随机生成器。

好问题!

未定义并不意味着它是随机的。考虑一下,在全局未初始化变量中获得的值是由系统或您的/其他应用程序运行时遗留在那里的。根据您的系统对不再使用的内存的处理和/或系统和应用程序生成的值类型,您可能会得到:

  1. 总是一样的。
  2. 成为一小部分价值观中的一员。
  3. 获取一个或多个小范围内的值。
  4. 在16/32/64位系统的指针上看到许多可以被2/4/8整除的值
  5. ...

您将得到的值完全取决于系统和/或应用程序留下的非随机值。因此,确实会有一些噪音(除非您的系统删除不再使用的内存),但您将从中提取的值池绝不是随机的。

对于局部变量,情况会变得更糟,因为它们直接来自您自己程序的堆栈。在执行其他代码期间,您的程序很有可能实际编写这些堆栈位置。我估计在这种情况下运气的机会非常低,而你所做的“随机”代码更改将尝试这种运气。

阅读有关随机性。正如你所看到的,随机性是一种非常特殊且难以获得的属性。一个常见的错误是,如果你只是采取一些难以跟踪的东西(比如你的建议),你会得到一个随机的值。

由于安全原因,必须清理分配给程序的新内存,否则信息可能会被使用,密码可能会从一个应用程序泄漏到另一个应用程序。只有在重用内存时,才会得到不同于0的值。很有可能,在堆栈上,前一个值是固定的,因为前一个内存的使用是固定的。

正如其他人所注意到的,这就是未定义行为(UB)。

实际上,它(可能)实际上(有点)管用。在x86[-64]架构上读取未初始化的寄存器确实会产生垃圾结果,并且可能不会做任何坏事(与例如Itanium相反,其中可以将寄存器标记为无效,因此读取会传播NaN之类的错误)。

但这里存在两个主要问题:

  1. 它不会特别随机。在这种情况下,你正在从堆栈中读取,所以你会得到之前在那里的任何东西。它可能是随机的,完全结构化的,你十分钟前输入的密码,或者你祖母的饼干食谱。

  2. It's Bad(大写'B')练习让这样的事情潜入你的代码。从技术上讲,编译器可以在每次读取未定义变量时插入reformat_hdd();。它不会,但无论如何你都不应该这样做。不要做不安全的事情。你做的例外越少,你在意外错误中就越安全所有时间。

UB更紧迫的问题是它使整个程序的行为没有定义。现代编译器可以使用它来省略大量代码甚至回到过去。玩UB就像维多利亚时代的工程师拆除一个活跃的核反应堆。有无数的事情会出错,而且您可能连一半的基本原则或实现的技术都不知道。它是可以的,但是你仍然不应该让它发生。看看其他漂亮的答案来了解细节。

还有,我会炒了你。

正如这里大多数人提到的未定义行为。未定义也意味着你可以得到一些有效的整数值(幸运的是),在这种情况下,这将更快(因为rand函数调用没有进行)。 但不要实际使用它。我相信这将会带来可怕的结果,因为运气并不总是与你同在

还没有提到,但是调用未定义行为的代码路径可以做编译器想做的任何事情,例如:

void updateEffect(){}

这肯定比你正确的循环快,因为有UB,是完全符合的。

未定义的行为是未定义的。这并不意味着你得到了一个未定义的值,这意味着程序可以执行任何东西并且仍然符合语言规范。

一个好的优化编译器应该

void updateEffect(){
for(int i=0;i<1000;i++){
int r;
int g;
int b;
star[i].setColor(r%255,g%255,b%255);
bool isVisible;
star[i].setVisible(isVisible);
}
}

并编译成noop。这当然比任何其他选择都要快。它的缺点是什么都做不了,但这就是未定义行为的缺点。

让我清楚地说:我们不会在程序中调用未定义的行为。这从来都不是一个好主意,就这样。这条规则很少有例外;例如,如果你是一个标准库实现者实现偏移量。如果您的情况属于这种例外,您可能已经知道这一点。在这种情况下,我们知道使用未初始化的自动变量是未定义的行为

编译器对未定义行为的优化变得非常积极,我们可以发现许多未定义行为导致安全缺陷的情况。最臭名昭著的例子可能是我在我对c++编译bug的回答?中提到的Linux内核空指针检查删除,其中编译器对未定义行为的优化将有限循环变成无限循环。

我们可以阅读CERT的危险的优化和因果关系的丧失 (视频),其中包括:

编译器编写者越来越多地利用undefined 在C和c++编程语言中的行为改进 优化。< / p > 通常,这些优化会产生干扰 开发人员对他们的产品进行因果分析的能力 源代码,即分析下游结果的依赖性

因此,这些优化正在消除 软件中的因果关系和软件的概率都在增加

.错误、缺陷和漏洞

具体来说,对于不确定值,C标准缺陷报告451:未初始化自动变量的不稳定性是一些有趣的读物。它还没有解决,但引入了不稳定的值的概念,这意味着值的不确定性可能在程序中传播,并且可以在程序的不同位置具有不同的不确定性值。

我不知道有什么例子会发生这种情况,但在这一点上,我们不能排除这种可能性。

真实的例子,而不是你期望的结果

你不太可能得到随机值。编译器可以优化整个循环。例如,用这个简化的例子:

void updateEffect(int  arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r ;
}
}

clang优化它(< a href = " http://gcc.godbolt.org/ % 7 b % 22 % 22% 3 a3版本% 2 c % 22 filterasm % 3 22% % 3一7 b % 22标签% 22% % 2 c % 22指令%一22% 3 % 3一2 c % 22 commentonly % 22% % 2 c % 22 colouriseasm % 22% 3一7 % d % 2 c % 22编译器% 3 22% % 5 b % 7 b % 22 sourcez % 3 22% % 22 fanw9glgjgbargbygqwc4fmciazb6dgqafbahaowzibo1a2gewamaugjqdewll2y1jcjagbejgg4iahmasa1hm7cekshwoxxk5sso16efjbewn4ntac % 2 bwa0a % 22% 2 c % 22编译器% 3 22% % 22 clang37x % 22% 2 c % % 3 22% % 22选项22-std % 3 dc % 2 b % 2 b14 % 20-O3 % 20-Wall % 20-Wextra % 20-fverbose-asm % 20-fno-inline % 22% 7 d % 5 d % 7 d”Rel ="noreferrer">see it live):

updateEffect(int*):                     # @updateEffect(int*)
retq

或者可能得到全0,就像这个修改后的情况:

void updateEffect(int  arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r%255 ;
}
}

现场观看:

updateEffect(int*):                     # @updateEffect(int*)
xorps   %xmm0, %xmm0
movups  %xmm0, 64(%rdi)
movups  %xmm0, 48(%rdi)
movups  %xmm0, 32(%rdi)
movups  %xmm0, 16(%rdi)
movups  %xmm0, (%rdi)
retq

这两种情况都是完全可以接受的未定义行为形式。

注意,如果我们在Itanium上,我们可以最终得到一个trap值:

< p >[…]如果寄存器恰好保存了一个特殊的“not-a-thing”值, 读取寄存器陷阱,除了一些指令[…]

其他重要事项

有趣的是,在UB Canaries项目中注意到gcc和clang之间的差异非常愿意利用未初始化内存的未定义行为。文章指出(我特别强调):

当然,我们需要完全清楚,任何这样的期望与语言标准无关,而是与特定编译器碰巧做什么有关,要么是因为该编译器的提供者不愿意利用UB 或者只是因为他们还没有抽出时间来利用它。当编译器提供者没有真正的保证存在时,我们愿意说,尚未开发的瑞银是定时炸弹:它们将等待下个月或明年编译器变得更加激进时失效。

正如Matthieu M.指出的,每一个C程序员应该知道的关于未定义行为#2/3也与这个问题有关。它在其他事情中说(我特别强调):

要意识到的重要而可怕的事情是只是any 基于未定义行为的优化可以在上开始触发 在未来的任何时候有错误的代码。内联,循环展开,内存 促销和其他优化将会越来越好,而且 它们存在的重要原因之一就是暴露次要因素

对我来说,这是非常不满意的,部分原因是编译器 不可避免地会被指责,但也因为这意味着巨大 C代码的body是等着爆炸的地雷。

为了完整起见,我可能应该提到实现可以选择将未定义的行为定义得很好,例如GCC允许通过联合使用类型双关语在c++中,这似乎是未定义的行为。如果是这种情况,实现应该记录它,这通常是不可移植的。

正如其他人所说,这将是快速的,但不是随机的。

大多数编译器对局部变量所做的是在堆栈上为它们抓取一些空间,而不是费心将其设置为任何东西(标准说它们不需要这样做,所以为什么要减慢生成的代码呢?)

在这种情况下,你将得到的值将取决于堆栈上之前的内容——如果你在这个函数之前调用一个有100个局部char变量设置为'Q'的函数,然后在该函数返回后调用你的函数,那么你可能会发现你的“随机”值表现得就像你已经memset()它们都是'Q'。

重要的是,你的例子函数尝试使用这个,这些值不会改变每次你读取他们,他们将是相同的每一次。所以你会得到100颗星星都设置成相同的颜色和能见度。

而且,没有什么说编译器不应该初始化这些值——所以将来的编译器可能会这样做。

总的来说:这是个坏主意,不要这么做。 (就像很多“聪明的”代码级优化一样…)

正如其他人已经提到的,这是未定义的行为(乌兰巴托),但它可能“工作”。

除了其他人已经提到的问题之外,我还看到了另一个问题(缺点)——它不能在C和c++以外的任何语言中工作。我知道这个问题是关于c++的,但是如果你能写出好的c++代码和Java代码,这不是问题,那为什么不呢?也许有一天有人将不得不将其移植到其他语言,并且搜索像这样由“魔术” UB引起的错误绝对是一场噩梦(特别是对于一个没有经验的C/ c++开发人员)。

在这里有关于另一个类似UB的问题。想象一下,你试图在不知道这个UB的情况下找到这样的bug。如果你想阅读更多关于C/ c++中这些奇怪的东西,请阅读链接中的问题答案,并查看 伟大的幻灯片。它将帮助你理解引擎盖下面是什么以及它是如何工作的;这不仅仅是另一个充满“魔力”的幻灯片。我确信即使是大多数有经验的C/c++程序员也能从中学到很多东西。

您的特定代码示例可能无法实现您所期望的功能。虽然从技术上讲,循环的每次迭代都为r、g和b值重新创建局部变量,但实际上它们在堆栈上是完全相同的内存空间。因此,它不会在每次迭代中重新随机化,你最终将为1000种颜色中的每一种分配相同的3个值,而不管r、g和b最初是多么随机。

事实上,如果它确实有效,我会非常好奇是什么让它重新随机化。我唯一能想到的就是在这个堆栈上有一个交错的中断,这是不太可能的。也许内部优化将它们作为寄存器变量,而不是真正的内存位置,在循环中寄存器被重用,这也会奏效,特别是如果设置可见性函数特别需要寄存器的话。不过,这远不是随机的。

< p >非常糟糕!坏习惯,坏结果。 考虑:< / p >
A_Function_that_use_a_lot_the_Stack();
updateEffect();

如果A_Function_that_use_a_lot_the_Stack()函数总是进行相同的初始化,则堆栈中保留相同的数据。这个数据就是我们得到的updateEffect(): 总是相同的价值!

如果操作得当,使用未初始化的数据来获得随机性并不一定是件坏事。事实上,OpenSSL正是这样做的,以播种它的PRNG。

显然,这种用法并没有很好地记录下来,因为有人注意到Valgrind抱怨使用未初始化的数据,并“修复”了它,导致PRNG的bug

所以你可以这样做,但你需要知道你在做什么,并确保任何阅读你的代码的人都理解这一点。

在某些情况下,使用类型“unsigned char*”可以安全地读取未初始化的内存[例如从malloc返回的缓冲区]。代码可以读取这样的内存,而不必担心编译器将因果关系抛出窗口,并且有时,为内存中可能包含的任何内容准备代码可能比确保不读取未初始化的数据更有效(一个常见的例子是在部分初始化的缓冲区上使用memcpy,而不是离散地复制所有包含有意义数据的元素)。

然而,即使在这种情况下,人们也应该始终假设,如果字节的任何组合特别烦人,那么读取它总是会产生字节的模式(如果某个模式在生产中是烦人的,但在开发中不是,那么这种模式直到代码进入生产中才会出现)。

在嵌入式系统中,读取未初始化的内存作为随机生成策略的一部分可能是有用的,在这种情况下,可以确保自上次系统通电以来,内存从未被写入过基本上非随机的内容,并且如果用于内存的制造过程导致其通电状态以半随机方式变化。即使所有设备总是产生相同的数据,代码也应该工作,但在某些情况下,例如一组节点每个都需要尽可能快地选择任意唯一ID,有一个“不太随机”的生成器,给一半的节点提供相同的初始ID可能比没有任何初始随机源更好。

有很多很好的答案,但请允许我补充另一个并强调一点,在确定性计算机中,没有什么是随机的。对于伪rng生成的数字和堆栈上为C/ c++局部变量保留的内存区域中发现的看似“随机”的数字都是如此。

但是…这里有一个关键的区别。

由优秀的伪随机生成器生成的数字具有统计上与真正的随机抽取相似的属性。例如,分布是均匀的。循环长度很长:在循环重复之前,你可以得到数百万个随机数。序列不是自相关的:例如,如果你取第2个、第3个或第27个数字,或者查看生成的数字中的特定数字,你不会开始看到奇怪的模式出现。

相比之下,留在堆栈上的“随机”数字没有任何这些属性。它们的值和明显的随机性完全取决于程序的构造方式、编译方式以及编译器对程序的优化方式。举例来说,这是你的想法的一个变体,作为一个自包含的程序:

#include <stdio.h>


notrandom()
{
int r, g, b;


printf("R=%d, G=%d, B=%d", r&255, g&255, b&255);
}


int main(int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
{
notrandom();
printf("\n");
}


return 0;
}

当我在Linux机器上用GCC编译这段代码并运行它时,结果是相当不愉快的确定性:

R=0, G=19, B=0
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255

如果您使用反汇编程序查看编译后的代码,您可以详细地重建正在发生的事情。第一次调用notrandom()使用了堆栈中之前未被该程序使用的区域;谁知道里面有什么。但是在调用notrandom()之后,会调用printf() (GCC编译器实际上会优化为调用putchar(),但没关系)并覆盖堆栈。因此,在接下来和随后的几次调用notrandom()时,堆栈将包含来自putchar()执行的过期数据,并且由于putchar()总是使用相同的参数调用,因此这些过期数据也将始终相同。

因此,这种行为绝对是没有什么随机的,以这种方式获得的数字也不具有编写良好的伪随机数生成器的任何理想属性。事实上,在大多数现实场景中,它们的值是重复的并且高度相关的。

事实上,和其他人一样,我也会认真考虑解雇那些试图把这个想法当作“高性能RNG”的人。

还有一种可能性需要考虑。

现代编译器(嗯,g++)非常聪明,它们会检查你的代码,看看哪些指令影响状态,哪些不影响状态,如果一条指令被保证不影响状态,g++会简单地删除那条指令。

接下来会发生什么。g++肯定会看到你正在读取,执行算术运算,保存,本质上是一个垃圾值,这会产生更多的垃圾。因为不能保证新的垃圾会比旧的垃圾更有用,所以它只会让你的循环消失。杂音!

这个方法很有用,但下面是我要做的。结合UB(未定义行为)与rand()速度。

当然,减少rand()s的执行,但是将它们混合在一起,这样编译器就不会做任何你不希望它做的事情。

我也不会解雇你。

你需要对“随机”的含义有一个定义。 一个合理的定义是,你得到的值应该没有什么相关性。这是可以测量的。以一种可控的、可复制的方式实现这一点也并非易事。所以未定义的行为肯定不是你想要的

我做了一个非常简单的测试,它根本不是随机的。

#include <stdio.h>


int main() {


int a;
printf("%d\n", a);
return 0;
}

每次我运行程序,它都输出相同的数字(在我的例子中是32767)——你不能得到比这更随机的了。这大概是运行时库中留在堆栈上的启动代码。由于每次程序运行时都使用相同的启动代码,并且在两次运行之间程序中没有其他变化,因此结果是完全一致的。

在任何你想要使用未初始化变量的地方使用7757。我从质数列表中随机选择了它:

  1. 这是被定义的行为

  2. 它保证不总是0

  3. 它是质数

  4. 它很可能是统计上随机的,就像未初始化的一样 李变量< / p > < / >

  5. 它可能比未初始化的变量快,因为它 值在编译时已知

一个好主意依赖于我们的任何逻辑语言未定义的行为。除了在这篇文章中所提到/讨论的内容之外,我想提到的是,使用现代c++方法/风格,这样的程序可能无法编译。

这在我之前的文章中提到过,其中包含汽车特性的优点和相同的有用链接。

https://stackoverflow.com/a/26170069/2724703 < a href = " https://stackoverflow.com/a/26170069/2724703 " > < / >

因此,如果我们改变上面的代码并用汽车替换实际的类型,程序甚至不能编译。

void updateEffect(){
for(int i=0;i<1000;i++){
auto r;
auto g;
auto b;
star[i].setColor(r%255,g%255,b%255);
auto isVisible;
star[i].setVisible(isVisible);
}
}

我喜欢你的思维方式。真的是跳出了框框。然而,这种权衡真的不值得。Memory-runtime权衡是一个东西,包括未定义的行为运行时是

知道自己使用如此“随机”的业务逻辑,一定会让您感到非常不安。我不会那么做的。