萨特这样说:
“在 C 和 C++ 类似的低级效率传统中,编译器通常不需要初始化变量,除非你明确地这样做(例如,局部变量,从构造函数初始化列表中省略的被遗忘的成员)”
我一直想知道为什么编译器不初始化int32和float之类的原语为0。如果编译器初始化它会对性能造成什么影响?它应该比不正确的代码更好。
萨特这样说:
“在 C 和 C++ 类似的低级效率传统中,编译器通常不需要初始化变量,除非你明确地这样做(例如,局部变量,从构造函数初始化列表中省略的被遗忘的成员)”
我一直想知道为什么编译器不初始化int32和float之类的原语为0。如果编译器初始化它会对性能造成什么影响?它应该比不正确的代码更好。
实际上,这个论点是不完整的。统一变量可能有两个原因:效率和缺乏合适的默认值。
1) 效率
大多数情况下,它是过去的遗留物,当时 C 编译器只是 C 到汇编翻译器并且不执行任何优化。
这些天来,我们有智能编译器和死存储消除,在大多数情况下可以消除冗余存储。演示:
int foo(int a) {
int r = 0;
r = a + 3;
return r;
}
转化为:
define i32 @foo(i32 %a) nounwind uwtable readnone {
%1 = add nsw i32 %a, 3
ret i32 %1
}
尽管如此,在某些情况下,即使是更智能的编译器也无法消除冗余存储,这可能会产生影响。对于稍后零碎初始化的大型数组...编译器可能没有意识到所有值最终都会被初始化,因此不会删除冗余写入:
int foo(int a) {
int* r = new int[10]();
for (unsigned i = 0; i <= a; ++i) {
r[i] = i;
}
return r[a % 2];
}
请注意下面的调用memset
(我需要为new
调用加上()
值初始化的后缀)。即使0
不需要,它也没有被消除。
define i32 @_Z3fooi(i32 %a) uwtable {
%1 = tail call noalias i8* @_Znam(i64 40)
%2 = bitcast i8* %1 to i32*
tail call void @llvm.memset.p0i8.i64(i8* %1, i8 0, i64 40, i32 4, i1 false)
br label %3
; <label>:3 ; preds = %3, %0
%i.01 = phi i32 [ 0, %0 ], [ %6, %3 ]
%4 = zext i32 %i.01 to i64
%5 = getelementptr inbounds i32* %2, i64 %4
store i32 %i.01, i32* %5, align 4, !tbaa !0
%6 = add i32 %i.01, 1
%7 = icmp ugt i32 %6, %a
br i1 %7, label %8, label %3
; <label>:8 ; preds = %3
%9 = srem i32 %a, 2
%10 = sext i32 %9 to i64
%11 = getelementptr inbounds i32* %2, i64 %10
%12 = load i32* %11, align 4, !tbaa !0
ret i32 %12
}
2)默认?
另一个问题是缺乏合适的值。虽然 afloat
可以完美地初始化为NaN
,但整数呢?没有表示没有值的整数值,根本没有!0
是一个候选者(除其他外),但有人可能会争辩说它是最差的候选者之一:它很可能是一个数字,因此可能对手头的用例具有特定含义;你确定你对这个默认的含义感到满意吗?
深思熟虑
最后,单元化变量还有一个优点:它们是可检测的。编译器可能会发出警告(如果它足够聪明的话),并且Valgrind 会引发错误。这使得逻辑问题可以被检测到,并且只有检测到的问题才能被纠正。
当然,一个标记值,例如NaN
,同样有用。不幸的是......整数没有。
初始化可能会以两种方式影响性能。
首先,初始化变量需要时间。当然,对于单个变量,它可能可以忽略不计,但正如其他人所建议的那样,它可以与大量变量、数组等相加。
其次,谁能说零是合理的默认值?对于每一个零是有用的默认值的变量,可能还有另一个不是。在这种情况下,如果您确实将其初始化为零,则会产生进一步的开销,将变量重新初始化为您实际想要的任何值。您基本上支付了两次初始化开销,而不是在默认初始化未发生时支付一次。请注意,无论您选择什么作为默认值(零或其他),这都是正确的。
鉴于存在开销,不初始化并让编译器捕获对未初始化变量的任何引用通常更有效。
基本上,变量引用内存中的一个位置,可以对其进行修改以保存数据。对于一个未初始化的变量,程序只需要知道这个位置在哪里,编译器通常会提前计算出来,因此不需要任何指令。但是当你希望它被初始化时(比如说,0),程序需要使用额外的指令来做到这一点。
一个想法可能是在程序启动时使用 memset 将整个堆清零,然后初始化所有静态内容,但这对于在读取之前动态设置的任何内容都不需要。这对于基于堆栈的函数也是一个问题,每次调用函数时都需要将其堆栈帧归零。简而言之,允许变量默认为未定义会更有效,特别是当堆栈经常被新调用的函数覆盖时。
使用 -Wmaybe-uninitialized 编译并找出答案。这些是编译器无法优化原始初始化的唯一地方。
至于堆...