windowsとlinuxのjavaコード実行時間の比較をしたあと気になったので調べることにしたんご。

 

【お題1】 long型整数1000億回加算処理時間

■c言語の場合

せ70> cat long.c
#include <stdio.h>
int main(void)
{
    long int i = 0;
    while(++i < 100000000000)
    {
    }
    printf("結果: %ld\n",i);
    return 0;
}

せ70> gcc long.c

せ70> time ./a.out

結果: 100000000000     ←1000億
real    3m13.558s          ←javaより遅い!なんで?
user    3m12.832s
sys     0m0.013s

■bashの場合

せ70> cat long.sh
#!/bin/bash
i=1
while ((i<= 10000000))         ←1000万
do
    ((i++))                           ←「((expression))」はbash組込コマンド
done
echo $i

せ70> time ./long.sh
10000001
real    0m54.967s           ←くっそ遅い!
user    0m47.747s
sys     0m7.007s

※ちなみに2回目は1m5.750sだった

せ70> cat long2.sh
#!/bin/bash
for ((i=1;i<=10000000;i++))       ←1000万
do
    :                                      ←何もしないbash組込コマンド
done
echo $i

せ70> time ./long2.sh
10000001
real    1m12.678s                  ←くっそ遅い!
user    1m3.821s
sys     0m8.578s

せ70> cat long3.sh
#!/bin/bash
i=1
while ((i<= 10000000))
do
    let i++                            ←letもbash組込コマンド
done
echo $i

せ70> time ./long3.sh

10000001
real    1m28.836s
user    1m19.534s
sys     0m8.893s

せ70> cat long4.sh
#!/bin/bash
i=0
while [ $i -lt 10000000 ]           ←1000万
do
    let i++
done
echo $i

せ70> time ./long4.sh
10000000
real    1m51.779s
user    1m40.069s
sys     0m11.273s

ちなみにループ内に外部コマンドを実行するとループ回数fork & execされるので異次元の遅さになる。

せ70> cat long5.sh
#!/bin/bash
for ((i=1;i<=100000;i++))                   ←10万やで!
do
    /usr/bin/echo "" > /dev/null      ←外部コマンドをfork & exec
done
echo $i

せ70> time ./long5.sh
100001
real    1m36.741s            ←激おそむかちゃっかファイヤー
user    1m3.495s
sys     0m42.305s

 

■結果

・java     :windowsもlinuxも約32秒

・c言語   :linuxで約194秒

・bash    :linuxで推定60万秒以上(外部コマンドのfork & exec無しで)

・bash    :外部コマンドのfork & execを数万回以上のループ内では使用禁止!

 

【お題2】 倍精度実数100億回加算処理時間

■c言語の場合

せ70> cat double.c
#include <stdio.h>
int main(void)
{
    long int i = 0;
    double x = 0.0e+0;
    while(++i <= 10000000000)
    {
        x = x + 1.0e+0;
    }
    printf("結果: %lf\n",x);
    return 0;
}

せ70> gcc double.c
せ70> time ./a.out

結果: 10000000000.000000
real    0m29.072s        ←javaより遅い!なんで?
user    0m28.952s
sys     0m0.005s

■結果

・java     :windowsもlinuxも約16秒

・c言語   :linuxで約29秒

 

 

【お題3】 倍精度実数100億回加算処理時間(変則版)

※100億回ループを回して1が3の倍数のときだけ倍精度実数1を加算する

■c言語の場合

せ70> cat double2.c
#include <stdio.h>
int main(void)
{
    long int i = 0;
    double x = 0.0e+0;
    while(++i <= 10000000000)
    {
        if(i % 3 == 0) {
        x = x + 1.0e+0;
        }
    }
    printf("結果: %lf\n",x);
    return 0;
}

せ70> gcc double2.c
せ70> time ./a.out

結果: 3333333333.000000
real    0m29.249s              ←やはりjavaより遅い
user    0m29.088s
sys     0m0.016s

※1回目は約26秒だった。

■結果

・java     :windowsもlinuxも約21秒

・c言語   :linuxで約29秒

 

理論上オーバーヘッドが一番少ないはずのcで書いたプログラムがjvm上で実行しているプログラムより遅いはずがない。

上記の結果から言えることは、恐らくだがgccは一番ズルをしてないコンパイラで、javacはコンパイルの段階で相当ズルをしていると思われる。

 

【分かったこと】

・ベンチマークテストは難しい。

・コンパイラは最適化によって馬鹿正直に単純なループ処理をしていないと思われる。

 

【お題】 コンパイラにズルさせないでベンチマークを手軽に比較する方法の模索

・世にある各種ベンチマーク計測ツールは、グラフィック処理性能とかサーバやミドルウェア用途と関係ない項目を含んでる。あるいは、計測項目が細かすぎてわからないw。さらに計測項目ひとつひとつがブラックボックスになってて何を調べているのか把握しづらい。

ベンチマーク計測ツールよりコンパイラの方が「賢かったら」上記のようにズルされてしまっててもはや「何を測ってるのか分からない」。ツールをうわべ的にいじってるだけで終わることになる。

・こちらのサイトではコンパイラがズルできなさそうな「ライプニッツ級数」とやらを計算させて、各プログラム開発言語の「地」の速度比較を試みていらっしゃる

色々な言語で計算速度を比較してみた

すばらすい。

さっそくまねしてみるんご。

■java

せ70> cat LeibnizFormula.java
public class LeibnizFormula {
  public static void main(String[] args){
      int i;
      double s=0d;
      for(i=0; i<=1e8; i++){
        s += Math.pow(-1.0, i)/(2.0*i+1.0);
      }
      System.out.println("Ans:" + 4*s);
    }
}

せ70> javac LeibnizFormula.java

・CentOS7.3で実行

せ70> time java LeibnizFormula
Ans:3.141592663589326
real    0m9.164s
user    0m9.074s
sys     0m0.047s

・windows server 2012R2で実行

PS C:\Users\Administrator\Desktop\20180829> measure-command { java LeibnizFormula }
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 10
Milliseconds      : 46
Ticks             : 100462185
TotalDays         : 0.000116275677083333
TotalHours        : 0.00279061625
TotalMinutes      : 0.167436975
TotalSeconds      : 10.0462185
TotalMilliseconds : 10046.2185

 

■C言語

・CentOS7.3で実行

せ70> cat LeibnizFormula.c
# include <stdio.h>
# include <math.h>
int main(void){
  int i;
  double s = 0.0;
  for(i = 0; i<=1e8; i++){
    s += pow(-1.0, i) / (2.0 * i + 1.0);
  };
  printf("Ans:%.16f\n",4*s);
  return 0;
}

せ70> gcc -lm -o LeibnizFormula.out LeibnizFormula.c
せ70> time ./LeibnizFormula.out

Ans:3.1415926635893259
real    0m4.419s
user    0m4.393s
sys     0m0.006s

 

【結果】

・java(CentOS7.3)                          9.164s
・java(windows Server 2012R2 x64)  10.046s
・c言語(CentOS7.3)                       4.419s
 
→やっと直観と合致した結果が得られたんご。
 

【サーバアプリやミドルウェアの処理速度計測方針(案)】

・IO処理部分と「オーソドックスな処理部分」を区別して考える。

・GPUのCUDAとかハイパースレッディングとかを駆使したプログラム部分を「オーソドックスな処理部分」と区別して考える。

・「オーソドックスな処理部分」とは、メモリとCPU間だけで完結する、すべてのプログラム開発言語共通の基本的な処理(算術演算、論理演算、配列処理、ループ、条件分岐、関数呼び出しなど)をする部分と考える。

 

サーバアプリやミドルウェアのプログラムはおそらく大抵「オーソドックスな処理」が大半を占め、言い換えれば「オーソドックスな処理速度」の比較で議論してよいと思われる。

もし、上記の処理速度にオーダーをまたぐような差異があれば何か実装上の問題があると考えられる。

ところで、ベンチマーク計測用のプログラムが「単純すぎて」コンパイラにズルされてしまったら正確な計測はできない。(それでもオーダーをまたいでなければ異常なしともいえる)

3GHzのCPUは、1秒間に30億回のCPU命令しか実行できない。

ゆえに上記の検証で行ったような1000億回の加算をまじめには実行していない。

コンパイラの最適化とは例えば「1から10000までの数値をループで加算」って処理をコンパイラは「(1+10000)*10000/2」ってズルしちゃうかもしれない(適用できる公式を検索して適用してしまう)。 しかし、逆を言えば、メジャーなプラットフォーム用のメジャーなコンパイラに月並みなテストコードを当てはめるのであれば、どのコンパイラでも「同じようなズル」をするわけだからたとえ1000億回の加算をまじめにやってなくても同じようなズルを使って処理していると予測できる。それでもコンパイラによってはズルの箇所とレベルが違うので差異が生じるということだと思う。この差異に対してwindowsとlinux上のjavaの時は重要視しないが、c言語とjavaの場合は無視できないという微妙な勘がはたらく。

勘があてになりそうにないときは、できるだけコンパイラがズルできなさそうな「メモリ⇔CPU間で完結する、適度に単純なテストプログラム」を探し出してテストするって流れになると思う。

(ここでも勘を使用しているwwwが、上記URLの方が採用した「ライプニッツ級数」ならC言語とjavaのベンチマーク比較に使えると直感できたのかというと、円周率の計算はスーパーコンピュータのベンチマーク競争によく使われるテーマでコンパイラのズルの上を行っていると直感して採用したんご。)

もちろん、サーバアプリやミドルウェアより高度なアルゴリズムを使った科学技術計算用プログラムの処理速度のベンチマークを計測するのには役に立たないと思われるが。