我对Project Euler problem 5有一些不同的解决方案,但是这个特定实现中两种语言/平台之间的执行时间差异引起了我的兴趣。我没有使用编译器标志进行任何优化,只是简单javac
(通过命令行)和csc
(通过Visual Studio)。
这是Java代码。它在55ms结束。
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#代码。它在320毫秒完成
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"); } } }
答案 0 :(得分:39)
StopWatch
类。答案 1 :(得分:24)
可以进行一些优化。也许Java JIT正在执行它们而CLR不是。
优化#1:
(x % a == 0) && (x % b == 0) && ... && (x % z == 0)
相当于
(x % lcm(a, b, ... z) == 0)
因此,在您的示例中,比较链可以替换为
if (i % 232792560 == 0) break;
(但是当然如果你已经计算过LCM,那么首先运行程序就没什么意义了!)
优化#2 :
这也是等效的:
if (i % (14549535 * 16)) == 0 break;
或
if ((i % 16 == 0) && (i % 14549535 == 0)) break;
可以用掩码替换第一个分区并与零进行比较:
if (((i & 15) == 0) && (i % 14549535 == 0)) break;
第二个除法可以用模数逆的乘法代替:
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编译器以这种方式实现指针减法。
答案 2 :(得分:12)
使这两者变得更接近的关键是确保比较公平。
首先确保与运行Debug构建相关的成本,像你一样加载pdb符号。
接下来,您需要确保不计算初始成本。显然这些都是实际成本,对某些人来说可能很重要,但在这种情况下,我们对循环本身很感兴趣。
接下来,您需要处理特定于平台的行为。如果您使用的是64位Windows机器,则可能在32位或64位模式下运行。在64位模式下,JIT在许多方面都有所不同,通常会大大改变生成的代码。具体而言,我会相应地猜测,您可以访问两倍的通用寄存器。
在这种情况下,循环的内部部分在天真地转换为机器代码时,需要将模数测试中使用的常量加载到寄存器中。如果没有足够的东西来保存循环中所需的一切,那么它必须将它们从内存中推入。即使来自level1缓存,与将它们全部保存在寄存器中相比,这将是一个重大的打击。
在VS 2010 MS changed the default target from anycpu to x86中。我没有像MSFT的资源或面向客户的知识,所以我不会试图再次猜测。但是,任何看过你正在进行的性能分析的人都应该尝试两种方法。
一旦解决了这些差异,这些数字似乎就更合理了。任何进一步的差异可能需要比有根据的猜测更好,相反,他们需要调查生成的机器代码中的实际差异。
对于优化编译器,我认为这有几件事情会很有趣。
这些都是完全猜测,应该被视为空闲的曲折。如果你想知道反汇编它。
答案 3 :(得分:2)
这是一个太短的任务,无法做适当的时机。你需要至少运行1000次,看看会发生什么。看起来你从命令行运行它们,在这种情况下你可能会比较两者的JIT编译器。尝试将两个按钮放在简单的GUI中,并且至少在返回经过的时间之前让按钮循环几百次。即使忽略JIT编译,也可能会因操作系统调度程序的粒度而失去时序。
哦,因为JIT ...只计算按下按钮的第二个结果。 :)
答案 4 :(得分:1)
也许是因为DateTime
对象的构造比System.currentTimeMillis
昂贵得多。
答案 5 :(得分:1)
在Java中我会使用System.nanoTime()。任何花费不到2秒的测试都应该运行更长时间。值得注意的是,Java非常擅长优化效率低下的代码或代码,而这些代码或代码无效。如果您优化了代码,那么更有趣的测试就是。
您正在尝试获得一个可以在不使用循环的情况下确定的解决方案。即另一种方式可以做得更好的问题。
您想要的因子为11到20,即2,2,2,2,3,3,5,7,11,13,17,19。将这些加在一起就可以得到答案。
答案 6 :(得分:1)
(从OP移动)
将目标从x86更改为anycpu已将平均执行时间从每次运行降低到84毫秒,低于282毫秒。也许我应该把它分成第二个线程?
<强>更新强>
感谢下面的Femaref谁pointed out some testing problems,实际上,在遵循他的建议之后,时间更短,表明VM的设置时间在Java中很重要,但可能不在C#中。在C#中,调试符号很重要。
我更新了我的代码以运行每个循环10,000次,并且最后只输出平均ms。我做的唯一重大改变是C#版本,我切换到[StopWatch类] [3]以获得更高的分辨率。我坚持了几毫秒,因为它已经足够好了。
<强>结果:强>
测试更改并不能解释为什么Java(仍然)比C#快得多。 C#性能更好,但这可以通过删除调试符号来完全解释。如果您阅读[Mike Two] [4]以及我对本OP所附评论的交流,您将看到我在五次C#代码中平均得到〜280ms,只需从Debug切换到Release
编号:
所有这些都使得区别无法解释。事实上,差异变得更糟。 Java从快5.8倍增加到快6.2倍。