c - 为什么我的程序仅根据我向 Clang 提供源文件操作数的顺序而表现不同?

标签 c scope linker 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 位于 main 之前:13.68s,main 位于 lex 之前:13.02s
  • -01 lex 位于 main 之前:6.91s,main 位于 lex 之前:6.65s
  • -O2 lex 位于 main 之前:7.58s,main 位于 lex 之前:7.50s
  • -O3 lex 在 main 之前:6.25s,main 在 lex 之前:7.40s

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

注释:

  • 源代码可见here .
  • 我在解释器中使用的 mandelbrot 基准可以在 here 中找到。 .

编辑:

  • 每个优化级别的可执行文件大小完全相同,但结构不同。
  • 目标文件与任一操作数顺序相同。
  • 无论操作数顺序如何,I/O 和解析过程都非常快,即使通过它运行 500 MiB 随机文件也不会导致任何变化,因此运行循环中会出现性能变化。
  • 在比较每个可执行文件的 objdump 后,在我看来,主要的(如果不是唯一的)差异是各部分(、等)的顺序,以及因此而改变的内存地址。<
  • 可以找到 objdump here .

最佳答案

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

首先,我得到了类似的结果。我在 cygwin 上使用 gcc。一些示例运行:

像这样构建:

$ 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

然后运行(多次确认,但这是一个示例运行)

$ 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就意味着在不再位于缓存中的页面之间跳转。或者它们的某种组合。 这只是一个假设,而不是理论。

因此,我将这些全局声明直接移至 main 中,并删除了静态声明。对变量保持零初始化。

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 数组)会直接扩展到目标代码中(从而增加目标代码本身的大小),并且实际上是二进制文件最终镜像的一部分。更小的代码通常是更快的代码。

全局空间中的 30K 缓冲区可能会在 lexmain 和 c 中的函数之间引入足够大的字节数。 -运行时本身影响代码调入和调出的方式。或者只是导致加载程序需要更长的时间来加载二进制文件。

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

所以换句话说,我想我找到了根本原因。但我并不能百分百确定为什么。没有进行大量的函数调用。因此,指令指针不会在 lex.o 中的代码和 main.o 中的代码之间跳转很多,以致缓存必须重新加载页面。

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

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

关于c - 为什么我的程序仅根据我向 Clang 提供源文件操作数的顺序而表现不同?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62670671/

相关文章:

c++ - ld 警告 : stack subl instruction is too different from dwarf stack size on OS X

c - GCC如何实现变长数组?

javascript - event 是一个全局变量,可以在回调链中的任何地方访问吗?

javascript - 函数内更改后,无法访问更改后的变量值

c++ - 如果模板类构造函数和成员函数的定义与其使用分开,则 G++ 链接器找不到它们

gcc - 我可以让 CMake 使用 gcc 增量链接生成 Makefile 吗?

c - 数据类型中的位数

c# - UINT8 中的自定义符号 - 如何转换为 C#?

c - Linux C,如何安排10个等待线程以FIFO方式执行?

使用 JQuery 的 Javascript 范围