如果您恰好使用2的幂,则左右移位显然比大多数甚至所有CPU上的乘法和除法运算更快。但是,它可能会降低某些读取器和某些算法的代码清晰度。位移对于性能是否真的是必要的,或者我是否可以期望编译器或VM注意到这种情况并对其进行优化(特别是当2的幂是文字时)?我主要对Java和.NET行为感兴趣,但欢迎深入了解其他语言实现。
答案 0 :(得分:88)
几乎任何有价值的环境都会为您优化这一点。如果没有,你就会有更大的鱼来炸。说真的,不要再浪费一点思考这个。你会知道什么时候出现性能问题。在你运行一个分析器之后,你就会知道是什么导致了它,并且应该很清楚如何修复它。
你永远不会听到有人说“我的应用程序太慢了,然后我开始用x * 2
随意替换x << 1
,一切都已修复!”性能问题通常通过找到减少工作量的方法来解决,而不是通过找到一种方法来以相同的速度快1%地完成相同的工作。
答案 1 :(得分:88)
今天的大多数编译器所做的不仅仅是转换乘法或除以2的幂来转换操作。在优化时,许多编译器可以优化乘法或除以编译时间常数,即使它不是2的幂。通常乘法或除法可以分解为一系列的移位和加法,如果这一系列操作会更快比乘法或除法,编译器将使用它。
对于除以常数,编译器通常可以将操作转换为乘以“幻数”后跟移位。这可以是主要的时钟周期保护程序,因为乘法通常比除法运算快得多。
Henry Warren's book, Hacker's Delight,有关于此主题的大量信息,在配套网站上也有很好的介绍:
另请参阅:
中的讨论(带有一个或两个链接)无论如何,所有这些归结为允许编译器处理微优化的繁琐细节。自从你自己的轮班工作超过了编译器已经有好几年了。
答案 2 :(得分:33)
在这些情况下,人类是错误的。
当他们试图猜测现代(以及所有未来的)编译器时,这一比例为99% 当他们试图同时猜测现代(以及所有未来)JIT时,这一比例为99.9% 当他们试图再次猜测现代(以及所有未来的)CPU优化时,这一比例为99.999%。以准确描述您想要完成的内容的方式进行编程,而不是如何进行编程。未来版本的JIT,VM,编译器和CPU都可以独立改进和优化。如果您指定的内容非常小且具体,那么您将失去所有未来优化的好处。
答案 3 :(得分:21)
您几乎可以肯定地依赖于二次幂乘法优化来进行换档操作。这是编译器构建的学生将学习的第一个优化之一。 :)
但是,我认为没有任何保证。您的源代码应该反映您的 intent ,而不是试图告诉优化器该做什么。如果您的数量较大,请使用乘法。如果您将一个位域从一个位置移动到另一个位置(想想RGB颜色操作),请使用移位操作。无论哪种方式,您的源代码都将反映您实际在做什么。
答案 4 :(得分:13)
请注意,向下移动和除法(在Java中肯定会)给出负数,奇数的不同结果。
int a = -7;
System.out.println("Shift: "+(a >> 1));
System.out.println("Div: "+(a / 2));
打印:
Shift: -4
Div: -3
由于Java没有任何无符号数,因此Java编译器无法对此进行优化。
答案 5 :(得分:8)
在我测试的计算机上,整数除法比其他操作慢<4到10倍。
当编译器可以用2的倍数替换除法并使你看不出差异时,除以2的倍数的除法明显变慢。
例如,我有一个(图形)程序有许多分区乘以255。 实际上我的计算是:
r = (((top.R - bottom.R) * alpha + (bottom.R * 255)) * 0x8081) >> 23;
我可以确保它比我以前的计算快得多:
r = ((top.R - bottom.R) * alpha + (bottom.R * 255)) / 255;
所以不,编译器无法完成所有优化技巧。
答案 6 :(得分:5)
我会问“你在做什么才重要?”首先设计代码以提高可读性和可维护性。通过标准乘法进行位移的可能性将使性能差异非常小。
答案 7 :(得分:3)
它取决于硬件。如果我们正在讨论微控制器或i386,那么移位可能会更快,但是,正如几个答案所述,您的编译器通常会为您进行优化。
在现代(Pentium Pro及其他)硬件上,流水线操作使得这完全无关紧要,并且偏离了常规路径通常意味着您可以获得比您可以获得的更多优化。
微观优化不仅浪费你的时间,也很难做到正确。
答案 8 :(得分:2)
根据this microbenchmark的结果,移位速度是划分速度的两倍(Oracle Java 1.7.0_72)。
答案 9 :(得分:1)
如果编译器(编译时常量)或JIT(运行时常量)知道除数或被乘数是2的幂并且正在执行整数运算,它会将它转换为适合你的转换。
答案 10 :(得分:1)
我很震惊,因为我刚刚编写了这段代码,并意识到换一档实际上比乘以二慢!
(编辑:在迈克尔迈尔斯的建议之后改变代码以停止溢出,但结果是一样的!这里有什么问题?)
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date before = new Date();
for (int j = 1; j < 50000000; j++) {
int a = 1 ;
for (int i = 0; i< 10; i++){
a *=2;
}
}
Date after = new Date();
System.out.println("Multiplying " + (after.getTime()-before.getTime()) + " milliseconds");
before = new Date();
for (int j = 1; j < 50000000; j++) {
int a = 1 ;
for (int i = 0; i< 10; i++){
a = a << 1;
}
}
after = new Date();
System.out.println("Shifting " + (after.getTime()-before.getTime()) + " milliseconds");
}
}
结果是:
乘以639毫秒
换档718毫秒
答案 11 :(得分:0)
大多数编译器会在适当的时候将乘法和除法转换为位移。这是最容易进行的优化之一。因此,您应该做更容易阅读和适合给定任务的内容。
答案 12 :(得分:-1)
这是对Savvas Dalkitsis所做基准测试的分析。虽然此测试检查乘法相对于位移的速度,但使用的值并不相同,请在下面的C#中查看显示值的代码)
for (int i = 0, j = 1, k = 1; i < 10; i++)
{
j = j * 2;
k <<= 2;
Console.WriteLine("{0} {1}", j, k);
}
使用C#中显示的值的Savvas Dalkitsis示例的等效代码将是
public static void Main(String[] args)
{
for (int i = 0, j = 1, k = 1; i < 10; i++)
{
j = j * 2; k <<= 2;
Console.WriteLine("{0} {1}", j, k);
}
Console.WriteLine("-----------------------------------------------");
DateTime before = DateTime.Now;
for (int j = 1; j < 500000000; j++)
{
int a = 1;
for (int i = 0; i < 10; i++) a *= 2;
}
DateTime after = DateTime.Now;
Console.WriteLine("Multiplying " + (after - before).ToString() + " milliseconds");
before = DateTime.Now;
for (int j = 1; j < 500000000; j++)
{
int a = 1;
for (int i = 0; i < 10; i++) a = a << 1;
}
after = DateTime.Now;
Console.WriteLine("Shifting " + (after - before).ToString() + " milliseconds");
Console.ReadKey();
}