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 -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
).
当我有时间的时候,我最终会发布一些基准.好问题.
Sir*_*Guy 38
实际上,您的代码段中没有足够的信息可以告诉我,我能想到的一件事就是别名.从我们的观点来看,很明显你不想p
和bitmap
指向内存中的相同位置,但是编译器不知道并且(因为p
是类型char*
)编译器必须使这个代码工作,即使p
和bitmap
重叠.
这意味着在这种情况下,如果循环bitmap->width
通过指针改变,则在稍后p
重新读取时必须看到bitmap->width
,这反过来意味着将其存储在局部变量中将是非法的.
话虽这么说,我相信一些编译器实际上有时会生成相同代码的两个版本(我已经看到了这方面的间接证据,但从未直接找到有关编译器在这种情况下正在做什么的信息),并快速检查是否有指针别名并运行更快的代码,如果它确定它是可以的.
话虽这么说,我支持我的评论只是测量两个版本的性能,我的钱是没有看到两个版本的代码之间的任何一致的性能差异.
在我看来,如果您的目的是了解编译器优化理论和技术,那么这样的问题是可以的,但如果您的最终目标是使程序运行得更快,则浪费时间(无用的微优化).
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就是将它直接分配给一个变量.
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"构建,并再次看到时间上的差异.
Rod*_*ddy 10
最初问的问题是:
值得优化吗?
我对此的回答(获得了上下投票的良好组合......)
让编译器担心它.
编译器几乎肯定会比你做得更好.并且无法保证您的"优化"优于"明显"代码 - 您测量过它吗?
更重要的是,您是否有证据证明您优化的代码会对您的程序性能产生任何影响?
尽管存在支持(现在看到混淆问题),我仍然对此作为有效答案感到满意.如果你不知道是否值得优化,可能不是.
当然,一个相当不同的问题是:
如何判断优化代码片段是否值得?
首先,您的应用程序或库是否需要比目前运行得更快?用户是否一直等待太久?你的软件是否预测昨天的天气而不是明天的天气?
只有您可以根据您的软件用途以及用户期望的内容来确定这一点.
假设您的软件确实需要一些优化,接下来要做的就是开始测量.Profilers会告诉您代码花费的时间.如果你的片段没有显示为瓶颈,那么最好不要管它.Profilers和其他测量工具也会告诉您您的更改是否有所作为.可以花费数小时来优化代码,但却发现你没有明显的差异.
无论如何,"优化"是什么意思?
如果您没有编写"优化"代码,那么您的代码应该尽可能清晰,简洁,简洁."过早优化是邪恶的"论证不是草率或低效代码的借口.
优化的代码通常会牺牲一些上述属性来提高性能.它可能涉及引入额外的局部变量,具有比预期范围更宽的对象,甚至可以反转正常的循环排序.所有这些可能都不太清晰或简洁,因此请记录代码(简要说明!),了解您为何这样做.
但通常,使用"慢"代码,这些微优化是最后的手段.首先要看的是算法和数据结构.有没有办法避免完成工作?线性搜索可以用二进制搜索替换吗?这里的链表比矢量更快吗?还是哈希表?我可以缓存结果吗?在这里做出"有效"的决策往往会影响性能一个数量级或更多!
我在这种情况下使用以下模式.它几乎与你的第一种情况一样短,并且优于第二种情况,因为它将临时变量保持在循环的本地.
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)
有两件事需要考虑.
A)优化运行的频率如何?
如果答案不是很频繁,例如只有当用户点击按钮时,如果它使您的代码不可读则不要打扰.如果答案是每秒1000次,那么您可能想要进行优化.如果它甚至有点复杂,一定要发表评论来解释正在发生的事情,以帮助下一个出现的人.
B)这会使代码更难维护/故障排除吗?
如果你没有看到性能的巨大提升,那么让你的代码只是为了节省几个时钟滴答而神秘不是一个好主意.很多人会告诉你,任何优秀的程序员都应该能够查看代码并弄清楚发生了什么.这是真的.问题在于,在商业世界中,花费额外的时间来计算成本.所以,如果你能让它更漂亮,那就去做吧.你的朋友会感谢你的.
那说我个人使用B的例子.