为什么模运算符是必要的?

我在一个文档中读到你可以用逻辑替换mod操作,如下所示:

代替:

int Limit = Value % Range; 

你做:

 int Limit = Value & (Range-1); 

但编译器仍然生成mod指令,我的问题基本上是:为什么编译器如果工作相同,就不会使用最有效的方法?

只有当它是2的幂时,你才能用模数替换模数。使用基本数学代替它而不用模数

 a = b % c; 

可以完成

 x = b % c; a = b / (x*c); 

让我们举一个例子来检查一下

 25 % 7 = 25 / 7 = 3 (integer math) 25 - (3 * 7) = 25 - 21 = 4 

无论如何我必须在我的计算器上执行此操作,因为我没有模运算符。

注意

 25 & (7-6) = 0x19 & 0x6 = 0x0 

所以你的替换不起作用。

大多数处理器不仅没有模数,而且许多处理器没有分数。 查看黑客喜悦书。

为什么你想要模数? 如果你已经烧掉硬件以进行分割,你可能愿意花更多的钱来添加模数。 大多数处理器将您的问题提升到一个新的水平,为什么在软件完成时可以实现硬件划分。 您的问题的答案是大多数处理器系列没有模数,并且许多处理器没有分歧,因为与软件解决方案相比,它不值得芯片占用空间,功耗等。 软件解决方案不那么痛苦/昂贵/有风险。

现在我假设你的问题不是胜利海报回答的问题。 对于范围是2的幂并且标识确实有效的情况……首先,如果在编译时未知范围,那么你必须进行减法和和,两个运算,也许是一个中间变量,即比模数更昂贵,编译器将错误地优化为减法而不是模数。 如果范围是2的幂并且在编译时已知,那么您的更好/更好的编译器将进行优化。 有时,特别是带有可变字长指令集,其中较小的指令可用于较大的指令,加载Range并执行模数可能不如加载大量非零位(符合您身份的范围在值中设置了一个位,其他位为零,0x100,0x40,0x8000等)并执行模数。 加载立即加模数可能比加载立即加上便宜,或者模数立即可能比立即加上便宜。 您必须检查指令集以及编译器如何实现解决方案。

我建议你发布一些它没有进行优化的例子,我假设我们可以发布很多关于编译器完成了你所期望的优化的例子。

嗯不…只有在Range是2的幂时才有效。

对于所有其他值,您仍需要模数%运算符。

使用负数时,还存在一些微妙的(可能是实现定义的)差异。


作为旁注:使用%运算符也可能更具可读性。

正如其他人所说,范围必须是2 ^ n-1,即便如此,如果在运行时完成,你也会遇到问题。

在最近的架构上(比方说,P4时代之后的任何事情),整数除法指令的延迟在26到50左右,最差情况下也是如此。 相比之下,乘法可以是1-3个循环,并且通常可以更好地并行完成。

DIV指令返回EAX中的商和EDX中的余数。 “余数”是自由的(模数是余数)。

如果你在运行时实现了范围可变的东西,如果你想使用&,你必须:

a)检查范围是否为2 ^ n-1,如果是,请使用您的&codepath:这是一个分支,可能的缓存未命中等等。增加巨大的延迟潜力b)如果它不是2 ^ n-1,请使用DIV指令

使用DIV而不是在方程式中添加分支(这可能会导致数百甚至数千个周期在高速缓存驱逐不良的情况下花费成本),这使得DIV成为明显的最佳选择。 最重要的是,如果您使用带有签名的数据类型,则需要进行转换(没有&用于混合数据类型,但有DIV用于转换)。 另外,如果DIV仅用于从模数中分支并且不使用其余结果,则推测执行可以很好地执行; 多个可以并行执行指令的管道进一步减轻了性能损失。

您必须记住,如果您使用的是真实代码,那么您的大量缓存将填充您正在处理的数据,以及您将很快或刚刚处理过的其他代码和数据。 你真的不想驱逐缓存页面并等待它们因为分支错误预测而进入页面。 在大多数情况下使用模数,你不只是去i = 7; d = i%4; 你正在使用更大的代码,它经常调用一个子程序,它本身就是一个(预测和缓存的)子程序调用。 另外你可能在循环中这样做,它本身也使用分支预测; 带有循环的嵌套分支预测在现代微处理器中处理得相当好,但它最终只是简单的愚蠢添加到它试图做的预测中。

总而言之,对于一般用例,使用DIV在现代处理器上更有意义; 由于缓存考虑因素和其他因素,编译器生成2 ^ n-1并不是真正的“优化”。 如果你真的需要微调那个整数除法,而你的整个程序依赖于它,你最终会将除数硬编码为2 ^ n-1并自己制作按位和逻辑。

最后,这有点咆哮 – 用于整数除法的专用ALU单元可以真正将延迟减少到大约6-8个周期,它只占用相对较大的芯片面积,因为数据路径最终约为128位宽并且当整数DIV工作得很好时,没有人拥有它的房地产。