有效地将无符号值除以2的幂,舍入

时间:2016-11-04 20:58:08

标签: c optimization x86 bit-manipulation division

我想有效地实现无符号 a 整数除法以2的任意幂向上舍入。所以我想要的是数学ceiling(p/q) 0 。在C中,草莓实现没有利用q的受限域,可能类似于以下函数 1

/** q must be a power of 2, although this version works for any q */
uint64_t divide(uint64_t p, uint64_t q) {
  uint64_t res = p / q;
  return p % q == 0 ? res : res + 1;
} 

...当然,我实际上并不想在机器级别使用除法或mod,因为即使在现代硬件上也需要许多周期。我正在寻找使用轮班和/或其他一些廉价操作的力量减少 - 利用q是2的幂的事实。

您可以假设我们有一个高效的lg(unsigned int x)函数,如果x是2的幂,则会返回x的base-2日志。

如果q为零,则未定义的行为正常。

请注意,简单的解决方案:(p+q-1) >> lg(q)一般不起作用 - 例如,尝试使用p == 2^64-100 and q == 256 2

平台详细信息

我对C中的解决方案感兴趣,这些解决方案很可能在各种平台上表现良好,但为了具体起见,奖励赏金,因为任何对性能的明确讨论都需要包含目标架构,我将具体说明我将如何测试它们:

  • Skylake CPU
  • gcc 5.4.0带有编译标记-O3 -march=haswell

使用gcc builtins(比如bitscan / leading zero builtins)很好,特别是我已经实现了我所说的lg()函数,如下所示:

inline uint64_t lg(uint64_t x) {
  return 63U - (uint64_t)__builtin_clzl(x);
}

inline uint32_t lg32(uint32_t x) {
  return 31U - (uint32_t)__builtin_clz(x);
}

我确认这些编译为一条bsr指令,至少使用-march=haswell,尽管明显涉及减法。您当然可以自由地忽略这些并在解决方案中使用您想要的任何其他内置组件。

基准

我为现有答案写了benchmark,并会在进行更改时分享和更新结果。

为一个小的,可能内联的操作编写一个很好的基准是非常困难的。当代码内联到调用站点时,函数的许多工作可能会消失,尤其是当它在循环 3 中时。

可以通过确保你的代码没有内联来简单地避免整个内联问题:在另一个编译单元中声明它。我尝试使用bench二进制文件,但实际上结果毫无意义。几乎所有的实现都是每次调用4或5个周期,但即使是虚拟方法,除了return 0之外什么都不做,需要相同的时间。所以你大多只是测量call + ret开销。此外,你几乎永远不会真正使用这样的功能 - 除非你搞砸了,否则它们可用于内联并且会改变所有内容

因此,我最关注的两个基准是在循环中反复调用被测方法,允许内联,跨功能优化,循环提升甚至矢量化。

有两种总体基准类型:延迟和吞吐量。关键区别在于,在延迟基准测试中,每次调用divide都取决于之前的调用,因此通常调用不能轻易重叠 4

uint32_t bench_divide_latency(uint32_t p, uint32_t q) {
    uint32_t total = p;                         
    for (unsigned i=0; i < ITERS; i++) {                
      total += divide_algo(total, q);                       
      q = rotl1(q);                         
    }
    return total;
  }

请注意,正在运行total取决于每个除法调用的输出,并且它也是divide调用的输入。

另一方面,吞吐量变量不会将一个除法的输出提供给后一个除法。这允许来自一个调用的工作与后续调用(由编译器,尤其是CPU)重叠,甚至允许向量化:

uint32_t bench_divide_throughput(uint32_t p, uint32_t q) { 
    uint32_t total = p;                         
    for (unsigned i=0; i < ITERS; i++) {                
      total += fname(i, q);                     
      q = rotl1(q);                     
    }                                   
    return total;                           
  }

请注意,我们在这里输入循环计数器作为被除数 - 这是变量,但它并不依赖于之前的除法调用。

此外,每个基准对于除数q都有三种行为:

  1. 编译时常数除数。例如,拨打divide(p, 8)。这在实践中很常见,当在编译时知道除数时,代码可以简单得多。
  2. 不变的除数。这里除数在编译时不知道,但对于整个基准测试循环是恒定的。这允许编译时常量的优化子集。
  3. 可变除数。除数在循环的每次迭代中都会发生变化。上面的基准函数显示了这个变体,使用&#34;向左旋转1&#34;指示改变除数。
  4. 将所有内容组合在一起,共有6个不同的基准。

    结果

    总体

    为了选择整体最佳算法,我查看了所提议算法的12个子集中的每一个:(latency, throughput) x (constant a, invariant q, variable q) x (32-bit, 64-bit)并为每个子测试分配了2分,1分或0分,如下所示:

    • 最佳算法(在5%容差范围内)得分为2。
    • &#34;足够接近&#34;算法(比最佳速度慢不超过50%)得分为1。
    • 其余算法得分为零。

    因此,最高总分为24,但没有算法实现。以下是总体结果:

    ╔═══════════════════════╦═══════╗
    ║       Algorithm       ║ Score ║
    ╠═══════════════════════╬═══════╣
    ║ divide_user23_variant ║    20 ║
    ║ divide_chux           ║    20 ║
    ║ divide_user23         ║    15 ║
    ║ divide_peter          ║    14 ║
    ║ divide_chrisdodd      ║    12 ║
    ║ stoke32               ║    11 ║
    ║ divide_chris          ║     0 ║
    ║ divide_weather        ║     0 ║
    ╚═══════════════════════╩═══════╝
    

    所以出于这个特定测试代码的目的,使用这个特定的编译器并在这个平台上,user2357112&#34; variant&#34; (... + (p & mask) != 0)表现最佳,与chux的建议相关(实际上是相同的代码)。

    以下是与上述相关的所有子分数:

    ╔══════════════════════════╦═══════╦════╦════╦════╦════╦════╦════╗
    ║                          ║ Total ║ LC ║ LI ║ LV ║ TC ║ TI ║ TV ║
    ╠══════════════════════════╬═══════╬════╬════╬════╬════╬════╬════╣
    ║ divide_peter             ║     6 ║  1 ║  1 ║  1 ║  1 ║  1 ║  1 ║
    ║ stoke32                  ║     6 ║  1 ║  1 ║  2 ║  0 ║  0 ║  2 ║
    ║ divide_chux              ║    10 ║  2 ║  2 ║  2 ║  1 ║  2 ║  1 ║
    ║ divide_user23            ║     8 ║  1 ║  1 ║  2 ║  2 ║  1 ║  1 ║
    ║ divide_user23_variant    ║    10 ║  2 ║  2 ║  2 ║  1 ║  2 ║  1 ║
    ║ divide_chrisdodd         ║     6 ║  1 ║  1 ║  2 ║  0 ║  0 ║  2 ║
    ║ divide_chris             ║     0 ║  0 ║  0 ║  0 ║  0 ║  0 ║  0 ║
    ║ divide_weather           ║     0 ║  0 ║  0 ║  0 ║  0 ║  0 ║  0 ║
    ║                          ║       ║    ║    ║    ║    ║    ║    ║
    ║ 64-bit Algorithm         ║       ║    ║    ║    ║    ║    ║    ║
    ║ divide_peter_64          ║     8 ║  1 ║  1 ║  1 ║  2 ║  2 ║  1 ║
    ║ div_stoke_64             ║     5 ║  1 ║  1 ║  2 ║  0 ║  0 ║  1 ║
    ║ divide_chux_64           ║    10 ║  2 ║  2 ║  2 ║  1 ║  2 ║  1 ║
    ║ divide_user23_64         ║     7 ║  1 ║  1 ║  2 ║  1 ║  1 ║  1 ║
    ║ divide_user23_variant_64 ║    10 ║  2 ║  2 ║  2 ║  1 ║  2 ║  1 ║
    ║ divide_chrisdodd_64      ║     6 ║  1 ║  1 ║  2 ║  0 ║  0 ║  2 ║
    ║ divide_chris_64          ║     0 ║  0 ║  0 ║  0 ║  0 ║  0 ║  0 ║
    ║ divide_weather_64        ║     0 ║  0 ║  0 ║  0 ║  0 ║  0 ║  0 ║
    ╚══════════════════════════╩═══════╩════╩════╩════╩════╩════╩════╝
    

    这里,每个测试都命名为XY,其中X在{Latency,Throughput}中,Y在{Constant Q,Invariant Q,Variable Q}中。因此,例如,LC是&#34;延迟测试,常数q&#34;。

    分析

    在最高级别,解决方案可以大致分为两类:快速(前6名终结者)和慢速(前2名)。差异较大:所有 fast 算法在至少两个子测试中是最快的,一般来说,当他们没有先完成时,他们会落入&# 34;足够接近&#34;类别(在stokechrisdodd的情况下,它们只是例外的失败矢量化。然而,算法在每次测试中得分为0(甚至不接近)。所以你可以通过进一步的考虑来消除慢速算法。

    自动矢量

    fast 算法中,一个很大的区分因素是auto-vectorize的能力。

    这些算法都没有能够在延迟测试中自动进行矢量化,这是有道理的,因为latency tests旨在将其结果直接提供给下一次迭代。所以你真的只能以连续的方式计算结果。

    然而,对于吞吐量测试,许多算法能够自动矢量化常数Q 不变Q 情况。在这两个测试中,除数q是循环不变的(在前一种情况下,它是一个编译时常量)。被除数是循环计数器,因此它是可变的,但是可预测的(特别是可以通过在前一个输入向量上加8来简单地计算红利矢量:[0, 1, 2, ..., 7] + [8, 8, ..., 8] == [8, 9, 10, ..., 15])。

    在这种情况下,gcc能够对peterstokechuxuser23user23_variant进行矢量化。由于某种原因,它无法对chrisdodd进行矢量化,可能是因为它包含了一个分支(但条件不严格阻止矢量化,因为许多其他解决方案都有条件元素但仍然是矢量化的)。影响是巨大的:矢量化的算法显示,与不具备其他速度的变体相比,吞吐量提高了8倍。

    但是,矢量化并不是免费的!以下是&#34;常数&#34;的函数大小。每个函数的变体,Vec?列显示函数是否已向量化:

    Size Vec? Name
     045    N bench_c_div_stoke_64
     049    N bench_c_divide_chrisdodd_64
     059    N bench_c_stoke32_64
     212    Y bench_c_divide_chux_64
     227    Y bench_c_divide_peter_64
     220    Y bench_c_divide_user23_64
     212    Y bench_c_divide_user23_variant_64
    

    趋势很明显 - 矢量化函数大约是非矢量化函数的4倍。这是因为核心循环本身更大(向量指令往往更大,并且有更多),并且因为循环设置,特别是后循环代码要大得多:例如,向量化版本需要减少到求和向量中的所有部分和。循环计数是固定的,是8的倍数,因此不生成尾部代码 - 但如果是可变的,则生成的代码会更大。

    此外,尽管运行时有很大的改进,gcc的矢量化实际上很差。这是彼得常规矢量化版本的摘录:

      on entry: ymm4 == all zeros
      on entry: ymm5 == 0x00000001 0x00000001 0x00000001 ...
      4007a4:       c5 ed 76 c4             vpcmpeqd ymm0,ymm2,ymm4
      4007ad:       c5 fd df c5             vpandn   ymm0,ymm0,ymm5
      4007b1:       c5 dd fa c0             vpsubd   ymm0,ymm4,ymm0
      4007b5:       c5 f5 db c0             vpand    ymm0,ymm1,ymm0
    

    此块在源自DWORD的8个ymm2元素上独立工作。如果我们将x作为DWORD的单个ymm2元素,y这些foud指令的传入值对应于:{/ p>

    ymm1

    因此,前三个指令可以简单地用第一个指令替换,因为状态在任何一种情况下都是相同的。因此,两个周期被添加到该依赖链(没有循环携带)和两个额外的uop。显然, x == 0 x != 0 x = x ? 0 : -1; // -1 0 x = x & 1; // 1 0 x = 0 - x; // -1 0 x = y1 & x; // y1 0 的优化阶段在某种程度上与这里的矢量化代码交互不良,因为在标量代码中很少遗漏这些简单的优化。同样地检查other vectorized versions同样显示了很多性能下降。

    分支与无分支

    几乎所有解决方案都编译为无分支代码,即使C代码具有条件或显式分支。条件部分足够小,编译器通常决定使用条件移动或某些变体。一个例外是gcc,它在所有吞吐量测试中使用分支(检查是否chrisdodd)编译,但没有一个延迟。以下是常量p == 0 吞吐量测试中的典型示例:

    q

    0000000000400e60 <bench_c_divide_chrisdodd_32>: 400e60: 89 f8 mov eax,edi 400e62: ba 01 00 00 00 mov edx,0x1 400e67: eb 0a jmp 400e73 <bench_c_divide_chrisdodd_32+0x13> 400e69: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0] 400e70: 83 c2 01 add edx,0x1 400e73: 83 fa 01 cmp edx,0x1 400e76: 74 f8 je 400e70 <bench_c_divide_chrisdodd_32+0x10> 400e78: 8d 4a fe lea ecx,[rdx-0x2] 400e7b: c1 e9 03 shr ecx,0x3 400e7e: 8d 44 08 01 lea eax,[rax+rcx*1+0x1] 400e82: 81 fa 00 ca 9a 3b cmp edx,0x3b9aca00 400e88: 75 e6 jne 400e70 <bench_c_divide_chrisdodd_32+0x10> 400e8a: c3 ret 400e8b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] 处的分支会跳过400e76的情况。实际上,编译器可能只是将第一次迭代剥离(显式计算其结果),然后完全避免跳转,因为之后它可以证明p == 0。在这些测试中,分支是完全可预测的,这可以为使用分支实际编译的代码提供优势(因为比较和分支代码基本上不在线并且接近自由),并且是原因的一个重要部分{ {1}}赢得吞吐量,变量q 情况。

    详细测试结果

    在这里,您可以找到一些详细的测试结果和测试本身的一些细节。

    延迟

    以下结果在1e9次迭代中测试每个算法。简单地通过将时间/呼叫乘以时钟频率来计算周期。您通常可以假设p != 0之类的内容与chrisdodd相同,但像4.01这样的较大偏差似乎是真实且可重复的。

    4.00的结果使用5.11但仅显示以供参考,因为此函数对大型divide_plusq_32失败。 (p + q - 1) >> lg(q)的结果是一个非常简单的函数:p + q,并且可以让您估计基准开销 5 (添加本身最多需要一个周期)。

    dummy

    吞吐量

    以下是吞吐量测试的结果。请注意,这里的许多算法都是自动矢量化的,因此对于那些来说,性能相对非常好:在很多情况下,这是一个循环的一小部分。一个结果是,与大多数延迟结果不同,64位函数相当慢,因为矢量化对于较小的元素大小更有效(尽管差距比我预期的要大)。

    return p + q

    a 至少通过指定 unsigned ,我们避免了与C和C ++中有符号整数的右移行为相关的整个蠕虫行。

    0 当然,这种表示法实际上并不适用于C ============================== Bench: Compile-time constant Q ============================== Function ns/call cycles divide_peter_32 2.19 5.67 divide_peter_64 2.18 5.64 stoke32_32 1.93 5.00 stoke32_64 1.97 5.09 stoke_mul_32 2.75 7.13 stoke_mul_64 2.34 6.06 div_stoke_32 1.94 5.03 div_stoke_64 1.94 5.03 divide_chux_32 1.55 4.01 divide_chux_64 1.55 4.01 divide_user23_32 1.97 5.11 divide_user23_64 1.93 5.00 divide_user23_variant_32 1.55 4.01 divide_user23_variant_64 1.55 4.01 divide_chrisdodd_32 1.95 5.04 divide_chrisdodd_64 1.93 5.00 divide_chris_32 4.63 11.99 divide_chris_64 4.52 11.72 divide_weather_32 2.72 7.04 divide_weather_64 2.78 7.20 divide_plusq_32 1.16 3.00 divide_plusq_64 1.16 3.00 divide_dummy_32 1.16 3.00 divide_dummy_64 1.16 3.00 ============================== Bench: Invariant Q ============================== Function ns/call cycles divide_peter_32 2.19 5.67 divide_peter_64 2.18 5.65 stoke32_32 1.93 5.00 stoke32_64 1.93 5.00 stoke_mul_32 2.73 7.08 stoke_mul_64 2.34 6.06 div_stoke_32 1.93 5.00 div_stoke_64 1.93 5.00 divide_chux_32 1.55 4.02 divide_chux_64 1.55 4.02 divide_user23_32 1.95 5.05 divide_user23_64 2.00 5.17 divide_user23_variant_32 1.55 4.02 divide_user23_variant_64 1.55 4.02 divide_chrisdodd_32 1.95 5.04 divide_chrisdodd_64 1.93 4.99 divide_chris_32 4.60 11.91 divide_chris_64 4.58 11.85 divide_weather_32 12.54 32.49 divide_weather_64 17.51 45.35 divide_plusq_32 1.16 3.00 divide_plusq_64 1.16 3.00 divide_dummy_32 0.39 1.00 divide_dummy_64 0.39 1.00 ============================== Bench: Variable Q ============================== Function ns/call cycles divide_peter_32 2.31 5.98 divide_peter_64 2.26 5.86 stoke32_32 2.06 5.33 stoke32_64 1.99 5.16 stoke_mul_32 2.73 7.06 stoke_mul_64 2.32 6.00 div_stoke_32 2.00 5.19 div_stoke_64 2.00 5.19 divide_chux_32 2.04 5.28 divide_chux_64 2.05 5.30 divide_user23_32 2.05 5.30 divide_user23_64 2.06 5.33 divide_user23_variant_32 2.04 5.29 divide_user23_variant_64 2.05 5.30 divide_chrisdodd_32 2.04 5.30 divide_chrisdodd_64 2.05 5.31 divide_chris_32 4.65 12.04 divide_chris_64 4.64 12.01 divide_weather_32 12.46 32.28 divide_weather_64 19.46 50.40 divide_plusq_32 1.93 5.00 divide_plusq_64 1.99 5.16 divide_dummy_32 0.40 1.05 divide_dummy_64 0.40 1.04 截断结果,因此============================== Bench: Compile-time constant Q ============================== Function ns/call cycles stoke32_32 0.39 1.00 divide_chux_32 0.15 0.39 divide_chux_64 0.53 1.37 divide_user23_32 0.14 0.36 divide_user23_64 0.53 1.37 divide_user23_variant_32 0.15 0.39 divide_user23_variant_64 0.53 1.37 divide_chrisdodd_32 1.16 3.00 divide_chrisdodd_64 1.16 3.00 divide_chris_32 4.34 11.23 divide_chris_64 4.34 11.24 divide_weather_32 1.35 3.50 divide_weather_64 1.35 3.50 divide_plusq_32 0.10 0.26 divide_plusq_64 0.39 1.00 divide_dummy_32 0.08 0.20 divide_dummy_64 0.39 1.00 ============================== Bench: Invariant Q ============================== Function ns/call cycles stoke32_32 0.48 1.25 divide_chux_32 0.15 0.39 divide_chux_64 0.48 1.25 divide_user23_32 0.17 0.43 divide_user23_64 0.58 1.50 divide_user23_variant_32 0.15 0.38 divide_user23_variant_64 0.48 1.25 divide_chrisdodd_32 1.16 3.00 divide_chrisdodd_64 1.16 3.00 divide_chris_32 4.35 11.26 divide_chris_64 4.36 11.28 divide_weather_32 5.79 14.99 divide_weather_64 17.00 44.02 divide_plusq_32 0.12 0.31 divide_plusq_64 0.48 1.25 divide_dummy_32 0.09 0.23 divide_dummy_64 0.09 0.23 ============================== Bench: Variable Q ============================== Function ns/call cycles stoke32_32 1.16 3.00 divide_chux_32 1.36 3.51 divide_chux_64 1.35 3.50 divide_user23_32 1.54 4.00 divide_user23_64 1.54 4.00 divide_user23_variant_32 1.36 3.51 divide_user23_variant_64 1.55 4.01 divide_chrisdodd_32 1.16 3.00 divide_chrisdodd_64 1.16 3.00 divide_chris_32 4.02 10.41 divide_chris_64 3.84 9.95 divide_weather_32 5.40 13.98 divide_weather_64 19.04 49.30 divide_plusq_32 1.03 2.66 divide_plusq_64 1.03 2.68 divide_dummy_32 0.63 1.63 divide_dummy_64 0.66 1.71 什么都不做。因此,请考虑伪符号而不是直接C。

    1 我也感兴趣的解决方案,其中所有类型都是/而不是ceiling

    2 一般情况下,由于溢出,uint32_tuint64_t导致问题的任何pq

    3 也就是说,函数应该处于一个循环中,因为只需要调用六个循环的微观函数的性能才真正重要一个相当紧凑的循环。

    4 这有点简化 - 只有被除数p + q >= 2^64依赖于前一次迭代的输出,所以有些与p处理有关的工作可以仍然是重叠的。

    5 但谨慎使用此类估算值 - 开销并不是简单的加法。如果开销显示为4个周期且某个函数q需要5个,那么f中实际工作的成本可能不准确f,因为执行重叠的方式。

9 个答案:

答案 0 :(得分:16)

这个答案是关于asm的理想内容;我们想要说服编译器为我们发射的东西。 (我并没有建议实际使用内联asm,除非在对编译器输出进行基准测试时作为比较点。https://gcc.gnu.org/wiki/DontUseInlineAsm)。

我确实设法从纯{C} ceil_div_andmask得到了非常好的asm输出,见下文。(它比Broadwell / Skylake的CMOV差,但可能不错尽管如此,用户23 / chux版本对于这两种情况看起来都更好。)它主要值得一提的是我让编译器发出我想要的asm的少数情况之一。

看起来Chris Dodd关于return ((p-1) >> lg(q)) + 1的一般概念,d = 0的特殊情况处理是最好的选择之一。即在asm中优化它的最佳实现很难被其他任何东西的最佳实现所击败。 Chux的(p >> lg(q)) + (bool)(p & (q-1))也具有优势(例如p->结果的延迟较低),而当q用于多个分区时,CSE更多Branches are cheap and very efficient if they predict very well。请参阅下文,了解gcc对其所做的延迟/吞吐量分析。

如果相同的e = lg(q)被重复用于多个被除数,或者相同的被除数被重用于多个除数,则不同的实现可以CSE更多的表达式。 他们也可以使用AVX2进行有效的矢量化

This is my work-in-progress on Godbolt,因此d==0上的分支将是最好的,如果它几乎从未采取过。如果d==0并不罕见,则无分支asm的平均表现会更好。理想情况下,我们可以用C语言编写一些内容,让gcc在配置文件引导的优化过程中做出正确的选择,并在任何一种情况下编译成好的asm。

由于最好的无分支asm实现不会增加很多延迟而不是分支实现,所以无分支可能是要走的路,除非分支在99%的时间内以相同的方式运行。这可能适用于p==0上的分支,但可能不太可能在p & (q-1)上进行分支。

很难引导gcc5.4发射任何最佳的东西。 on Godbolt)。

我认为此算法的Skylake的最佳序列如下。 (显示为AMD64 SysV ABI的独立函数,但假设编译器会发出类似内联到循环中的内容而没有附加RET,则讨论吞吐量/延迟。)

从计算d-1到检测d == 0 进行分支,而不是单独的测试&amp;科。很好地减少了uop数量,特别是在SnB系列中,JC可以与SUB进行宏观融合。

ceil_div_pjc_branch:
 xor    eax,eax          ; can take this uop off the fast path by adding a separate xor-and-return block, but in reality we want to inline something like this.
 sub    rdi, 1
 jc    .d_was_zero       ; fuses with the sub on SnB-family
 tzcnt  rax, rsi         ; tzcnt rsi,rsi also avoids any false-dep problems, but this illustrates that the q input can be read-only.
 shrx   rax, rdi, rax
 inc    rax
.d_was_zero:
 ret
  • Fused-domain uops:5(不计算ret),其中一个是xor-zero(无执行单元)
  • 成功进行分支预测的HSW / SKL延迟:
    • (d == 0):没有数据依赖于d或q,打破了dep链。 (控制对d的依赖以检测错误预测并退出分支)。
    • (d!= 0):q-&gt;结果:tzcnt + shrx + inc = 5c
    • (d!= 0): d-&gt;结果:sub + shrx + inc = 3c
  • 吞吐量:可能只是瓶颈上的uop吞吐量

我已经尝试但未能通过减法让gcc从CF分支,但它总是想要进行单独的比较。我知道在减去两个变量之后,gcc可以被哄骗到CF上的分支,但是如果一个是编译时常量,这可能会失败。 (IIRC,这通常编译为使用无符号变量的CF测试:foo -= bar; if(foo>bar) carry_detected = 1;

无分支与ADC / SBB处理d==0案例。零处理仅向关键路径添加一条指令(相对于d == 0没有特殊处理的版本),但也将另一条指令从INC转换为sbb rax, -1以使CF撤消{{1 }}。在Broadwell之前使用CMOV更便宜,但需要额外的指令来设置它。

-= -1
  • 融合域uops:SKL上的5(不计算ret)。 7关于HSW
  • SKL延迟:
    • q-&gt;结果:tzcnt + shrx + sbb = 5c
    • d-&gt;结果:sub + adc + shrx(q从这里开始)+ sbb = 4c
  • 吞吐量:TZCNT在p1上运行。 SBB,ADC和SHRX仅在p06上运行。所以我认为我们每次迭代的p06为3 uop瓶颈,使得这次运行最好每1.5c一次迭代

如果q和d同时准备就绪,请注意此版本可以与TZCNT的3c延迟并行运行SUB / ADC。如果两者都来自相同的缓存未命中缓存行,那么它当然是可能的。在任何情况下,在d->结果依赖链中尽可能晚地引入dep是一个优点。

gcc5.4似乎不太可能从C中获取此信息。有一个固有的附加携带,但gcc使它完全混乱。它不会对ADC或SBB使用立即操作数,并将进位存储在每个操作之间的整数寄存器中。 gcc7,clang3.9和icc17都会从中产生可怕的代码。

ceil_div_pjc_asm_adc:
 tzcnt  rsi, rsi
 sub    rdi, 1
 adc    rdi, 0          ; d? d-1 : d.  Sets CF=CF
 shrx   rax, rdi, rsi
 sbb    rax, -1         ; result++ if d was non-zero
 ret    

CMOV修复整个结果:q->结果的延迟更差,因为它在d->结果dep链中使用得更快。

#include <x86intrin.h>
// compiles to completely horrible code, putting the flags into integer regs between ops.
T ceil_div_adc(T d, T q) {
  T e = lg(q);
  unsigned long long dm1;  // unsigned __int64
  unsigned char CF = _addcarry_u64(0, d, -1, &dm1);
  CF = _addcarry_u64(CF, 0, dm1, &dm1);
  T shifted = dm1 >> e;
  _subborrow_u64(CF, shifted, -1, &dm1);
  return dm1;
}
    # gcc5.4 -O3 -march=haswell
    mov     rax, -1
    tzcnt   rsi, rsi
    add     rdi, rax
    setc    cl
    xor     edx, edx
    add     cl, -1
    adc     rdi, rdx
    setc    dl
    shrx    rdi, rdi, rsi
    add     dl, -1
    sbb     rax, rdi
    ret
  • 融合域uops:SKL上的5(不计算ret)。 6关于HSW
  • SKL延迟:
    • q-&gt;结果:tzcnt + shrx + lea + cmov = 6c(比ADC / SBB差1c)
    • d-&gt;结果:sub + shrx(q从这里开始)+ lea + cmov = 4c
  • 吞吐量:TZCNT在p1上运行。 LEA是第15页。 CMOV和SHRX是p06。 SUB是p0156。理论上,只有瓶颈的融合域uop吞吐量,因此每1.25c一次迭代。由于有很多独立的操作,来自SUB或LEA窃取p1或p06的资源冲突不应该是吞吐量问题,因为每1.25c只有1个,没有端口被只能在该端口上运行的uop所饱和。

CMOV获取SUB的操作数:我希望我能找到一种方法来为以后的指令创建一个操作数,该指令在需要时会产生零,而不依赖于q,e ,或SHRX结果。如果ceil_div_pjc_asm_cmov: tzcnt rsi, rsi sub rdi, 1 shrx rax, rdi, rsi lea rax, [rax+1] ; inc preserving flags cmovc rax, zeroed_register ret d之前或同时准备就绪,这将有所帮助。

这并没有达到这个目标,并且在循环中需要额外的7字节q

mov rdx,-1

具有昂贵CMOV的BDW前BD的低延迟版本,使用SETCC为AND创建掩码。

ceil_div_pjc_asm_cmov:
 tzcnt  rsi, rsi
 mov    rdx, -1
 sub    rdi, 1
 shrx   rax, rdi, rsi
 cmovnc rdx, rax
 sub    rax, rdx       ; res += d ? 1 : -res
 ret    

仍然 4c延迟来自d->结果(和来自q->结果的6),因为SETC / DEC与SHRX / INC并行发生。总uop计数:8。这些insn中的大多数可以在任何端口上运行,因此它应该是每2个时钟1个。

当然,对于pre-HSW,您还需要替换SHRX。

我们可以让gcc5.4发出几乎同样好的东西:(仍然使用单独的TEST而不是基于ceil_div_pjc_asm_setcc: xor edx, edx ; needed every iteration tzcnt rsi, rsi sub rdi, 1 setc dl ; d!=0 ? 0 : 1 dec rdx ; d!=0 ? -1 : 0 // AND-mask shrx rax, rdi, rsi inc rax and rax, rdx ; zero the bogus result if d was initially 0 ret 设置掩码,但是与上面相同的指令)。见{{3}}。

sub rdi, 1

当编译器知道T ceil_div_andmask(T p, T q) { T mask = -(T)(p!=0); // TEST+SETCC+NEG T e = lg(q); T nonzero_result = ((p-1) >> e) + 1; return nonzero_result & mask; } 非零时,它会利用并制作好的代码:

p

Chux / user23_variant

p->结果只有3c延迟,而常数q可以CSE很多。

// http://stackoverflow.com/questions/40447195/can-i-hint-the-optimizer-by-giving-the-range-of-an-integer
#if defined(__GNUC__) && !defined(__INTEL_COMPILER)
#define assume(x) do{if(!(x)) __builtin_unreachable();}while(0)
#else
#define assume(x) (void)(x) // still evaluate it once, for side effects in case anyone is insane enough to put any inside an assume()
#endif

T ceil_div_andmask_nonzerop(T p, T q) {
  assume(p!=0);
  return ceil_div_andmask(p, q);
}
    # gcc5.4 -O3 -march=haswell
    xor     eax, eax             # gcc7 does tzcnt in-place instead of wasting an insn on this
    sub     rdi, 1
    tzcnt   rax, rsi
    shrx    rax, rdi, rax
    add     rax, 1
    ret

在TZCNT之前执行SETCC将允许就地TZCNT,保存T divide_A_chux(T p, T q) { bool round_up = p & (q-1); // compiles differently from user23_variant with clang: AND instead of return (p >> lg(q)) + round_up; } xor eax, eax # in-place tzcnt would save this xor edx, edx # target for setcc tzcnt rax, rsi sub rsi, 1 test rsi, rdi shrx rdi, rdi, rax setne dl lea rax, [rdx+rdi] ret 。我还没看过这是如何在循环中内联的。

  • Fused-domain uops:HSW / SKL上的8(不计算ret)
  • HSW / SKL延迟:
    • q-&gt;结果:(tzcnt + shrx(p)| sub + test(p)+ setne)+ lea(或add)= 5c
    • d-&gt;结果:测试(从这里开始的dep)+ setne + lea = 3c 。 (shrx-> lea链更短,因而不是关键路径)
  • 吞吐量:可能只是瓶颈在前端,每​​2c一次。保存xor eax,eax应该将速度提高到每1.75c一个(但当然任何循环开销都将成为瓶颈的一部分,因为前端瓶颈就是这样)。

答案 1 :(得分:11)

uint64_t exponent = lg(q);
uint64_t mask = q - 1;
//     v divide
return (p >> exponent) + (((p & mask) + mask) >> exponent)
//                       ^ round up

单独计算&#34; round up&#34;部分避免了(p+q-1) >> lg(q)的溢出问题。根据编译器的智能程度,可以表达&#34; round up&#34;部分为((p & mask) != 0),没有分支。

答案 2 :(得分:8)

对于C中的无符号整数,除以2的幂的有效方式是右移 - 右移一除二(向下舍入),因此向右移n除以2 n (四舍五入)。

现在你要围绕向上而不是向下,这可以通过首先添加2 n -1,或等效地在班次之前减去一个并在之后添加一个(0除外)。这可以解决类似的问题:

unsigned ceil_div(unsigned d, unsigned e) {
    /* compute ceil(d/2**e) */
    return d ? ((d-1) >> e) + 1 : 0;
}

可以通过使用布尔值d来删除条件,用于加1和减1:

unsigned ceil_div(unsigned d, unsigned e) {
    /* compute ceil(d/2**e) */
    return ((d - !!d) >> e) + !!d;
}

由于其尺寸和速度要求,该功能应该是静态内联的。它可能不会对优化器产生不同,但参数应该是const。如果必须在许多文件之间共享,请在标题中定义:

static inline unsigned ceil_div(const unsigned d, const unsigned e){...

答案 3 :(得分:8)

  

有效地将无符号值除以2的幂,向上舍入

[重写]给定OP的clarification concerning power-of-2

当不考虑溢出时,圆形或天花板部分很容易。简单地添加q-1,然后转移。

否则,因为舍入的可能性取决于p小于q的所有位,所以在它们被移出之前首先需要检测这些位。

uint64_t divide_A(uint64_t p, uint64_t q) {
  bool round_up = p & (q-1);
  return (p >> lg64(q)) + round_up;
} 

这假定代码具有高效的lg64(uint64_t x)函数,如果x是2的幂,则返回x的base-2日志。

答案 4 :(得分:7)

如果$超过两个权力(哎呀),我的旧回答不起作用。所以我的新解决方案,使用p中提供的__builtin_ctzll()__builtin_ffsll()函数 0 (作为奖励,提供您提到的快速对数!):< / p>

gcc

请注意,假设uint64_t divide(uint64_t p,uint64_t q) { int lp=__builtin_ffsll(p); int lq=__builtin_ctzll(q); return (p>>lq)+(lp<(lq+1)&&lp); } 为64位。它必须稍微调整一下。

这里的想法是,如果我们需要溢出当且仅当long long的尾随零比p少。请注意,对于2的幂,尾随零的数量等于对数,因此我们也可以将此内置函数用于日志。

q部分仅适用于&&lp为零的角落情况:否则会在此处输出p

0 不能同时使用1因为__builtin_ctzll()未定义。{/ p>

答案 5 :(得分:6)

如果可以保证红利/除数不超过63(或31)位,您可以使用问题中提到的以下版本。注意如果p + q使用全部64位,它们将如何溢出。如果SHR指令在进位标志中移位,那么这将是正常的,但AFAIK则不会。

uint64_t divide(uint64_t p, uint64_t q) {
  return (p + q - 1) >> lg(q);
}

如果无法保证这些约束,您可以执行floor方法,然后添加1,如果它将向上舍入。这可以通过检查被除数中的任何位是否在除数范围内来确定。 注意:p&amp; -p提取2s补码机上的最低设置位或the BLSI instruction

uint64_t divide(uint64_t p, uint64_t q) {
  return (p >> lg(q)) + ( (p & -p ) < q );
}

哪个clang编译成:

    bsrq    %rax, %rsi
    shrxq   %rax, %rdi, %rax
    blsiq   %rdi, %rcx
    cmpq    %rsi, %rcx
    adcq    $0, %rax
    retq

这有点罗嗦并使用一些较新的指令,所以也许有一种方法可以在原始版本中使用进位标志。让我们来看看: RCR指令执行但看起来会更糟......也许是SHRD指令......就像这样(目前无法测试)

    xor     edx, edx     ;edx = 0 (will store the carry flag)
    bsr     rcx, rsi     ;rcx = lg(q) ... could be moved anywhere before shrd
    lea     rax, [rsi-1] ;rax = q-1 (adding p could carry)
    add     rax, rdi     ;rax += p  (handle carry)
    setc    dl           ;rdx = carry flag ... or xor rdx and setc
    shrd    rax, rdx, cl ;rax = rdx:rax >> cl
    ret

另外1条指令,但应该与旧处理器兼容(如果它可以工作......我总是得到一个源/目的地交换 - 随意编辑)

附录:

  

我已经实现了lg()函数,我说如下:

inline uint64_t lg(uint64_t x) {
  return 63U - (uint64_t)__builtin_clzl(x);
}

inline uint32_t lg32(uint32_t x) {
  return 31U - (uint32_t)__builtin_clz(x);
}

快速日志功能没有完全优化到clang和ICC上的bsr,但你可以这样做:

#if defined(__x86_64__) && (defined(__clang__) || defined(__INTEL_COMPILER))
static inline uint64_t lg(uint64_t x){
  inline uint64_t ret;
  //other compilers may want bsrq here
  __asm__("bsr  %0, %1":"=r"(ret):"r"(x));
  return ret;
}
#endif

#if defined(__i386__) && (defined(__clang__) || defined(__INTEL_COMPILER))    
static inline uint32_t lg32(uint32_t x){
  inline uint32_t ret;
  __asm__("bsr  %0, %1":"=r"(ret):"r"(x));
  return ret;
}
#endif

答案 6 :(得分:5)

已经有很多人类的脑力应用于这个问题,在C中有几个很好的答案以及彼得·科德斯的答案,这些答案涵盖了你在asm中所希望的最好的答案,关于尝试将其映射回C的说明。

因此,当人类在罐头上踢球的时候,我想看看有些蛮横的计算能力!为此,我使用斯坦福大学的STOKE superoptimizer试图找到解决这个问题的32位和64位版本的好方法。

通常情况下,superoptimizer通常类似于强力搜索所有可能的指令序列,直到您通过某个指标找到最佳指令。当然,something like 1,000 instructions会快速失控,超过几个指令 1 。一方面,STOKE采用引导随机方法:它随机地对现有候选程序进行突变,在每一步评估成本函数,使性能和正确性生效。无论如何,这就是单行 - 如果激发了你的好奇心,那就有plenty of papers

所以在几分钟内,STOKE找到了一些非常有趣的解决方案。它找到了现有解决方案中的几乎所有高级想法,以及一些独特的想法。例如,对于32位函数,STOKE找到了这个版本:

neg rsi                         
dec rdi                         
pext rax, rsi, rdi
inc eax
ret

它根本不使用任何前导/尾随计数或移位指令。差不多,它使用neg esi将除数转换为高位为1的掩码,然后pext使用该掩码有效地进行移位。除了这个技巧之外,它使用了用户QuestionC使用的相同技巧:递减p,移位,递增p - 但它甚至可以用于零除数,因为它使用64位寄存器来区分零情况和MSB -set large p case。

我将此算法的C version添加到基准测试中,并将其添加到结果中。它与其他优秀的算法竞争,在变量Q&#34;案例。它会进行矢量化,但不像其他32位算法那样好,因为它需要64位数学运算,因此矢量一次只能处理一半的元素。

更好的是,在32位的情况下,它提出了各种解决方案,这些解决方案使用的一些直观解决方案在边缘情况下失败的事实发生在&#34;只是工作&#34;如果您使用64位操作作为其中的一部分。例如:

tzcntl ebp, esi      
dec esi             
add rdi, rsi        
sarx rax, rdi, rbp
ret

这相当于我在问题中提到的return (p + q - 1) >> lg(q)建议。这通常不起作用,因为对于大p + q它溢出,但对于32位pq,这个解决方案通过在64位中执行重要部分而工作得很好。使用some casts将其转换回C,它实际上会发现使用lea将在一个指令中添加 1

stoke_32(unsigned int, unsigned int):
        tzcnt   edx, esi
        mov     edi, edi          ; goes away when inlining
        mov     esi, esi          ; goes away when inlining
        lea     rax, [rsi-1+rdi]
        shrx    rax, rax, rdx
        ret

当内联到已经将值零扩展到rdirsi的内容时,它就是一个3指令解决方案。独立函数定义需要mov指令进行零扩展,因为how the SysV x64 ABI works

对于64位功能,它没有想出任何能够破坏现有解决方案的东西,但它确实提出了一些巧妙的东西,例如:

  tzcnt  r13, rsi      
  tzcnt  rcx, rdi      
  shrx   rax, rdi, r13 
  cmp    r13b, cl        
  adc    rax, 0        
  ret

那个人计算两个参数的尾随零,然后如果q的尾随零比p少,则会在结果中加1,因为那个&#39; s什么时候你需要围捕。聪明!

一般情况下,它理解你需要shl tzcnt非常快(就像大多数人一样),然后提出了大量其他解决方案来解决调整问题结果导致舍入。它甚至设法在多个解决方案中使用blsibzhi。这是一个5指令解决方案:

tzcnt r13, rsi                  
shrx  rax, rdi, r13             
imul  rsi, rax                   
cmp   rsi, rdi                    
adc   rax, 0                    
ret 

它基本上是一个&#34;乘法和验证&#34;方法 - 截取res = p \ q,将其相乘,如果它与p不同,则添加一个:return res * q == p ? ret : ret + 1。凉。但并不比彼得的解决方案更好。 STOKE似乎在其延迟计算中存在一些缺陷 - 它认为上面的延迟为5 - 但它更像是8或9,具体取决于架构。因此,它有时会在基于其有缺陷的延迟计算的解决方案中缩小。

1 有趣的是,尽管这种暴力方法在5-6条指令附近达到了它的可行性:如果你假设你可以通过消除SIMD来削减指令数量300 x87指令,那么你需要大约28天的时间来尝试所有300 ^ 5 5个指令序列,每秒1,000,000候选人。您可以通过各种优化将其减少1000倍,即5指令序列不到一小时,6指令可能一周。碰巧,这个问题的大多数最佳解决方案都属于5-6指令窗口......

2 然而,这将是lea ,因此STOKE发现的序列仍然是我优化的最佳值,即延迟。

答案 7 :(得分:3)

你可以这样做,比较除n / d除以(n-1) / d

#include <stdio.h>

int main(void) {
    unsigned n;
    unsigned d;
    unsigned q1, q2;
    double actual;

    for(n = 1; n < 6; n++) {
        for(d = 1; d < 6; d++) {
            actual = (double)n / d;
            q1 = n / d;
            if(n) {
                q2 = (n - 1) / d;
                if(q1 == q2) {
                    q1++;
                }
            }
            printf("%u / %u = %u (%f)\n", n, d, q1, actual);
        }
    }

    return 0;
}

节目输出:

1 / 1 = 1 (1.000000)
1 / 2 = 1 (0.500000)
1 / 3 = 1 (0.333333)
1 / 4 = 1 (0.250000)
1 / 5 = 1 (0.200000)
2 / 1 = 2 (2.000000)
2 / 2 = 1 (1.000000)
2 / 3 = 1 (0.666667)
2 / 4 = 1 (0.500000)
2 / 5 = 1 (0.400000)
3 / 1 = 3 (3.000000)
3 / 2 = 2 (1.500000)
3 / 3 = 1 (1.000000)
3 / 4 = 1 (0.750000)
3 / 5 = 1 (0.600000)
4 / 1 = 4 (4.000000)
4 / 2 = 2 (2.000000)
4 / 3 = 2 (1.333333)
4 / 4 = 1 (1.000000)
4 / 5 = 1 (0.800000)
5 / 1 = 5 (5.000000)
5 / 2 = 3 (2.500000)
5 / 3 = 2 (1.666667)
5 / 4 = 2 (1.250000)
5 / 5 = 1 (1.000000)

<强>更新

我发布了原始问题的早期答案,该答案有效,但没有考虑算法的效率,或者除数总是2的幂。执行两个分区是不必要的昂贵。

我在64位系统上使用MSVC 32位编译器,因此我无法为所需目标提供最佳解决方案。但这是一个有趣的问题所以我已经找到了最好的解决方案将发现2 ** n除数的位。使用库函数log2有效,但速度很慢。做我自己的转变要好得多,但仍然是我最好的C解决方案

unsigned roundup(unsigned p, unsigned q)
{
    return p / q + ((p & (q-1)) != 0);
}

我的内联32位汇编程序解决方案速度更快,但当然不会回答这个问题。我通过假设eax作为函数值返回来窃取一些周期。

unsigned roundup(unsigned p, unsigned q)
{
    __asm {
        mov eax,p
        mov edx,q
        bsr ecx,edx     ; cl = bit number of q
        dec edx         ; q-1
        and edx,eax     ; p & (q-1)
        shr eax,cl      ; divide p by q, a power of 2
        sub edx,1       ; generate a carry when (p & (q-1)) == 0
        cmc
        adc eax,0       ; add 1 to result when (p & (q-1)) != 0
    }
}                       ; eax returned as function value

答案 8 :(得分:2)

如果您的编译器使用算术右移(通常为真),这看起来很有效并适用于签名。

#include <stdio.h>

int main (void)
{
    for (int i = -20; i <= 20; ++i) {
        printf ("%5d %5d\n", i, ((i - 1) >> 1) + 1);
    }
    return 0;
}

使用>> 2除以4,>> 3除以8,&amp; ct。高效lg在那里工作。

你甚至可以除以1! >> 0