【问题标题】:Why is this simple sum broken by compiler optimizations if I don't initialize the sum variable?如果我不初始化 sum 变量,为什么编译器优化会破坏这个简单的 sum?
【发布时间】:2014-09-21 17:26:07
【问题描述】:

我正在将 C 代码编译成 Matlab MEX 文件。 Matlab 默认开启优化 (-O2)。我有一个简单的规范化例程,它被这个打破了。到这里使用 sum 时,值只有 0.995,而应该是 1.000:

int N = 10000;
double *w, sum;

for(i=0; i<N; i++) {
    w[i] = 1.0/N;
}

...a couple unrelated operations...

for(k=0; k<N; k++) {
    sum += w[k];
}
for(k=0; k<N; k++) {
    w[k] = w[k]/sum;
}

当我使用-g 编译时,一切都很好。所以我认为这一定不符合 ISO 标准。然后我发现了我缺少的东西:

sum = 0.0;

这解决了它。所以我猜编译器决定我不关心该变量的确切值,因为我没有费心正确初始化它?有人愿意解释一下吗?

编辑: 是的,我知道它是未定义的,但这并不能解释它为什么以及如何影响该总和的优化。 编译器如何做出明确的决定,即“即使他正在读取总和,他也不能关心值。”它是否以某种方式跟踪未定义的值?

【问题讨论】:

  • 您的代码很容易发现未定义的行为。一旦编译器发现未定义的行为,它就可以并且会做任何它喜欢的事情。

标签: c compiler-optimization


【解决方案1】:

把自己放在编译器的位置。您正在实施 C 标准要求和保证的内容。

所以你编译代码,没有优化,得到这样的东西,逐行编译代码,没有任何分析:

- add stack space for `sum`
...
- initialize k to 0
beginning_of_for:
- if k < N is not met, goto after_for
-   add w[k] to sum
-   increment k
-   goto beginning_of_for
after_for:
- ...
- (for example) print sum

它运行并且最初 sum 碰巧包含值 0。您看不到任何奇怪的行为,因为您很幸运(或者更不幸)。

现在他们告诉你,编译器,优化代码。您需要环顾四周并排除任何不必要的操作以节省尽可能多的时间和/或空间。你四处走动,发现 sum 未初始化。按照标准,这意味着您可以保证以后不会从该变量中读取(即使程序员证明已经这样做了,但您不在乎,因为标准说您不这样做需要照顾)。此外,如果您将一个未初始化的值添加到某个值,您会得到另一个未初始化的值,再次保证您不会稍后读取它。

所以这是你作为编译器的假设:

- variable X is defined but not initialized
- some operations that don't read from X
- operation that writes to X a value that is not uninitialized
- operations that read from X

从您的角度来看,在 X 使用不依赖于其他未初始化值的值初始化之前,从 X 读取的任何值都可以给出任意值,因此您可以只使用 0 而不是实际读取从那个值。更重要的是,任何基于未初始化值的写入都可以被丢弃,因为结果仍然是未初始化的值,因此它可以是任何值。

换句话说,您之前未优化的代码:

- add stack space for `sum`
...
- initialize k to 0
beginning_of_for:
- if k < N is not met, goto after_for
-   add w[k] to sum
-   increment k
-   goto beginning_of_for
after_for:
- ...
- (for example) print sum

分析如下:

Pass 1:

- add stack space for `sum`            [sum uninitialized]
...
- initialize k to 0                    [keep this as is]
beginning_of_for:
- if k < N is not met, goto after_for  [keep this as is]
-   add w[k] to sum                    [remove this line: sum is still uninitialized]
-   increment k                        [keep this as is]
-   goto beginning_of_for              [keep this as is]
after_for:
- ...
- (for example) print sum              [use 0 or whatever instead of sum]

这给出了这个:

- add stack space for `sum`
...
- initialize k to 0
beginning_of_for:
- if k < N is not met, goto after_for
-   increment k
-   goto beginning_of_for
after_for:
- ...
- (for example) print whatever

下一轮优化如下:

Pass 2:

- add stack space for `sum`            [sum uninitialized]
...
- initialize k to 0                    [replace 0 with N because of (1)]
beginning_of_for:
- if k < N is not met, goto after_for  [remove because of (1)]
-   increment k                        [remove because of (1)]
-   goto beginning_of_for              [remove because of (1)]
after_for:
- ...
- (for example) print whatever         [keep this as is]

(1) the for loop is empty.  `k` is `int` so it is guaranteed it will not overflow
    (Note: signed integer overflow is **undefined behavior** according to
    the standard), so the loop terminates with a single side effect: `k` reaches
    `N`.  So there is no point in actually looping.

现在你的代码变成了:

- add stack space for `sum`
...
- initialize k to N
- ...
- (for example) print whatever

在最后一关,你会得到:

 Pass 3:

- add stack space for `sum`            [remove because sum is unused]
...
- initialize k to N                    [remove because k is unused]
- ...
- (for example) print whatever         [keep this as is]

这意味着你最后剩下的是:

- (for example) print whatever

这就是优化如何导致你的整个代码因为未初始化的变量而被丢弃。

【讨论】:

  • 这回答了我的问题,即优化器正在显式跟踪未初始化的值。这里发生的与我看到的并不完全相同,但正如我们所知,所有编译器和优化器都会有所不同。关键是他们将根据变量的初始化状态做出同步决策。
  • 不确定您所说的 同步决策 是什么意思,但是优化器确实会跟踪未初始化的值,并且可以像我上面提到的那样删除代码。
  • 这意味着在我的具体情况下,编译器并行化了总和并决定在下一次读取sum 之前不需要完全完成并行总和的结果......所以它在读取之前没有尝试锁定 sum。其实很聪明。
  • @NeilTraft,并行化不是最好的词,因为它通常意味着尝试在单独的线程中执行。但是,您可以认为标准规定的执行流程在某种程度上是并行的。简单地说,标准规定如果语句A依赖于语句B,则在之后执行,但如果它们是独立的,则可以按任意顺序执行。但它有很多细节不适合这里。你可以随时阅读the standard!请注意,该链接指向 C11,它具有非常不同的执行模型。
  • 好的,所以我猜它没有并行化。编译器优化并没有那么远,是吗?也许标准不允许这样做。所以它只是改变了顺序,并且有一些打印语句足以(偶然)导致顺序正确。
猜你喜欢
  • 2015-05-13
  • 2021-02-01
  • 1970-01-01
  • 2012-10-08
  • 2016-04-22
  • 2012-10-25
  • 2023-01-10
  • 1970-01-01
  • 2020-06-23
相关资源
最近更新 更多