为什么这个Java代码比相同的C#代码快6倍?

我有几个不同的解决scheme来解决Project Euler问题5 ,但是这个特定实现中的两种语言/平台之间的执行时间差异使我感到兴奋。 我没有做任何优化与编译器标志,只是普通的javac (通过命令行)和csc (通过Visual Studio)。

这是Java代码。 它在55ms结束。

 公共类Problem005b
 {
     public static void main(String [] args)
     {
         long begin = System.currentTimeMillis();
         int i = 20;
        而(真)
         {
            如果(
                     (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)
                 )
             {
                打破;
             }
            我+ = 20;
         }
         long end = System.currentTimeMillis();
        的System.out.println(ⅰ);
         System.out.println(end-begin +“ms”);
     } 
}

这是相同的C#代码。 它在320ms完成

 使用系统;

命名空间ProjectEuler05
 {
    课题005
     {
         static void Main(String [] args)
         {
             DateTime begin = DateTime.Now;
             int i = 20;
            而(真)
             {
                如果(
                         (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)
                     )
                     {
                        打破;
                     }
                我+ = 20;
             }
             DateTime结束= DateTime.Now;
             TimeSpan过去了=结束 - 开始;
             Console.WriteLine(ⅰ);
             Console.WriteLine(elapsed.TotalMilliseconds +“ms”);
         }
     }
 } 

使这两者接近的关键是确保比较公平。

首先确保与运行Debug版本相关的成本,像你一样加载pdb符号。

接下来,您需要确保没有计入初始成本。 显然这些是真正的成本,对某些人来说可能很重要,但在这种情况下,我们对循环本身感兴趣。

接下来,您需要处理平台特定的行为。 如果您使用的是64位Windows机器,则可能是以32位或64位模式运行。 在64位模式下,JIT在很多方面是不同的,通常会相应地改变所产生的代码。 具体而言,我猜是有针对性的,你可以访问两倍的通用寄存器。

在这种情况下,当天真地翻译成机器代码时,循环的内部部分将需要将在模数testing中使用的常量加载到寄存器中。 如果没有足够的东西来保存循环中所需要的一切,那么它必须将它们从内存中推入。 即使来自第一级caching,与将其全部保存在寄存器中相比,这将是一个重大的打击。

在VS 2010中,MS 将默认目标从anycpu更改为x86 。 我没有像MSFT的资源或面向客户的知识,所以我不会再次猜测。 然而,任何看着你正在做的性能分析的人都应该尝试两者。

一旦解决了这些差距,这个数字就显得更加合理了。 任何进一步的差异可能需要比教育的猜测好,而他们需要调查生成的机器代码的实际差异。

有几件事我认为对于优化编译器来说很有趣。

  • 这些finnw已经提到:
    • 有趣的lcm选项,但我看不到一个编译器作家困扰。
    • 减less乘法和掩蔽的分割。
      • 我对这方面的知识还不够多,但是其他人已经试过注意到,他们在更新的英特尔芯片上大幅调用了分频器。
      • 也许你可以用SSE2来安排一些复杂的东西。
      • 当然,模16操作已经成熟,可以转换成掩码或转换。
    • 编译器可以发现没有任何testing有副作用。
      • 它可以推测性地尝试在一个超级标量处理器上同时评估其中的几个,这可以将事情抽得更快一些,但是在很大程度上取决于编译器布局与OO执行引擎的交互性。
    • 如果注册压力太高,您可以将常量作为单个variables来实现,在每个循环开始时设置,然后随着您的增加而增加。

这些都是完全的猜测,应该被看作是无所顾忌的曲折。 如果你想知道拆卸它。

  1. 要定时执行代码,您应该使用StopWatch类。
  2. 另外,你必须考虑到JIT,运行时等,所以让testing运行足够的时间(比如10,000,100,000次),并得到某种平均。 多次运行代码非常重要, 而不是程序。 所以写一个方法,然后在主要方法中循环来获得你的测量结果。
  3. 从程序集中删除所有debugging的东西,并让代码在发行版本中独立运行

有几个优化可能。 也许Java JIT正在执行它们,而CLR则不是。

优化#1:

 (x % a == 0) && (x % b == 0) && ... && (x % z == 0) 

相当于

 (x % lcm(a, b, ... z) == 0) 

所以在你的例子中,比较链可以被replace

 if (i % 232792560 == 0) break; 

(当然,如果你已经计算过LCM,那么首先运行程序就没有意义了!)

优化#2

这也是等价的:

 if (i % (14549535 * 16)) == 0 break; 

要么

 if ((i % 16 == 0) && (i % 14549535 == 0)) break; 

第一个分区可以replace为一个掩码,然后比较零:

 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编译器通过这种方式来实现指针减法。

DateTime对于基准testing并不准确(如此处所述(请阅读注释)),请查看秒表类

也许是因为DateTime对象的构造比System.currentTimeMillis贵得多。

这对于做适当的时机来说太短了。 你需要运行至less1000次,看看会发生什么。 它看起来像你从命令行运行这些,在这种情况下,你可能会比较两个JIT编译器。 试着把两个button都放在一个简单的graphics用户界面中,然后让这个button至less循环数百次,然后再返回经过的时间。 即使忽略JIT编译,时间可能会被OS调度程序的粒度抛出。

哦,而且因为JIT …只计算button按下的第二个结果。 🙂

在Java中,我会使用System.nanoTime()。 任何less于2秒的testing应该运行更长的时间。 值得注意的是,Java在优化低效的代码或代码时是相当不错的。 如果您优化了代码,则更有趣的testing。

您正试图获得一个您可以确定的解决scheme,而无需使用循环。 即另一种方式会更好的问题。

你想得到11到20的因子,即2,2,2,2,3,3,5,7,11,13,17,19。 把它们相乘,你就有了答案。

(从OP中移出)

将目标从x86更改为anycpu已将平均执行时间从282ms降低到每次运行84ms。 也许我应该把它分成第二个线程?

更新:
由于下面的Femaref 指出了一些testing问题 ,事实上,在遵循他的build议之后,时间越来越短,这表明Java在Java中的设置时间是显着的,但是可能不是在C#中。 在C#中,debugging符号是重要的。

我更新了我的代码运行每个循环10000次,只输出平均毫秒结束。 我做的唯一重要的变化是在C#版本中,我切换到[StopWatch类] [3]以获得更高的分辨率。 我坚持了毫秒,因为它足够好。

结果:
testing的变化并不能解释Java为什么(仍然)比C#更快。 C#的性能更好,但这可以通过删除debugging符号完全解释。 如果你阅读[Mike Two] [4],并且交换了这个OP所附带的注释,那么通过从Debug到Release的切换,你会发现我在C#代码的五次运行中平均得到了〜280ms的平均值。

编号:

  • 未经修改的Java代码的10,000个计数循环给了我平均45ms(从55ms下降)
  • 使用StopWatch类的C#代码的10,000个计数循环给了我平均282毫秒(从320毫秒)

所有这些都使得原因不明。 事实上,差别越来越大。 Java的速度提高了5.8倍,速度提高了6.2倍。