考虑以下代码(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)的注意事项:对于那些对罢工文本感到疑惑的人来说,原来的问题,如同措辞一样,非常接近于偏离主题的领域,并且尽管是积极的,但非常接近被关闭反馈。这些已经被打乱了。但请不要惩罚那些解决这些问题的答案的回答者。
答案 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
.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
优化版本的生成程序集实际上加载了width
width
常量,而不是在每次迭代时计算{{1}}偏移量的未优化版本(lea
)
当我有时间的时候,我最终会发布一些基准。好问题。
答案 1 :(得分:38)
实际上,您的代码段中没有足够的信息可以告诉我,我能想到的一件事就是别名。从我们的观点来看,很明显你不希望p
和bitmap
指向内存中的相同位置,但编译器并不知道那个和(因为p
的类型为char*
),即使p
和bitmap
重叠,编译器也必须使此代码有效。
这意味着在这种情况下,如果循环通过指针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 rule。 In short:
“严格别名是由C(或C ++)编译器做出的假设,取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名。)”
[...]
规则的例外是
char*
,允许指向任何类型。
该例外也适用于unsigned
和signed
char
指针。
您的代码就是这种情况:您正在修改*p
到p
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
的值可以在迭代之间更改。有几个最常见的原因:
iterator::end()
或container::size()
因此很难预测它是否会始终返回相同的结果。总结(我的个人意见)对于需要高水平优化的地方你需要自己做,在其他地方只留下它,编译器可能会优化它,如果没有大的差异代码可读性是主要目标。