为什么我的程序仅根据我将源文件操作数提供给 Clang 的顺序执行不同?

Why is my program performing differently based only on the order I give source file operands to Clang?

我有一个包含两个源文件的 Brainfuck 解释器项目,改变源文件作为 Clang 的操作数的顺序,没有别的,导致一致的性能差异。

我正在使用 Clang,参数如下:

无论优化级别如何,都会出现性能差异。

基准测试结果:

哪个顺序执行得更差在优化级别之间并不总是一致的,但对于每个级别,相同的操作数顺序总是比另一个执行得更差。

备注:

编辑:

我没有完整的答案。但我想我知道是什么导致了链接排序之间的差异。

首先,我得到了类似的结果。我在 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 缓冲区可能会在 lexmain 和 c-[= 中的函数之间引入足够大的字节数56=]时间本身会影响代码调入和调出的方式。或者只是导致加载程序需要更长的时间来加载二进制文件。

换句话说,全局变量会导致代码膨胀并增加对象大小。通过将数组声明移动到堆栈,内存分配几乎是即时的。现在 lex 和 main 的链接可能适合内存中的同一页。此外,由于变量在堆栈上,编译器可能会更自由地进行优化。

所以换句话说,我想我找到了根本原因。但我不是 100% 确定为什么。没有进行大量的函数调用。所以它不像指令指针在 lex.o 中的代码和 main.o 中的代码之间跳来跳去,以至于缓存不得不重新加载页面。

更好的测试可能是找到一个更大的输入文件来触发更长的 运行。这样,我们就可以看到 运行 两个原始构建之间的时间增量是固定的还是线性的。

任何更深入的了解都需要进行一些实际的代码分析、检测或二进制分析。