我在文档中读到你可以用逻辑替换mod操作,如下所示:
相反:
int Limit = Value % Range;
你这样做:
int Limit = Value & (Range-1);
但编译器仍会生成mod指令,我的问题基本上是:如果编译器的工作方式相同,为什么编译器不使用最有效的方法呢?
答案 0 :(得分:26)
嗯不......仅当Range
是2的幂时才有效。
对于所有其他值,您仍需要模数%
运算符。
使用负数时,还存在一些微妙的(可能是实现定义的)差异。
作为旁注:使用%
运算符也可能更具可读性。
答案 1 :(得分:14)
只有当它是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等)并执行模数。加载立即加模数可能比加载立即加上便宜,或者模数立即可能比立即加上便宜。您必须检查指令集以及编译器如何实现解决方案。
我建议您发布一些不进行优化的示例,我假设我们可以发布许多编译器完成您期望的优化的示例。
答案 2 :(得分:0)
正如其他人所说,范围必须是2 ^ n-1,即便如此,如果在运行时完成,你也会遇到问题。
在最近的体系结构上(比方说,P4时代之后的任何事情),整数除法指令的延迟在26到50左右,最坏情况周期。相比之下,乘法可以是1-3个循环,并且通常可以更好地并行完成。
DIV指令返回EAX中的商和EDX中的余数。 “余数”是自由的(模数是余数)。
如果你在运行时实现了范围可变的东西,如果你想使用&amp ;,你必须:
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并使得bitwise&逻辑自己。
最后,这有点咆哮 - 用于整数除法的专用ALU单元可以真正将延迟减少到大约6-8个周期,它只占用相对较大的芯片面积,因为数据路径最终约为128比特宽,当整数DIV工作得很好时,没有人拥有它的空间。