在C/C++中获取正数模的最快方法

pes8fvy9  于 2022-12-24  发布在  C/C++
关注(0)|答案(9)|浏览(225)

通常在内部循环中,我需要以“环绕”的方式索引数组,以便(例如)如果数组大小为100,而我的代码要求元素-2,则应将元素98指定给它。在许多高级语言(如Python)中,可以简单地使用my_array[index % array_size]完成此操作,但由于某些原因,C的整数运算(通常)向零舍入,而不是始终向下舍入,因此当给定负的第一个参数时,其模运算符返回负的结果。
通常我知道index不会小于-array_size,在这种情况下,我只执行my_array[(index + array_size) % array_size],但是,有时候这是不能保证的,在这种情况下,我想知道实现一个总是正的模函数的最快方法,有几个“聪明”的方法可以在没有分支的情况下实现它,比如

inline int positive_modulo(int i, int n) {
    return (n + (i % n)) % n;
}

inline int positive_modulo(int i, int n) {
    return (i % n) + (n * (i < 0));
}

当然,我可以分析这些来找出哪个是我的系统上最快的,但我不禁担心我可能错过了一个更好的,或者我的机器上最快的东西可能在另一个机器上很慢。
那么有没有一个标准的方法来做到这一点,或者一些聪明的技巧,我错过了,这可能是最快的可能的方式?
另外,我知道这可能是一厢情愿的想法,但如果有一种方法可以做到这一点,可以自动矢量化,这将是惊人的。

8i9zcol2

8i9zcol21#

我学习的标准方法是

inline int positive_modulo(int i, int n) {
    return (i % n + n) % n;
}

这个函数实际上是第一个没有abs的变量(实际上,abs会使它返回错误的结果),如果一个优化编译器能够识别这种模式并将其编译成计算"无符号模"的机器码,我不会感到惊讶。
编辑:
接下来是第二个变体:首先,它也包含一个bug--n < 0应该是i < 0
这个变体看起来不像是分支,但是在很多架构上,i < 0会编译成一个条件跳转,无论如何,用i < 0? n: 0替换(n * (i < 0))至少会一样快,这样就避免了乘法;此外,它更"干净",因为它避免了将bool重新解释为int。
至于这两个变体中哪一个更快,这可能取决于编译器和处理器的架构--给这两个变体计时,然后再看。

cyej8jka

cyej8jka2#

以2的幂为模,如下所示(假设以二进制补码表示):

return i & (n-1);
vltsax25

vltsax253#

大多数时候,编译器非常擅长优化您的代码,因此通常最好保持代码的可读性(以便编译器和其他开发人员都知道您在做什么)。
由于数组大小总是正数,我建议你把商定义为unsigned,编译器会把小的if/else块优化成没有分支的条件指令:

unsigned modulo( int value, unsigned m) {
    int mod = value % (int)m;
    if (mod < 0) {
        mod += m;
    }
    return mod;
}

这将创建一个非常小的函数,没有分支:

modulo(int, unsigned int):
        mov     eax, edi
        cdq
        idiv    esi
        add     esi, edx
        mov     eax, edx
        test    edx, edx
        cmovs   eax, esi
        ret

例如,modulo(-5, 7)返回2
不幸的是,由于商是未知的,所以它们必须执行整数除法,这与其他整数运算相比有点慢。如果你知道数组的大小是2的幂,我建议把这些函数定义放在头文件中,这样编译器就可以把它们优化成一个更有效的函数。下面是函数unsigned modulo256(int v) { return modulo(v,256); }

modulo256(int):                          # @modulo256(int)
        mov     edx, edi
        sar     edx, 31
        shr     edx, 24
        lea     eax, [rdi+rdx]
        movzx   eax, al
        sub     eax, edx
        lea     edx, [rax+256]
        test    eax, eax
        cmovs   eax, edx
        ret

参见组装:https://gcc.godbolt.org/z/DG7jMw
查看与投票最多的答案的比较:http://quick-bench.com/oJbVwLr9G5HJb0oRaYpQOCec4E4

编辑:事实证明,Clang能够生成一个函数,而不需要任何条件移动指令(这比常规算术运算的开销更大)。这种差异在一般情况下是完全可以忽略不计的,因为整数除法大约占用总时间的70%。
基本上,Clang将value右移以将其符号位扩展到m的整个宽度(即,当为负时为0xffffffff,否则为0),该宽度用于屏蔽mod + m中的第二操作数。

unsigned modulo (int value, unsigned m) {
    int mod = value % (int)m;
    m &= mod >> std::numeric_limits<int>::digits;
    return mod + m;
}
mwyxok5s

mwyxok5s4#

使用二进制补码符号位传播获取可选加数的一种老派方法:

int positive_mod(int i, int m)
{
    /* constexpr */ int shift = CHAR_BIT*sizeof i - 1;
    int r = i%m;
    return r+ (r>>shift & m);
}
tjjdgumg

tjjdgumg5#

在C/C ++中获取正数模的最快方法
下面的代码快吗?--可能没有其他代码快,但是对于all1a,b来说,它简单而且功能正确--不像其他代码。

int modulo_Euclidean(int a, int b) {
  int m = a % b;
  if (m < 0) {
    // m += (b < 0) ? -b : b; // Avoid this form: -b is UB when b == INT_MIN
    m = (b < 0) ? m - b : m + b;
  }
  return m;
}

[Edit 2022年]
here开始,添加了处理INT_MIN mod -1和检测mod 0的测试。

int modulo_Euclidean2(int a, int b) {
  if (b == 0) TBD_Code(); // Perhaps return -1 to indicate failure?
  if (b == -1) return 0; // This test needed to prevent UB of `INT_MIN % -1`.
  int m = a % b;
  if (m < 0) {
    // m += (b < 0) ? -b : b; // Avoid this form: it is UB when b == INT_MIN
    m = (b < 0) ? m - b : m + b;
  }
  return m;
}

各种其他答案都有mod(a,b)的弱点,尤其是当b < 0
有关b < 0的更多信息,请参见Euclidean division

inline int positive_modulo(int i, int n) {
    return (i % n + n) % n;
}

i % n + n溢出时失败(假设i, n较大)-未定义的行为。

return i & (n-1);

依赖于n作为2的幂。(公平地说,答案确实提到了这一点。)

int positive_mod(int i, int n)
{
    /* constexpr */ int shift = CHAR_BIT*sizeof i - 1;
    int m = i%n;
    return m+ (m>>shift & n);
}

n < 0时经常失败。例如,positive_mod(-2,-3) --> -5

int32_t positive_modulo(int32_t number, int32_t modulo) {
    return (number + ((int64_t)modulo << 32)) % modulo;
}

必须使用2个整数宽度。(答案确实提到了这一点。)
modulo < 0失败。positive_modulo(2, -3)--〉-1。

inline int positive_modulo(int i, int n) {
    int tmp = i % n;
    return tmp ? i >= 0 ? tmp : tmp + n : 0;
}

n < 0时经常失败。例如,positive_modulo(-2,-3) --> -5
1例外情况:在C中,当a/b溢出时,a%b没有定义,如a/0INT_MIN/-1

c7rzv4ha

c7rzv4ha6#

如果你可以升级到一个更大的类型(并且在更大的类型上进行模运算),这段代码只进行一次模运算,没有if:

int32_t positive_modulo(int32_t number, int32_t modulo) {
    return (number + ((int64_t)modulo << 32)) % modulo;
}
xxb16uws

xxb16uws7#

如果您想避免所有条件路径(包括上面生成的条件移动,(例如,如果您需要此代码进行矢量化,或在恒定时间内运行),您可以使用符号位作为掩码:

unsigned modulo(int value, unsigned m) {
  int shift_width = sizeof(int) * 8 - 1;
  int tweak = (value >> shift_width);
  int mod = ((value - tweak) % (int) m) + tweak;
  mod += (tweak & m);
  return mod;
}

这是quickbench results你可以看到gcc在泛型情况下更好,对于clang在泛型情况下速度是一样的,因为clang在泛型情况下生成无分支代码,无论如何,这个技术都是有用的,因为编译器不能总是产生特定的优化,你可能需要手工滚动它来生成向量代码。

lkaoscv7

lkaoscv78#

也可以执行array[(i+array_size*N) % array_size],其中N是一个足够大的整数,可以保证参数为正,但又足够小,不会溢出。
当array_size为常数时,有一些方法可以计算模数而不用除法,除了2的幂方法外,还可以计算位组乘以2^i % n的加权和,其中i是每组中的最低有效位:
例如,32位整数0xaabbccdd%100 = dd + cc*[2]56 + bb*[655]36 + aa*[167772]16,具有(1+56+36+16)*255 = 27795的最大范围。通过重复应用和不同的细分,可以将运算减少到很少的条件减法。
常见的做法还包括倒数为2^32 / n的除法近似,这通常可以处理相当大范围的参数。

i - ((i * 655)>>16)*100; // (gives 100*n % 100 == 100 requiring adjusting...)
tyky79it

tyky79it9#

你的第二个例子比第一个例子要好,乘法运算比if/else运算复杂,所以使用这个例子:

inline int positive_modulo(int i, int n) {
    int tmp = i % n;
    return tmp ? i >= 0 ? tmp : tmp + n : 0;
}

相关问题