问题描述
我一直在对工作中的某些代码进行性能优化,并偶然发现了一些奇怪的行为,我将其归结为以下C ++代码的简单片段:
#include <stdint.h>
void Foo(uint8_t*& out)
{
out[0] = 1;
out[1] = 2;
out[2] = 3;
out[3] = 4;
}
然后我用clang(在Windows上)用以下命令编译它:clang -S -O3 -masm=intel test.cpp
。这将导致以下汇编:
mov rax,qword ptr [rcx]
mov byte ptr [rax],1
mov rax,qword ptr [rcx]
mov byte ptr [rax + 1],2
mov rax,qword ptr [rcx]
mov byte ptr [rax + 2],3
mov rax,qword ptr [rcx]
mov byte ptr [rax + 3],4
ret
为什么clang生成的代码会反复将out
参数解引用到rax
寄存器中?这似乎是一个很明显的优化,故意没有进行,所以问题是为什么?
有趣的是,我尝试将uint8_t
更改为uint16_t
,结果生成了更好的机器代码:
mov rax,qword ptr [rcx]
movabs rcx,1125912791875585
mov qword ptr [rax],rcx
ret
解决方法
仅由于严格的别名,编译器就无法进行这种优化,因为uint8_t
始终*被定义为unsigned char
。因此,它可以指向任何内存位置,这意味着它也可以指向自身,并且因为您将其作为引用传递,所以写操作可能会对函数内部产生副作用。
此处含糊不清,但正确,用法取决于非缓存读取:
#include <cassert>
#include <stdint.h>
void Foo(uint8_t*& out)
{
uint8_t local;
// CANNOT be used as a cached value further down in the code.
uint8_t* tmp = out;
// Recover the stored pointer.
uint8_t **orig =reinterpret_cast<uint8_t**>(out);
// CHANGES `out` itself;
*orig=&local;
**orig=5;
assert(local==5);
// IS NOT EQUAL even though we did not touch `out` at all;
assert(tmp!=out);
assert(out==&local);
assert(*out==5);
}
int main(){
// True type of the stored ptr is uint8_t**
uint8_t* ptr = reinterpret_cast<uint8_t*>(&ptr);
Foo(ptr);
}
这也解释了为什么uint16_t
生成“优化”代码的原因,因为uin16_t
永远不会*是(unsigned) char
,因此编译器可以自由地假定它不会别名其他指针类型,例如自身
*也许某些无关紧要的平台具有不同的字节大小。那不是重点。