微优化指针+无符号+1

Micro optimize pointer + unsigned + 1

本文关键字:无符号 指针 优化      更新时间:2023-10-16

尽管很难相信构造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]足够便携,也足够好。但我能做得更好吗?

请注意,对于u使用std::size_t而不是unsigned将消除整个问题,但会在附近产生更大的性能问题。将u转换为std::size_t就足够了,也许这是我能做的最好的。但对于一个不完美的解决方案来说,这相当冗长。

简单地对CCD_ 21进行编码使得选择比CCD_。如果代码的模板化程度更低、更稳定,我可以将它们全部设置为(p+1)[u],然后配置文件,然后将一些代码切换回p[u+1]。但模板化往往会破坏这种方法(一条源代码行出现在概要文件中的许多位置,加起来是严重时间,但不是单独的严重时间)。

GCC、ICC和MSVC应该是高效的编译器。

答案必然是编译器和目标特定的,但即使1ULL比任何目标体系结构上的指针都宽,一个好的编译器也应该对其进行优化。哪2';如果只需要结果的低部分,则可以在不将输入中的高位归零的情况下使用s补码整数运算?解释了为什么截断为指针宽度的更宽计算将给出与首先使用指针宽度进行计算相同的结果。这就是为什么当1ULL导致+操作数提升为64位类型时,编译器甚至可以在32位机器(或具有x32 ABI的x86-64)上对其进行优化。(或者对于long long是128b的某个体系结构,在某个64位ABI上)。


1ULL对于64位和具有clang的32位看起来是最佳的。无论如何,您并不关心32位,但是gcc浪费了return p[u + 1ULL];中的一条指令。所有其他情况都被编译为具有缩放索引+4+p寻址模式的单个负载。因此,除了一个编译器的优化失败之外,1ULL对于32位来说也很好。(我认为这不太可能是一个叮当作响的bug,优化是非法的)。

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在针对x32 ABI(具有32位指针的长模式和类似于SySV AMD64的寄存器调用ABI)时没有犯这个错误。它使用32位地址大小前缀来避免使用edi的上部32b。

令人烦恼的是,当它可以通过使用64位有效地址来保存一个字节的机器代码时,它仍然使用地址大小前缀(当没有机会溢出/进位到上层32生成低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长。

无论如何,对于需要更多寄存器和更好ABI的指针密集型代码来说,x32仍然是一个有趣的选择,而不会出现8B指针的缓存未命中。


64位asm必须将保存参数的寄存器的上部32清零(使用mov edi,edi),但在内联时会消失。查看微小函数的godbolt输出是测试这一点的有效方法。

如果我们想双重确保编译器不会自食其果,并在它应该知道它已经为零的时候将鞋面32归零,我们可以使用通过引用传递的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]