在C ++中,我是否应该费心缓存变量,还是让编译器进行优化? (混叠)

时间:2015-11-24 16:03:04

标签: c++ performance caching optimization strict-aliasing

考虑以下代码(p属于unsigned char*类型,bitmap->width属于某种整数类型,具体哪个是未知的,取决于我们的某些外部库的版本。重新使用):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

是否值得优化[...]

可能会出现这样的情况,即通过编写以下内容可以产生更有效的结果:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

...或者编译器优化是否微不足道?

你会认为&#34;更好&#34;码?

来自编辑(Ike)的注意事项:对于那些对罢工文本感到疑惑的人来说,原来的问题,如同措辞一样,非常接近于偏离主题的领域,并且尽管是积极的,但非常接近被关闭反馈。这些已经被打乱了。但请不要惩罚那些解决这些问题的答案的回答者。

13 个答案:

答案 0 :(得分:81)

乍一看,我认为编译器可以为激活了优化标志的两个版本生成等效的程序集。当我检查它时,我很惊讶地看到结果:

来源unoptimized.cpp

注意:此代码无意执行。

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

来源optimized.cpp

注意:此代码无意执行。

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

汇编

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

装配(未优化的)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

汇编(optimized.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

DIFF

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

优化版本的生成程序集实际上加载了width width常量,而不是在每次迭代时计算{{1}}偏移量的未优化版本(lea

当我有时间的时候,我最终会发布一些基准。好问题。

答案 1 :(得分:38)

实际上,您的代码段中没有足够的信息可以告诉我,我能想到的一件事就是别名。从我们的观点来看,很明显你不希望pbitmap指向内存中的相同位置,但编译器并不知道那个和(因为p的类型为char*),即使pbitmap重叠,编译器也必须使此代码有效。

这意味着在这种情况下,如果循环通过指针bitmap->width更改p,那么稍后重新读取bitmap->width时必须看到这一点,这反过来意味着存储它在一个局部变量中是非法的。

话虽如此,我相信有些编译器实际上有时会生成相同代码的两个版本(我已经看到了这方面的间接证据,但从未直接找到有关编译器在这种情况下做什么的信息),并快速检查如果指针别名并运行更快的代码,如果它确定它可以。

话虽如此,我支持我的评论只是衡量两个版本的性能,我的钱是没有看到两个版本的代码之间的任何一致的性能差异。

在我看来,如果您的目的是了解编译器优化理论和技术,那么这样的问题是可以的,但如果您的最终目标是使程序运行得更快,则浪费时间(无用的微优化)

答案 2 :(得分:24)

其他答案指出,将指针操作提升出循环可能会改变定义的行为,因为别名规则允许char为别名设置别名,因此即使在大多数情况下它显然是正确的,也不是编译器允许的优化对于一个人类程序员。

他们还指出,从性能的角度来看,将操作从循环中提升通常但并不总是一种改进,从可读性的角度来看往往是负面的。

我想指出,通常有第三种方式&#34;。而不是计算你想要的迭代次数,你可以倒数到零。这意味着迭代次数仅在循环开始时需要一次,之后不必存储。更好的是在汇编程序级别它通常不需要显式比较,因为递减操作通常会设置标志,指​​示计数器在递减之前(进位标志)和之后(零标志)是否为零。

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

请注意,此版本的循环给出x值在1..width范围内而不是范围0 ..(width-1)。这在你的情况下并不重要,因为你并没有真正使用x来做任何事情,除了它需要注意的事情。如果你想要一个x值在0 ..(width-1)范围内的倒计时循环,你可以这样做。

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

如果你愿意,你也可以摆脱上面例子中的演员阵容,而不用担心它对比较规则的影响,因为你所做的只是bitmap-&gt; width就是将它直接赋值给变量。 / p>

答案 3 :(得分:23)

好的,伙计们,所以我用GCC -O3测量了(在Linux x64上使用GCC 4.9)。

事实证明,第二个版本的运行速度提高了54%!

所以,我觉得混​​淆是事情,我没想过。

[编辑]

我再次尝试使用__restrict__定义的所有指针的第一个版本,结果是相同的。很奇怪。别名不是问题,或者由于某种原因,编译器即使用__restrict__也不能很好地优化它。

[编辑2]

好吧,我认为我几乎能够证明别名是问题所在。我重复了我的原始测试,这次是使用数组而不是指针:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

并测量(必须使用“-mcmodel = large”来链接它)。然后我试了一下:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

测量结果是相同的 - 似乎编译器能够自己优化它。

然后我尝试了原始代码(带有指针p),这次p类型为std::uint16_t*时。同样,结果是相同的 - 由于严格的混叠。然后我尝试使用“-fno-strict-aliasing”构建,并再次看到时间上的差异。

答案 4 :(得分:11)

此处唯一可以阻止优化的是strict aliasing ruleIn short

  
    

“严格别名是由C(或C ++)编译器做出的假设,取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名。)”

  
     

[...]

     

规则的例外是char*,允许指向任何类型。

该例外也适用于unsignedsigned char指针。

您的代码就是这种情况:您正在修改*pp unsigned char*,因此编译器必须认为它可以指向bitmap->width。因此,bitmap->width的缓存是无效的优化。此优化预防行为显示在YSC's answer

当且仅当p指向非char和非decltype(bitmap->width)类型时,缓存才会成为可能的优化。

答案 5 :(得分:10)

最初问的问题是:

  

值得优化吗?

我对此的回答(获得了上下投票的良好组合......)

  

让编译器担心它。

     

编译器几乎肯定会比你做得更好。和   我们无法保证您的“优化”优于   “明显的”代码 - 你测量过吗?

     

更重要的是,您有任何证据证明您正在优化的代码   对您的计划的表现有什么影响?

尽管存在downvotes(现在看到别名问题),我仍然对此作为有效答案感到满意。 如果您不知道是否值得优化某些内容,则可能不是。

当然,一个相当不同的问题是:

  

如何判断是否值得优化代码片段?

首先,您的应用程序或库是否需要比目前运行得更快?用户是否一直等待太久?你的软件是否预测昨天的天气而不是明天的天气?

根据您的软件用途以及用户的期望,您才能真正说出这一点。

假设您的软件确实需要一些优化,接下来要做的就是开始测量。 Profilers会告诉您代码花费的时间。如果你的片段没有显示为瓶颈,那么最好不要管它。 Profilers和其他测量工具也会告诉您您的更改是否有所作为。可以花费数小时来优化代码,但却发现你没有明显的差异。

  

无论如何,'优化'是什么意思?

如果您没有编写“优化”代码,那么您的代码应该尽可能清晰,简洁,简洁。 “过早优化是邪恶的”论证不是草率或低效代码的借口。

优化代码通常会牺牲上面的一些属性来提高性能。它可能涉及引入额外的局部变量,具有比预期范围更宽的对象,甚至可以反转正常的循环排序。所有这些可能都不太清晰或简洁,因此请记录代码(简要说明!),了解您为何这样做。

但通常,对于“慢”代码,这些微优化是最后的选择。首先要看的是算法和数据结构。有没有办法避免完成工作?线性搜索可以用二进制搜索替换吗?这里的链表比矢量更快吗?还是哈希表?我可以缓存结果吗?在这里做出“有效”的决策往往会影响性能一个数量级或更多!

答案 6 :(得分:6)

我在这种情况下使用以下模式。它几乎和你的第一种情况一样短,并且比第二种情况要好,因为它将临时变量保持在循环的本地。

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

使用低于智能编译器,调试版本或某些编译标志的速度会更快。

Edit1 :在循环外放置一个常量操作是良好的编程模式。它显示了对机器操作基础知识的理解,特别是在C / C ++中。我认为,证明自己的努力应该放在那些不遵循这种做法的人身上。如果编译器惩罚好的模式,那么它就是编译器中的一个错误。

编辑2::我已经在vs2013上测量了我对原始代码的建议,得到了%1的改进。我们可以做得更好吗?简单的手动优化比x64机器上的原始循环提高了3倍,而无需借助异国情调的指令。下面的代码假设小端系统和正确对齐的位图。 TEST 0是原始(9秒),TEST 1更快(3秒)。我敢打赌,有人可以让它更快,测试的结果将取决于位图的大小。很快将来,编译器将能够生成始终如一的最快代码。我担心,当编译器也是程序员AI时,这将是未来,所以我们将失去工作。但是现在,只需编写代码,表明您知道不需要循环中的额外操作。

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}

答案 7 :(得分:5)

有两件事需要考虑。

A)优化运行的频率是多少?

如果答案不常见,例如只有当用户点击按钮时,如果它使您的代码无法读取,请不要理会。如果答案是每秒1000次,那么您可能想要进行优化。如果它甚至有点复杂,一定要发表评论来解释正在发生的事情,以帮助下一个人来。

B)这会使代码难以维护/排除故障吗?

如果你没有看到性能的巨大提升,那么让你的代码神秘地保存几个时钟滴答并不是一个好主意。很多人会告诉你,任何优秀的程序员都应该能够查看代码并弄清楚发生了什么。这是真的。问题在于,在商业世界中,花费额外的时间来计算成本。所以,如果你能让它更漂亮,那就去做吧。你的朋友会感谢你的。

那说我亲自使用B的例子。

答案 8 :(得分:4)

编译器能够优化很多东西。对于您的示例,您应该考虑可读性,可持续性以及遵循代码标准的内容。有关可以优化的内容(使用GCC)的更多信息,请参阅this blog post

答案 9 :(得分:4)

作为一般规则,让编译器为您进行优化,直到您确定应该接管为止。这个逻辑与性能无关,而是与人类的可读性无关。在浩大的大多数情况下,程序的可读性比其性能更重要。您的目标应该是编写一个人类更容易阅读的代码,然后在您确信性能比代码的可维护性更重要时,只需要担心优化。

一旦确实看到性能很重要,就应该在代码上运行一个分析器来确定哪些循环效率低下,并单独优化它们。可能确实存在您希望进行优化的情况(特别是如果您转向使用STL容器的C ++),但可读性方面的成本很高。

此外,我可以想到它实际上可以减慢代码速度的病态情况。例如,考虑编译器无法证明bitmap->width在整个过程中是不变的情况。通过添加width变量,您可以强制编译器在该范围内维护局部变量。如果由于某些特定平台的原因,额外的变量阻止了一些堆栈空间优化,它可能必须重新组织它如何发送字节码,并产生效率较低的东西。

例如,在Windows x64上,如果函数将使用多于1页的局部变量,则必须在函数的前导码中调用特殊的API调用__chkstk。此功能使Windows有机会管理他们用于在需要时扩展堆栈的防护页面。如果您的额外变量将堆栈使用量从1页以下推到1页或以上,那么您的函数现在必须在每次输入时调用__chkstk。如果你要在慢速路径上优化这个循环,你实际上可能比在慢速路径上保存的速度慢得多!

当然,它有点病态,但这个例子的重点是你实际上可以减慢编译器的速度。它只是表明您必须分析您的工作以确定优化的位置。与此同时,请不要以任何方式牺牲可读性,以进行可能或不重要的优化。

答案 10 :(得分:4)

比较错误,因为这两个代码段

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

不等同

在第一种情况下,width是依赖的而不是const,并且不能假设它在后续迭代之间可能不会改变。 因此无法优化,但必须是checked at every loop

在优化的情况下,局部变量在程序执行期间的某个时刻被赋予bitmap->width的值。 编译器可以验证这实际上没有改变。

您是否考虑过多线程,或者该值可能是外部依赖的,因此其值是不稳定的。如果你不告诉我们如何期望编译器能够解决所有这些问题呢?

编译器只能像你的代码一样好。

答案 11 :(得分:2)

除非您知道编译器如何优化代码,否则最好通过保持代码可读性和设计来进行自己的优化。实际上,很难检查我们为新编译器版本编写的每个函数的汇编代码。

答案 12 :(得分:1)

编译器无法优化bitmap->width,因为width的值可以在迭代之间更改。有几个最常见的原因:

  1. 多线程。编译器无法预测其他线程是否即将改变值。
  2. 内部循环修改,有时判断变量是否会在循环内更改并不容易。
  3. 这是函数调用,例如iterator::end()container::size()因此很难预测它是否会始终返回相同的结果。
  4. 总结(我的个人意见)对于需要高水平优化的地方你需要自己做,在其他地方只留下它,编译器可能会优化它,如果没有大的差异代码可读性是主要目标。