【问题标题】:Speed up C program without using conditional compilation在不使用条件编译的情况下加速 C 程序
【发布时间】:2012-02-13 08:56:46
【问题描述】:

我们正在开发一种模型检查工具,它可以执行某些搜索例程数十亿次。我们有不同的搜索例程,当前使用预处理器指令选择这些例程。这不仅非常不方便,因为我们每次做出不同的选择时都需要重新编译,而且还会使代码难以阅读。现在是开始新版本的时候了,我们正在评估是否可以避免条件编译。

这是一个非常人为的例子,展示了效果:

/* program_define */

#include <stdio.h>
#include <stdlib.h>

#define skip 10

int main(int argc, char** argv) {
    int i, j;
    long result = 0;

    int limit = atoi(argv[1]);

    for (i = 0; i < 10000000; ++i) {
        for (j = 0; j < limit; ++j) {
            if (i + j % skip == 0) {
                continue;
            }
            result  += i + j;
        }
    }

    printf("%lu\n", result);
    return 0;
}

这里,变量skip 是一个影响程序行为的值的示例。不幸的是,每次我们需要skip 的新值时,我们都需要重新编译。

让我们看看另一个版本的程序:

/* program_variable */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
    int i, j;
    long result = 0;

    int limit = atoi(argv[1]);
    int skip = atoi(argv[2]);

    for (i = 0; i < 10000000; ++i) {
        for (j = 0; j < limit; ++j) {
            if (i + j % skip == 0) {
                continue;
            }
            result  += i + j;
        }
    }

    printf("%lu\n", result);
    return 0;
}

这里,skip 的值作为命令行参数传递。这增加了很大的灵活性。但是,这个程序要慢得多:

$ time ./program_define 1000 10
50004989999950500

real 0m25.973s
user 0m25.937s
sys  0m0.019s

对比

$ time ./program_variable 1000 10
50004989999950500

real 0m50.829s
user 0m50.738s
sys  0m0.042s

我们正在寻找一种将值传递给程序(通过命令行参数或文件输入)的有效方法,该方法之后将永远不会改变。有没有办法优化代码(或告诉编译器)以使其更有效地运行?

非常感谢任何帮助!


评论:

正如 Dirk 在他的 comment 中所写,这与具体示例无关。我的意思是一种替换if 的方法,该if 评估一个设置一次然后永远不会更改(例如命令行选项)的变量,该函数在字面上被 数十亿次 调用更有效的构造。我们目前使用预处理器来定制所需的函数版本。如果有更好的不需要重新编译的方法就好了。

【问题讨论】:

  • 也许你不应该使用不断改变和重新格式化你的代码的预处理指令?
  • 有趣的是,这两个示例的性能在我的机器上几乎无法区分(64 位 Ubuntu,gcc 4.4.3 和 -O3)。
  • 差异几乎可以肯定是因为如果skip 在编译时已知,编译器可以只使用乘法和移位为j % skip 生成更好的代码。
  • 坏消息是在优化器不知道确切值的情况下,除法(并且% 算作除法)通常会变慢。如果效果更快,您可以玩一些技巧,用一个更多的乘法/加法/移位代替特定值的除法。原则上,如果您在优化之前有合适的中间形式,则不必重新编译整个程序以重新运行优化器以获得新值。您可以尝试使用带有链接时优化的编译器,看看它是否可以在您重新链接新值时进行优化。
  • 哦,这听起来可能很奇怪,但您可以在 C# 或 Java 中尝试一下。由于它们是 JIT 编译的,它们比典型的 C++ 工具链优化得晚,所以你可能会很幸运。诀窍是在编译到字节码之后,但在优化代码之前,弄清楚如何提供值。

标签: c conditional-compilation optimization c-preprocessor


【解决方案1】:

您可以尝试使用 GCC likely/unlikely 内置函数(例如 here)或配置文件引导优化(例如 here)。另外,你打算(i + j) % 10 还是i + (j % 10)% 运算符具有更高的优先级,因此您编写的代码正在测试后者。

【讨论】:

  • 这个答案最接近我的问题。谢谢!
【解决方案2】:

您可能在没有优化的情况下进行编译,这将导致您的程序在每次检查时加载跳过,而不是文字 10。尝试将 -O2 添加到编译器的命令行,和/或使用

register int skip;

【讨论】:

【解决方案3】:

您可以查看libdivide,当除数直到运行时才知道时,它可以进行快速除法:(libdivide is an open source library for optimizing integer division)。

如果您使用 a - b * (a / b)(但使用 libdivide)计算 a % b,您可能会发现它更快。

【讨论】:

    【解决方案4】:

    我在我的系统上运行了您的 program_variable 代码以获得性能基准:

    $ gcc -Wall test1.c
    $ time ./a.out 1000 10
    50004989999950500
    
    real    0m55.531s
    user    0m55.484s
    sys     0m0.033s
    

    如果我用-O3 编译test1.c,那么我得到:

    $ time ./a.out 1000 10
    50004989999950500
    
    real    0m54.305s
    user    0m54.246s
    sys     0m0.030s
    

    在第三个测试中,我手动设置了limitskip 的值:

    int limit = 1000, skip = 10;
    

    然后我重新运行测试:

    $ gcc -Wall test2.c
    $ time ./a.out
    50004989999950500
    
    real    0m54.312s
    user    0m54.282s
    sys     0m0.019s
    

    取消atoi() 呼叫并没有太大的不同。但是,如果我在打开-O3 优化的情况下进行编译,那么我会遇到减速带:

    $ gcc -Wall -O3 test2.c
    $ time ./a.out
    50004989999950500
    
    real    0m26.756s
    user    0m26.724s
    sys     0m0.020s
    

    ersatz atoi() function 添加一个#define 宏有一点帮助,但作用不大:

    #define QSaToi(iLen, zString, iOut) {int j = 1; iOut = 0; \
        for (int i = iLen - 1; i >= 0; --i) \
            { iOut += ((zString[i] - 48) * j); \
            j = j*10;}}
    
    ...
    int limit, skip;
    QSaToi(4, argv[1], limit);
    QSaToi(2, argv[2], skip);
    

    和测试:

    $ gcc -Wall -O3 -std=gnu99 test3.c
    $ time ./a.out 1000 10
    50004989999950500
    
    real    0m53.514s
    user    0m53.473s
    sys     0m0.025s
    

    昂贵的部分似乎是那些 atoi() 调用,如果这是 -O3 编译之间的唯一区别。

    也许您可以编写一个二进制文件,循环测试limitskip 的各种值,类似于:

    #define NUM_LIMITS 3
    #define NUM_SKIPS 2
    ...
    int limits[NUM_LIMITS] = {100, 1000, 1000};
    int skips[NUM_SKIPS] = {1, 10};
    int limit, skip;
    ...
    for (int limitIdx = 0; limitIdx < NUM_LIMITS; limitIdx++)
        for (int skipIdx = 0; skipIdx < NUM_SKIPS; skipIdx++)
            /* per-limit, per-skip test */
    

    如果您在编译之前就知道您的参数,也许您可​​以这样做。如果您希望结果在单独的文件中,您可以使用 fprintf() 将输出写入每个限制、每个跳过的文件输出。

    【讨论】:

      【解决方案5】:

      一个愚蠢的答案,但是您可以在 gcc 命令行上传递定义并使用 shell 脚本运行整个事情,该脚本会根据命令行参数重新编译和运行程序

      #!/bin/sh
      skip=$1
      out=program_skip$skip
      if [ ! -x $out ]; then 
          gcc -O3 -Dskip=$skip -o $out test.c
      fi
      time $out 1000
      

      【讨论】:

        【解决方案6】:

        我在 program_define 和 program_variable 之间也得到了大约 2 倍的减速,分别是 26.2 秒和 49.0 秒。然后我尝试了

        #include <stdio.h>
        #include <stdlib.h>
        
        int main(int argc, char** argv) {
            int i, j, r;
            long result = 0;
        
            int limit = atoi(argv[1]);
            int skip = atoi(argv[2]);
        
            for (i = 0; i < 10000000; ++i) {
                for (j = 0, r = 0; j < limit; ++j, ++r) {
                    if (r == skip) r = 0;
                    if (i + r == 0) {
                        continue;
                    }
                    result  += i + j;
                }
            }
        
            printf("%lu\n", result);
            return 0;
        }
        

        使用额外的变量来避免代价高昂的除法,结果时间为 18.9 秒,明显优于使用静态已知常数的模数。然而,这种辅助变量技术只有在变化很容易预测的情况下才有希望。

        【讨论】:

          【解决方案7】:

          另一种可能性是使用模运算符来消除:

          #include <stdio.h>
          #include <stdlib.h>
          
          int main(int argc, char** argv) {
              int i, j;
              long result = 0;
          
              int limit = atoi(argv[1]);
              int skip = atoi(argv[2]);
              int current = 0;
          
              for (i = 0; i < 10000000; ++i) {
                  for (j = 0; j < limit; ++j) {
                      if (++current == skip) {
                          current = 0;
                          continue;
                      }
                      result  += i + j;
                  }
              }
          
              printf("%lu\n", result);
              return 0;
          }
          

          【讨论】:

            【解决方案8】:

            如果那是实际代码,你有几种方法可以优化它:

            (i + j % 10==0) 仅在 i==0 时为真,因此您可以在 i&gt;0 时跳过整个 mod 操作。此外,由于 i + j 在每个循环中仅增加 1,因此您可以将 mod 提升并简单地设置一个变量,当它达到 skip 时增加和重置(正如其他答案中所指出的那样)。

            【讨论】:

              【解决方案9】:

              我对 Niels 询问的程序有点熟悉。 周围有很多有趣的答案(谢谢),但答案略微错过了问题的精神。给定的示例程序实际上只是示例程序。受预处理器语句影响的逻辑要复杂得多。 最后,它不仅仅是执行模运算或简单的除法。它是关于保留或跳过某些过程调用,在其他两个操作之间执行操作等,定义数组的大小等。

              所有这些东西都可以通过命令行参数设置的变量来保护。但这太昂贵了,因为这些例程、语句、内存分配中的许多都被执行了十亿次。也许这可以更好地解决问题。仍然对您的想法很感兴趣。

              德克

              【讨论】:

                【解决方案10】:

                如果您使用 C++ 而不是 C,则可以使用模板,以便可以在编译时进行计算,甚至可以进行递归。 请看 C++ template meta programming

                【讨论】:

                  【解决方案11】:

                  您还可以在程序中已有所有可能的函数实现,并在运行时更改函数指针以选择您实际使用的函数。

                  你可以使用来避免你必须编写重复的代码:

                  #define MYFUNCMACRO(name, myvar) void name##doit(){/* time consuming code using myvar */}
                  
                  MYFUNCMACRO(TEN,10)
                  MYFUNCMACRO(TWENTY,20)
                  MYFUNCMACRO(FOURTY,40)
                  MYFUNCMACRO(FIFTY,50)
                  

                  如果您需要太多这些宏(数百个?),您可以编写一个代码生成器,它会自动为一系列值写入 cpp 文件。

                  我没有编译也没有测试代码,但也许你看到了原理。

                  【讨论】:

                    猜你喜欢
                    • 2016-10-26
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-04-17
                    • 1970-01-01
                    • 2021-01-17
                    • 2015-08-17
                    • 2018-07-22
                    • 1970-01-01
                    相关资源
                    最近更新 更多