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

Yar*_*Tal 114 c++ optimization performance caching strict-aliasing

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

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

值得优化它[...]

可能存在这样一种情况,即通过编写可以产生更有效的结果:

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

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

您认为什么是"更好"的代码?

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

YSC*_*YSC 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;
}
Run Code Online (Sandbox Code Playgroud)

资源 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;
}
Run Code Online (Sandbox Code Playgroud)

汇编

  • $ 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
Run Code Online (Sandbox Code Playgroud)

装配(优化)

    .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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

优化版本的生成程序集实际上会加载(lea)width常量,而不像未优化版本那样计算width每次迭代时的偏移量(movq).

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

  • 请****永远**使用函数`main`来测试优化.Gcc故意将其标记为冷,因此禁用了一些优化.我不知道这是不是这样,但这是一个很重要的习惯. (13认同)
  • 如果在未优化的情况下转换为"const unsigned"而不是"unsigned",那么查看代码是否生成方式会很有趣. (3认同)
  • @MarcGlisse你是100%正确的.我匆匆写了它,我会改进它. (3认同)
  • 这里是[godbolt上一个编译单元中的两个函数]的链接(http://goo.gl/9NiWOS),假设`bitmap`是全局的.非CSEd版本使用内存操作数来"cmp",在这种情况下这不是perf的问题.如果它是本地的,编译器可以假设其他指针不能"了解"它并指向它.将涉及全局变量的表达式存储在临时变量中并不是一个坏主意,只要它改善(或不伤害)可读性,或者性能是至关重要的.除非有很多事情发生,否则这些当地人通常只能居住在寄存器中,而且永远不会被泄漏. (3认同)
  • @MarkRansom我想它应该没有什么区别:成为const的"承诺"仅在单一比较期间,而不是整个循环 (2认同)

Sir*_*Guy 38

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

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

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

话虽这么说,我支持我的评论只是测量两个版本的性能,我的钱是没有看到两个版本的代码之间的任何一致的性能差异.

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

  • 根据我的经验,"限制"在很大程度上是命中注定的.MSVC是我见过的唯一一个似乎正确执行它的编译器.ICC通过函数调用丢失别名信息,即使它们是内联的.除非您将每个输入参数声明为"restrict"(包括成员函数的"this"),否则GCC通常无法获得任何好处. (4认同)
  • @GuyGreer - 在这种情况下,不会有`restrict`限定符作为别名问题的答案吗? (3认同)

plu*_*ash 24

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

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

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

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

请注意,此版本的循环给出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;
}
Run Code Online (Sandbox Code Playgroud)

如果你愿意,你也可以摆脱上面例子中的演员表,而不用担心它对比较规则的影响,因为你所做的只是bitmap-> width就是将它直接分配给一个变量.

  • 如果你想编写看起来更像是最佳asm的循环,可以使用`do {} while()`,因为在ASM中你最后用条件分支创建循环.通常的`for(){}`和`while(){}`循环需要额外的指令来在循环之前测试循环条件,如果编译器无法证明它总是运行至少一次.无论如何,当检查循环是否应该运行一次,或者它是否更具可读性时,使用`for()`或`while()`是有用的. (3认同)
  • 我已经看到第二种情况被格式化为"x - > 0",导致"downto"运算符.挺滑稽的.PS我不认为为结束条件做一个变量对可读性是负面的,它实际上可能是相反的. (2认同)

Yar*_*Tal 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;
}
Run Code Online (Sandbox Code Playgroud)

并测量(必须使用"-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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 这似乎应该是一个评论,虽然它在技术上回答了这个问题.还要注意,遗憾的是你还没有证明别名就是这样.看起来很可能,当然看似合理,但这与结论不一样. (4认同)
  • @ YaronCohen-Tal这么快两倍?令人印象深刻,但不是我所理解的"快54%"的意思! (3认同)
  • 我只是想知道为什么你开始使用变量"i",你的循环中有"x"? (2认同)

eml*_*lai 11

这里唯一可以阻止优化的是严格的别名规则.简而言之:

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

[...]

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

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

这是你的代码的情况:你修改*pp这是一个unsigned char*,所以编译器必须假定它可能指向bitmap->width.因此缓存bitmap->width是无效的优化.这种优化防止行为在YSC的答案中显示.

当且仅当p指向chardecltype(bitmap->width)类型和非类型时,缓存是否可能是优化.


Rod*_*ddy 10

最初问的问题是:

值得优化吗?

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

让编译器担心它.

编译器几乎肯定会比你做得更好.并且无法保证您的"优化"优于"明显"代码 - 您测量过它吗?

更重要的是,您是否有证据证明您优化的代码会对您的程序性能产生任何影响?

尽管存在支持(现在看到混淆问题),我仍然对此作为有效答案感到满意.如果你不知道是否值得优化,可能不是.

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

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

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

只有您可以根据您的软件用途以及用户期望的内容来确定这一点.

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

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

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

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

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

  • 当您迭代位图图像的宽度时,循环逻辑可能是循环中花费的时间的重要部分.而不是担心过早优化,在这种情况下,最好从一开始就开发出有效的最佳实践. (12认同)
  • @MarkRansom部分同意:但"最佳实践"可能是:使用现有的库或API调用来填充图像,或者b:让GPU为您完成.它绝不应该是OP所暗示的那种无法测量的微优化.你怎么知道这个代码不止一次被执行过,或者是那个大于16像素宽的位图......? (4认同)

0kc*_*ats 6

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

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

使用少于智能编译器,调试版本或某些编译标志会更快.

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

Edit2 ::我已经在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;
}
Run Code Online (Sandbox Code Playgroud)


sou*_*abr 5

有两件事需要考虑.

A)优化运行的频率如何?

如果答案不是很频繁,例如只有当用户点击按钮时,如果它使您的代码不可读则不要打扰.如果答案是每秒1000次,那么您可能想要进行优化.如果它甚至有点复杂,一定要发表评论来解释正在发生的事情,以帮助下一个出现的人.

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

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

那说我个人使用B的例子.


Gui*_*cot 4

编译器能够优化很多东西。对于您的示例,您应该追求可读性、可维护性以及遵循代码标准的内容。有关可以优化的内容(使用 GCC)的更多信息,请参阅此博客文章