在 C/C++ 中获得正模的最快方法
Posted
技术标签:
【中文标题】在 C/C++ 中获得正模的最快方法【英文标题】:Fastest way to get a positive modulo in C/C++ 【发布时间】:2013-02-06 11:25:49 【问题描述】:通常在我的内部循环中,我需要以“环绕”方式对数组进行索引,因此(例如)如果数组大小为 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));
当然,我可以分析这些以找出我的系统上最快的,但我不禁担心我可能错过了更好的一个,或者我的机器上的快速可能在不同的机器上很慢.
那么有没有一种标准的方法可以做到这一点,或者我错过了一些可能是最快的方法?
另外,我知道这可能是一厢情愿,但如果有一种方法可以自动矢量化,那就太棒了。
【问题讨论】:
你是否一直在修改相同的数字? 然后,您需要对模数进行硬编码,或者将其作为编译时常量放入。这样一来,您将获得比您可以使用该标志玩的任何技巧更好的性能。 嗯,修改 2 的幂是微不足道的;你只要& (n-1)
不管标志。
我很惊讶没有人指出这一点,但在 C % 中,它不是模数,它返回余数。如果您查看文档,即使 fmod 也会返回余数:cplusplus.com/reference/cmath/fmod 所以我认为调用这个正模数很奇怪,因为您正在寻找的行为是模数应该是:en.wikipedia.org/wiki/Modular_arithmetic
使用(i % n) + (n * (i < 0))
,我看到的结果是n
而不是0
负精确倍数,例如 (-3, 3) -> 3。
【参考方案1】:
我学习的标准方法是
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
至少会一样快,从而避免了乘法;此外,它“更干净”,因为它避免将 bool 重新解释为 int。
至于这两个变体中哪一个更快,这可能取决于编译器和处理器架构——对这两个变体计时,然后看看。不过,我认为没有比这两种变体更快的方法了。
【讨论】:
Nitpick:它实际上不会矢量化,因为通常没有 SIMD 支持模数。 将n
分解到模板中会更有效吗?在函数不能内联的情况下,编译器或许可以玩一些技巧来提高性能。
哎呀,你对 abs() 的看法是对的,我已经把它从我的问题中编辑出来了。
还更正了第二个示例中的错字。 (我真的应该先测试它们。)
请注意,对于 (-3 mod 3) 使用 (i % n) + (n * (i < 0))
或 (i % n) + (i < 0 ? n : 0)
,结果为 3:(-3 % 3) == 0
和 (3 * (-3 < 0)) == 3
,可能不是预期的结果。【参考方案2】:
以二的幂为模,以下工作(假设二进制补码表示):
return i & (n-1);
【讨论】:
非常感谢!如果有人对一般情况有一个很好的答案,我会留下这个问题,但我可能最终会使用这个。 这里的n
是什么? n mod i
或 i mod n
?
答案很简单,但我会非常小心。请记住,不同的架构通常以不同的方式存储负数。因此,负数的按位运算符不会因不同的编译器和/或架构而有所不同。
i mod n
== i & (n-1)
n
是 2 的幂,mod
是上述正模。 (仅供参考:modulus
是考虑模运算时“除数”的常用数学术语)。
@GrijeshChauhan:明确规定了限制:n
必须是 2 的幂,并且数字必须使用补码(过去 20 年生产的几乎每台计算机)。什么时候会失败?【参考方案3】:
大多数时候,编译器非常擅长优化您的代码,因此通常最好让您的代码保持可读性(让编译器和其他开发人员都知道您在做什么)。
由于您的数组大小始终为正,我建议您将商定义为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;
【讨论】:
谢谢,这很有趣。同样有趣的是,指定 29 可以节省一些通用函数,即使 2 的幂甚至更快。我也在 g++ 上运行了基准测试,结果相似。我接受这个答案是因为我认为它确实取代了其他投票率更高的答案中的信息。 如果您想知道具体的方法,有书籍/网站可以为您提供有关此的更多信息:例如 PowerPC Compiler Writer's Guide 在第 52 至 61 页有关于此的部分,而 Matt Godbolt 在他的“我的编译器最近为我做了什么?”中谈到了这一点。谈话,在第 35 分钟 谢谢。我已经更新了答案,包括为什么不使用条件移动会更快,即使我只看到常量除法的改进(使用 GCC)而不是一般情况。 此代码不正确。它不适用于 modulo(-x, x) 并在这种情况下返回 x。 你必须右移 mod,而不是 value。【参考方案4】:使用二进制补码符号位传播获取可选加数的老式方法:
int positive_mod(int i, int m)
/* constexpr */ int shift = CHAR_BIT*sizeof i - 1;
int r = i%m;
return r+ (r>>shift & m);
【讨论】:
老派难以阅读的技巧。我喜欢。虽然我想知道(i>>shift & n)
是否会更快,因为位移运算将不得不等待模运算完成。
它会更快,但它会给出不正确的结果,例如-2 模式 2。
开枪,你是对的。既然你提到了它,(i % n) + (n * (i < 0))
也是如此。
假设 CHAR_BIT 是一个全球竞赛(系统的?) sizeof 是什么?我不明白是 CHAR_BIT*(sizeof(i)) -1 还是
@J.Schultke 好吧,我还是改了一些名字来解决可能的混淆,现在m
是模数,r
是结果,不是这个数字是什么@ 987654326@的左边。【参考方案5】:
在 C/C++ 中获得正模的最快方法
下面快吗? - 可能不如其他人快,但对所有人来说都简单且功能正确1a,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;
其他各种答案都有mod(a,b)
的弱点,尤其是在b < 0
时。
请参阅 Euclidean division 了解有关 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
作为 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
时经常失败。 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 < 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
时经常失败。 e、g、positive_modulo(-2,-3) --> -5
1 例外:在 C 中,a/b
溢出时未定义 a%b
,如 a/0
或 INT_MIN/-1
。
【讨论】:
解释其他答案的失败是有帮助的。 你能详细说明为什么+=
会导致 UB 吗?
@cassepipe +=
很好,但-b
b == INT_MAN
是 UB。添加注释以回答。
@chux-ReinstateMonica 谢谢!实际上 INT_MIN 在 int 范围内没有正等价物,因为它会比 INT_MAX 高 1。这是因为en.wikipedia.org/wiki/Two%27s_complement (不久前为像我这样的初学者提供的)【参考方案6】:
如果您有能力升级到更大的类型(并对更大的类型进行模运算),则此代码只进行一次模运算,如果:
int32_t positive_modulo(int32_t number, int32_t modulo)
return (number + ((int64_t)modulo << 32)) % modulo;
【讨论】:
【参考方案7】:如果你想避免所有条件路径(包括上面生成的条件移动,(例如如果你需要这段代码进行向量化,或者在恒定时间内运行),你可以使用符号位作为掩码:
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 生成 分支免费代码。无论如何,该技术很有用,因为不能总是依赖编译器来产生特定的优化,您可能不得不手动滚动它以获得矢量代码。
【讨论】:
我知道 OP 不需要恒定时间,因为它用于数组查找,但这已被链接为计算模数的快速方法,someone 可能需要在固定的时间内完成,所以我认为值得一提。 您的上帝螺栓链接有错误,因为您执行的是无符号除法而不是有符号(您缺少演员表)。 Intel 目前不支持整数除法作为向量单元,Arm 也不支持,但它们并不是唯一具有向量单元的 CPU,它们将来可能会进行整数除法。跨度> 我稍微看了一下,当m
不是常数时(只需运行您的链接并清除缓存的结果),快速工作台结果显示相同的性能。如果您像m &= value < 0? UINT_MAX : 0u; mod += m;
一样编写代码,GCC 会报告相同的程序集,这比使用右移更具可读性(右移只是在设置符号位时添加一个全为 1 的位掩码)。事实证明,Clang 做的事情比让编译器做脏活更进一步,这通常是一个好主意。
如果您需要它在恒定时间内运行,那么依赖编译器是个坏主意。【参考方案8】:
您也可以使用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...)
【讨论】:
【参考方案9】:你的第二个例子比第一个好。乘法比 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 添加到结果中 1) 也许只是return tmp < 0 ? tmp + n : tmp;
。 2) 这个答案比highly rated one 有一个优势,因为它永远不会溢出。
重新声明为“它”不清楚:这个答案永远不会溢出。 (优势)(if n > 0
)。 other answer 可能会溢出。 (弱点)。以上是关于在 C/C++ 中获得正模的最快方法的主要内容,如果未能解决你的问题,请参考以下文章