为什么我的程序仅根据我将源文件操作数提供给 Clang 的顺序执行不同?
Why is my program performing differently based only on the order I give source file operands to Clang?
我有一个包含两个源文件的 Brainfuck 解释器项目,改变源文件作为 Clang 的操作数的顺序,没有别的,导致一致的性能差异。
我正在使用 Clang,参数如下:
clang -I../ext -D VERSION=\"1.0.0\" main.c lex.c
clang -I../ext -D VERSION=\"1.0.0\" lex.c main.c
无论优化级别如何,都会出现性能差异。
基准测试结果:
-O0
lex before main: 13.68s, main before lex: 13.02s
-01
lex before main: 6.91s, main before lex: 6.65s
-O2
lex before main: 7.58s, main before lex: 7.50s
-O3
lex before main: 6.25s, main before lex: 7.40s
哪个顺序执行得更差在优化级别之间并不总是一致的,但对于每个级别,相同的操作数顺序总是比另一个执行得更差。
备注:
编辑:
- 每个优化级别的可执行文件大小完全相同,但结构不同。
- 目标文件与任一操作数顺序相同。
- 无论操作数顺序如何,I/O 和解析过程都非常快,即使 运行 通过它处理一个 500 MiB 的随机文件也不会导致任何变化,因此 运行循环。
- 在比较每个可执行文件的 objdump 后,在我看来,主要的(如果不是唯一的)区别是部分(、等)的顺序,以及因此而改变的内存地址。
- 可以找到 objdumps here。
我没有完整的答案。但我想我知道是什么导致了链接排序之间的差异。
首先,我得到了类似的结果。我在 cygwin 上使用 gcc。一些示例 运行s:
这样的建筑:
$ gcc -I../ext -D VERSION=\"1.0.0\" main.c lex.c -O3 -o mainlex
$ gcc -I../ext -D VERSION=\"1.0.0\" lex.c main.c -O3 -o lexmain
然后运行ning(多次确认,但这里有一个例子运行)
$ time ./mainlex.exe input.txt > /dev/null
real 0m7.377s
user 0m7.359s
sys 0m0.015s
$ time ./lexmain.exe input.txt > /dev/null
real 0m6.945s
user 0m6.921s
sys 0m0.000s
然后我注意到这些声明:
static char arr[30000] = { 0 }, *ptr = arr;
static tok_t **dat; static size_t cap, top;
这让我意识到 30K 的零字节数组被插入到程序的链接中。这可能会导致页面加载命中。如果 main
中的代码与 lex
中的函数位于同一页面中,链接顺序可能会受到影响。或者只是访问 array
意味着在一个不再在缓存中的页面之间跳转。或者它们的某种组合。 这只是一个假设,不是理论。
所以我把这些global的声明直接移到了main中,去掉了static声明。保留变量的零初始化。
int main(int argc, char *argv[]) {
char arr[30000] = { 0 }, *ptr = arr;
tok_t **dat=NULL; size_t cap=0, top=0;
这肯定会将目标代码和二进制大小缩小 30K,并且堆栈分配应该接近即时。
当我 运行 两种方式时,我的性能几乎相同。事实上,两者的构建速度 运行 都更快。
$ time ./mainlex.exe input.txt > /dev/null
real 0m6.385s
user 0m6.359s
sys 0m0.015s
$ time ./lexmain.exe input.txt > /dev/null
real 0m6.353s
user 0m6.343s
sys 0m0.015s
我不是页面大小、代码分页甚至链接器和加载器如何操作方面的专家。但我确实知道全局变量,包括那个 30K 数组,直接扩展到目标代码中(因此增加了目标代码本身的大小)并且实际上是二进制文件最终映像的一部分。更小的代码通常是更快的代码。
全局 space 中的 30K
缓冲区可能会在 lex
、main
和 c-[= 中的函数之间引入足够大的字节数56=]时间本身会影响代码调入和调出的方式。或者只是导致加载程序需要更长的时间来加载二进制文件。
换句话说,全局变量会导致代码膨胀并增加对象大小。通过将数组声明移动到堆栈,内存分配几乎是即时的。现在 lex 和 main 的链接可能适合内存中的同一页。此外,由于变量在堆栈上,编译器可能会更自由地进行优化。
所以换句话说,我想我找到了根本原因。但我不是 100% 确定为什么。没有进行大量的函数调用。所以它不像指令指针在 lex.o 中的代码和 main.o 中的代码之间跳来跳去,以至于缓存不得不重新加载页面。
更好的测试可能是找到一个更大的输入文件来触发更长的 运行。这样,我们就可以看到 运行 两个原始构建之间的时间增量是固定的还是线性的。
任何更深入的了解都需要进行一些实际的代码分析、检测或二进制分析。
我有一个包含两个源文件的 Brainfuck 解释器项目,改变源文件作为 Clang 的操作数的顺序,没有别的,导致一致的性能差异。
我正在使用 Clang,参数如下:
clang -I../ext -D VERSION=\"1.0.0\" main.c lex.c
clang -I../ext -D VERSION=\"1.0.0\" lex.c main.c
无论优化级别如何,都会出现性能差异。
基准测试结果:
-O0
lex before main: 13.68s, main before lex: 13.02s-01
lex before main: 6.91s, main before lex: 6.65s-O2
lex before main: 7.58s, main before lex: 7.50s-O3
lex before main: 6.25s, main before lex: 7.40s
哪个顺序执行得更差在优化级别之间并不总是一致的,但对于每个级别,相同的操作数顺序总是比另一个执行得更差。
备注:
编辑:
- 每个优化级别的可执行文件大小完全相同,但结构不同。
- 目标文件与任一操作数顺序相同。
- 无论操作数顺序如何,I/O 和解析过程都非常快,即使 运行 通过它处理一个 500 MiB 的随机文件也不会导致任何变化,因此 运行循环。
- 在比较每个可执行文件的 objdump 后,在我看来,主要的(如果不是唯一的)区别是部分(、等)的顺序,以及因此而改变的内存地址。
- 可以找到 objdumps here。
我没有完整的答案。但我想我知道是什么导致了链接排序之间的差异。
首先,我得到了类似的结果。我在 cygwin 上使用 gcc。一些示例 运行s:
这样的建筑:
$ gcc -I../ext -D VERSION=\"1.0.0\" main.c lex.c -O3 -o mainlex
$ gcc -I../ext -D VERSION=\"1.0.0\" lex.c main.c -O3 -o lexmain
然后运行ning(多次确认,但这里有一个例子运行)
$ time ./mainlex.exe input.txt > /dev/null
real 0m7.377s
user 0m7.359s
sys 0m0.015s
$ time ./lexmain.exe input.txt > /dev/null
real 0m6.945s
user 0m6.921s
sys 0m0.000s
然后我注意到这些声明:
static char arr[30000] = { 0 }, *ptr = arr;
static tok_t **dat; static size_t cap, top;
这让我意识到 30K 的零字节数组被插入到程序的链接中。这可能会导致页面加载命中。如果 main
中的代码与 lex
中的函数位于同一页面中,链接顺序可能会受到影响。或者只是访问 array
意味着在一个不再在缓存中的页面之间跳转。或者它们的某种组合。 这只是一个假设,不是理论。
所以我把这些global的声明直接移到了main中,去掉了static声明。保留变量的零初始化。
int main(int argc, char *argv[]) {
char arr[30000] = { 0 }, *ptr = arr;
tok_t **dat=NULL; size_t cap=0, top=0;
这肯定会将目标代码和二进制大小缩小 30K,并且堆栈分配应该接近即时。
当我 运行 两种方式时,我的性能几乎相同。事实上,两者的构建速度 运行 都更快。
$ time ./mainlex.exe input.txt > /dev/null
real 0m6.385s
user 0m6.359s
sys 0m0.015s
$ time ./lexmain.exe input.txt > /dev/null
real 0m6.353s
user 0m6.343s
sys 0m0.015s
我不是页面大小、代码分页甚至链接器和加载器如何操作方面的专家。但我确实知道全局变量,包括那个 30K 数组,直接扩展到目标代码中(因此增加了目标代码本身的大小)并且实际上是二进制文件最终映像的一部分。更小的代码通常是更快的代码。
全局 space 中的 30K
缓冲区可能会在 lex
、main
和 c-[= 中的函数之间引入足够大的字节数56=]时间本身会影响代码调入和调出的方式。或者只是导致加载程序需要更长的时间来加载二进制文件。
换句话说,全局变量会导致代码膨胀并增加对象大小。通过将数组声明移动到堆栈,内存分配几乎是即时的。现在 lex 和 main 的链接可能适合内存中的同一页。此外,由于变量在堆栈上,编译器可能会更自由地进行优化。
所以换句话说,我想我找到了根本原因。但我不是 100% 确定为什么。没有进行大量的函数调用。所以它不像指令指针在 lex.o 中的代码和 main.o 中的代码之间跳来跳去,以至于缓存不得不重新加载页面。
更好的测试可能是找到一个更大的输入文件来触发更长的 运行。这样,我们就可以看到 运行 两个原始构建之间的时间增量是固定的还是线性的。
任何更深入的了解都需要进行一些实际的代码分析、检测或二进制分析。