可能很难相信构造p[u+1]
出现在代码的最内层循环中的几个地方,我保持这样,对它进行微观优化会使运行数天的操作产生数小时的差异。
通常*((p+u)+1)
效率最高。有时*(p+(u+1))
效率最高。很少*((p+1)+u)
是最好的。 (但是,当后者更好时,优化程序通常可以将*((p+1)+u)
转换为*((p+u)+1)
,并且无法将*(p+(u+1))
转换为其他任何一个。
p
是指针,u
是无符号的。在实际代码中,它们中的至少一个(更可能两者)在表达式被评估的点处已经在寄存器中。这些事实对我的问题至关重要。
在32位(在我的项目放弃对它的支持之前),所有三个都具有完全相同的语义,任何一半不错的编译器只选择三者中最好的,程序员永远不需要关心。
在这些64位用法中,程序员知道这三者具有相同的语义,但编译器不知道。就编译器所知,何时将u
从32位扩展到64位的决定会影响结果。
告诉编译器这三种语义是否相同且编译器应该选择最快的语法的最简洁方法是什么?
在一个Linux 64位编译器中,我几乎使用p[u+1L]
,这使得编译器能够在通常最好的*((p+u)+1)
和有时更好的*(p+( (long)(u) + 1) )
之间智能地进行选择。在极少数情况下,*(p+(u+1))
仍然优于其中的第二个,但有一点丢失了。
显然,这在64位Windows中没有用。既然我们已经删除了32位支持,那么p[u+1LL]
可能足够便携且足够好。但我能做得更好吗?
请注意,std::size_t
使用unsigned
代替u
可以消除整个问题,但会在附近产生更大的性能问题。将u
投射到std::size_t
就好了,也许是我能做的最好的事情。但对于一个不完美的解决方案来说,这是非常冗长的。
简单编码(p+1)[u]
使得选择更有可能优于p[u+1]
。如果代码的模板化程度较低且更稳定,我可以将它们全部设置为(p+1)[u]
,然后将配置文件切换回p[u+1]
。但模板往往会破坏这种方法(单个源代码行出现在配置文件中的很多位置,加上严重的时间,但不是单独的严重时间)。
对此应该有效的编译器是GCC,ICC和MSVC。
答案 0 :(得分:2)
答案不可避免地是编译器和目标特定的,但即使1ULL
比任何目标体系结构上的指针都宽,一个好的编译器也应该优化它。 Which 2's complement integer operations can be used without zeroing high bits in the inputs, if only the low part of the result is wanted?解释了为什么截断到指针宽度的更宽计算将产生与首先使用指针宽度进行计算相同的结果。这就是为什么当1ULL
导致+
操作数升级到64位类型时,编译器甚至可以在32位机器(或带有x32 ABI的x86-64)上对其进行优化。 (或者对于long long
为128b的某些架构,在某些64位ABI上。)
1ULL
looks optimal for 64bit, and for 32bit with clang。你无论如何都不关心32位,但是gcc在return p[u + 1ULL];
中浪费了一条指令。所有其他情况都使用scaled-index + 4 + p寻址模式编译为单个加载。因此,除了一个编译器的优化失败之外,1ULL
也适用于32位。 (我认为它不太可能是一个铿锵的错误,并且优化是非法的。)
int v1ULL(std::uint32_t u) { return p[u + 1ULL]; }
// ... load u from the stack
// add eax, 1
// mov eax, DWORD PTR p[0+eax*4]
而不是
mov eax, DWORD PTR p[4+eax*4]
有趣的是,gcc 5.3 doesn't make this mistake when targeting the x32 ABI(具有32位指针的长模式和类似于SySV AMD64的寄存器调用ABI)。它使用32位地址大小的前缀来避免使用edi
的上部32b。
令人讨厌的是,它仍然使用地址大小的前缀,因为它可以通过使用64位有效地址来保存一个字节的机器代码(当没有机会溢出/进入upper32时产生低于低的地址4GiB)。通过引用传递指针就是一个很好的例子:
int x2 (char *&c) { return *c; }
// mov eax, DWORD PTR [edi] ; upper32 of rax is zero
// movsx eax, BYTE PTR [eax] ; could be byte [rax], saving one byte of machine code
呃,其实我忘记了。 32位地址可能符号扩展到64b,而不是零扩展。如果是这种情况,它也可以使用movsx
作为第一条指令,但这会花费一个字节,因为movsx
的操作码比mov
长。< / p>
无论如何,x32对于需要更多寄存器和更好的ABI的指针重码仍然是一个有趣的选择,而没有8B指针的缓存未命中。
64位asm必须将保存参数的寄存器的upper32(使用mov edi,edi
)归零,但在内联时会消失。查看用于微小功能的Godbolt输出是测试它的有效方法。
如果我们想要确保编译器没有在脚中射击并且在它应该知道它已经为零时将其置零,我们可以使用参考传递的arg来创建测试函数
int v1ULL(const std::uint32_t &u) { return p[u + 1ULL]; }
// mov eax, DWORD PTR [rdi]
// mov eax, DWORD PTR p[4+rax*4]