我对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");
}
}
}
7 回答
使这两者变得更加接近的关键是确保比较公平 .
首先确保与运行Debug构建相关的成本,像您一样加载pdb符号 .
接下来,您需要确保不计算初始化成本 . 显然这些都是实际成本,对某些人来说可能很重要,但在这种情况下,我们对循环本身感兴趣 .
接下来,您需要处理特定于平台的行为 . 如果您使用的是64位Windows机器,则可能在32位或64位模式下运行 . 在64位模式下,JIT在许多方面都有所不同,通常会大大改变生成的代码 . 具体来说,我猜是有针对性地,您可以访问两倍的通用寄存器 .
在这种情况下,循环的内部部分在天真地转换为机器代码时,需要将模数测试中使用的常数加载到寄存器中 . 如果没有足够的东西来保存循环中所需的一切,那么它必须将它们从内存中推入 . 即使来自level1缓存,与将其全部保存在寄存器中相比,这将是一个重大的打击 .
在VS 2010 MS changed the default target from anycpu to x86 . 我没有像资源或面向客户的MSFT知识,所以我不会试图再次猜测 . 然而,任何看待你正在做的性能分析的人都应该尝试两者 .
一旦解决了这些差异,这些数字似乎就更合理了 . 任何进一步的差异可能需要比有根据的猜测更好,相反,他们需要调查生成的机器代码中的实际差异 .
关于优化编译器,我认为这有几件事情很有意思 .
已经提到过的人:
lcm选项很有趣,但我看不到编译器编写者的烦恼 .
将除法减少到乘法和掩蔽 .
我对此并不了解,但是other people have tried请注意,他们会在更新的英特尔芯片上更好地调出分频器 .
也许你甚至可以用SSE2安排复杂的事情 .
当然,模16操作已经成熟,可以转换为蒙版或移位 .
编译器可以发现没有任何测试有副作用 .
它可以推测性地尝试同时评估其中的几个,在超级标量处理器上,这可以提高速度,但在很大程度上取决于编译器布局与OO执行引擎的交互程度 .
如果寄存器压力很小,你可以将常量实现为单个变量,在每个循环的开始处设置,然后随着时间的推移递增 .
这些都是完全猜测,应该被视为空闲的曲折 . 如果你想知道拆解它 .
要对代码执行进行计时,您应该使用StopWatch类 .
此外,您必须考虑JIT,运行时等,因此让测试运行足够的次数(例如10,000次,100,000次)并获得某种平均值 . 重要的是多次运行 code , not 该程序 . 所以编写一个方法,并在main方法中循环以获得测量结果 .
从程序集中删除所有调试内容,让代码在发布版本中独立运行
有一些可能的优化 . 也许Java JIT正在执行它们而CLR不是 .
Optimization #1:
相当于
因此,在您的示例中,比较链可以替换为
(但是当然如果你已经计算过LCM,那么首先运行程序就没什么意义了!)
Optimization #2 :
这也是等价的:
要么
第一个分区可以用掩码替换并与零进行比较:
第二个除法可以用模数逆的乘法代替:
JIT使用它有点不太可能,但它并不像你想象的那么牵强 - 一些C编译器以这种方式实现指针减法 .
这是一个太短的任务,无法做适当的时机 . 您需要至少运行1000次并查看会发生什么 . 看起来你正在从命令行运行它们,在这种情况下你可能会比较两者的JIT编译器 . 尝试将两个按钮放在简单的GUI中,并且至少在返回经过的时间之前让按钮循环几百次 . 即使忽略JIT编译,时间也可能因为粒度而被抛弃OS调度程序 .
哦,因为JIT ...只计算按下按钮的第二个结果 . :)
也许是因为
DateTime
对象的构造要比System.currentTimeMillis
贵得多 .在Java中我会使用System.nanoTime() . 任何花费不到2秒的测试都应该运行更长时间 . 值得注意的是,Java非常擅长优化效率低下的代码或代码,而不执行任何操作 . 如果您优化了代码,那么更有趣的测试就是 .
您正在尝试获得一个可以在不使用循环的情况下确定的解决方案 . 即一个以另一种方式做得更好的问题 .
你想要的因子是11到20,即2,2,2,2,3,3,5,7,11,13,17,19 . 将这些相乘并得到答案 .
(从OP移动)
将目标从x86更改为anycpu已将平均执行时间从每次运行降低到84毫秒,低于282毫秒 . 也许我应该把它分成第二个线程?
UPDATE:
感谢下面的Femaref谁pointed out some testing problems,实际上,在遵循他的建议之后,时间更短,表明VM的设置时间在Java中很重要,但可能不在C#中 . 在C#中,调试符号很重要 .
我更新了我的代码以运行每个循环10,000次,并且只输出最后的平均ms . 我做的唯一重大改变是C#版本,我切换到[StopWatch类] [3]以获得更高的分辨率 . 我坚持了几毫秒,因为它足够好了 .
Results:
测试改变了对本OP附带的评论的交换,你会看到我只需要通过从Debug调试到Release就可以在五次C#代码中获得平均约280ms的时间 .
编号:
未经修改的Java代码的10,000计数循环给了我平均45ms(从55ms开始)
使用StopWatch类的C#代码的10,000计数循环给了我平均282ms(从320ms开始)
所有这些都使得差异无法解释 . 事实上,差异变得更糟 . Java从快5.8倍增加到快6.2倍 .