基本上,我希望尽可能快地在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);
}
对于一个指针,有很多非常有效的方法可以做到这一点,其中许多方法在x86和x64上的同一页中读取缓冲区的末尾是否安全?但似乎没有一种特别有效的不牺牲准确性的双指针方法。
方法从这里开始,假定ptr1
在rdi
中开始并且ptr2
在rsi
中开始。负载大小将由常数LSIZE
表示。
假阳性快速
// cycles, bytes
movl %edi, %eax // 0 , 2 # assuming mov-elimination
orl %esi, %eax // 0 , 5 # which Ice Lake disabled
andl $4095, %eax // 1 , 10
cmpl $(4096 - LSIZE), %eax // 2 , 15
ja L(page_cross)
/* less bytes
movl %edi, %eax // 0 , 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, %eax // 1 , 11
xorl %esi, %edx // 1 , 14
orl %edx, %eax // 2 , 17
testl $4096, %eax // 3 , 22
jnz L(page_cross)
- 延迟:4c
- 吞吐量:实测约1.75c(注意Icelake的
lea
输出比旧CPU高( - 字节: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位中的任何值时,它才能等于或大于这个数字。我们不能很容易地测试所有指定的位是否都是一,所以我们用test edi, (4096 - size)
反转这些位并测试它们是否都是零。
注意,通过使用neg
而不是not
(-a = ~a + 1
,所以如果所有低5位值都为零,那么在反转后它们变为1,加1将其带入测试区域,这使其成为a % 4096 == 0
的假阳性,但隐藏了a % 4096 == 4096 - size
的假阳性(,可以将假阳性转移到a % 4096 == 0
(我认为这更糟,因为它更有可能发生?(。