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


70

通常,在我的内部循环中,我需要以“环绕”方式为数组建立索引,以便(例如)如果数组大小为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));
}

当然,我可以对它们进行概要分析以找出哪个是系统上最快的,但是我不禁担心自己可能错过了更好的系统,或者我的计算机上的速度在另一台计算机上可能较慢。

那么,有没有一种标准的方法来执行此操作,或者我错过了一些巧妙的技巧,而这可能是最快的方法?

另外,我知道这可能是一厢情愿的想法,但是如果有一种方法可以自动矢量化,那就太神奇了。


您是否一直在修改相同的数字?
Mysticial

1
然后,您将需要硬编码模数,或将其作为编译时常数放入。这样一来,您将获得比使用标牌可以玩的任何花招更好的性能。
2013年

2
好吧,修改2的幂是微不足道的。& (n-1)无论迹象如何,您都可以做。
nneonneo

2
没有人指出这一点,我感到很惊讶,但是C%不是模数,它返回余数。如果您查看文档,则fmod也会返回余数:cplusplus.com/reference/cmath/fmod因此,我认为将其称为正模数是很奇怪的,因为您要查找的行为是应该假定的模数:en。 wikipedia.org/wiki/Modular_arithmetic
leetNightshade 2014年

1
随着(i % n) + (n * (i < 0))我看到的结果n,而不是0消极确切的倍数,如(-3,3) - > 3
兰德尔·惠特曼

Answers:


18

大多数时候,编译器非常擅长优化代码,因此通常最好使代码保持可读性(让编译器和其他开发人员都知道您在做什么)。

由于您的数组大小始终为正,因此建议您将商定义为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)return 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;
}

谢谢,这很有趣。同样有趣的是,即使2的幂更快,指定29也可以节省泛型函数。我也在g ++上运行了基准测试,结果相似。我接受此答案,因为我认为它确实会取代其他投票较高的答案中的信息。
纳撒尼尔(Nathaniel)

1
如果您想知道确切的方法,可以通过书/网站为您提供更多信息:例如,《 PowerPC编译器指南》第52至61页对此进行了介绍,而Matt Godbolt在他的“最近我的编译器为我做了什么?” 演讲,在第35分钟
加布里埃尔·拉维尔

1
谢谢。我已经更新了答案,以包括为什么不使用条件移动会更快,即使我只看到了恒定除法(而不是一般情况)的改进(使用GCC)。
豪尔赫·贝隆

1
该代码不正确。它不适用于modulo(-x,x)并在这种情况下返回x。
Jan Schultke '20

1
您必须改为righshift mod,而不是值。
Jan Schultke '20

79

我学到的标准方法是

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

此函数本质上是您不带的第一个变体abs(实际上,这会使它返回错误的结果)。如果优化的编译器能够识别这种模式并将其编译为计算“无符号模”的机器代码,我不会感到惊讶。

编辑:

继续进行第二个变体:首先,它也包含一个错误-n < 0应该是i < 0

这个变体看起来好像不是分支,但是在许多体系结构上,i < 0它将编译为条件跳转。在任何情况下,这将是至少一样快,以取代(n * (i < 0))i < 0? n: 0,这避免了乘法; 此外,它是“更干净的”,因为它避免将布尔值重新解释为int。

至于这两个变体中哪个更快,这可能取决于编译器和处理器体系结构-对这两个变体进行计时。不过,我认为没有比这两个变体更快的方法了。


Nitpick:它实际上不会矢量化,因为通常不支持SIMD模数。
Mysticial

1
将其n分解为模板会更有效吗?在无法内联函数的情况下,编译器可能会发挥一些技巧来提高性能。
亚历克斯·张伯伦

糟糕,您对abs()的看法是正确的,我出于我的问题对其进行了编辑。
纳撒尼尔(Nathaniel)

还纠正了第二个示例中的错字。(我真的应该先对它们进行测试。)
Nathaniel

请注意,对于(-3 mod 3)使用(i % n) + (n * (i < 0))(i % n) + (i < 0 ? n : 0),结果为3:(-3 % 3) == 0(3 * (-3 < 0)) == 3,可能不是所需的结果。
Qwertie 2013年

29

模二的幂,以下工作(假设二进制补码表示):

return i & (n-1);

非常感谢!万一有人对一般情况有一个好的答案,我将保留这个问题,但是我可能最终会使用这个问题。
纳撒尼尔(Nathaniel)

1
这是n什么 n mod i还是i mod n
ixSci

1
答案很简单,我会非常小心。请记住,不同的体系结构通常以不同的方式存储负数。因此,负数上的按位运算符不能随不同的编译器和/或体系结构而不同。
mity

2
i mod n== i & (n-1)whenn是2的幂,并且mod是上述正mod。(FYI:modulus当考虑取模运算时,是“除数”的通用数学术语)。
nneonneo

7
@GrijeshChauhan:明确说明了局限性:n必须是2的幂,数字必须使用2的补码(过去20年中生产的每台计算机几乎都是)。否则什么时候会失败?
nneonneo

10

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

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

老派难读的hack。我喜欢。尽管我想知道是否(i>>shift & n)可能更快,否则移位操作将不得不等待模运算完成。
aaaaaaaaaaaa

这将是更快,但它会给出如不正确的结果-2 MOD 2
jthill

射击,你是对的。现在您提到了,这也是正确的(i % n) + (n * (i < 0))
aaaaaaaaaaaa 2013年

假设CHAR_BIT是(系统的)全球竞赛,sizeof是什么?我不知道它是否为CHAR_BIT *(sizeof(i))
Francesco Boi,

1
@ J.Schultke好的,我还是改了一些名字以解决可能的混乱,现在m是模数,r是结果,这还没n剩多少。
jthill

3

如果您有能力升级为更大的类型(并对更大的类型进行模运算),则此代码将执行单个模运算,如果没有,则为:

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

3

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

以下快吗?-可能不如其他人快,但对于所有1 个人来说都是简单且功能正确的a,b-与其他人不同。

int modulo_Euclidean(int a, int b) {
  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


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

i % n + n溢出时失败(请考虑大i, n)-未定义的行为。


return i & (n-1);

依靠n两个的幂。(公平的回答确实提到了这一点。)


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。e,g,positive_mod(-2,-3) --> -5


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

必须使用2个整数宽度。(公平的回答确实提到了这一点。)
与失败modulo < 0positive_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。e,g,positive_modulo(-2,-3) --> -5


1例外:在C中,如或中那样溢出a%b时未定义。a/ba/0INT_MIN/-1


1
解释其他答案的失败是有帮助的。
NateS

3

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

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的结果您可以看到在gcc上,在一般情况下效果更好。对于clang,它在通用情况下的速度相同,因为clang在通用情况下会生成无分支代码。无论如何,该技术都是有用的,因为不能总是依靠编译器来进行特定的优化,并且您可能必须手动滚动以获取矢量代码。


1
我知道OP不需要恒定的时间,因为它是用于数组查找的,但是这已被链接为快速计算模数的方法,有人可能需要在恒定时间内进行模数计算,因此我认为值得一提。
凯尔·巴特

1
您的Godbolt链接有一个错误,因为您执行的是无符号除法而不是有符号的除法(您缺少演员表)。
豪尔赫·贝隆

英特尔目前不支持整数除法作为矢量单位,而Arm也不支持整数除法,但是它们并不是唯一具有矢量单位的CPU,将来它们可能会得到整数除法。
凯尔·巴特

1
我给出了一个小小的外观,当m不是恒定值时,快速基准测试结果显示出相同的性能(只需运行链接即可清除缓存的结果)。如果您像m &= value < 0? UINT_MAX : 0u; mod += m;这样编码,GCC会报告相同的程序集,这比使用右移更具可读性(当设置符号位时,右移只是添加全1s位掩码)。Clang正确地做事的事实比让编译器做些肮脏的工作更能证明这一点通常是个好主意。
豪尔赫·贝隆

如果您需要它在恒定时间内运行,那么依靠编译器是个坏主意。
凯尔·巴特

2

您也可以这样做array[(i+array_size*N) % array_size],其中N是足够大的整数以保证正参数,但又足够小而不溢出。

当array_size恒定时,有一些无需模数即可计算模量的技术。除了两种方法的功效外,还可以计算位组的加权总和乘以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...)

1

您的第二个示例比第一个示例更好。乘法是比if / else操作更复杂的操作,因此请使用以下命令:

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

1)您是对的,我编辑了代码。2)如果i为负,则返回为负,i%n返回负数,例如-102%100返回-2,因此您只需将n添加到结果中
SkYWAGz 2015年

1)也许很简单return tmp < 0 ? tmp + n : tmp;。2)这个答案比评级很高的一个优点在于它不会溢出。
chux-恢复莫妮卡

重新声明为“它”还不清楚:这个答案永远不会溢出。(优点)(if n > 0)。在其他的答案可能会溢出。(弱点)。
chux-恢复莫妮卡
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.