微优化指针+无符号+1
Micro optimize pointer + unsigned + 1
尽管很难相信构造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]
- 如何理解将半精度指针转换为无符号长指针和相关的内存对齐
- 在函数中返回无符号字符数组,但不返回指针
- 如何返回实际值(在我的例子中是无符号字符数组)而不是来自 C++ 函数的指针?
- C++:使用没有位移位的指针将无符号字符转换为无符号 int
- 将(N 个字节)无符号字符指针转换为浮点数和双 C++
- 在函数内初始化无符号字符指针将返回空指针
- 使用指针选择长无符号变量中的数字
- 是否可以将无符号字符数组reinterpret_cast到仅包含C++中无符号字符成员的结构指针
- 指针是否被视为C++中的无符号值?
- 尝试将返回无符号值的函数分配给指向无符号的指针时,继续获取 SIGSEGV
- 指向较大的无符号字符数组的每个第 n 个元素的指针数组
- 取消引用无符号字符指针并将其值存储到字符串中
- 将无符号 int 的指针传递到长 int 的指针
- 将无符号int地址视为指针,并将其解引用为字节数组
- 如何将自定义类指针转换为无符号int
- 向无符号字符指针添加短int
- 尝试创建一个 3D 矢量,该矢量将保留指向指向无符号字符的指针的指针
- 使用指针将字符* 转换为无符号字符*
- 解析指向位操作数 C++ 的无符号 char 指针
- 类型转换无效指针指向无符号短整型