局部变量初始化是否必须?

时间:2008-09-26 14:02:20

标签: c++ c performance optimization

未经初始化的本地人造成的维护问题(特别是指针)对于那些已经做过一些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    $33, -4(%ebp)
>       movl    $4, 8(%esp)
>       movl    $66, 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 ++的答案,但请明确语言/环境(以及可重复的证据,而不是推测!)

17 个答案:

答案 0 :(得分:11)

简短回答:声明变量尽可能接近第一次使用,如果仍需要则初始化为“零”。

答案很长:如果在函数开头声明变量,并且在以后不使用它,则应该重新考虑变量的位置作为本地范围。然后,您通常可以立即为其分配所需的值。

如果必须声明它未初始化,因为它是在条件中分配的,或者通过引用传递并分配给它,将它初始化为null等效值是一个好主意。如果您在-Wall下编译,编译器有时可以保存您,因为如果您在初始化之前读取变量,它将发出警告。但是,如果将其传递给函数,则无法向您发出警告。

如果您安全地将其设置为等效的null,那么如果您传递它的函数会覆盖它,则不会造成任何伤害。但是,如果你传递给它的函数使用了这个值,你几乎可以保证失败一个断言(如果你有一个),或者至少在第二个断言你使用一个空对象。随机初始化可以做各种不好的事情,包括“工作”。

答案 1 :(得分:6)

如果你认为初始化是多余的,那就是。我的目标是编写尽可能人性化的代码。不必要的初始化会使未来的读者感到困惑。

C编译器在捕获单位化变量的使用方面已经相当不错,所以现在这种危险很小。

不要忘记,通过“假”初始化,你交易一个危险 - 崩溃使用垃圾(这导致一个很容易找到和修复的错误)在另一个 - 程序采取错误的行动基于假值(这会导致很难找到的错误)。选择取决于应用程序。对某些人来说,永远不会崩溃至关重要。对于大多数人来说,最好尽快捕捉到这个错误。

答案 2 :(得分:5)

这是过早优化是万恶之源

的一个很好的例子

完整的引用是:

  

毫无疑问,效率的严重性会导致滥用。程序员浪费了大量时间来考虑或担心程序中非关键部分的速度,而这些效率尝试实际上在考虑调试和维护时会产生很大的负面影响。 我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源。   然而,我们不应该放弃那个关键的3%的机会。一个好的程序员不会因为这样的推理而自满,他会明智地仔细研究关键代码;但只有在确定了该代码之后。

这来自Donald Knuth。你会相信谁...你的同事还是Knuth?
我知道我的钱在哪里......

回到最初的问题:“我们应该进行MANDATE初始化吗?” 我会这样说:

  

变量应该进行初始化,除非可以证明通过不初始化可以实现重要的性能增益。带着难得的号码......

答案 3 :(得分:4)

我不确定是否有必要“强制要求”,但我个人认为初始化变量总是更好。如果应用程序的目的是尽可能紧密,那么C / C ++就是为此而开放的。但是,我认为我们中的许多人已经烧了一两次没有初始化变量并假设它包含一个有效值(例如指针),而实际上并没有。地址为零的指针比在该特定位置的最后一个存储器内容中具有随机垃圾更容易检查。我认为在大多数情况下,它不再是性能问题,而是一个清晰和安全的问题。

答案 4 :(得分:3)

让我告诉你一个关于我在1992年和之后工作的产品的故事,为了这个故事的目的,我们将调用Stackrobat。我被分配了一个错误导致应用程序在Mac上崩溃,但在Windows上没有,哦,这个错误无法可靠地重现。质量保证花了一个星期的大部分时间来制定一个可能有十分之一的食谱。

由于实际崩溃发生在执行此操作之后很长时间,因此追踪根本原因。

最终,我通过为编译器编写自定义代码分析器来跟踪它。编译器很乐意为全局prof_begin()和prof_end()函数注入调用,您可以自由地实现它们。我写了一个从堆栈中获取返回地址的分析器,找到了堆栈帧创建指令,将堆栈中的块定位为代表该函数的本地,并用一层美味的垃圾涂上它们,如果有的话会导致总线错误元素被解除引用。

这引发了初始化之前使用的六个指针错误,包括我正在寻找的错误。

发生的事情是,大多数情况下,如果他们被取消引用,那么堆栈恰好具有显然是良性的价值。其他时候,这些值会导致应用程序对其自己的堆进行霰弹,并在稍后的某个时间取出应用程序。

我花了两周多的时间试图找到这个错误。

课程:初始化您的本地人。如果有人抨击您的表现,请向他们展示此评论并告诉他们您宁愿花两周时间运行分析代码并修复瓶颈,而不是必须追踪这样的错误。调试工具和堆检查器已经变得更好了,因为我不得不这样做,但坦率地说,他们可以更好地弥补这种不良做法的错误。

除非你在一个小型系统(嵌入式等)上运行,否则本地人的初始化应该几乎是免费的。 MOVE / LOAD指令非常非常快。首先将代码编写为可靠且可维护的。重新考虑它是第二个。

答案 5 :(得分:3)

它应该主要是强制性的。其原因与性能无关,而是使用单元化变量的危险。但是,有些情况下它看起来很荒谬。例如,我见过:

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 ???

请注意,我们正在立即处理错误,因此如果统计信息失败,将会出现什么问题。

答案 6 :(得分:2)

有时您需要一个变量作为占位符(例如使用ftime函数),因此在调用初始化函数之前初始化它们没有意义。

然而,在我看来,要注意你已经意识到陷阱的事实并不是坏事,

uninitialized time_t t;
time( &t );

答案 7 :(得分:2)

这仅适用于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.

当然,在复制构建与默认构造+分配时会调用完全不同的代码。此外,对复制构造函数的单个调用可能比构造后的赋值更有效。

答案 8 :(得分:1)

是的:始终初始化您的变量,除非您有非常的理由不这样做。如果我的代码不需要特定的初始值,我通常会将变量初始化为一个值,如果后面的代码被破坏,保证会出现明显的错误。

答案 9 :(得分:1)

始终将局部变量初始化为零。如你所见,它没有真正的表现。

int i = 0;
struct myStruct m = {0};

如果是这样,你基本上会添加1或2个汇编指令。事实上,很多C运行时都会在“Release”版本中为你做这个,你不会改变它。

但是你应该把它搞定,因为你现在可以得到这种保证。

不初始化的一个原因与调试有关。一些运行时,例如。 MS CRT将使用您可以识别的预定和记录的模式初始化内存。因此,当您通过内存填充时,您可以看到内存确实未初始化且尚未使用和重置。这在调试中很有帮助。但那是在调试期间。

答案 10 :(得分:1)

在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;

答案 11 :(得分:1)

性能?如今?也许当CPU以10mhz运行时它确实有意义,但今天它几乎不成问题。始终初始化它们。

答案 12 :(得分:0)

有时变量用于“收集”较长嵌套ifs / elses块的结果......在这些情况下,我有时会保持变量未初始化,因为 后面应该初始化其中一个条件分支。

诀窍是:如果我最初没有初始化它然后在长if / else块中有一个错误所以变量从未被分配,我可以看到Valgrind中的错误:-)当然需要频繁运行通过Valgrind的代码(理想情况下是常规测试)。

答案 13 :(得分:0)

作为一个简单的例子,你能确定它将被初始化为什么(C / C ++)吗?

bool myVar;

我们在产品中遇到了一个问题,有时会在屏幕上绘制图像,有时则不会,通常取决于构建的是谁的机器。事实证明,在我的机器上它被初始化为假,并且在同事机器上它被初始化为真。

答案 14 :(得分:0)

我认为在大多数情况下使用默认值初始化变量是一个坏主意,因为它只是隐藏了很容易在未初始化变量中找到的错误。如果您忘记获取并设置实际值,或意外删除获取代码,您可能从未注意到它,因为0在许多情况下是合理的值。大多数情况下,使用值&gt;&gt;触发这些错误要容易得多。 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,取决于函数的功能,很难找到,甚至还有一个错误。

答案 15 :(得分:0)

只是次要观察。初始化仅在原始类型上或在由const函数指定时易于优化。

a = foo();

a = foo2();

无法轻松优化,因为foo可能有副作用。

在时间之前堆积分配也可能导致巨大的性能命中。拿一个像

这样的代码
void foo(int x)

{

ClassA * instance = new ClassA();

// ...做一些与“实例”无关的事情......   如果(X→5)   {

delete instance;

return;

}

// ..做一些使用实例的东西

}

在这种情况下,只需在您使用它时声明实例,并仅在那里初始化它。并且编译器无法为您优化,因为构造函数可能具有代码重新排序会发生更改的副作用。

编辑:我无法使用代码列表功能:P

答案 16 :(得分:0)

正如你对performacne所展示的那样,它并没有什么不同。编译器将(在优化版本中)检测是否写入局部变量而不读取和删除代码,除非它有其他副作用。

那说:如果你用简单的陈述来初始化东西只是为了确保它已经初始化,这样做很好..我个人不这样做,原因有一个:

它欺骗那些可能在以后维护您的代码的人认为需要初始化。那个小foo = 0;会增加代码复杂性。除此之外,这只是一个品味问题。

如果您通过复杂的语句对变量进行unnessesary初始化,则可能会产生副作用。

例如:

  float x = sqrt(0);

如果幸运的话,可以通过编译器对其进行优化,并使用聪明的编译器。使用一个不那么聪明的编译器,它也可能导致代价高昂且无法使用的函数调用,因为sqrt可以 - 作为副作用 - 设置errno变量。

如果您调用自己定义的函数,我最好的选择是,编译器总是假设它们可能有副作用而不是优化它们。如果函数恰好位于同一个翻译单元中,或者您打开了整个程序优化,则可能会有所不同。