Why is this Java code 6x faster than the identical C# code?
对于ProjectEuler问题5,我有几个不同的解决方案,但是在这个特定的实现中,两种语言/平台之间的执行时间差异让我很感兴趣。我没有使用编译器标志进行任何优化,只使用简单的
这里是Java代码。在55毫秒内完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class Problem005b { public static void main(String[] args) { long begin = System.currentTimeMillis(); int i = 20; while (true) { if ( (i % 19 == 0) && (i % 18 == 0) && (i % 17 == 0) && (i % 16 == 0) && (i % 15 == 0) && (i % 14 == 0) && (i % 13 == 0) && (i % 12 == 0) && (i % 11 == 0) ) { break; } i += 20; } long end = System.currentTimeMillis(); System.out.println(i); System.out.println(end-begin +"ms"); } } |
这是相同的C代码。在320ms内完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using System; namespace ProjectEuler05 { class Problem005 { static void Main(String[] args) { DateTime begin = DateTime.Now; int i = 20; while (true) { if ( (i % 19 == 0) && (i % 18 == 0) && (i % 17 == 0) && (i % 16 == 0) && (i % 15 == 0) && (i % 14 == 0) && (i % 13 == 0) && (i % 12 == 0) && (i % 11 == 0) ) { break; } i += 20; } DateTime end = DateTime.Now; TimeSpan elapsed = end - begin; Console.WriteLine(i); Console.WriteLine(elapsed.TotalMilliseconds +"ms"); } } } |
号
可以进行一些优化。也许JavaJIT正在执行它们,而CLR不是。
优化1:
1 | (x % a == 0) && (x % b == 0) && ... && (x % z == 0) |
等于
1 | (x % lcm(a, b, ... z) == 0) |
号
因此在您的示例中,比较链可以替换为
1 | if (i % 232792560 == 0) break; |
(当然,如果您已经计算了LCM,那么首先运行程序没有什么意义!)
优化2:
这也相当于:
1 | if (i % (14549535 * 16)) == 0 break; |
。
或
1 | if ((i % 16 == 0) && (i % 14549535 == 0)) break; |
第一个除法可以用一个掩码替换,并与零进行比较:
1 | if (((i & 15) == 0) && (i % 14549535 == 0)) break; |
。
第二个除法可以用模逆乘法代替:
1 2 3 4 5 6 7 8 9 | final long LCM = 14549535; final long INV_LCM = 8384559098224769503L; // == 14549535**-1 mod 2**64 final long MAX_QUOTIENT = Long.MAX_VALUE / LCM; // ... if (((i & 15) == 0) && (0 <= (i>>4) * INV_LCM) && ((i>>4) * INV_LCM < MAX_QUOTIENT)) { break; } |
。
JIT不太可能使用这种方法,但它并不像您想象的那样牵强——一些C编译器以这种方式实现指针减法。
使这两者更接近的关键是确保比较是公平的。
首先要确保与运行调试构建、像您一样加载PDB符号相关的成本。
接下来,您需要确保不计算初始成本。显然,这些都是真正的成本,对某些人来说可能很重要,但在本例中,我们对循环本身很感兴趣。
接下来,您需要处理特定于平台的行为。如果您在64位Windows计算机上,则可能正在32位或64位模式下运行。在64位模式下,JIT在许多方面都不同,通常会对生成的代码进行较大的修改。具体地说,我会有针对性地猜测,您可以访问的通用寄存器数量是普通用途寄存器的两倍。
在这种情况下,当幼稚地转换为机器代码时,循环的内部部分需要加载到寄存器中模块测试中使用的常量。如果没有足够的内存来保存循环中需要的所有内容,那么它必须将它们从内存中推入。即使来自一级缓存,与将其全部保存在寄存器中相比,这也是一个重大的打击。
在vs 2010中,ms将默认目标从anycpu更改为x86。我没有什么能比得上MSFT的资源或面向客户的知识,所以我不会再去猜测。但是,任何关注您正在进行的性能分析的人都应该同时尝试这两种方法。
一旦这些差距消除,这些数字似乎就更加合理了。任何进一步的差异可能需要比有根据的猜测更好的猜测,相反,它们需要调查生成的机器代码中的实际差异。
我认为对于一个优化的编译器来说,这有几个方面是有趣的。
- 芬诺已经提到的那些:
- LCM选项很有意思,但我看不到编译器编写器有问题。
- 将除法简化为乘法和掩蔽法。
- 我对此知之甚少,但其他人也注意到,他们称最近英特尔芯片的分频器明显更好。
- 也许你甚至可以用SSE2来安排一些复杂的事情。
- 当然,模16操作已经成熟,可以转换为掩码或移位。
- 编译器可以发现所有测试都没有副作用。
- 它可以推测地尝试一次评估其中的几个,在超标量处理器上,这可以使事情发展得更快,但很大程度上取决于编译器布局与OO执行引擎的交互效果。
- 如果寄存器压力很紧,可以将常量实现为单个变量,在每个循环的开始处设置,然后在执行过程中递增。
这些都是彻头彻尾的猜测,应该被视为闲散的曲流。如果你想知道拆卸它。
这项任务太短,无法为其进行适当的时间安排。您需要同时运行至少1000次,然后看看会发生什么。看起来像是从命令行运行这些命令,在这种情况下,您可能会比较两种类型的JIT编译器。尝试将两个按钮都放在一个简单的图形用户界面中,在返回经过的时间之前,至少让那个按钮在这个界面上循环几百次。即使忽略了JIT编译,操作系统调度器的粒度也可能导致时间延迟。
哦,因为准时…只计算按下按钮的第二个结果。:)
(从OP移动)
将目标从x86更改为anycpu已将每次运行的平均执行时间从282ms降低到84ms。也许我应该将其拆分为第二个线程?
更新:感谢下面的FEMAREF谁指出了一些测试问题,事实上,在遵循他的建议后,时间较低,这表明VM设置时间在Java中是重要的,但可能不在C.*中。在C中,调试符号非常重要。
我更新了我的代码以运行每个循环10000次,并且只输出结束时的平均毫秒数。我所做的唯一重大改变是C版本,在C版本中,我切换到[秒表类][3]以获得更高的分辨率。我坚持毫秒是因为它足够好。
结果:测试的变化并不能解释为什么Java仍然比C语言快得多。C性能更好,但可以通过删除调试符号来完全解释这一点。如果您阅读了[Mike 2][4]并与我交换了此操作附带的评论,您将看到,仅通过从调试切换到发布,我在五次C代码运行中平均获得约280ms。
数字:
- 未修改的Java代码的10000计数循环使我的平均值为45毫秒(从55毫秒下降)。
- 使用秒表类对C代码进行10000次计数循环,平均为282毫秒(低于320ms)。
所有这些都无法解释两者之间的差异。实际上,差分变得更糟了。Java从-5.8x快到6.2x快。
在Java中,我将使用Stase.NeimTime[()。任何不到2秒的测试都应运行更长时间。值得注意的是,Java很擅长优化效率低下的代码或代码。更有趣的测试是,如果您优化了代码。
您试图得到一个不使用循环就可以确定的解决方案。也就是说,用另一种方法可以做得更好的问题。
你需要11到20的因子的乘积,即2,2,2,3,3,5,7,11,13,17,19。把它们相乘,你就得到了答案。
也许是因为建造