当总计十亿个整数时,c比java慢

时间:2014-11-08 09:05:45

标签: java c++

情况就是这样:

cat sum100000000.cpp && cat sum100000000.java 
#include <cstdio>

using namespace std;

int main(){
  long N=1000000000,sum=0;
  for( long i=0; i<N; i++ ) sum+= i;
  printf("%ld\n",sum);
}


public class sum100000000 {
    public static void main(String[] args) {
        long sum=0;
        for(long i = 0; i < 1000000000; i++) sum += i;
        System.out.println(sum);
    }
}

结果如下:

time ./a.out && time java sum100000000
499999999500000000

real    0m2.675s
user    0m2.673s
sys 0m0.002s
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
499999999500000000

real    0m0.439s
user    0m0.470s
sys 0m0.027s

在反汇编的二进制文件中没有看到任何异常。但似乎c二进制显然更慢。这是我无法理解的。

我的猜测是工具链可能存在一些问题

clang -v
Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.4.0
Thread model: posix

uname -a
Darwin MacBook-Pro.local 13.4.0 Darwin Kernel Version 13.4.0: Sun Aug 17 19:50:11 PDT 2014; root:xnu-2422.115.4~1/RELEASE_X86_64 x86_64

添加:c / cpp编译时没有任何特殊的二进制文件。这不会改变结果。

gcc sum1b.cpp
clang sum1b.cpp

添加:对于那些与llvm有关的人,实际上没有改变任何东西

$ gcc sum100000000.cpp && time ./a.out
gcc sum100000000.cpp && time ./a.out
499999999500000000

real    0m2.722s
user    0m2.717s
sys 0m0.003s

修改:O2快得多:但这看起来像是作弊

$ otool -tV a.out
otool -tV a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000f50    pushq   %rbp
0000000100000f51    movq    %rsp, %rbp
0000000100000f54    leaq    0x37(%rip), %rdi ## literal pool for: "%ld
"
0000000100000f5b    movabsq $0x6f05b59b5e49b00, %rsi
0000000100000f65    xorl    %eax, %eax
0000000100000f67    callq   0x100000f70 ## symbol stub for: _printf
0000000100000f6c    xorl    %eax, %eax
0000000100000f6e    popq    %rbp
0000000100000f6f    ret

我现在确信这与优化有关,所以现在问题更多的是关于JIT为加速计算做了什么呢?

2 个答案:

答案 0 :(得分:31)

问题很可能是您没有在启用优化的情况下编译C版本。如果启用积极优化,gcc生成的二进制文件应该会获胜。 JVM的JIT很好,但简单的事实是JVM必须加载然后在运行时应用JIT; gcc可以在编译时优化二进制文件。

离开所有gcc标志会给我一个表现相当慢的二进制文件,就像你的一样。使用-O2为我提供了一个二进制文件,几乎失去了Java版本。使用-O3给我一个轻松击败Java版本的版本。 (这是在我的Linux Mint 16 64位机器上,gcc 4.8.1和Java 1.8.0_20 [例如,Java 8 Update 20]。)larsmans检查了{{1}的反汇编}版本并向我保证编译器没有预先计算结果(我的C和程序集fu现在非常弱;很多感谢larsmans进行双重检查)。有趣的是,感谢Mat的调查,看起来这实际上是我使用-O3 4.8.1的副产品;早期版本和更高版本的gcc似乎愿意预先计算结果。但是,对我们来说是一个意外的事故。

这是我的纯C版本[我还更新了它,以便考虑Ajay's comment关于你在Java版本中使用常量,但是C版本中的变量gcc(没有制作)任何真正的差异,但......)]:

N

sum.c

我的Java版本与你的版本没有变化,除了我很容易忘记零:

#include <stdio.h> int main(){ long sum=0; long i; for( i=0; i<1000000000; i++ ) sum+= i; printf("%ld\n",sum); }

sum.java

结果:

C二进制运行(通过public class sum { public static void main(String[] args) { long sum=0; for(long i = 0; i < 1000000000; i++) sum += i; System.out.println(sum); } } 编译):

$ time ./a.out
499999999500000000

real    0m2.436s
user    0m2.429s
sys 0m0.004s

Java运行(没有特殊标志编译,没有特殊的运行时标志运行):

$ time java sum
499999999500000000

real    0m0.691s
user    0m0.684s
sys 0m0.020s

Java运行(编译时没有特殊标志,运行gcc sum.c微小改进):

$ time java -server -noverify sum
499999999500000000

real    0m0.651s
user    0m0.649s
sys 0m0.016s

C二进制运行(通过-server -noverify编译):

$ time ./a.out
499999999500000000

real    0m0.733s
user    0m0.732s
sys 0m0.000s

C二进制运行(通过gcc -O2 sum.c编译):

$ time ./a.out
499999999500000000

real    0m0.373s
user    0m0.372s
sys 0m0.000s

以下是我gcc -O3 sum.c版本main的{​​{1}}结果:

0000000000400470 :
  400470:   66 0f 6f 1d 08 02 00    movdqa 0x208(%rip),%xmm3        # 400680 
  400477:   00 
  400478:   31 c0                   xor    %eax,%eax
  40047a:   66 0f ef c9             pxor   %xmm1,%xmm1
  40047e:   66 0f 6f 05 ea 01 00    movdqa 0x1ea(%rip),%xmm0        # 400670 
  400485:   00 
  400486:   eb 0c                   jmp    400494 
  400488:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40048f:   00 
  400490:   66 0f 6f c2             movdqa %xmm2,%xmm0
  400494:   66 0f 6f d0             movdqa %xmm0,%xmm2
  400498:   83 c0 01                add    $0x1,%eax
  40049b:   66 0f d4 c8             paddq  %xmm0,%xmm1
  40049f:   3d 00 65 cd 1d          cmp    $0x1dcd6500,%eax
  4004a4:   66 0f d4 d3             paddq  %xmm3,%xmm2
  4004a8:   75 e6                   jne    400490 
  4004aa:   66 0f 6f e1             movdqa %xmm1,%xmm4
  4004ae:   be 64 06 40 00          mov    $0x400664,%esi
  4004b3:   bf 01 00 00 00          mov    $0x1,%edi
  4004b8:   31 c0                   xor    %eax,%eax
  4004ba:   66 0f 73 dc 08          psrldq $0x8,%xmm4
  4004bf:   66 0f d4 cc             paddq  %xmm4,%xmm1
  4004c3:   66 0f 7f 4c 24 e8       movdqa %xmm1,-0x18(%rsp)
  4004c9:   48 8b 54 24 e8          mov    -0x18(%rsp),%rdx
  4004ce:   e9 8d ff ff ff          jmpq   400460 

正如我所说,我的assembly-fu非常弱,但我在那里看到一个循环,而不是编译器已完成数学运算。

为了完整起见,objdump -d a.out的结果的-O3部分:

  public static void main(java.lang.String[]);
    Code:
       0: lconst_0
       1: lstore_1
       2: lconst_0
       3: lstore_3
       4: lload_3
       5: ldc2_w        #2                  // long 1000000000l
       8: lcmp
       9: ifge          23
      12: lload_1
      13: lload_3
      14: ladd
      15: lstore_1
      16: lload_3
      17: lconst_1
      18: ladd
      19: lstore_3
      20: goto          4
      23: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      26: lload_1
      27: invokevirtual #5                  // Method java/io/PrintStream.println:(J)V
      30: return

它不是在字节码级别预先计算结果;我不能说JIT正在做什么。

答案 1 :(得分:3)

java优化字节代码进行比较时,优化gcc字节代码是有意义的。接受的答案不包括-server -noverify个开关。可选地,可以应用-Xcomp,但不建议这样做。

  1. CPU:Intel(R)Core(TM)i7-4770 CPU @ 3.40GHz

  2. java版“1.8.0_25” Java(TM)SE运行时环境(版本1.8.0_25-b17) Java HotSpot(TM)64位服务器VM(内置25.25-b02,混合模式)

  3. gcc(Ubuntu 4.8.2-19ubuntu1)4.8.2

  4. sum.java with flags:

    time java -server -noverify sum
    499999999500000000
    
    real    0m0.299s
    user    0m0.303s
    sys 0m0.004s
    

    和sum.c with 03 flag:

    time ./a.out 
    499999999500000000
    
    real    0m0.233s
    user    0m0.233s
    sys 0m0.000s