x86_64检查2次加载/存储的功率是否会对2个指针进行分页交叉



基本上,我希望尽可能快地在x86_64程序集中实现以下功能。(其中foobar可能类似于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上的同一页中读取缓冲区的末尾是否安全?但似乎没有一种特别有效的不牺牲准确性的双指针方法。

方法从这里开始,假定ptr1rdi中开始并且ptr2rsi中开始。负载大小将由常数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 = 4000rsi = 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的延迟,虽然不算太差,但它的吞吐量更差,而且它的代码占用空间要大得多。

问题

  1. 这两种方法中的任何一种在延迟、吞吐量或字节方面都可以改进吗?一般来说,我最感兴趣的是延迟>吞吐量>字节

我的总体目标是像假阳性一样快速得到正确的病例。

编辑:修正了正确版本中的错误。

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(我认为这更糟,因为它更有可能发生?(。

最新更新