局部变量初始化应该是强制性的吗?
对于一些进行过c / c ++维护或者增强的人来说,未初始化的局部变量引起的维护问题(尤其是指针)会很明显,但是我仍然看到它们,并偶尔听到性能影响作为其理由。
很容易在c中证明冗余初始化已被优化:
$ less test.c #include <stdio.h> main() { #ifdef INIT_LOC int a = 33; int b; memset(&b,66,sizeof(b)); #else int a; int b; #endif a = 0; b = 0; printf ("a = %i, b = %i\n", a, b); } $ gcc --version gcc (GCC) 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)
[未优化:]
$ gcc test.c -S -o no_init.s; gcc test.c -S -D INIT_LOC=1 -o init.s; diff no_in it.s init.s 22a23,28 > movl , -4(%ebp) > movl , 8(%esp) > movl , 4(%esp) > leal -8(%ebp), %eax > movl %eax, (%esp) > call _memset 33a40 > .def _memset; .scl 3; .type 32; .endef
[优化:]
$ gcc test.c -O -S -o no_init.s; gcc test.c -O -S -D INIT_LOC=1 -o init.s; diff no_init.s init.s $
那么,WRT的性能在什么情况下强制变量初始化不是一个好主意?
如果适用,则无需将答案限制为c / c ++,但请清楚了解语言/环境(以及可再现的证据,比猜测更可取!)
解决方案
有时我们需要使用变量作为占位符(例如,使用ftime
函数),因此在调用初始化函数之前对其进行初始化没有任何意义。
但是,在我看来,注释一下我们已经意识到的隐患并不是一件坏事,
uninitialized time_t t; time( &t );
我不确定是否有必要"强制使用它们",但是我个人认为初始化变量总是更好。如果应用程序的目的是尽可能紧凑,则为此目的打开C / C ++。但是,我认为我们很多人都因为不初始化变量而假定它实际上没有包含有效值(例如指针)而被烧一两次。与指针是否具有来自该特定位置的最后一个内存内容的随机垃圾相比,检查地址为零的指针要容易得多。我认为在大多数情况下,这不再是性能问题,而是清晰度和安全性问题。
简短的答案:将变量声明为尽可能接近首次使用,并在需要时将其初始化为"零"。
长答案:如果我们在函数开始时声明了一个变量,并且直到以后才使用它,则应重新考虑将变量放置在尽可能局部的范围内。然后,我们通常可以立即为其分配所需的值。
如果由于必须在有条件的情况下对其进行赋值或者通过引用传递并对其进行赋值而必须将其声明为未初始化,则将其初始化为等效于null的值是一个好主意。如果在-Wall下进行编译,编译器有时可以为我们省钱,因为如果在初始化变量之前从变量中读取该变量,它将发出警告。但是,如果将其传递给函数,它不会警告我们。
如果我们放心地将它设置为等效于空值,则将其传递给它的函数覆盖它不会对我们造成任何损害。但是,如果将其传递给的函数使用该值,则可以保证在断言失败(如果有断言)的情况下,或者至少对使用空对象的第二个断言进行断节。随机初始化会做各种坏事,包括"工作"。
表现?如今?也许当CPU以10MHz运行时确实是有道理的,但是今天几乎没有问题了。始终对其进行初始化。
正如我们所展示的对Performacne的看法一样,这没有什么不同。编译器(在优化的版本中)将检测是否写入了局部变量而不会被读取,并删除代码,除非它具有其他副作用。
就是说:如果我们使用简单的语句初始化东西只是为了确保它已初始化就可以。.我个人不这样做,原因有一个:
它使以后可能维护代码的人误以为需要初始化。那个小foo = 0;会增加代码复杂度。除此之外,这只是一个品味问题。
如果我们不必要地通过复杂的语句初始化变量,则可能会有副作用。
例如:
float x = sqrt(0);
如果幸运的话,可以由编译器优化,并且可以使用聪明的编译器。对于不太聪明的编译器,它也可能导致代价高昂且不必要的函数调用,因为sqrt的副作用是可以设置errno变量。
如果调用自己定义的函数,那么最好的选择是,编译器始终假定它们可能会产生副作用,并且不会对其进行优化。如果功能恰好位于同一翻译单元中,或者我们已打开整个程序优化,则可能会有所不同。
在C / C ++中,我完全同意看法。
在Perl中,当我创建一个变量时,它会自动设置为默认值。
my ($val1, $val2, $val3, $val4); print $val1, "\n"; print $val1 + 1, "\n"; print $val2 + 2, "\n"; print $val3 = $val3 . 'Hello, SO!', "\n"; print ++$val4 +4, "\n";
最初它们都设置为undef。 Undef是一个错误值,是一个占位符。由于是动态类型,如果我向其中添加一个数字,它会假定我的变量是一个数字,并用eqivilent的false值0代替undef。自动替换。
[jeremy@localhost Code]$ ./undef.pl 1 2 Hello, SO! 5
因此,对于Perl而言,至少要尽早声明,不要担心。特别是由于大多数程序具有许多变量。我们使用的行数更少,并且无需进行显式初始化即可看起来更干净。
my($x, $y, $z);
:-)
my $x = 0; my $y = 0; my $z = 0;
这是一个很好的例子,过早的优化是万恶之源
完整的报价是:
There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.
这来自唐纳德·克努斯(Donald Knuth)。我们要相信谁...同事或者Knuth?
我知道我的钱在哪里...
回到最初的问题:"我们应该初始化吗?"
我这样说:
Variables should be initialize, except in situation where it can be demonstrated there is a significant performance gain to be realized by not initializing. Come armed with hard numbers...
有时,使用变量来"收集"更长的嵌套ifs / elses的结果...在那些情况下,有时我会将变量保持未初始化状态,因为稍后应通过条件分支之一对其进行初始化。
诀窍是:如果首先让它保持未初始化状态,然后在长if / else块中存在一个错误,那么就永远不会分配该变量,我可以在Valgrind中看到该错误:-)当然,这需要经常运行代码最好是通过Valgrind进行常规测试)。
始终将局部变量至少初始化为零。如我们所见,没有真正的表现。
int i = 0; struct myStruct m = {0};
基本上,我们要添加1或者2条汇编指令。实际上,许多C运行时将在"发布"版本上为我们执行此操作,并且我们将无需进行任何更改。
但是我们应该启动它,因为现在我们将获得保证。
不初始化的原因之一与调试有关。一些运行时,例如。 MS CRT将使用我们可以识别的预定文档格式来初始化内存。因此,当我们遍历内存时,可以看到内存确实未初始化,并且尚未使用和重置。这对调试很有帮助。但这是在调试过程中。
它应该主要是强制性的。这样做的原因与性能无关,而是使用统一变量的危险。但是,在某些情况下,它看起来简直太荒谬了。例如,我看过:
struct stat s; s.st_dev = -1; s.st_ino = -1; s.st_mode = S_IRWXU; s.st_nlink = 0; s.st_size = 0; // etc... s.st_st_ctime = -1; if(stat(path, &s) != 0) { // handle error return; }
WTF ???
请注意,我们会立即处理该错误,因此毫无疑问,如果统计信息失败,该怎么办。
如果我们认为初始化是多余的,那么它是。我的目标是编写尽可能人为可读的代码。不必要的初始化会使将来的读者困惑。
C编译器在捕捉统一变量的使用方面已经变得非常擅长,因此现在的危险已降至最低。
别忘了,通过进行"假"初始化,我们会在另一个程序上基于虚假值(该错误导致一个错误)在另一个程序上使用垃圾(这会导致很容易找到和修复的错误)带来崩溃的危险。很难找到的错误)。选择取决于应用程序。对于某些人来说,永不崩溃是至关重要的。对于大多数人来说,最好尽快发现该错误。
这仅适用于C ++,但是这两种方法之间有一定的区别。
假设我们有一个类MyStuff,并且想通过另一个类对其进行初始化。我们可以执行以下操作:
// Initialize MyStuff instance y // ... MyStuff x = y; // ...
这实际上是调用x的副本构造函数。与以下内容相同:
MyStuff x(y);
这与以下代码不同:
MyStuff x; // This calls the MyStuff default constructor. x = y; // This calls the MyStuff assignment operator.
当然,在复制构造与默认构造+分配时会调用完全不同的代码。同样,对复制构造函数的单次调用可能比构造后进行赋值的效率更高。
作为一个简单的示例,我们可以确定将初始化为(C / C ++)的什么吗?
bool myVar;
我们的产品存在问题,该产品有时会在屏幕上绘制图像,有时却无法在屏幕上绘制图像,这通常取决于制造该产品的机器。原来,在我的机器上,它被初始化为false,而在同事的机器上,它被初始化为true。
我认为在大多数情况下,用默认值初始化变量是一个坏主意,因为它只是隐藏了一些错误,而这些错误很容易在未初始化的变量中发现。如果忘记获取和设置实际值,或者意外删除了获取代码,则可能永远不会注意到它,因为在许多情况下0是一个合理的值。通常,用值>> 0触发这些错误要容易得多。
例如:
void func(int n) { int i = 0; ... // Many lines of code for (;i < n; i++) do_something(i);
一段时间后,我们将添加其他内容。
void func(int n) { int i = 0; for (i = 0; i < 3; i++) do_something_else(i); ... // Many lines of code for (;i < n; i++) do_something(i);
现在,第二个循环不是从0开始,而是从3开始,这取决于要执行的功能,甚至很难发现一个错误。
只是次要的观察。初始化仅在基元类型上或者在由const函数分配时容易进行优化。
a = foo();
a = foo2();
无法轻松优化,因为foo可能会有副作用。
同样,在时间之前分配堆可能会导致巨大的性能损失。像这样的代码
void foo(int x)
{
ClassA * instance =新的ClassA();
// ...做一些与"实例"无关的事情...
如果(x> 5)
{
delete instance; return;
}
// ..做一些使用实例的事情
}
在这种情况下,只需在使用实例时声明实例,然后仅在实例中对其进行初始化。否编译器无法为我们优化此操作,因为构造函数可能会产生副作用,导致代码重新排序会发生变化。
编辑:我无法使用代码列表功能:P
让我告诉我们一个有关我在1992年工作的产品的故事,此后,出于这个故事的目的,我们将其称为Stackrobat。我被分配了一个错误,该错误导致该应用程序在Mac上崩溃,但在Windows上没有崩溃,哦,该错误不能可靠地再现。质量检查花费了一周的大部分时间才能得出有效的食谱,每10遍就有1遍。
自从真正的崩溃发生在采取措施之后,这真是令人难以置信。
最终,我通过为编译器编写自定义代码分析器来对其进行跟踪。编译器很高兴将对全局prof_begin()和prof_end()函数的调用注入,我们可以自由地实现它们。我编写了一个探查器,该探查器从堆栈中获取了返回地址,找到了堆栈框架创建指令,在堆栈上找到了代表该函数本地变量的块,并在其上覆盖了一层可口的废话,如果有的话,这会导致总线错误元素已取消引用。
这捕获了初始化之前正在使用的指针的一些错误,包括我正在寻找的错误。
发生的事情是,在大多数情况下,如果堆栈被取消引用,则堆栈中的值显然是无害的。在其他时候,这些值会使应用程序散弹其自身的堆,从而在更晚的某个时间取出该应用程序。顶一下。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。.。
我花了两个多星期的时间来尝试查找此错误。
课程:初始化本地人。如果有人对我们表现不佳,请向他们显示此评论,并告诉他们我们宁愿花两周时间来运行性能分析代码并修复瓶颈,而不必跟踪此类错误。自从我不得不这样做以来,调试工具和堆检查器已经变得越来越好,但是坦率地说,它们可以弥补这种不良做法带来的错误。
除非我们在小型系统(嵌入式等)上运行,否则本地初始化几乎是免费的。 MOVE / LOAD指令非常非常快。首先编写可靠且可维护的代码。将其重构为性能更高的第二位。
是的:除非我们有充分的理由不这样做,否则请始终初始化变量。如果我的代码不需要特定的初始值,那么我经常将变量初始化为一个值,如果后面的代码损坏,该值将保证出现公然错误。