【问题标题】:Should I store C array values in local variables if I'm going to reference them repeatedly?如果我要重复引用它们,我应该将 C 数组值存储在局部变量中吗?
【发布时间】:2009-03-19 15:50:00
【问题描述】:

我正在编写一些优化的 C 代码,这些代码基本上通过一个数组运行并对每个元素执行一些操作。它的作用取决于元素的当前值,例如:

for (i=0; i < a_len; i++) {
    if (a[i] == 0) {
        a[i] = f1(a[i]);
    } else if (a[i] % 2 == 0) {
        a[i] = f2(a[i]);
    } else {
        a[i] = 0;
}

在动态语言工作多年后,我回到了 C,我的实践一直是尝试编写简单的代码,而不是为我可以直接引用的东西创建大量局部变量,例如 a[i]多于。 我非常清楚最好的做法是编写可读的代码并相信编译器比你更聪明并且会进行良好的优化。

如果我在汇编程序中编写上面的代码,我会将 a[i] 加载到寄存器中一次,然后每次只使用该值,因为我知道 a[] 是私有内存并且不会在引用之间更改。但是,即使是智能编译器也可能每次都加载,因为它不能确定内存没有改变。 (或者我是否必须显式声明“a” volatile 以使编译器不进行此优化?)。

所以,我的问题是:我是否应该通过这样的局部变量重写来获得更好的性能:

for (i=0; i < a_len; i++) {
    val = a[i];
    if (val == 0) {
        a[i] = f1(val);
    } else if (val % 2 == 0) {
        a[i] = f2(val);
    } else {
        a[i] = 0;
}

或者像 -O3 这样的东西会自动为我处理这个问题吗?我正在优化的代码需要几天时间才能运行,因此即使是适度的改进也会产生影响。

【问题讨论】:

  • 很大程度上取决于“a”是什么(从哪里来。如果它是一个函数参数,事情会比它是一个本地数组更复杂)。在你的情况下,所有子句都是互斥的,我认为一个体面的编译器会产生相同的汇编代码。
  • 使用寄存器 T val = a[i];然后将其存储在寄存器中以供以后访问。基本上你期望编译器为你做/优化什么,但你确定。
  • 公用子表达式消除应该让编译器为您执行此操作。此外,对于您的特定示例,假设您的编译器没有特殊情况,使用 (!(val & 0x1)) 测试均匀性比 val %2 == 0 更快。
  • 当然,如果 a[] 是 volatile,那么 CSE 就不可能发生,这两个例子可能不等价。

标签: c arrays optimization


【解决方案1】:

显而易见的答案当然是首先以最易读/简单/易懂的方式编写它,然后尽可能优化地对其进行编译,然后对其进行基准测试和分析。

如果它们是瓶颈,那么优化 before you even know 是没有意义的。如果编译器自动进行转换,您只会让代码变得更糟,花费时间,而且绝对没有任何回报。除了可能很酷的感觉,但随着时间的推移会逐渐消失。 :)

【讨论】:

    【解决方案2】:

    首先为了可读性而编写它。就个人而言,我发现所有的下标都伤害了我的眼睛,所以我可能会写得更像:

    for (i=0; i < a_len; i++) {
    
        int val = a[i];  /* or whatever type */
        int result = 0;  /* default result */
    
        if (val == 0) {
            result = f1(val);
        } else if (val % 2 == 0) {
            result = f2(val);
        } 
    
        a[i] = result;
    }
    

    我猜编译器会生成类似的代码并进行优化。但是,如果其中一个稍微(只是非常轻微)好一点,我不会感到震惊。我敢打赌,如果有的话,那一定是使用本地人的。

    另外,通过将使用索引遍历数组更改为使用指针遍历数组,您可能会获得非常轻微的改进。同样,这取决于编译器和情况。

    for (p=&a[0]; p < &a[a_len]; ++p) {
    
        int val = *p;    /* or whatever type */
        int result = 0;  /* default result */
    
        if (val == 0) {
            result = f1(val);
        } else if (val % 2 == 0) {
            result = f2(val);
        } 
    
        *p = result;
    }
    

    而且,是的,我知道这些是微优化,通常甚至不应该担心(请首先编写代码以确保可读性和正确性) - 我只是指出一些选项对于何时可能需要进行微优化(这些建议必须通过对特定情况的分析来支持)。

    至于编译器是否会重复从 a[i] 之类的东西重新加载,这取决于控制流以及被访问的对象是全局对象还是已获取其地址并将其传递给其他对象。

    如果对象是全局对象或已获取其地址并且您调用函数,通常编译器必须假定该对象可能已被该函数修改并且必须重新加载它。当使用指针将信息传递给函数时,也会发生类似的问题。使用 locals 可以帮助缓解这个问题,因为编译器可以很容易地确定 local 没有被调用的函数修改,除非使用 local 的地址。编译器也可以尝试通过使用某种全局优化来解决这个问题(例如 MSVC 在链接时所做的)。

    即使数组a 是全局的,您的示例代码也可能没有真正遇到这个问题,因为在调用其中任何一个函数后您不会从数组中重新读取值(您只写入它)。


    我想知道为什么 Markdown 会从代码格式化的块中删除空白行?

    【讨论】:

    • 很好的答案。很遗憾,您不得不迎合“没有过早优化”的人群,而不是仅仅回答这个人的问题并假设他知道自己在说什么,但我看到好的答案因为不这样做而被遗忘。
    • @Matt J - 谢谢,但请注意,我经常处于“没有过早优化”的人群中。估计看我心情吧。在这里,我实际上认为(对我而言)更具可读性的代码可能也更容易让编译器优化。但我怀疑这个示例代码是否真的会有所不同。
    • 我当然尽量不要过早地优化自己的代码,但是在回答别人的问题时,如果没有迹象表明提问者不称职,有时只回答问题更合适。经常, “不优化”答案的投票方式高于有用的答案。
    【解决方案3】:

    两个版本在 GCC 中生成完全相同的代码,只要打开 -O 或更高版本。所以我的建议是做任何你喜欢的更好的方式(我更喜欢没有局部变量)。

    【讨论】:

      【解决方案4】:

      函数f1f2 似乎共享相同的签名。他们的行为有何不同?你真的需要外面的支票吗?或者,您可以将逻辑嵌入到一个函数中吗?

      如果您有一个if-else 梯形图而不是只有两个这样的函数,请尝试使用函数指针数组来代替。使用a[ i ] 的值来索引该数组并调用正确的函数。

      手动优化通常是容易出错的微优化。最好将此任务留给编译器。如果你真的需要优化,看大局,想想算法、设计、层等等。

      至于您的问题:是的,如果 a[ i ] 未声明为 volatile,大多数编译器可能会优化内存读取。

      【讨论】:

      • 这只是一个例子。问题实际上是内存读取和偏移算法是否会被优化出来。
      • 被接受为迄今为止唯一真正回答问题的答案。
      • 有时编译器在处理“可能有别名”的指针时不会优化代码。在您的情况下,如果编译器正在获取 function(int * a) 则编译器可能会假定 ptr 到 a 是别名,因此不会优化。
      • 如果您将指针质量为“int *restrict a”,那么编译器将知道“a”没有被别名,它会优化。
      • restrict 是 C99 的补充。与 C99 兼容的实现很少。
      【解决方案5】:

      dirkGentley 的回答:

      是的,大多数编译器都可能 优化内存读取应该 [ 我]

      有时编译器在处理“可能有别名”的指针时不会优化代码。在您的情况下,尼克,如果您将“a”作为函数参数函数(int * a),那么编译器可能会假设指向“a”的指针是别名,因此不会优化。

      如果您将指针质量为“int *restrict a”,那么编译器将知道“a”没有被别名,它会优化。

      100% 知道编译器是否在优化的唯一方法是 check the assembly!

      【讨论】:

        【解决方案6】:

        C 中的数组本质上是一个指针。

        局部变量很便宜。

        我发现第一个示例更易于阅读,因为我没有质疑“val”的用途。如果“val”和“a”有更好的名字,我敢说第二个例子会提高可读性。

        【讨论】:

          【解决方案7】:

          你说“如果我用汇编程序编写上面的代码......”所以我假设你知道汇编语言。

          我的建议:查看代码关键部分的编译器输出,了解实际情况。

          【讨论】:

            【解决方案8】:
            1. 除非您知道必须这样做,否则不要优化
            2. 您的编译器可能会做正确的事情
            3. 我发现后一个版本更容易阅读
            4. (边缘)更容易将后一个版本与多线程程序中的副作用隔离开来

            【讨论】:

            • 我猜有人不同意你的观点。
            【解决方案9】:

            优化提示

            我可能首先考虑将var 作为指针,而不是我本地变量可能会更好。那么你也不要使用变量的双重存储

            int* var;//Int or whatever type a[] is
            for (i=0; i < a_len; i++) {
                val = &a[i];
                if (*val == 0) {
                    f1(val);//// Set the valur inside f1
                } else if (*val % 2 == 0) {
                    f2(val);// Set the valur inside f2
                } else {
                    *val = 0;
            }
            

            当您对结果不感兴趣时​​,优化代码的提示可能是避免使用 '%' 运算符。这取决于您的编译器,但事实证明这对我来说更快(使用宏来提高可读性):

            #define is_divisible(dividend, divisor) ((((dividend)/(divisor)) * (divisor))==(dividend))
            

            使用:

            else if (is_divisible(val,2)) {
            

            这更快,至少在我测试过的大多数情况下。

            编辑:确实,仅使用 '% 2' 使用模数计算时利润并没有那么大,但是如果您曾经在比 2 更大的计算上徘徊进行模数运算并且只对模数感兴趣,那么返回零我的宏在我用过的所有编译器中都更快

            【讨论】:

            • 这段代码正是模数运算符的大多数实现。如果您要测试均匀度,测试 LSBit 比这要快得多。即“else if (!(val & 0x1))”。
            • 好吧,正如我所说的那样,我知道的那些也计算/重新计算计算中的其余部分。宏不看零以外的任何东西,但我看到你在使用'% 2'时指出了
            猜你喜欢
            • 2017-03-10
            • 1970-01-01
            • 2016-05-21
            • 2019-08-27
            • 2014-03-18
            • 1970-01-01
            • 1970-01-01
            • 2012-10-30
            • 2016-12-14
            相关资源
            最近更新 更多