Translating Why const Doesn't Make C Code Faster.

This commit is contained in:
LazyWolf Lin 2019-09-10 17:55:45 +08:00
parent ab40a5d68c
commit b6c2e55b59

View File

@ -7,14 +7,14 @@
[#]: via: (https://theartofmachinery.com/2019/08/12/c_const_isnt_for_performance.html)
[#]: author: (Simon Arneaud https://theartofmachinery.com)
为什么 `const` 不能让 C 代码跑得更快?
为什么 `const` 不能让 `C` 代码跑得更快?
======
在几个月前的一篇文章里,我曾说过“[有个一个流行的传言,`const` 可以帮助编译器优化 C 和 C++ 代码][1]”。我觉得我需要解释一下,尤其是曾经我自己也以为这是显然对的。我将会用一些理论和人工构造的例子论证,然后在一个真正的代码库 Sqlite 上做一些实验和基准测试。
在几个月前的一篇文章里,我曾说过“[有个一个流行的传言,`const` 有助于编译器优化 `C``C++` 代码][1]”。我觉得我需要解释一下,尤其是曾经我自己也以为这是显然对的。我将会用一些理论和人工构造的例子论证,然后在一个真正的代码库 `Sqlite` 上做一些实验和基准测试。
### 一个简单的测试
让我们从一个最简单、最明显的例子开始,以前认为这是一个 `const` 让 C 代码跑得更快的例子。首先,假设我们有如下两个函数声明:
让我们从一个最简单、最明显的例子开始,以前认为这是一个 `const``C` 代码跑得更快的例子。首先,假设我们有如下两个函数声明:
```
void func(int *x);
@ -39,7 +39,7 @@ void constByArg(const int *x)
}
```
调用 `printf()`CPU 会通过指针从 RAM 中取得 `*x` 的值。很显然,`constByArg()` 会稍微快一点,因为编译器知道 `*x` 是常量,因此不需要在调用 `constFunc()` 之后再次获取它的值。它仅是打印相同的东西。对吧?让我们来看下 GCC 在如下编译选项下生成的汇编代码:
调用 `printf()`CPU 会通过指针从 RAM 中取得 `*x` 的值。很显然,`constByArg()` 会稍微快一点,因为编译器知道 `*x` 是常量,因此不需要在调用 `constFunc()` 之后再次获取它的值。它仅是打印相同的东西。对吧?让我们来看下 `GCC` 在如下编译选项下生成的汇编代码:
```
$ gcc -S -Wall -O3 test.c
@ -75,14 +75,14 @@ byArg:
函数 `byArg()` 和函数 `constByArg()` 生成的汇编代码中唯一的不同之处是 `constByArg()` 有一句汇编代码 `call constFunc@PLT`,这正是源码中的调用。关键字 `const` 本身并没有造成任何字面上的不同。
好了,这是 GCC 的结果。或许我们需要一个更聪明的编译器。Clang 会有更好的表现吗?
好了,这是 `GCC` 的结果。或许我们需要一个更聪明的编译器。`Clang` 会有更好的表现吗?
```
$ clang -S -Wall -O3 -emit-llvm test.c
$ view test.ll
```
这是 IR 代码LLVM 的中间语言)。它比汇编代码更加紧凑,所以我可以把两个函数都导出来,让你可以看清楚我所说的“除了调用外,没有任何字面上的不同”是什么意思:
这是 `IR` 代码(`LLVM` 的中间语言)。它比汇编代码更加紧凑,所以我可以把两个函数都导出来,让你可以看清楚我所说的“除了调用外,没有任何字面上的不同”是什么意思:
```
; Function Attrs: nounwind uwtable
@ -106,7 +106,7 @@ define dso_local void @constByArg(i32*) local_unnamed_addr #0 {
}
```
### Something that (sort of) works
### 一些有效的代码
接下来是一组 `const` 能够真正产生作用的代码:
@ -164,7 +164,7 @@ localVar:
.cfi_endproc
```
LLVM 生成的 IR 代码中更明显。在 `constLocalVar()` 中,第二次调用 `printf()` 之前的 `load` 会被优化掉:
`LLVM` 生成的 `IR` 代码中更明显。在 `constLocalVar()` 中,第二次调用 `printf()` 之前的 `load` 会被优化掉:
```
; Function Attrs: nounwind uwtable
@ -184,7 +184,7 @@ define dso_local void @localVar() local_unnamed_addr #0 {
好吧,现在,`constLocalVar()` 成功的优化了 `*x` 的重新读取,但是可能你已经注意到一些问题:`localVar()` 和 `constLocalVar()` 在函数体中做了同样的 `constFunc()` 调用。如果编译器能够推断出 `constFunc()` 没有修改 `constLocalVar()` 中的 `*x`,那为什么不能推断出完全一样的函数调用也没有修改 `localVar()` 中的 `*x`
这个解释更贴近于为什么 C 语言的 `const` 不能作为优化手段的核心。C 语言的 `const` 有两个有效的含义:它可以表示这个变量是某个可能是常数也可能不是常数的数据的一个只读别名,或者它可以表示这变量真正的常量。如果你移除了一个指向常量的指针的 `const` 属性并写入数据,那结果将是一个未定义行为。另一方面,如果是一个指向非常量值的 `const` 指针,将就没问题。
这个解释更贴近于为什么 `C` 语言的 `const` 不能作为优化手段的核心。`C` 语言的 `const` 有两个有效的含义:它可以表示这个变量是某个可能是常数也可能不是常数的数据的一个只读别名,或者它可以表示这变量真正的常量。如果你移除了一个指向常量的指针的 `const` 属性并写入数据,那结果将是一个未定义行为。另一方面,如果是一个指向非常量值的 `const` 指针,将就没问题。
这份 `constFunc()` 的可能实现揭示了这意味着什么:
@ -213,7 +213,7 @@ void doubleIt(int *x)
但是为什么不一致呢?如果编译器能够推断出 `constLocalVar()` 中调用的 `constFunc()` 不会修改它的参数,那么肯定也能继续在其他 `constFunc()` 的调用上实施相同的优化,对吧?不。编译器不能假设 `constLocalVar()` 根本没有运行。 If it isnt (say, because its just some unused extra output of a code generator or macro), `constFunc()` can sneakily modify data without ever triggering UB.
你可能需要重复阅读上述说明和示例,但不要担心它听起来很荒谬,它确实是的。不幸的是,对 `const` 变量进行写入是最糟糕的未定义行为:大多数情况下,编译器不知道它是否将会是未定义行为。所以,大多数情况下,编译器看见 `const` 时必须假设它未来可能会被移除掉,这意味着编译器不能使用它进行优化。这在实践中是正确的,因为真实的 C 代码会在“明确知道后果”下移除 `const`
你可能需要重复阅读上述说明和示例,但不要担心它听起来很荒谬,它确实是的。不幸的是,对 `const` 变量进行写入是最糟糕的未定义行为:大多数情况下,编译器不知道它是否将会是未定义行为。所以,大多数情况下,编译器看见 `const` 时必须假设它未来可能会被移除掉,这意味着编译器不能使用它进行优化。这在实践中是正确的,因为真实的 `C` 代码会在“明确知道后果”下移除 `const`
简而言之,很多事情都可以阻止编译器使用 `const` 进行优化,包括使用指针从另一内存空间接受数据,或者在堆空间上分配数据。更糟糕的是,在大部分编译器能够使用 `const` 的情况,它都不是必须的。例如,任何像样的编译器都能推断出下面代码中的 `x` 是一个常量,甚至都不需要 `const`
@ -231,7 +231,7 @@ TLDR`const` 对优化而言几乎无用,因为:
### C++
如果你在使用 C++ 那么有另外一个方法让 `const` 能够影响到代码的生成。你可以用 `const` 和非 `const` 的参数重载同一个函数,而非 `const` 版本的代码可能可以优化(由程序员优化而不是编译器)掉某些拷贝或者其他事情。
如果你在使用 `C++` 那么有另外一个方法让 `const` 能够影响到代码的生成。你可以用 `const` 和非 `const` 的参数重载同一个函数,而非 `const` 版本的代码可能可以优化(由程序员优化而不是编译器)掉某些拷贝或者其他事情。
```
void foo(int *p)
@ -253,11 +253,11 @@ int main()
}
```
一方面,我不认为这会在实际的 C++ 代码中大量使用。另一方面,为了导致差异,程序员需要做出编译器无法做出的假设,因为它们不受语言保护。
一方面,我不认为这会在实际的 `C++` 代码中大量使用。另一方面,为了导致差异,程序员需要做出编译器无法做出的假设,因为它们不受语言保护。
### 用 Sqlite3 进行实验
### 用 `Sqlite3` 进行实验
有了足够的理论和例子。那么 `const` 在一个真正的代码库中有多大的影响呢?我将会在 Sqlite (version 3.30.0) 的代码库上做一个测试,因为:
有了足够的理论和例子。那么 `const` 在一个真正的代码库中有多大的影响呢?我将会在 `Sqlite`版本3.30.0的代码库上做一个测试,因为:
* 它真正地使用了 `const`
* 它不是一个简单的代码库(超过 20 万行代码)
@ -266,7 +266,7 @@ int main()
此外,作者和贡献者们已经进行了多年的性能优化工作,因此我能断言他们没有错过任何明显的优化。
此外,作者和贡献者们已经进行了多年的性能优化工作,因此我能确定他们没有错过任何有显著效果的优化。
#### 配置
@ -278,9 +278,9 @@ int main()
(GNU) `sed` 可以将一些东西添加到每个文件的顶端,比如 `sed -i '1i#define const' *.c *.h`
在编译期间使用脚本生成 Sqlite 代码稍微有点复杂。幸运的是当 `const` 代码和非 `const` 代码混合时,编译器会产生了大量的提醒,因此很容易发现它并调整脚本来包含我的反 `const` 代码段。
在编译期间使用脚本生成 `Sqlite` 代码稍微有点复杂。幸运的是当 `const` 代码和非 `const` 代码混合时,编译器会产生了大量的提醒,因此很容易发现它并调整脚本来包含我的反 `const` 代码段。
Directly diffing the compiled results is a bit pointless because a tiny change can affect the whole memory layout, which can change pointers and function calls throughout the code. Instead I took a fingerprint of the disassembly (`objdump -d libsqlite3.so.0.8.6`), using the binary size and mnemonic for each instruction. For example, this function:
直接比较编译结果毫无意义,因为任意微小的改变就会影响整个内存布局,这可能会改变整个代码中的指针和函数调用。因此,我用每个指令的二进制大小和汇编代码作为反汇编代码(`objdump -d libsqlite3.so.0.8.6`)。举个例子,这个函数:
```
000000000005d570 <sqlite3_blob_read>:
@ -289,17 +289,17 @@ Directly diffing the compiled results is a bit pointless because a tiny change c
5d57c: 0f 1f 40 00 nopl 0x0(%rax)
```
would turn into something like this:
将会变成这样:
```
sqlite3_blob_read 7lea 5jmpq 4nopl
```
I left all the Sqlite build settings as-is when compiling anything.
在编译时,我保留了所有 `Sqlite` 的编译设置。
#### Analysing the compiled code
#### 分析编译后的代码
The `const` version of libsqlite3.so was 4,740,704 bytes, about 0.1% larger than the 4,736,712 bytes of the non-`const` version. Both had 1374 exported functions (not including low-level helpers like stuff in the PLT), and a total of 13 had any difference in fingerprint.
The `const` version of `libsqlite3.so` was 4,740,704 bytes, about 0.1% larger than the 4,736,712 bytes of the non-`const` version. Both had 1374 exported functions (not including low-level helpers like stuff in the PLT), and a total of 13 had any difference in fingerprint.
A few of the changes were because of the dumb preprocessor hack. For example, heres one of the changed functions (with some Sqlite-specific definitions edited out):
@ -330,7 +330,7 @@ static int64_t doubleToInt64(double r){
Removing `const` makes those constants into `static` variables. I dont see why anyone who didnt care about `const` would make those variables `static`. Removing both `static` and `const` makes GCC recognise them as constants again, and we get the same output. Three of the 13 functions had spurious changes because of local `static const` variables like this, but I didnt bother fixing any of them.
Sqlite uses a lot of global variables, and thats where most of the real `const` optimisations came from. Typically they were things like a comparison with a variable being replaced with a constant comparison, or a loop being partially unrolled a step. (The [Radare toolkit][3] was handy for figuring out what the optimisations did.) A few changes were underwhelming. `sqlite3ParseUri()` is 487 instructions, but the only difference `const` made was taking this pair of comparisons:
`Sqlite` uses a lot of global variables, and thats where most of the real `const` optimisations came from. Typically they were things like a comparison with a variable being replaced with a constant comparison, or a loop being partially unrolled a step. (The [Radare toolkit][3] was handy for figuring out what the optimisations did.) A few changes were underwhelming. `sqlite3ParseUri()` is 487 instructions, but the only difference `const` made was taking this pair of comparisons:
```
test %al, %al
@ -350,7 +350,7 @@ je <sqlite3ParseUri+0x717>
#### Benchmarking
Sqlite comes with a performance regression test, so I tried running it a hundred times for each version of the code, still using the default Sqlite build settings. Here are the timing results in seconds:
`Sqlite` comes with a performance regression test, so I tried running it a hundred times for each version of the code, still using the default `Sqlite` build settings. Here are the timing results in seconds:
| const | No const
---|---|---