在C ++中,我应该费心去cachingvariables,还是让编译器进行优化? (混叠)

考虑下面的代码( punsigned char*的types,而bitmap->width是一些整数types,它是未知的,取决于我们使用的外部库的版本):

 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; } 

…或者这是编译器优化这个微不足道的?

你认为什么是“更好”的代码?

编者注(艾克):对于那些想知道三文治文本的人来说,原来的这个问题,如同所说的那样,离危险的地方很近,尽pipe得到了正面的反馈,但却非常接近封闭。 这些已经被打破了。 但是,请不要惩罚处理这些问题的答复者。

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

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

大会(unoptimized.s)

  .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 

大会(优化。)

  .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 

优化版本生成的程序集实际上会加载( leawidth常量,与计算每次迭代( movq )的width偏移量的未优化版本不同。

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

实际上你的代码片段中没有足够的信息可以告诉,而我能想到的一个就是别名。 从我们的angular度来看,很明显,你不希望pbitmap指向内存中的相同位置,但是编译器不知道(因为p的types是char* )编译器必须即使pbitmap重叠,此代码也可以工作。

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

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

这就是说,我坚持评论这两个版本的性能,我的钱没有看到两个版本的代码之间有任何一致的性能差异。

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

其他答案已经指出,将指针操作从循环中提出可能会改变已定义的行为,这是由于混叠规则允许char混淆了任何内容,因此对于编译器来说不是一个可允许的优化,尽pipe在大多数情况下对于人来说显然是正确的程序员。

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

我想指出,通常有“第三条路”。 而不是数到你想要的迭代次数,你可以倒数到零。 这意味着迭代的次数在循环开始时只需要一次,不需要在循环之后保存。 更好的是,在汇编程序级别,通常不需要进行明确的比较,因为递减操作通常会设置标志,指​​示计数器在递减之前(进位标志)和之后(零标志)是否为零。

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

请注意,此版本的循环给出的x值范围是1..width,而不是范围0 ..(width-1)。 这在你的情况并不重要,因为你实际上并没有使用x,但它是需要注意的。 如果你想要一个数值下降循环的范围为0 ..(宽度-1)x值你可以做。

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

如果你愿意的话,你也可以摆脱上面例子中的types转换,而不用担心它会影响比较规则,因为所有你正在做的bitmap-> width将它直接赋值给一个variables。

好的,大家好,我已经测量了GCC -O3 (在Linux x64上使用GCC 4.9)。

原来,第二个版本跑得快了54%!

所以,我认为别名是事情,我没有想过。

[编辑]

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

[编辑2]

好的,我想我已经能够certificate别名是问题了。 我重复了我原来的testing,这次使用一个数组而不是一个指针:

 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 =大”来链接它)。 然后我试着:

 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 ),这次pstd::uint16_t*types。 再次,结果是一样的 – 由于严格的别名。 然后我试着用“-fno-strict-aliasing”来build造,再次看到了时间的差异。

最初的问题是:

值得优化吗?

而我的答案(赢得了一个很好的混合上下的票..)

让编译器担心它。

编译器几乎肯定会比你做得更好。 而且不能保证你的“优化”比“明显”的代码更好 – 你有没有测量过?

更重要的是,你有没有certificate你正在优化的代码对程序的性能有什么影响?

尽pipe降价(现在看到了别名问题),但我仍然对此感到高兴,作为一个有效的答案。 如果你不知道是否值得优化,那可能不是。

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

我怎么知道是否值得优化一段代码?

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

只有你可以根据你的软件是什么和你的用户的期望真正地告诉你的。

假设你的软件需要一些优化,接下来要做的就是开始测量。 Profiler会告诉你你的代码在哪里花费时间。 如果你的片段没有显示为瓶颈,那么最好不要pipe它。 分析器和其他测量工具也会告诉你,如果你的变化有所作为。 可能花费数小时试图优化代码,只是发现你没有发现明显的差异。

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

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

优化的代码通常牺牲了上面的性能的一些属性。 它可能涉及引入额外的局部variables,具有比预期更广的对象,甚至颠倒正常的循环顺序。 所有这些可能都不那么清晰或简洁,所以请记下代码(简要介绍一下)为什么要这样做。

但是,通常,使用“缓慢”的代码,这些微观优化是最后的手段。 首先看看algorithm和数据结构。 有没有办法避免做这个工作? 线性search可以用二进制代替吗? 链表会比vector快吗? 或者一个哈希表? 我可以caching结果吗? 在这里做出高效的决定通常会影响到一个数量级以上的性能!

这里唯一可以防止优化的是严格的锯齿规则 。 总之 :

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

[…]

规则的例外是一个char* ,它可以指向任何types。

exception也适用于unsigned和有signed char指针。

在你的代码中就是这种情况:你通过p来修改*p ,这是一个unsigned char* ,所以编译器必须假定它可以指向位图 – bitmap->width 。 因此, bitmap->width的caching是无效的优化。 YSC的回答显示了这种优化防止行为。

当且仅当p指向非char和非decltype(bitmap->width)types时,caching才是可能的优化。

我在这种情况下使用以下模式。 它几乎和你的第一个例子一样短,比第二个例子要好,因为它把临时variables保存在循环中。

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

这比使用智能编译器,debugging版本或某些编译标志更快。

编辑1 :在循环之外放置一个常量操作是一个很好的编程模式。 它显示了对机器操作基本知识的理解,尤其是在C / C ++中。 我认为,certificate自己的努力应该放在不遵循这种做法的人身上。 如果编译器惩罚一个好的模式,这是编译器中的一个错误。

编辑2 :我在vs2013上testing了我对原始代码的build议,得到了%1的改进。 我们可以做得更好吗? 一个简单的手动优化比x64机器上的原始循环提高了3倍,而不依赖于特殊的指令。 下面的代码假设小端系统和正确alignment的位图。 testing0是原始的(9秒),testing1是更快的(3秒)。 我敢打赌,有人可以做得更快,testing的结果将取决于位图的大小。 编译器将来一定会很快产生一致的最快的代码。 我担心这将是未来的编译器也将是一个程序员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; } 

有两件事情要考虑。

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

如果答案不是很常见,比如只有当用户点击一个button,那么不要打扰,如果它使你的代码不可读。 如果答案是每秒1000次,那么你可能会想要去优化。 如果这个问题有点复杂,那么一定要发表评论来解释下一步如何帮助下一个人。

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

如果你没有看到性能上的巨大增益,那么只要简单地节省一些时钟就可以让代码变得晦涩难懂,这不是一个好主意。 很多人会告诉你,任何一个优秀的程序员都应该能够看到代码并找出发生的事情。 这是真的。 问题在于,在商业世界中,花费额外的时间花费金钱。 所以,如果你可以更漂亮的阅读然后做。 你的朋友会为此感谢你。

那就是说我亲自使用B例子。

编译器能够优化很多东西。 对于你的例子,你应该追求可读性,可保持性和遵循你的代码标准。 有关可以优化哪些内容(使用GCC)的更多信息,请参阅此博客文章 。

作为一般规则,让编译器为你做优化,直到你确定你应该接pipe。 这个逻辑与性能无关,而与人的可读性有关。 在绝大多数情况下,程序的可读性比其性能更重要。 你应该着眼于编写更容易让人阅读的代码,然后只有当你确信性能比代码的可维护性更重要时才担心优化。

一旦你看到性能问题,你应该在代码上运行一个分析器,以确定哪些循环效率低下,并单独优化。 确实有些情况下,你想要做这种优化(特别是如果你迁移到STL容器涉及的C ++),但是在可读性方面的成本很高。

另外,我可以考虑可能会使代码减慢的病态情况。 例如,考虑编译器无法certificate位图 – bitmap->width在整个过程中不变的情况。 通过添加widthvariables,您可以强制编译器在该范围内维护一个局部variables。 如果出于某种平台特定的原因,额外的variables阻碍了一些堆栈空间的优化,那么它可能不得不重新组织如何发出字节码,并产生效率较低的东西。

例如,在Windows x64上,如果函数将使用多于1页的局部variables,则必须在函数的前导码中调用特殊的API调用__chkstk 。 这个函数让Windows有机会pipe理他们用来在需要时扩展堆栈的防护页。 如果您的额外variables将堆栈使用率从低于1页上升到高于或等于1页,那么您的函数现在每次input时都必须调用__chkstk 。 如果要在慢速path上优化此循环,则实际上可能会减慢快速path,而不是在慢速path上保存!

当然,这有点病态,但是这个例子的关键是你可以放慢编译器的速度。 这只是表明你必须分析你的工作,以确定优化去的地方。 同时,请不要以任何方式牺牲可读性来进行可能或可能不重要的优化。

自两个代码片段以来, 比较是错误

 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是依赖的而不是常量,并且不能认为它在后续迭代之间可能不会改变。 因此它不能被优化,但必须在每个循环中检查

在你优化的情况下,在程序执行期间的某个地方,局部variables被分配了位图 – bitmap->width的值。 编译器可以validation这实际上并没有改变。

您是否考虑过multithreading,或者可能是外部依赖的价值,使其价值不稳定。 如果你不告诉,那么如何期望编译器能够把所有这些东西都弄清楚?

编译器只能像你的代码所做的那样做。

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

编译器不能优化位图 – bitmap->width因为bitmap->width值可以在迭代之间改变。 有几个最常见的原因:

  1. multithreading。 编译器无法预测其他线程是否要更改值。
  2. 在循环内部进行修改,有时并不容易判断variables是否会在循环内部被改变。
  3. 它是函数调用,例如iterator::end()container::size()所以很难预测它是否总是返回相同的结果。

To sum up (my personal opinion) for places that requires high level of optimization you need to do that by yourself, in other places just leave it, compiler may optimize it or not, if there is no big difference code readability is main target.