首页 > 解决方案 > x86_64 检查 2 个加载/存储的幂是否会为 2 个指针分页

问题描述

基本上我希望尽快在 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(假设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. 这些方法中的任何一种都可以在延迟、吞吐量或字节方面得到改善吗?通常我对延迟>吞吐量>字节最感兴趣?

我的总体目标是与误报一样快地获得正确的案例。

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

CPU:就我个人而言,我正在调整带有 AVX512 的 CPU,因此 Skylake Server、Icelake 和 Tigerlake,但这个问题针对的是整个 Sandybridge 系列。

标签: assemblyx86-64micro-optimization

解决方案


在 处有一个误报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,因此我们将这些位反转并测试它们是否都为 0 test edi, (4096 - size)


请注意,您可以通过使用而不是(将误报转移到a % 4096 == 0(我认为它更糟,因为它更有可能发生?),因此如果所有低 5 位值都为零,则在反转后它们变为 1 并加一携带它进入测试区域,使其成为 的误报,但隐藏了 ) 的误报。negnot-a = ~a + 1a % 4096 == 0a % 4096 == 4096 - size


推荐阅读