我知道未初始化的局部变量是未定义的行为(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 回答
未定义的行为意味着编译器的作者可以自由地忽略这个问题,因为程序员永远无权抱怨发生的任何事情 .
理论上,当进入UB land anything can happen (包括daemon flying off your nose)时,通常意味着编译器作者不会关心,对于局部变量,该值将是该点处的堆栈存储器中的任何值 .
这也意味着通常内容将是“奇怪的”但是固定的或稍微随机的或可变的但具有明显的明显模式(例如,在每次迭代时增加值) .
肯定你 cannot 期望它是一个像样的随机发电机 .
正如其他人所说,这是未定义的行为(UB) .
在实践中,它(可能)实际上(有点)工作 . 从x86 [-64]体系结构上的未初始化寄存器读取确实会产生垃圾结果,并且可能不会做任何坏事(与例如Itanium相反,其中registers can be flagged as invalid,因此读取传播错误,如NaN) .
但有两个主要问题:
It won't be particularly random. 在这种情况下,你're reading from the stack, so you'将获得之前的任何内容 . 这可能是有效的随机,完全结构化,十分钟前输入的密码,或祖母的cookie配方 .
It's Bad (capital 'B') 练习让这样的事情蔓延到你的代码中 . 从技术上讲,每次读取未定义的变量时,编译器都可以插入
reformat_hdd();
. 它不会,但你不应该做不安全的事情 . 你做的例外越少,你就越容易意外犯错 .UB更紧迫的问题是它使整个程序的行为未定义 . 现代编译器可以使用它来消除大量代码甚至go back in time . 与UB一起玩就像维多利亚时代的工程师正在拆除现场核反应堆 . 有's a zillion things to go wrong, and you probably won'知道一半的基本原则或实施技术 . 它可能没问题,但你仍然不应该让它发生 . 看看其他很好的答案细节 .
而且,我会解雇你 .
让我清楚地说出来: we do not invoke undefined behavior in our programs . 从来没有一个好主意 . 这条规则很少有例外;例如,如果你是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 Causality(video),其中包括:
特别是对于不确定的值,C标准defect report 451: Instability of uninitialized automatic variables做了一些有趣的阅读 . 它还没有得到解决,但引入了摇摆值的概念,这意味着值的不确定性可能通过程序传播,并且在程序的不同点可能具有不同的不确定值 .
我不知道发生这种情况的任何例子,但在这一点上我们不能排除它 .
Real examples, not the result you expect
您不太可能获得随机值 . 编译器可以完全优化离开循环 . 例如,通过这个简化的案例:
clang优化它(see it live):
或者可能得到全零,就像这个修改过的情况一样:
see it live:
这两种情况都是完全可接受的未定义行为形式 .
注意,如果我们在Itanium上,我们可以end up with a trap value:
Other important notes
值得注意的是variance between gcc and clang noted in the UB Canaries project关于他们是否愿意利用与未初始化内存相关的未定义行为 . 文章指出(强调我的):
正如Matthieu M.指出What Every C Programmer Should Know About Undefined Behavior #2/3也与这个问题有关 . 它说除其他外(强调我的):
为了完整起见,我应该提一下,实现可以选择明确定义未定义的行为,例如gcc allows type punning through unions而in C++ this seems like undefined behavior . 如果是这种情况,实现应该记录它,这通常是不可移植的 .
不,这太可怕了 .
使用未初始化变量的行为在C和C中都是未定义的,并且这种方案不太可能具有理想的统计属性 .
如果你想要一个"quick and dirty"随机数发生器,那么
rand()
是你最好的选择 . 在其实现中,它所做的只是乘法,加法和模数 .我知道的最快的生成器要求你使用
uint32_t
作为伪随机变量I
的类型,并使用I = 1664525 * I + 1013904223
生成连续的值 . 您可以选择任何您想要的初始值
I
(称为种子) . 显然你可以编写内联代码 . 无符号类型的标准保证环绕充当模数 . (数字常数由杰出的科学程序员Donald Knuth亲自挑选 . )好问题!
未定义并不意味着它是随机的 . 想一想,您在全局未初始化变量中获得的值是由系统或您/其他应用程序运行的 . 根据系统对不再使用的内存和/或系统和应用程序生成的值的不同,您可能会得到:
总是一样的 .
是一小组 Value 观之一 .
获取一个或多个小范围内的值 .
从16/32/64位系统上的指针中查看2/4/8可分割的多个值
......
您将获得的值完全取决于系统和/或应用程序留下的非随机值 . 所以,确实会有一些噪音(除非你的系统不再使用内存),但你所绘制的 Value 池绝不是随机的 .
局部变量的情况变得更糟,因为它们直接来自您自己程序的堆栈 . 您的程序很可能在执行其他代码期间实际编写这些堆栈位置 . 我估计在这种情况下运气的可能性非常低,你做的“随机”代码改变试试这个运气 .
阅读randomness . 因为你认为如果你只是拿一些's hard to track (like your suggestion) you' ll得到一个随机值的常见错误 .
许多好的答案,但允许我添加另一个,并强调在确定性计算机中,没有任何东西是随机的 . 对于由伪RNG产生的数字和在堆栈上为C / C局部变量保留的存储区域中找到的看似“随机”数字都是如此 .
但是......有一个至关重要的区别 .
由良好的伪随机生成器生成的数字具有使其在统计上类似于真正随机抽取的属性 . 例如,分布是统一的 . 循环长度很长:在循环重复之前,您可以获得数百万个随机数 . 序列不是自相关的:例如,如果你取每个第2,第3或第27个数字,或者如果你查看生成数字中的特定数字,你就不会看到出现奇怪的模式 .
相反,堆栈上留下的“随机”数字没有这些属性 . 它们的 Value 观及其明显的随机性完全取决于程序的构建方式,编译方式,以及编译器如何优化它 . 举例来说,这是您作为自包含程序的想法的变体:
当我在Linux机器上使用GCC编译此代码并运行它时,结果是相当不愉快的确定性:
如果您使用反汇编程序查看已编译的代码,则可以详细地重建正在进行的操作 . 对notrandom()的第一次调用使用了之前该程序未使用的堆栈区域;谁知道那里有什么但是在调用notrandom()之后,调用了printf()(GCC编译器实际上优化了对putchar()的调用,但没关系)并且覆盖了堆栈 . 因此,在下一次和随后的时间,当调用notrandom()时,堆栈将包含来自执行putchar()的陈旧数据,并且由于putchar()总是使用相同的参数调用,因此这个陈旧的数据将始终相同,太 .
因此,对于这种行为绝对没有任何随机性,以这种方式获得的数字也没有具有良好编写的伪随机数生成器的任何所需属性 . 实际上,在大多数现实场景中,它们的值将是重复的并且高度相关 .
事实上,和其他人一样,我也会认真考虑解雇一个试图将这个想法作为“高性能RNG”的人 .
未定义的行为未定义 . 这并不意味着您获得了未定义的值,这意味着程序可以执行任何操作并仍然符合语言规范 .
一个好的优化编译器应该采取
并将其编译为noop . 这肯定比任何替代方案都快 . 它的缺点是它不会做任何事情,但这是未定义行为的缺点 .
尚未提及,但允许调用未定义行为的代码路径可以执行编译器所需的任何操作,例如,
这肯定比你正确的循环更快,因为UB,完全符合要求 .
由于安全原因,必须清理分配给程序的新内存,否则可能会使用该信息,并且密码可能会从一个应用程序泄漏到另一个应用程序 . 只有当你重用内存时,才会得到不同于0的值 . 而且很有可能,在堆栈中,之前的值只是固定的,因为之前使用的内存是固定的 .
您的特定代码示例可能无法满足您的期望 . 虽然从技术上讲,循环的每次迭代都会重新创建r,g和b值的局部变量,但实际上它是堆栈上完全相同的内存空间 . 因此,每次迭代都不会重新随机化,并且最终会为1000种颜色中的每种颜色分配相同的3个值,无论r,g和b的单独和最初是多么随机 .
事实上,如果确实有效,我会非常好奇它是什么让它重新随机化 . 我唯一能想到的就是一个交错的中断,它堆叠在堆栈顶上,极不可能 . 也许内部优化将那些保持为寄存器变量而不是真正的存储器位置,其中寄存器在循环中进一步向下使用,也可以做到这一点,特别是如果集合可见性函数特别是寄存器饥饿 . 仍然,远非随机 .
因为这里的大多数人都提到了未定义的行为 . 未定义也意味着您可以获得一些有效的整数值(幸运的是),在这种情况下,这将更快(因为没有进行rand函数调用) . 但实际上并没有使用它 . 我相信这会产生可怕的结果,因为运气不会一直伴随着你 .
特别糟糕!坏习惯,结果不好 . 考虑:
如果函数
A_Function_that_use_a_lot_the_Stack()
始终进行相同的初始化,则会使堆栈上的数据相同 . 那些数据就是我们所谓的updateEffect()
: always same value! .一世进行了一个非常简单的测试,它根本不是随机的 .
每次我运行该程序时,它都会打印相同的数字(在我的情况下为
32767
) - 你不能随意得到更多的随机数 . 这可能是堆栈中剩余的运行时库中的启动代码 . 由于每次程序运行时它都使用相同的启动代码,并且在运行之间程序中没有其他任何变化,因此结果完全一致 .您需要定义“随机”的含义 . 一个明智的定义涉及你得到的 Value 应该没有多少相关性 . 这是你可以测量的东西 . 以可控,可重复的方式实现也并非易事 . 所以未定义的行为肯定不是你想要的 .
在某些情况下,可以使用类型"unsigned char*"安全地读取未初始化的内存[例如,从
malloc
]返回的缓冲区 . 代码可以读取这样的内存,而不必担心编译器会将因果关系抛到窗口之外,并且有时为代码准备内存可能包含的内容可能比确保未读取未初始化数据更有效(一个常见的例子就是在部分初始化的缓冲区上使用memcpy
而不是离散地复制包含有意义数据的所有元素 .然而,即使在这种情况下,也应该总是假设如果字节的任何组合将特别无理取闹,那么读取它将总是产生该字节模式(并且如果某个模式在 生产环境 中是无理取闹的,但在开发中不是,那么在代码处于 生产环境 状态之前,模式不会出现 .
读取未初始化的内存可能是嵌入式系统中随机生成策略的一部分,在嵌入式系统中可以确保自上次系统上电以来,内存从未使用基本上非随机的内容编写,并且如果制造用于存储器的过程使其电源接通状态以半随机方式变化 . 即使所有设备始终产生相同的数据,代码也应该有效,但是在例如一组节点每个都需要尽可能快地选择任意唯一ID,具有“非常随机”的生成器,其给予一半节点相同的初始ID可能比根本没有任何初始随机源更好 .
正如其他人所说,它会很快,但不是随机的 .
大多数编译器会为局部变量做的是在堆栈中为它们占用一些空间,但不要把它设置为任何东西(标准说它们不需要,所以为什么要减慢你生成的代码?) .
在这种情况下,你返回后返回的值为'll get will depend on what was on previously on the stack - if you call a function before this one that has a hundred local char variables all set to ' Q ' and then call you',然后你'll probably find your 1806829 values behave as if you' ve
memset()
将它们全部返回到'Q' .重要的是,对于尝试使用它的示例函数,每次读取它们时这些值都不会改变,它们每次都会相同 . 因此,您将获得100颗星,所有颜色和可见度都相同 .
此外,没有任何说明编译器不应该初始化这些值 - 所以未来的编译器可能会这样做 .
一般来说:坏主意,不要这样做 . (就像很多“聪明”的代码级优化真的...)
正如其他人已经提到的,这是未定义的行为(UB),但它可能是"work" .
除了其他人已经提到过的问题之外,我还看到了另外一个问题(缺点) - 它不能用于除C和C之外的任何语言 . 我知道这个问题是关于C的,但是如果你能编写好的C代码和Java代码并且它不是问题那么为什么不呢?也许有一天有人会把它移植到其他语言并寻找由“魔术”引起的错误 . 这样的UB肯定会是一场噩梦(特别是对于没有经验的C / C开发人员) .
Here有关于另一个类似UB的问题 . 想象一下,在不知道这个UB的情况下,你试图找到这样的bug . 如果你想在C / C中阅读更多关于这些奇怪的事情,请阅读链接中的问题答案,并参见this GREAT 幻灯片 . 它将帮助您了解's under the hood and how it'的工作原理;很明显,即使是大多数经验丰富的C / C程序员也可以从中学到很多东西 .
Not 一个好主意,依靠我们对语言未定义行为的任何逻辑 . 除了这篇文章中提到/讨论过的内容之外,我想提一下,使用现代C方法/风格,这样的程序可能无法编译 .
这是在提到的我之前的帖子包含了 auto 功能的优点和有用的链接 .
https://stackoverflow.com/a/26170069/2724703
因此,如果我们更改上面的代码并用 auto 替换实际类型,程序甚至不会编译 .
我喜欢你的思维方式 . 真的在盒子外面 . 然而,权衡真的不值得 . 内存运行时权衡是一件事,包括运行时的未定义行为是 not .
它必须让你感到非常不安,因为我知道你正在使用这种“随机”作为你的业务逻辑 . 我不这样做 .
在每个想要使用未初始化变量的地方使用
7757
. 我从素数列表中随机选择它:它是定义的行为
保证不总是0
这是素数
它可能与未初始化的变量一样具有统计随机性
它可能比未初始化的变量更快,因为它的值在编译时是已知的
还有一种可能性需要考虑 .
现代编译器(ahem g)是如此智能,以至于他们通过你的代码来查看哪些指令影响状态,什么不能,以及如果一条指令保证不影响状态,g将简单地删除该指令 .
所以这就是将要发生的事情 . g肯定会看到你正在阅读,执行算术,保存,本质上是垃圾值,这会产生更多的垃圾 . 由于无法保证新垃圾比旧垃圾更有用,它只会取消您的循环 . BLOOP!
这种方法很有用,但这就是我要做的 . 将UB(未定义的行为)与rand()速度组合 .
当然,减少
rand()
已执行,但将它们混合在一起,因此编译器并不想要它 .我不会解雇你 .
如果做得好,使用未初始化的数据进行随机性并不一定是坏事 . 事实上,OpenSSL正是为了实现其PRNG而实现的 .
显然,这种用法没有很好地记录,因为有人注意到Valgrind抱怨使用未初始化的数据和"fixed"它,导致bug in the PRNG .
所以你可以做到,但你需要知道你在做什么,并确保阅读你的代码的任何人都理解这一点 .