问题描述
基本上,我希望尽快在 x86_64 程序集中实现以下内容。 (其中 foo
和 bar
可能类似于 glibc 的手写 asm strcpy 或 strcmp,我们希望从宽向量开始,但没有分页加载的安全和/或性能缺点当不需要时。或 AVX-512 掩码存储:故障抑制可确保正确性,但如果必须实际抑制目标中的故障,则速度很慢。)
#define TYPE __m256i
int has_page_cross(void * ptr1,void * ptr2) {
uint64_t ptr1_u64 = (uint64_t)ptr1;
uint64_t ptr2_u64 = (uint64_t)ptr2;
ptr1_u64 &= 4095;
ptr2_u64 &= 4095;
if((ptr1_u64 + sizeof(TYPE)) > 4096
|| (ptr2_u64 + sizeof(TYPE)) > 4096) {
// There will be a page cross
return foo_handling_page_cross(ptr1,ptr2);
}
return bar_with_no_page_cross(ptr1,ptr2);
}
对于一个指针,有很多非常有效的方法可以做到这一点,其中许多在 Is it safe to read past the end of a buffer within the same page on x86 and x64? 中进行了讨论,但对于两个指针,似乎没有一种不牺牲准确性的特别有效的方法。
> 方法从这里开始假设 ptr1
从 rdi
开始,ptr2
从 rsi
开始。负载大小将由常数 LSIZE
表示。
快速处理误报
// cycles,bytes
movl %edi,%eax // 0,2 # assuming mov-elimination
orl %esi,5 # which Ice Lake disabled
andl $4095,%eax // 1,10
cmpl $(4096 - LSIZE),%eax // 2,15
ja L(page_cross)
/* less bytes
movl %edi,2
orl %esi,%eax // 1,5
sall $20,%eax // 2,8
cmpl $(4096 - LSIZE) << 20,%eax // 3,13
ja L(page_cross)
*/
- 延迟:3c
- 吞吐量:~1.08c 测得(两个版本)。
- 字节数:13b
这种方法很好,因为它速度快,延迟为 3c(假设消除了 movl %edi,%eax
),具有高吞吐量,并且对于前端来说非常紧凑。
明显的缺点是它会有误报,即rdi = 4000
、rsi = 95
。我认为它的性能应该作为一个完整正确解决方案的目标。
较慢但正确
这是我能想到的最好的
// cycles,bytes
leal (LSIZE - 1)(%rdi),%eax // 0,4
leal (LSIZE - 1)(%rsi),%edx // 0,8
xorl %edi,11
xorl %esi,%edx // 1,14
orl %edx,%eax // 2,17
testl $4096,%eax // 3,22
jnz L(page_cross)
- 延迟:4c
- 吞吐量:测量到约 1.75c(Icelake 的注意事项比旧 cpu 的 tput
lea
高) - 字节数:21b
它有 4c 的延迟,还不错,但它的吞吐量更差,而且代码占用空间更大。
问题
- 这两种方法中的任何一种都可以在延迟、吞吐量或字节方面得到改进吗?一般来说,我对延迟 > 吞吐量 > 字节最感兴趣?
我的总体目标是尽可能快地获得正确的案例。
编辑: 修正了正确版本中的错误。
cpu: 就我个人而言,我正在使用 AVX512 调整 cpu,因此 Skylake Server、Icelake 和 Tigerlake 但这个问题是针对整个 Sandybridge 系列的。
解决方法
如果在 a % 4096 == 4096 - size
处有一个误报,您可以使用:
~a & (4096 - size) == 0
翻译成汇编:
not edi
not esi
test edi,(4096 - size)
jz crosses-page-boundary
test esi,(4096 - size)
jz crosses-page-boundary
(2 cycle latency)
说明:对于 size=32,我们希望地址的最后 12 位大于 4096 - 32 = 4064 = 0b1111'1110'0000
。我们知道,只有当一个数字的前导 1 位和低 5 位中的任何内容都相同时,它才能等于或大于该数字。我们无法轻松测试所有指定的位是否都为 1,因此我们将这些位取反并使用 test edi,(4096 - size)
测试它们是否都为 0。
请注意,您可以通过使用 a % 4096 == 0
而不是 neg
(not
,因此如果所有低 5 位值都为零,则在反转后它们变为 1 并加一将其带入测试区域,这使其成为 -a = ~a + 1
的误报,但隐藏了 {{1} 的误报}).