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

时间:2015-07-31 06:52:43

标签: c++ c undefined-behavior garbage

我知道未初始化的局部变量是未定义的行为( 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);
    }
}

更快
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);
    }
}

并且比其他随机数发生器更快?

22 个答案:

答案 0 :(得分:295)

正如其他人所说,这是未定义的行为(UB)。

在实践中,它(可能)实际上(有点)工作。从x86 [-64]架构上的未初始化寄存器读取确实会产生垃圾结果,并且可能不会做任何坏事(与例如Itanium相反,registers can be flagged as invalid,因此读取传播错误,如NaN)

但有两个主要问题:

  1. 它不会特别随机。在这种情况下,您正在从堆栈中读取,因此您将获得之前的任何内容。这可能是有效的随机,完全结构化的,您在十分钟前输入的密码,或您祖母的饼干食谱。

  2. 这是不好的(资本&#39; B&#39;)练习让这样的事情蔓延到您的代码中。从技术上讲,每次读取未定义的变量时,编译器都可以插入reformat_hdd();。它赢了,但你无论如何都不应该这样做。别做不安全的事情。你做的例外越少,你的意外错误所有的时间越安全。

    UB更紧迫的问题是它使整个程序的行为未定义。现代编译器可以使用它来消除代码的大量甚至go back in time。与UB一起玩就像维多利亚时代的工程师正在拆除现场核反应堆。有很多事情要出错,你可能不知道一半的基本原则或实施技术。 可能可以,但你仍然不应该让它发生。请查看其他详细的答案。

  3. 另外,我解雇了你。

答案 1 :(得分:196)

让我说清楚:我们不会在程序中调用未定义的行为。从来没有一个好主意。这条规则很少有例外;例如,如果您是library implementer implementing offsetof。如果你的案件属于这种例外,你可能已经知道了。在这种情况下,我们know using uninitialized automatic variables is undefined behavior

编译器对未定义行为的优化变得非常积极,我们可以发现许多未定义行为导致安全漏洞的情况。最臭名昭着的案例可能是我在Linux kernel null pointer check removal中提到的my answer to C++ compilation bug?,其中围绕未定义行为的编译器优化将有限循环变为无限循环。

我们可以阅读CERT的Dangerous Optimizations and the Loss of Causalityvideo),其中包括:

  

编译器编写者越来越多地利用未定义的优势   C和C ++编程语言中的行为改进   优化

     

这些优化经常会干扰   开发人员对其进行因果分析的能力   源代码,即分析下游结果的依赖性   以前的结果。

     

因此,这些优化正在消除   软件中的因果关系并且正在增加软件的概率   缺陷,缺陷和漏洞。

特别是对于不确定的值,C标准defect report 451: Instability of uninitialized automatic variables会产生一些有趣的读数。它尚未得到解决,但引入了摇摆值的概念,这意味着值的不确定性可能会在程序中传播,并且可能在程序的不同点具有不同的不确定值。

我不知道发生这种情况的任何例子,但在这一点上我们不能排除它。

真实示例,而不是您期望的结果

您不太可能获得随机值。编译器可以完全优化离开循环。例如,使用这个简化的案例:

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

clang将其优化( see it live ):

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

或者可能获得所有零,就像这个修改过的情况一样:

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

see it live

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,我们可以end up with a trap value

  

[...]如果寄存器碰巧有一个特殊的非物质价值,   读取寄存器陷阱,除了一些指令[...]

其他重要说明

值得注意的是variance between gcc and clang noted in the UB Canaries project关于他们是否愿意利用未初始化内存的未定义行为。文章指出(强调我的):

  

当然,我们需要完全清楚自己,任何这样的期望都与语言标准无关,而且与特定编译器碰巧发生的事情有关,要么是因为编译器的提供者不愿意利用它UB 或仅仅是因为他们尚未利用它。如果没有来自编译器提供商的真正保证,我们想说尚未开发的UB是时间炸弹:他们等待下个月或明年当编译器获得更多时更新攻击性。

Matthieu M.指出What Every C Programmer Should Know About Undefined Behavior #2/3也与这个问题有关。它说除了其他事情(强调我的):

  

要意识到的重要和可怕的事情是只是任何   基于未定义行为的优化可以开始被触发   将来任何时候的错误代码。内联,循环展开,内存   促销和其他优化将继续变得更好,并且a   他们存在的重要部分原因是暴露中学   优化如上所述。

     

对我来说,这是非常不满意的,部分是因为编译器   不可避免地最终会受到指责,但也因为它意味着巨大的   C代码的主体是等待爆炸的地雷。

为了完整起见,我应该提一下,实现可以选择明确定义未定义的行为,例如gcc allows type punning through unions in C++ this seems like undefined behavior。如果是这种情况,实现应该记录它,这通常是不可移植的。

答案 2 :(得分:162)

不,这很可怕。

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

如果你想要快速而又肮脏的&#34;随机数生成器,然后rand()是你最好的选择。在其实现中,它所做的只是乘法,加法和模数。

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

I = 1664525 * I + 1013904223

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

答案 3 :(得分:42)

好问题!

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

  1. 始终如一。
  2. 是一小组价值观之一。
  3. 获取一个或多个小范围内的值。
  4. 从16/32/64位系统上的指针看到2/4/8可分割的许多值
  5. ...
  6. 您获得的值完全取决于系统和/或应用程序留下的非随机值。所以,确实会有一些噪音(除非你的系统不再使用内存),但你所绘制的价值池绝不是随机的。

    局部变量的情况变得更糟,因为它们直接来自您自己程序的堆栈。您的程序很可能在执行其他代码期间实际编写这些堆栈位置。我估计在这种情况下运气的可能性非常低,而且随机的&#39;代码改变你试试这个运气。

    了解randomness。正如您所见,随机性是一种非常具体且难以获得的属性。如果您只是采取难以跟踪的内容(例如您的建议),那么您将获得随机值,这是一个常见的错误。

答案 4 :(得分:32)

许多好的答案,但请允许我添加另一个,并强调在确定性计算机中,没有任何东西是随机的。这对于伪RNG和看似随机的数字生成的数字都是如此。在堆栈上为C / C ++局部变量保留的内存区域中找到的数字。

但......有一个至关重要的区别。

良好的伪随机生成器生成的数字具有使其在统计上与真正随机抽取相似的属性。例如,分布是统一的。循环长度很长:在循环重复之前,您可以获得数百万个随机数。序列不是自相关的:例如,如果你采用每个第2,第3或第27个数字,或者如果你查看生成的数字中的特定数字,你将不会看到出现奇怪的模式。

相比之下,&#34;随机&#34;堆栈上留下的数字没有这些属性。它们的值及其明显的随机性完全取决于程序的构造方式,编译方式以及编译器如何优化程序。举例来说,这是您作为一个独立程序的想法的变体:

#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&#34;&#34;

的人。

答案 5 :(得分:29)

未定义的行为意味着编译器的作者可以自由地忽略这个问题,因为程序员永远无权抱怨发生的任何事情。

虽然理论上在进入UB时任何事情都可能发生(包括daemon flying off your nose)通常意味着编译器作者不关心,对于局部变量,值将是是那个时候堆栈内存中的任何内容。

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

肯定你不能期望它是一个体面的随机生成器。

答案 6 :(得分:28)

未定义未定义的行为。这并不意味着您获得了未定义的值,这意味着该程序可以执行任何并且仍然符合语言规范。

一个好的优化编译器应该采用

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。这肯定比任何替代方案都快。它的缺点是它不会做任何事情,但这是未定义行为的缺点。

答案 7 :(得分:18)

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

答案 8 :(得分:18)

尚未提及,但允许调用未定义行为的代码路径可以执行编译器所需的任何操作,例如。

void updateEffect(){}

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

答案 9 :(得分:13)

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

事实上,如果确实有效,我会非常好奇它是什么让它重新随机化。我唯一能想到的就是一个交错的中断,它堆叠在堆栈顶上,极不可能。也许内部优化将那些保持为寄存器变量而不是真正的存储器位置,其中寄存器在循环中进一步向下使用,也可以做到这一点,特别是如果集合可见性函数特别是寄存器饥饿。仍然,远非随机。

答案 10 :(得分:12)

真的很糟糕!坏习惯,结果不好。 考虑:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

如果函数A_Function_that_use_a_lot_the_Stack()总是进行相同的初始化,它会在堆栈上留下相同的数据。这些数据就是我们调用的updateEffect()总是相同的值!

答案 11 :(得分:12)

因为这里的大多数人都提到了未定义的行为。未定义也意味着您可以获得一些有效的整数值(幸运的是),在这种情况下,这将更快(因为没有进行rand函数调用)。 但实际上并没有使用它。我相信这会产生可怕的结果,因为运气不会一直伴随着你。

答案 12 :(得分:11)

我进行了一项非常简单的测试,并且它根本不是随机的。

#include <stdio.h>

int main() {

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

每次我运行该程序时,它都打印了相同的数字(在我的情况下为32767) - 你不能比那更随意。这可能是堆栈中剩余的运行时库中的启动代码。由于每次程序运行时它都使用相同的启动代码,并且在运行之间程序中没有其他变化,因此结果完全一致。

答案 13 :(得分:10)

您需要定义“随机”的含义。 一个明智的定义涉及你得到的价值应该没有多少相关性。这是你可以测量的东西。以受控,可重复的方式实现也并非易事。所以未定义的行为当然不是你想要的。

答案 14 :(得分:7)

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

然而,即使在这种情况下,也应该总是假设如果字节的任何组合将特别无理取闹,那么读取它将总是产生该字节模式(并且如果某个模式在生产中是无理取闹的,而在开发中则不是,这样的模式在代码投入生产之前不会出现。)

读取未初始化的内存可能是嵌入式系统中随机生成策略的一部分,其中可以确保自上次启动系统以来内存从未使用基本上非随机的内容编写,并且如果用于存储器的制造过程导致其通电状态以半随机方式变化。即使所有设备始终产生相同的数据,代码也应该有效,但是在例如一组节点,每个节点都需要尽快选择任意唯一ID,具有&#34;非常随机的&#34;给一半节点提供相同初始ID的生成器可能比没有任何初始随机源更好。

答案 15 :(得分:5)

正如其他人所说,它会很快,但不是随机的。

大多数编译器为局部变量做的是在堆栈上为它们占用一些空间,但不要把它设置为任何东西(标准说它们不需要,所以为什么要减慢你生成的代码的速度?)。

在这种情况下,您将获得的值将取决于之前在堆栈上的内容 - 如果您在此之前调用一个函数,其中有一百个本地char变量全部设置为'Q'然后调用您'在返回之后重新运行,然后您可能会发现您的“随机”值的行为就像您将memset()全部归结为“Q”一样。

重要的是,对于尝试使用它的示例函数,每次读取它们时这些值都不会改变,它们每次都会相同。因此,您将获得100颗全部设置为相同的颜色和可见度。

此外,没有任何说明编译器不应该初始化这些值 - 因此未来的编译器可能会这样做。

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

答案 16 :(得分:3)

正如其他人已经提到的那样,这是未定义的行为( UB ),但它可能&#34;工作&#34;。

除了其他人已经提到的问题之外,我还看到另一个问题(缺点) - 它不能用于除C和C ++之外的任何语言。我知道这个问题是关于C ++的,但是如果你能编写好C ++和Java代码的代码并且它不是问题那么为什么不呢?也许有一天有人必须把它移植到其他语言并搜索由&#34;魔术技巧引起的错误&#34; 这样的UB肯定会是一场噩梦(特别是对于没有经验的C / C ++)开发者)。

Here对另一个类似的UB存在疑问。想象一下,在不知道这个UB的情况下,你试图找到这样的bug。如果您想在C / C ++中阅读有关此类奇怪事物的更多信息,请阅读链接中的问题答案,并参阅this GREAT 幻灯片。它将帮助您了解幕后的内容以及它的工作方式;它不仅仅是另一个充满了魔法&#34;的幻灯片。我非常确定即使是大多数经验丰富的C / c ++程序员也可以从中学到很多东西。

答案 17 :(得分:3)

在您想要使用未初始化变量的每个地方使用7757。我从素数列表中随机选择它:

  1. 它是定义的行为

  2. 保证不总是0

  3. 是素数

  4. 它可能与未初始化一样具有统计随机性 变量

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

答案 18 :(得分:3)

一个好主意,依靠我们对语言未定义行为的任​​何逻辑。除了本文中提到/讨论过的内容之外,我想提一下,使用现代C ++方法/风格,这样的程序可能无法编译。

我之前的帖子中提到了这一点,其中包含了自动功能的优势以及相同的有用链接。

https://stackoverflow.com/a/26170069/2724703

因此,如果我们更改上面的代码并用 auto 替换实际类型,程序甚至都不会编译。

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

答案 19 :(得分:3)

我喜欢你的思维方式。真的在盒子外面。然而,权衡真的不值得。 内存 - 运行时权衡是一件事,包括运行时的未定义行为是

如果您知道自己正在使用这种“随机”作为业务逻辑,那么它必须让您感到非常不安。我不这样做。

答案 20 :(得分:1)

There is one more possibility to consider.

Modern compilers (ahem g++) are so intelligent that they go through your code to see what instructions affect state, and what don't, and if an instruction is guaranteed to NOT affect the state, g++ will simply remove that instruction.

So here's what will happen. g++ will definitely see that you are reading, performing arithmetic on, saving, what is essentially a garbage value, which produces more garbage. Since there is no guarantee that the new garbage is any more useful than the old one, it will simply do away with your loop. BLOOP!

This method is useful, but here's what I would do. Combine UB (Undefined Behaviour) with rand() speed.

Of course, reduce rand()s executed, but mix them in so compiler doesn't do anything you don't want it to.

And I won't fire you.

答案 21 :(得分:-1)

如果做得好,使用未初始化的数据进行随机性并不一定是件坏事。实际上,OpenSSL正是为了实现其PRNG而实现的。

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

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