クーの自由研究

マスターのかえるのクーは、弟子達の召喚術により新たな依り代を得てⅡ世として復活しました。

Javaの速度を測ってみる(爆速疑惑の検証)

本当にJavaはネイティブコンパイラに迫るほど速い!?

いま、老舗コーヒーを飲むべきか、新種?のカニを求むるべきか。それが問題な今日この頃です。かえるのクーの助手「井戸中 聖」(いとなか セイ)でございます。

ジャワコーヒー 

さて、最近(特にJava17LTSあたり?)の オブジェクトnew オペレーションが爆速になっている疑惑について検証します。さび色蟹(Rust)に食指が動いておりましたが、コーヒー(Java)党に鞍替えするかの分岐点におります。

 VS    (フェリスくん)

これまでのおはなし

ひさしぶりにJavaのmain()プログラムをベンチマークしたらC++より速い!?現象がおきました。調査の結果、

・Javaがマルチスレッドで全力で動作する+

・JavaのJIT(ジャストインタイムコンパイラ)の改善らしい+

・C++/Rustは連動配列が苦手!

により起こった逆転現象だったことがわかりました。

いつからこんなにJavaが速くなったのでしょう。というのが今回のお題です。

「自分のやりたいこと *1」の前段階の関するベンチマークなので、一般的な事案ではないのでご了承ください。

Javaのいままでの感想

ちなみに:Java業務開発の現場(いわゆるSI:システムインテグレーション)では10年前の枯れ切った技術を使うことが多くあります。(いわゆる枯れ専)最近は(Javaのフレームワークである)Springが標準でつかわれるようになっています。少し前はJava8が多かったのですが、(ようやく)Java11の指定のあるプロジェクトが増えてきた感じです。Oracle社のJava有償化しちゃうぜ宣言!(勘違い *2 )事件がいまだに尾を引いている感じで。Javaのバージョン更新/選定には必要以上に皆慎重です。(枯れ専なので、LTSバージョン以外は評価:ベンチマークすらしない状況です。)

個人的にはJavaは「お仕事で使う言語」なので、趣味のプログラムで使用したことがありません。なお、お仕事でのJavaの感想は、「SpringなどのWebコンテナ上で動かす限りは、Javaは極めて優秀で安定してる」感じです。(コンテナのおかげでマルチスレッド、マルチタスク動作です。ただし速いと思ったことはありません。)それ以前のJavaは遅くて重いイメージ(もしくは体験)しかありません。

それが、単体でC++/Rustより速かったのですから、混乱しきりでした。(中間言語の実行でVMがマルチスレッドするのと、ネイティブがマルチスレッドするのはまったく別の次元なので。(JITコンパイルがあるにせよ)VMがピュアコンパイルより速いなんて。。。)JavaはJITしたら、通常コーディングはシングルスレッドになると思い込んでました。

実験要領

・Javaの簡単なクラスとそれを使用して4億/20億個のオブジェクトをnewして、30秒間保持後、集計計算する時間を測定する。

・前回からの調整として:lombok(get/setを自動生成するetc)を使用していたが、今回はlombokを使用せず自分でget/setを書く。

・前回eclipseを使用したが、今回は純粋なJavaJDKのみを使用する。

・バージョンを遡りながら、プログラムの実行時間を実測する。

(バージョンは1つづつ遡る。現実的でなくなるレベルで終了する。)

 

共通レギュレーション

・CPU Intel CoreX i9-9940X(Socket2066)

・Memory 128GB (DDR4-3200):前回は64GB、今回実験用に組み替え。

・Javaの初期メモリ割り当ては16GB/最大メモリ割り当ては120GB

・オブジェクトは4億/20億個 (スワップしない程度の量)

ソースコードは末尾に貼ります。(この前とほとんど同じですが記録のため。)

実験結果:

(例によって実験しながらこの記事を書いています。結果は随時更新します。)

実験の一例(20億個をJava SE 17LTSで実行した例です)

コンパイル

C:\work\Java\JavaSE17\jdk-17.0.7_windows-x64_bin\jdk-17.0.7\bin\javac.exe -encoding utf8 artifact/MainClass.java artifact/Neuron.java 

実行

C:\work\Java\JavaSE17\jdk-17.0.7_windows-x64_bin\jdk-17.0.7\bin\java.exe -Xms16g -Xmx120g artifact/MainClass

この測定ではデータ生成: 44.280 sec、集計: 12.559 secでした。爆速です!

各バージョンの測定は随時記載します。なお、対象は64Bit版です。

JDK

 

Vendor

(Year)

準備/集計(4億)

sec

準備/集計(20億)

sec

備考

(Version)

JavaSE20 Oracle(2023.3) 9.033 / 2.380 45.331 / 12.634 (20.0.1)
OpenJDK20 Oracle 9.204 / 2.406 46.140 / 12.924 (20.0.1)
corretto-20JDK Amazon 9.027 / 2.336 45.269 / 12.480 (20.0.1)
TemurinJDK20 Eclipse Adoptium 9.128 / 2.313 46.811 / 13.156 (20.0.1)
JavaSE19 Oracle(2022.9) 9.053 / 2.396 45.619 / 12.866 (19.0.2)
OpenJDK19 Oracle 9.183 / 2.322 44.749 / 15.431 (19 +36)
JavaSE18 Oracle(2022.3) 8.627 / 2.271 44.062 / 13.896 (18.0.2.1)
OpenJDK18 Oracle 8.502 / 2.476 42.790 / 14.974 (18 +36)

JavaSE17

Oracle(2021.9) 9.153 / 2.219 44.643 / 12.380 (17.0.7)
OpenJDK17LTS Oracle 9.168 / 2.519 44.942 / 12.821 (17 +35)
corretto-11JDKLTS Amazon 9.048 / 2.249 44.746 / 12.595 (17.0.7)
TemurinJDK17LTS Eclipse Adoptium 9.180 / 2.241 44.735 / 12.593 (17.0.7)
microsoftJDK17LTS Microsoft 9.046 / 2.268 45.932 / 13.215 (17.0.7)
JavaSE16 Oracle(2021.3) 9.273 / 2.518 46.137 / 12.622 (16.0.2)
OpenJDK16 Oracle 9.243 / 2.598 46.922 / 12.350 (16 +36)
JavaSE15 Oracle(2020.9) 8.904 / 2.468 45.380 / 14.060 (15.0.2)
OpenJDK15 Oracle 8.998 / 2.510 46.085 / 14.716 (15 +36)
JavaSE14 Oracle(2020.3) 9.372 / 3.968 48.705 / 15.781 (14.0.2)
OpenJDK14 Oracle 9.412 / 4.035 49.774 / 16.893 (14 +36)
JavaSE13 Oracle(2019.9) 9.833 / 4.342 48.098 / 20.589 (13.0.2)
OpenJDK13 Oracle 9.950 / 4.385 48.824 / 22.638 (13 +33)
JavaSE12 Oracle(2019.3) 9.592 / 4.592 48.304 / 23.229 (12.0.2)
OpenJDK12 Oracle 9.906 / 4.578  47.752 / 24.962 (12 +32)
JavaSE11 Oracle(2018.9) 9.591 / 4.593 47.895 / 21.573 (11.0.18)
OpenJDK11LTS Oracle 9.897 / 4.574 48.937 / 20.279 (11.0.2)
corretto-11JDKLTS Amazon 9.622 / 4.577 51.050 / 22.099 (11.0.19)
TemurinJDK11LTS Eclipse Adoptium 9.725 / 4.635 48.700 / 20.174 (11.0.19)
microsoftJDK11LTS Microsoft 9.825 / 4.628 48.450 / 18.652 (11.0.19)
JavaSE10 Oracle(2018.3) 11.961 / 4.218 62.005 / 16.684 (10.0.2)
OpenJDK10 Oracle 11.447 / 4.452 58.525 / 15.657 (10 +44)
JavaSE9 Oracle(2017.9) 11.357 / 4.220 55.234 / 16.004 (9.0.4)
OpenJDK9 Oracle 12.057 /
4.126
57.413 / 14.772 (9 +181)
JavaSE8(up) Oracle 289.891 / 2.431 2246.992 / 2331.344 (1.8.0_361)20億はSWAP発生
JavaSE8 Oracle(2014.3) 301.590 / 2.546 2220.679 / 3171.594 (1.8.0_202)20億はSWAP発生
OpenJDK8LTS 64Bit版入手できず . . .
corretto-8JDKLTS

Amazon

248.530 / 2.370 2078.166 / 2446.272 (1.8.0_372)20億はSWAP発生
TemurinJDK8LTS Temurin 318.823 / 2.358 2630.939 /17679.078 (1.8.0_372)20億はSWAP発生
JavaSE7 Oracle(2011.7) 160.002 / 68.439 1460.139 / 528.810 (1.7.0_80)
OpenJDK7 64Bit版入手できず . . .
JavaSE6 SunMicro(2006.12) 91.751 / 43.463 980.238 / 170.677 (1.6.0_45)
JavaSE5 SunMicro(2004.9) ※エラー測定不能 - (1.5.0_22)実行時エラー

 

お題の答え:

Javaはいつから速くなったのでしょう。

答え→速くなったのは OpenJDK9(2017.9)からです。

そして、もっと速くなる予定です。(GraalVMは(JITではない)静的事前コンパイルもサポートしていくらしい(?それって普通のピュアコンパイラでVMの範疇外では?)。本件と関係ないけど、JavaScript/Pythonなどの高速実行環境としての地位も狙っているらしい。)

実行速度改善が継続しています。現時点では(テスト範囲では)OpenJDK17LTS以降は速度が安定しています。(JDK20までは変化は微小です)

・OpenJDK9からメモリ管理が大幅に変更になっています。

・Hotspot/JITに関する実装が別物になっています。

興味のある方はご覧ください。

Javaの新JITコンパイラ、Graalを解説

1.3 JFRの歴史 - JRockitからOpenJDKまで · 入門: JDK Flight Recoder

https://www.oracle.com/webfolder/technetwork/jp/javamagazine/java-mj12-architect.pdf

(かなり前の記事だが、この実施プロジェクトがJdk9に反映されたらしい)

https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MA16-JIT.pdf

 

JDK9:

JEP 214: Remove GC Combinations Deprecated in JDK 8 (JDK8のGCひどいのでやめ)

JEP 243: Java-Level JVM Compiler Interface(インターフェースを変えて"Javaで"コンパイラを作成することを容易にした。GraalVMもこれを使うよ。)

JEP 284: New HotSpot Build System (JITが劇的に改善)

より最近の動向として

オラクル、OpenJDKに静的なネイティブイメージの生成機能を組み込む方針を明らかに。GraalVMのOpenJDKへのコントリビュートで - Publickey

Javaのネイティブバイナリ生成可能なGraalVMの全機能が無料に、最適化コンパイラやG1ガベージコレクションを含む。本番環境でも利用可能 - Publickey

ヤケドしそうなくらい熱いです!

実験後の所感

OpenJDK8はメモリ使用量の挙動もみていても、メモリ管理がそれ以前のバージョンと比べてバランスが崩れている感が強いです。これがLTSだったと思うと悲惨な感じがしますが、(わたくしの)開発現場では問題視されたことはなかったです。

それにしてもOpenJDK9の速度向上は劇的です。もはやVMの皮をかぶったネイティブコンパイラ(羊の皮をかぶった狼的な?)な感じすらします。

最近の技術が手軽に使えるコンパイラを欲していましたが、Javaに傾きかけています。(個人的にCは好きだけど、取り扱い注意な刃物すぎ。C++は言語思想がいまだに受け入れられない。Pythonのコンパイラ系は扱いが難しく、決定打が(個人的に)まだない。)悪く言えば、Rustの学習のモチベーションが激下がりです。(Javaで十分じゃん的な感じ)

もし、貴方もJavaに対して10年前のイメージを持たれているのであれば、情報をupdateしましょう!Javaはネイティブコンパイラにハンディなしで太刀打ちできる言語になってきています。そして、メニーコアな環境との相性は抜群です!昨今の言語ベンチマークでのJava善戦は決して眉唾モノではありませんでした。🦀

Java OpenJDK17LTS はお勧めです!

速度が要求されるシミュレーション用途でも使えます。

『半年毎にリリースする!』のを本気にやっていることに情熱を感じます!

ところで、

縄文コヒー(javaCoffe)☕、弥生(安い)蟹🦀、どっちが好き?

www.youtube.com

蛇足:

な~んだ。最初からChatGPT大先生に聞けばよかった。

一番最近のJava速度向上(の大きな話題)はJDK9のGraalVM/JITコンパイラの導入で、ネイティブイメージと高速化が可能となったことをご存知でした。

プロンプト: Javaはいつから速くなったのでしょう。

大先生:~~~JDK 9(2017年):GraalVM JITコンパイラが導入され、ネイティブイメージの生成や高速化が可能となりました。~~~Javaのパフォーマンスは継続的に改善されており、~~~

先生はなんでも知っている。しらないことでも知っている。(話をつくる)

どうでもいいソース

MainClass.java

package artifact;

import java.util.ArrayList;
import java.util.List;

public class MainClass {
    final static int  NEURO_COUT = 2_000_000_000; // オブジェクト個数
    final static int SLEEP_TIME = 30_000; // 待機時間設定(msec)

    public static void main(String[] args) {
        System.out.println("Hello World!(Java)");
        System.out.println("Java=" + System.getProperty("java.vendor")
            + "|" + System.getProperty("java.version")
            + "|JavaVM=" + System.getProperty("java.vm.specification.vendor")
            + "|" + System.getProperty("java.vm.specification.version")
        );
        Runtime rt = Runtime.getRuntime();
        // reportMemory(rt);
       
        long startTime = System.currentTimeMillis();
        //Map<Integer, Neuron> nMap = new HashMap<Integer, Neuron>();
        List<Neuron> nList = new ArrayList<Neuron>(NEURO_COUT);
        // データをNEURO_COUT件準備 JavaではclassをNewして準備
        System.out.println(String.format("準備オブジェクト数:%,d", NEURO_COUT));
        for (Integer i = 0; i < NEURO_COUT; i++) {
            Neuron nr = new Neuron();
            nr.setActivate(i);
            nr.setBias(1.01);
            nList.add(nr);
            //nVec.add(nr);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(String.format("データ生成処理時間:%.3f sec" ,(endTime - startTime)/1000.0));

        // キャッシュ等抑制のため開始を少し待つ(おまじない)
        System.out.println(String.format("待機中:%.3f sec" ,SLEEP_TIME / 1000.0));
        try {
            Thread.sleep(SLEEP_TIME);
        } catch (InterruptedException e) {
        }
        double sum = 0.0;
        double dummy = 0.0;
        startTime = System.currentTimeMillis();

        System.out.println("calc start");
        // データを件数分加算
        for (Integer i = 0; i < NEURO_COUT; i++) {
            Neuron nr = nList.get(i);
            sum += nr.getActivate();
            dummy += nr.getBias();
        }
        endTime = System.currentTimeMillis();
        System.out.println("sum: " + sum);
        System.out.println("dummy" + dummy);
        System.out.println(String.format("データ集計処理時間:%.3f sec" , (endTime - startTime)/1000.0));
        reportMemory(rt);
    }
   
    private static void reportMemory(Runtime rt) {
        System.out.println("=== Memory ===");
        System.out.println(String.format("total: %,d bytes",rt.totalMemory()));
        System.out.println(String.format("free : %,d bytes" ,rt.freeMemory()));
        System.out.println(String.format("max  : %,d bytes" ,rt.maxMemory()));
    }
}

Neuron.java

package artifact;

public class Neuron {
    double bias;
    double activate;

    public double getBias() {
        return this.bias;
    }
    public void setBias(double bias) {
        this.bias = bias;
    }

    public double getActivate() {
        return this.activate;
    }
    public void setActivate(double activate) {
        this.activate = activate;
    }
   
}

*1:大脳160億個のニューロンをJavaのオブジェクトモデルで動作させ、人間の脳をシミュレーションし、あわよくば自分の脳を蒸留学習させちゃう実験w

*2:OpenJDKはいつだって商用無償です。Java全体が有償化に向かう訳では決してありません。なおOracle JavaSExxは商用有償になっているのでご注意を!