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

考虑下面的代码(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;
}

还是编译器根本不需要优化?

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

来自编辑(艾克)的注释: 对于那些对三振出局的文字感到好奇的人来说,原来的问题,按照措辞,是偏离主题的危险接近,尽管得到了积极的反馈,但是已经非常接近关闭了。这些已经被剔除了。然而,请不要惩罚回答这些问题的回答者。

9500 次浏览

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

最初的问题是:

值得优化吗?

我对此的回答是(获得了上下两种选票的良好组合... ...)

让编译器来操心吧。

编译器几乎肯定会比你做得更好 不能保证你的“优化”比 “显而易见”的代码-你测量过吗? ?

更重要的是,您是否有任何证据证明您正在优化的代码 对你的节目表现有什么影响吗?

尽管投了反对票(现在看到了别名的问题) ,我仍然很高兴这是一个有效的答案

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

我怎样才能判断优化一段代码是否值得呢?

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

基于软件的用途和用户的期望,只有您能够真正说明这一点。

假设您的软件确实需要一些优化,接下来要做的就是开始度量。分析器会告诉你代码在哪里花费时间。如果您的片段没有显示为瓶颈,那么最好不要管它。剖析器和其他测量工具也会告诉您您的更改是否产生了影响。花费数小时尝试优化代码,却发现没有明显的效果,这是可能的。

你说的“优化”到底是什么意思?

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

经过优化的代码通常会为了性能而牺牲上面的一些属性。它可能涉及到引入额外的局部变量,拥有比预期范围更宽的对象,甚至逆转正常的循环顺序。所有这些可能不那么清晰或简洁,因此对代码编写文档(简要地说!)你为什么要这么做。

但通常,对于“慢”代码,这些微优化是最后的手段。首先要研究的是算法和数据结构。有什么办法可以完全避免做这项工作吗?线性搜索可以用二进制搜索代替吗?这里链表会比向量快吗?还是大麻桌?我可以缓存结果吗?在这里做出好的“高效”决定通常会影响数量级的表现!

At first glance, I thought the compiler could generate equivalent assembly for both versions with optimization flags activated. When I checked it, I was surprised to see the result:

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

Compilation

  • $ 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

Assembly (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 -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

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

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

有两件事要考虑。

A)优化多久运行一次?

如果答案不经常出现,比如只有当用户单击按钮时,那么不要担心它会使代码不可读。如果答案是每秒1000次,那么您可能会希望进行优化。如果它甚至有点复杂,一定要在评论中解释正在发生的事情,以帮助下一个出现的家伙。

B) Will this make the code harder to upkeep/troubleshoot?

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

也就是说我个人会使用 B 的例子。

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

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

也就是说,我相信有些编译器实际上有时会生成同一代码的两个版本(我见过这样的间接证据,但从来没有直接寻找编译器在这种情况下做什么的信息) ,并迅速检查指针别名和运行更快的代码,如果它确定它是可以的。

尽管如此,我还是坚持简单地测量两个版本的性能的观点,我的赌注是在两个版本的代码之间没有看到任何一致的性能差异。

在我看来,如果你的目的是学习编译器最佳化理论和技术,这样的问题是可以的,但是如果你的最终目标是让程序运行得更快,那就是浪费时间(一个无用的微优化)。

好了,伙计们,我已经测量过了,使用 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- 别名”构建,再次看到了时间上的差异。

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

一旦发现性能很重要,就应该对代码运行一个分析器,以确定哪些循环效率低下,并对这些循环进行单独优化。在某些情况下,您可能确实希望进行这种优化(特别是当您迁移到 C + + 时,涉及到 STL 容器时) ,但是在可读性方面的代价是巨大的。

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

例如,在 Windows x64上,如果函数将使用超过1页的局部变量,则必须在函数的前导部分调用一个特殊的 API 调用 __chkstk。这个函数让窗口有机会管理它们在需要时用来展开堆栈的保护页。如果您的额外变量将堆栈使用量从低于1页推高到高于或等于1页,那么您的函数现在必须在每次输入时调用 __chkstk。如果要在慢速路径上优化这个循环,实际上可能会使快速路径的速度降低的比在慢速路径上保存的更快!

Sure, it's a bit pathological, but the point of that example is that you can actually slow the compiler down. It just shows that you do have to profile your work to determine where the optimizations go. In the mean time, please don't sacrifice readability in any way for an optimization that may or may not matter.

其他答案指出,将指针操作提升到循环之外可能会改变已定义的行为,因为别名规则允许 char 使用任何别名,因此不允许编译器进行优化,尽管在大多数情况下,对人类程序员来说,这显然是正确的。

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

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

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

注意,这个版本的循环给出的 x 值在1范围内。.宽度而不是范围0。.(阔度-1)。这在你的情况下并不重要,因为你实际上并没有使用 x 做任何事情,但是这是需要注意的事情。如果想要一个 x 值在0范围内的倒计时循环。.(宽度-1)你可以做。

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

如果您不担心它对比较规则的影响,也可以去掉上面示例中的强制转换,因为您使用位图-> width 所做的一切都是将它直接赋值给一个变量。

这里唯一可以阻止优化的是 < strong > 严格的别名规则 :

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

[…]

The exception to the rule is a char*, which is allowed to point to any type.

此异常也适用于 unsignedsignedchar指针。

This is the case in your code: You're modifying *p through p which is an unsigned char*, so the compiler 必须的 assume that it could point to bitmap->width. Hence the caching of bitmap->width is an invalid optimization. This optimization-preventing behavior is shown in YSC 的回答.

如果且仅当 p指向非 char和非 decltype(bitmap->width)类型,那么缓存将是一种可能的优化。

比较是错误的开始的两个代码片段

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是依赖的而不是常量,并且不能假设它在后续迭代之间不会变化。因此它不能被优化,但是必须在每个循环 检查 < a href = “ https://stackoverflow. com/a/33898749/4345926”> .

在优化的情况下,在程序执行的某个时刻,局部变量局部变量被赋值为 bitmap->width

您是否考虑过多线程处理,或者该值可能是外部依赖的,因此它的值是不稳定的。如果你不说出来,你怎么能指望编译器能解决所有这些问题呢?

编译器只能做到代码允许的程度。

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

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

使用小于智能编译器、调试版本或某些编译标志时,这将更快。

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

Edit2:: I've measured my suggestion against original code on vs2013, got %1 improvement. Can we do better? A simple manual optimization gives 3 times improvement over the original loop on x64 machine without resorting to exotic instructions. The code below assumes little endian system and properly aligned bitmap. TEST 0 is original (9 sec), TEST 1 is faster (3 sec). I bet somebody could make this even faster, and the result of the test would depend on the size of the bitmap. Definitely soon in the future, compiler will be able to produce consistently fastest code. I afraid this will be the future when the compiler will be also a programmer AI, so we would be out of work. But for now, just write code that shows that you know that extra operation in the loop is not needed.

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

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

编译器无法优化 bitmap->width,因为 width的值可以在迭代之间更改:

  1. Multi-threading. Compiler cannot predict if other thread is about to change value.
  2. 修改循环内部,有时很难判断变量是否会在循环内部被修改。
  3. 它是函数调用,例如 iterator::end()container::size(),因此很难预测它是否总是返回相同的结果。

总结一下(我个人的观点) ,对于需要高水平优化的地方,你需要自己来做,在其他地方,只需要离开它,编译器可能会优化它或不优化,如果没有很大的差异,代码可读性是主要目标。