首页 文章

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

提问于
浏览
43

我对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 回答

  • 1

    使这两者变得更加接近的关键是确保比较公平 .

    首先确保与运行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执行引擎的交互程度 .

    • 如果寄存器压力很小,你可以将常量实现为单个变量,在每个循环的开始处设置,然后随着时间的推移递增 .

    这些都是完全猜测,应该被视为空闲的曲折 . 如果你想知道拆解它 .

  • 24
    • 要对代码执行进行计时,您应该使用StopWatch类 .

    • 此外,您必须考虑JIT,运行时等,因此让测试运行足够的次数(例如10,000次,100,000次)并获得某种平均值 . 重要的是多次运行 codenot 该程序 . 所以编写一个方法,并在main方法中循环以获得测量结果 .

    • 从程序集中删除所有调试内容,让代码在发布版本中独立运行

  • 39

    有一些可能的优化 . 也许Java JIT正在执行它们而CLR不是 .

    Optimization #1:

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

    相当于

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

    因此,在您的示例中,比较链可以替换为

    if (i % 232792560 == 0) break;
    

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

    Optimization #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编译器以这种方式实现指针减法 .

  • 12

    这是一个太短的任务,无法做适当的时机 . 您需要至少运行1000次并查看会发生什么 . 看起来你正在从命令行运行它们,在这种情况下你可能会比较两者的JIT编译器 . 尝试将两个按钮放在简单的GUI中,并且至少在返回经过的时间之前让按钮循环几百次 . 即使忽略JIT编译,时间也可能因为粒度而被抛弃OS调度程序 .

    哦,因为JIT ...只计算按下按钮的第二个结果 . :)

  • 2

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

  • 1

    在Java中我会使用System.nanoTime() . 任何花费不到2秒的测试都应该运行更长时间 . 值得注意的是,Java非常擅长优化效率低下的代码或代码,而不执行任何操作 . 如果您优化了代码,那么更有趣的测试就是 .

    您正在尝试获得一个可以在不使用循环的情况下确定的解决方案 . 即一个以另一种方式做得更好的问题 .

    你想要的因子是11到20,即2,2,2,2,3,3,5,7,11,13,17,19 . 将这些相乘并得到答案 .

  • 1

    (从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倍 .

相关问题