萨特这样说:
“在 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) 默认?
另一个问题是缺乏合适的值。虽然
float
可以完美地初始化为 NaN
,但整数呢?不存在代表没有值的整数值,根本没有! 0
是一个候选者(除其他外),但有人可能会说它是最差的候选者之一:它是一个非常有可能的数字,因此可能对当前的用例具有特定的含义;您确定您对这个默认含义感到满意吗?
值得深思
最后,统一变量有一个明显的优点:它们是可检测的。编译器可能会发出警告(如果它足够聪明),并且 Valgrind will 会引发错误。这使得逻辑问题可检测,并且只有检测到的问题才能得到纠正。
当然,哨兵值(例如
NaN
)也同样有用。不幸的是...没有整数。
初始化可能会通过两种方式影响性能。
首先,初始化变量需要时间。当然,对于单个变量来说,它可能可以忽略不计,但正如其他人所建议的,它可以与大量变量、数组等相加。
第二,谁说零是合理的默认值?对于每个默认值为零的变量,可能还有另一个默认值不是有用的变量。在这种情况下,如果您确实初始化为零,则会产生进一步的开销,将变量重新初始化为您实际想要的任何值。如果没有发生默认初始化,您实际上需要支付两次初始化开销,而不是一次。请注意,无论您选择什么作为默认值(零或其他值),这都是正确的。
考虑到存在开销,不初始化并让编译器捕获对未初始化变量的任何引用通常会更有效。
基本上,变量引用内存中可以修改以保存数据的位置。 对于未初始化的变量,程序只需要知道这个位置在哪里,编译器通常会提前计算出这一点,因此不需要任何指令。 但是当你希望它被初始化(比如0)时,程序需要使用额外的指令来做到这一点。
一个想法可能是在程序启动时使用 memset 将整个堆清零,然后初始化所有静态内容,但这对于在读取之前动态设置的任何内容都不需要。 对于基于堆栈的函数来说,这也是一个问题,每次调用函数时都需要将其堆栈帧清零。 简而言之,允许变量默认为未定义会更有效,特别是当堆栈经常被新调用的函数覆盖时。
使用 -Wmaybe-uninitialized 进行编译并找出答案。这些是编译器无法优化原始初始化的唯一地方。
至于堆...