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);
}

对于一个指针,有很多非常有效的方法可以做到这一点,其中许多在 Is it safe to read past the end of a buffer within the same page on x86 and x64? 中进行了讨论,但对于两个指针,似乎没有一种不牺牲准确性的特别有效的方法

> 方法

从这里开始假设 ptr1rdi 开始,ptr2rsi 开始。负载大小将由常数 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 = 4000rsi = 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 的延迟,还不错,但它的吞吐量更差,而且代码占用空间更大。

问题

  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 位中的任何内容都相同时,它才能等于或大于该数字。我们无法轻松测试所有指定的位是否都为 1,因此我们将这些位取反并使用 test edi,(4096 - size) 测试它们是否都为 0。


请注意,您可以通过使用 a % 4096 == 0 而不是 neg (not ,因此如果所有低 5 位值都为零,则在反转后它们变为 1 并加一将其带入测试区域,这使其成为 -a = ~a + 1 的误报,但隐藏了 {{1} 的误报}).