Java VM V.S. ネイティブコード

ソフトとハードの進歩で,Java仮想マシン実行ってどれくらい速くなったのか調べてみたくなりました.

あんまり深く考えていなかったので,Javaで書いたことのないシンプレクティック数値積分法のコードを書いてみました.
手法は一番簡単なシンプレクティック・オイラー法で,コードは汎用的に実装関数を抽象クラスで書いたものを用意して,ハミルトニアン微分関数を実装する実用的な形に.

すると,基底クラスはこんな感じです.

abstract class S_Euler
{
  public double tau,p,q;
  public S_Euler(double t, double p0, double q0)
  {
    tau = t;
    p = p0;
    q = q0;
  }
  abstract double f(double p, double q);
  abstract double g(double p, double q);
  void step()
  {
    p = p - tau*f(p,q);
    q = q + tau*g(p,q);
  }
  double get_p()
  {
    return(p);
  }
  double get_q()
  {
    return(q);
  }
}

これをスーパークラスにした派生クラスは,こんな感じです.普通に調和振動子の例です.

class Pendulum extends S_Euler
{
  Pendulum(double t, double p, double q)
  {
    super(t,p,q);
  }
  public double f(double p,double q)
  {
    return(q);
  }
  public double g(double p,double q)
  {
    return(p);
  }
  public static void main(String argc[])
  {
    Pendulum a = new Pendulum(0.01,1,0);
    long start = System.currentTimeMillis();
    for(int i=0;i<10000000;++i)
    {
      a.step();
      if(i%1000000==0)
      {
        System.out.println(a.p+","+a.q);
      }
    }
    System.out.println("time="+(System.currentTimeMillis()-start));
  }

}

さて.まずはJava仮想マシンを通して実行してみます.

> gcj42 -C Pendulum.java S-Euler.java ; jar42 cvfm Pendulum.jar META-INF/MANIFEST.MF *.class
  adding: Pendulum.class        (in=983) (out=606) (deflated 38%)
  adding: S_Euler.class (in=555) (out=349) (deflated 37%)
  adding: META-INF/     (in=0) (out=0) (stored 0%)
  adding: META-INF/MANIFEST.MF  (in=70) (out=68) (deflated 3%)
total bytes=1608, compressed=1023 -> 36% savings
> time java -jar Pendulum.jar Pendulum
1.0,0.01
-0.9368734759859007,-0.35438428629884006
0.7586961792021539,0.6552491571791168
-0.4873487970738525,-0.8756475362411043
0.156153573002246,0.9885138650309242
0.1942177615102147,-0.9799878319059567
-0.520738593131847,0.8511164583954232
0.783311219273834,-0.6177255219286855
-0.9496909619511764,0.30847610459007824
0.9994459075076086,0.038655070663285565
time=555
0.815u 0.262s 0:01.01 105.9%    116+903k 0+6io 1pf+0w

1千万回のループが555ミリ秒とは速いです.

次にGNUのgcj42のネイティブコードを作り実行します.

> gcj42 -o Pendulum.out --main=Pendulum Pendulum.java S-Euler.java; time ./pendulum.out
1.0,0.01
-0.9368734759859007,-0.35438428629884006
0.7586961792021539,0.6552491571791168
-0.4873487970738525,-0.8756475362411043
0.156153573002246,0.9885138650309242
0.1942177615102147,-0.9799878319059567
-0.520738593131847,0.8511164583954232
0.783311219273834,-0.6177255219286855
-0.9496909619511764,0.30847610459007824
0.9994459075076086,0.038655070663285565
time=983
1.095u 0.023s 0:01.20 92.5%     10+1689k 0+0io 32pf+0w

ネイティブと思っていたコードの方が遅い?!

いや,結果は微妙なようです.
time=で出る値はJavaが実行され始めてからのものですが,よく見るとtimeの値がネイティブコード事項の方がユーザタイムが長い.
実際,両者のファイルサイズを比べると,

-rw-r--r--  wheel  983 Dec 14 23:22 Pendulum.class
-rw-r--r--  wheel  555 Dec 14 23:22 S_Euler.class
-rwxr-xr-x  wheel  16033 Dec 14 23:26 Pendulum.out

この2桁サイズ違いのI/O遅延のせいかなぁ...と思ったりします.

それにしても仮想マシン経由でも遅くないですね.
ちなみに今回のテストに使ったJava仮想マシンは,FreeBSD上の

> java -version
java version "1.6.0_07"
Diablo Java(TM) SE Runtime Environment (build 1.6.0_07-b02)
Diablo Java HotSpot(TM) Client VM (build 10.0-b23, mixed mode)

ってやつを使いました.
こいつが性能良いのかな...詳しくないので,追々と調べてみます.

今度は一時間くらいかかるベンチをやらないと違いが分からないかもしれません.