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

为什么我的程序只根据我给源文件操作数给Clang的顺序执行不同的操作?,c,scope,linker,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 无论优化级别如何,都可以看到性能差异 基准结果: -O0lex-before-main:13.68s,main-before-lex:13.02s

我有一个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
优化级别之间,哪个顺序执行得更差并不总是一致的,但对于每个级别,相同的操作数顺序总是比另一个操作数顺序执行得更差

注:

  • 可以找到源代码
  • 我在解释器中使用的mandelbrot基准可以找到
编辑:

  • 每个优化级别的可执行文件大小完全相同,但结构不同
  • 对象文件与任一操作数顺序相同
  • 无论操作数顺序如何,I/O和解析过程都非常快,即使在其中运行500 MiB随机文件也不会导致任何变化,因此在运行循环中会出现性能变化
  • 在比较每个可执行文件的objdump时,我觉得主要的区别(如果不是唯一的话)是节(、等)的顺序,以及因此而更改的内存地址
  • 可以找到objdump

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

      首先,我得到了类似的结果。我在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
      
      然后运行(多次确认,但这里有一个运行示例)

      然后我注意到这些声明:

      static char arr[30000] = { 0 }, *ptr = arr;
      static tok_t **dat; static size_t cap, top;
      
      这让我意识到30K的零字节数组被插入到程序的链接中。这可能会导致页面加载命中。链接顺序可能会影响
      main
      中的代码是否与
      lex
      中的函数位于同一页面内。或者仅仅访问
      数组
      就意味着在缓存中不再存在的页面之间跳转。或者它们的组合这只是一个假设,不是一个理论。

      所以我将这些全局的声明直接移动到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
      缓冲区可能会在
      lex
      main
      中的函数和c-runtime本身之间引入足够大的字节数,从而影响代码的分页方式。或者只是导致加载程序加载二进制文件的时间更长

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

      换句话说,我想我找到了根本原因。但我不能100%确定原因。没有太多的函数调用。因此,它不像指令指针在lex.o和main.o中的代码之间跳得太多,以至于缓存必须重新加载页面

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


      任何进一步的洞察都需要进行一些实际的代码分析、插装或二进制分析。

      生成的二进制文件中是否有一个比另一个大得多,或者大小相似?您是否比较了可执行文件?它们不同吗?编译器可能与此无关。Clang(或gcc)将为每个源文件生成相同的目标代码,而不管编译顺序如何。这意味着,给定对象代码传递给链接器的顺序,链接器的行为会有所不同。话虽如此,我可以用gcc复制您的结果。所以现在我很好奇,在编译时和编译时都有不同的方法。如果不尝试,很难根据您的示例输入判断特定优化级别的特定版本的clang会选择哪些优化。如果您有兴趣找到答案,您必须通过启用I-will来进行调查,并使用
      fputc
      语句注释分别为这两个版本对I/O(读取文件)、解析函数和运行循环进行基准测试和评测。我有一些预感,但不知道代码的哪一部分以不同的速度执行,在这一点上,任何事情都只是猜测。
      $ 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