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

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


当前回答

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

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

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

其他回答

有很多很好的答案,但请允许我补充另一个并强调一点,在确定性计算机中,没有什么是随机的。对于伪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

If you looked at the compiled code with a disassembler, you could reconstruct what was going on, in detail. The first call to notrandom() used an area of the stack that was not used by this program previously; who knows what was in there. But after that call to notrandom(), there is a call to printf() (which the GCC compiler actually optimizes to a call to putchar(), but never mind) and that overwrites the stack. So the next and subsequent times, when notrandom() is called, the stack will contain stale data from the execution of putchar(), and since putchar() is always called with the same arguments, this stale data will always be the same, too.

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

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

不,太糟糕了。

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

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

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

I = 1664525 * I + 1013904223

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

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

理论上,当进入UB域时,任何事情都可能发生(包括守护进程从你鼻子上飞出去),通常意味着编译器作者不会关心,对于局部变量,其值将是当时堆栈内存中的任何值。

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

当然,你不能指望它是一个不错的随机生成器。

正如其他人已经提到的,这是未定义的行为(UB),但它可能“有效”。

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

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

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

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

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

Increasingly, compiler writers are taking advantage of undefined behaviors in the C and C++ programming languages to improve optimizations. Frequently, these optimizations are interfering with the ability of developers to perform cause-effect analysis on their source code, that is, analyzing the dependence of downstream results on prior results. Consequently, these optimizations are eliminating causality in software and are increasing the probability of software faults, defects, and vulnerabilities.

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

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

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

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

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

Clang优化了它(看现场):

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值:

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

其他重要事项

有趣的是,在UB Canaries项目中,gcc和clang在利用未初始化内存的未定义行为方面存在差异。文章指出(重点是我的):

Of course we need to be completely clear with ourselves that any such expectation has nothing to do with the language standard and everything to do with what a particular compiler happens to do, either because the providers of that compiler are unwilling to exploit that UB or just because they have not gotten around to exploiting it yet. When no real guarantee from the compiler provider exists, we like to say that as-yet unexploited UBs are time bombs: they’re waiting to go off next month or next year when the compiler gets a bit more aggressive.

正如Matthieu M.指出的,每个C程序员应该知道的关于未定义行为#2/3也与这个问题有关。它说了很多(重点是我的):

The important and scary thing to realize is that just about any optimization based on undefined behavior can start being triggered on buggy code at any time in the future. Inlining, loop unrolling, memory promotion and other optimizations will keep getting better, and a significant part of their reason for existing is to expose secondary optimizations like the ones above. To me, this is deeply dissatisfying, partially because the compiler inevitably ends up getting blamed, but also because it means that huge bodies of C code are land mines just waiting to explode.

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