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

ggr*_*grr 319 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

是不是比它快

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

ima*_*ett 296

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

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

但有两个主要问题:

  1. 它不会特别随机. 在这种情况下,你正在从堆栈中读取,所以你将获得之前的任何东西.这可能是有效的随机,完全结构化,十分钟前输入的密码,或祖母的cookie配方.

  2. 让(像''''''''''''''''''''''''''''''''''''''''' 从技术上讲,reformat_hdd();每次读取未定义的变量时,编译器都可以插入.它不会,但你不应该这样做.不要做不安全的事情.你所做的例外越少,就越安全意外失误所有的时间.

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

而且,我会解雇你.

  • 我真的反对"它有点作品"的概念.即使它今天是真的,它不是,它可能随时由于更积极的编译器而改变.编译器可以用`unreachable()`替换任何读取并删除程序的一半.这确实也发生在实践中.在我认为的某些Linux发行版中,这种行为完全抵消了RNG.这个问题中的大多数答案似乎都假设未初始化的值表现得像一个值.那是假的. (57认同)
  • @Potatoswatter:Itanium寄存器可以包含NaT(Not Thing),它实际上是一个"未初始化的寄存器".在Itanium上,当你没有写入它时从寄存器读取可能会中止你的程序(在这里阅读更多内容:http://blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162. ASPX).因此,有一个很好的理由为什么读取未初始化的值是未定义的行为.这也可能是Itanium不是很受欢迎的一个原因:) (39认同)
  • *此外,我会解雇你*似乎是一个相当愚蠢的说法,假设良好的做法,这应该在代码审查,讨论,永远不会再发生.因为我们正在使用正确的警告标志,所以肯定会被捕获,对吧? (25认同)
  • 这个答案听起来好像*"在理论上调用未定义的行为是坏的,但在实践中它不会真正伤害你"*.那是错的.从可能导致UB***(并且可能*将*)的表达式中收集熵会导致[之前收集的所有熵都丢失](http://kqueue.org/blog/2012/06/25/more-randomness -或更少/).这是一个严重的危险. (19认同)
  • @Michael实际上,它是.如果程序在任何时候都有未定义的行为,编译器可以以影响调用未定义行为的**之前的代码*的方式优化程序.有各种各样的文章和演示,这些令人难以置信的可以得到这里是一个非常好的:http://blogs.msdn.com/b/oldnewthing/archive/2014/06/27/10537746.aspx(其中包括如果程序中的任何路径调用UB,则表示所有投注均已关闭的标准) (16认同)
  • _"而且,我会解雇你."_也许吧.但我们在这里处于Undefined Behavior土地.你最终可能会给他加薪:) (10认同)
  • @FrostRocket:为了"创造性"而坚持做出被记录为错误和不安全的事情的人是项目的责任,无论是商业还是自由和开放源码软件.你回复的评论可能是不必要的煽动性,但人们需要停止这样做或出去的情绪是非常有效的. (8认同)
  • "我会解雇你?" 多么傲慢的说法.他试图想出一个解决问题的创造性方法.希望没有人为你工作. (7认同)
  • 不幸的是,这个答案虽然经过彻底的推翻,但实际上是不正确的.我建议你阅读Shafik的答案,以及[每个C程序员应该知道的未定义行为](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14的.html),以获得关于未定义行为的细微之处更好地把握(短的吧:编译器要求不确定的行为不会发生,并且被允许删除,将触发它的代码的任何分支). (6认同)
  • 在任何架构上都没有"未初始化的注册"这样的东西.机器不了解初始化或缺乏初始化; 这是一种语言结构. (5认同)
  • 我必须在这里同意@usr,你可以在[我的回答](http://stackoverflow.com/a/31746063/1708801)中看到我的直播神话例子,至少对于clang来说这不是一种工作. (2认同)
  • @supercat:我不是试图对优点进行论证,声明某些特定的东西未定义.这是一个巨大的讨论,它在这里是偏离主题的.我所说的是你声称UB在2010年之前相对安全,并且使用UB进行DCE的积极编译器优化使其不安全,不受支持,并且在我看来,是假的.非显而易见(对于编译器)无效别名,序列点违规,对未初始化对象的访问等的情况都会导致诸如在不同点处对表达式的不一致评估之类的事情,这是非常不安全的. (2认同)

Sha*_*our 207

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

编译器对未定义行为的优化变得非常积极,我们可以发现许多未定义行为导致安全漏洞的情况.最臭名昭着的案例可能是我在回答C++编译bug时提到的Linux内核空指针检查删除围绕未定义行为的编译器优化将有限循环变为无限循环.

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

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

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

因此,这些优化消除了软件中的因果关系,并增加了软件故障,缺陷和漏洞的可能性.

特别是对于不确定值,C标准缺陷报告451:未初始化自动变量的不稳定性使得一些有趣的读数.它还没有得到解决,但引入了摇摆值的概念,这意味着的不确定性可能通过程序传播,并且在程序的不同点可能具有不同的不确定值.

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

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

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

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r ;
    }
}
Run Code Online (Sandbox Code Playgroud)

clang优化它(现场直播):

updateEffect(int*):                     # @updateEffect(int*)
    retq
Run Code Online (Sandbox Code Playgroud)

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

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

看到它直播:

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
Run Code Online (Sandbox Code Playgroud)

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

注意,如果我们在Itanium上,我们最终会得到一个陷阱值:

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

其他重要说明

值得注意的是,在UB Canaries项目中注意到gcc和clang之间差异,以及它们是否愿意利用与未初始化内存相关的未定义行为.文章指出(强调我的):

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

正如Matthieu M.指出每个C程序员应该知道的关于未定义行为的内容#2/3也与此问题相关.它说除其他外(强调我的):

我们意识到,和可怕的是,几乎所有 被触发基于未定义行为可以开始优化对bug的代码在将来的任何时间.内联,循环展开,内存促销和其他优化将继续变得更好,并且他们存在的重要部分原因是暴露上述二次优化.

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

为了完整起见,我应该提一下,实现可以选择明确定义未定义的行为,例如gcc允许通过联合进行类型惩罚,在C++中这似乎是未定义的行为.如果是这种情况,实现应该记录它,这通常是不可移植的.

  • 利用UB有效地成为优秀黑客的商标.这种传统现在已经持续了50年或更长时间.不幸的是,现在要求计算机将坏人的影响降到最低.我非常喜欢弄清楚如何使用UB机器代码或端口读/写等来做很酷的事情.我90年代,当操作系统不能保护用户自己时. (2认同)
  • 如果你有一个特定的程序集,那么使用它,不要写不合规的C.然后每个人都会知道你正在使用一个特定的非便携技巧.并不是坏人意味着你不能使用UB,它是英特尔等在芯片上做的伎俩. (2认同)
  • @ 500-InternalServerError因为它们可能不容易检测到,或者在一般情况下可能根本检测不到,因此无法禁止它们.这与可以检测到的语法违规有所不同.我们还有不正确和形成不良的诊断要求,这通常将理论上可以检测到的不良形成的程序与理论上无法可靠检测的程序分开. (2认同)

Bat*_*eba 164

不,这太可怕了.

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

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

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

I = 1664525 * I + 1013904223

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

  • @Jay比较一个快速和脏的单一化变量?这是一个更好的解决方案. (24认同)
  • 您提供的"线性同余"生成器适用于简单应用程序,但仅适用于非加密应用程序.可以预测其行为.参见Don Knuth本人的"[解密线性同余加密](http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1056997)"(IEEE信息论交易,第31卷) (9认同)
  • 公平地说,他没有问是否是一个好的随机数发生器.他问是否快.嗯,是的,它可能是禁食的.但结果不会是随机的. (3认同)
  • `rand()`不适合用途,在我看来应该完全弃用.这些天你可以下载免费许可和极其优越的随机数发生器(例如Mersenne Twister),这些发生器的速度非常快,所以真的没有必要继续使用高度缺陷的`rand()` (2认同)

mea*_*ers 42

好问题!

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

  1. 总是一样.
  2. 是一小组价值观之一.
  3. 获取一个或多个小范围内的值.
  4. 从16/32/64位系统上的指针查看2/4/8可分割的许多值
  5. ...

您将获得的值完全取决于系统和/或应用程序留下的非随机值.所以,确实会有一些噪音(除非你的系统不再使用内存),但你所绘制的价值池绝不是随机的.

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

阅读随机性.正如您将看到的随机性是一个非常具体且难以获得的属性.这是一个常见的错误,认为如果你只是采取一些难以追踪的东西(比如你的建议),你会得到一个随机值.

  • ...而且这会遗漏所有编译器优化,这将完全消除该代码. (7认同)

Vik*_*oth 32

许多好的答案,但允许我添加另一个,并强调在确定性计算机中,没有任何东西是随机的.这对于伪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;
}
Run Code Online (Sandbox Code Playgroud)

当我在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
Run Code Online (Sandbox Code Playgroud)

如果您使用反汇编程序查看已编译的代码,则可以详细地重建正在进行的操作.对notrandom()的第一次调用使用了之前该程序未使用的堆栈区域; 谁知道那里有什么 但是在调用notrandom()之后,调用了printf()(GCC编译器实际上优化了对putchar()的调用,但没关系)并且覆盖了堆栈.因此,在下一次和随后的时间,当调用notrandom()时,堆栈将包含来自执行putchar()的陈旧数据,并且由于putchar()总是使用相同的参数调用,因此这个陈旧的数据将始终相同,太.

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

事实上,和其他人一样,我也会认真考虑解雇一个试图将这个想法作为"高性能RNG"的人.

  • 但那时,那不是一台确定性的计算机,是吗?您现在依赖环境投入.在任何情况下,这都超出了对未初始化存储器中传统伪RNG与"随机"位的讨论.另外......看看/ dev/random的描述,以了解实施者走出多远来确保随机数在加密方面是安全的......正是因为输入源不是纯粹的,不相关的量子噪声,相反,潜在的高度相关的传感器读数只有很小的随机性.它也很慢. (2认同)

650*_*502 29

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

虽然从理论上讲,当进入UB土地时,任何事情都可能发生(包括从你的鼻子飞出守护进程)通常意味着编译器作者根本不关心,对于局部变量,该值将是此时堆栈内存中的任何内容.

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

当然,你不能指望它是一个像样的随机发电机.


Mar*_*ijn 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并将其编译为noop.这肯定比任何替代方案都快.它的缺点是它不会做任何事情,但这是未定义行为的缺点.

  • 很大程度上取决于编译器的目的是帮助程序员生成满足域要求的可执行文件,还是目的是生成最"有效"的可执行文件,其行为将符合C标准的最低要求,考虑这种行为是否有用.关于前一个目标,让代码使用r,g,b的一些任意初始值,或者如果可行的话触发调试器陷阱,比将代码转换为nop更有用.关于后一个目标...... (3认同)
  • ...最佳编译器应确定哪些输入将导致上述方法执行,并消除任何仅在接收到此类输入时才相关的代码. (2认同)

Arn*_*rne 18

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


Cal*_*eth 18

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

void updateEffect(){}
Run Code Online (Sandbox Code Playgroud)

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


小智 13

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

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


Ali*_*zmi 12

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


Fra*_*e_C 12

特别糟糕!坏习惯,结果不好.考虑:

A_Function_that_use_a_lot_the_Stack();
updateEffect();
Run Code Online (Sandbox Code Playgroud)

如果函数A_Function_that_use_a_lot_the_Stack()始终进行相同的初始化,则会使堆栈上的数据相同.这些数据就是我们所说的updateEffect():永远是同样的价值!.


Bar*_*mar 11

我进行了一个非常简单的测试,它根本不是随机的.

#include <stdio.h>

int main() {

    int a;
    printf("%d\n", a);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

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


Zso*_*ari 10

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


sup*_*cat 7

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

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

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

  • "如果字节的任何组合将特别无理取闹,那么读取它将总是产生字节模式" - 直到你编码处理该模式,此时它不再是无理取闹,将来会读取不同的模式. (2认同)

小智 5

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

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

在这种情况下,您将获得的值将取决于之前在堆栈上的内容 - 如果您在此之前调用一个具有一百个本地char变量的函数全部设置为'Q'然后在之后调用您的函数返回,然后你可能会发现你的"随机"值的行为就好像你把memset()它们全都变成'Q'一样.

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

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

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

  • 你正在做一些关于*会*会发生什么的强烈预测,尽管由于UB没有保证这一点.在实践中也是如此. (2认同)

归档时间:

查看次数:

18486 次

最近记录:

7 年,5 月 前