Alibaba Dragonwell JDK のコルーチン実装 Wisp2 の調査

  • AlibabaはOpenJDKのディストリビュータ
  • Dragonwell JDKという名前のJDKを開発、提供している
  • Dragonwell JDKにはWispというコルーチン実装が組み込まれている
    • Kotlin のコルーチンとは違って、JVMのスレッド実装を入れ替える構造になっている
  • どのような特性があるのか計測した

環境

コンポーネント 利用したバージョン
Docker Engine Docker Desktop 3.4.0 (Windows 10, WSL based engine)
Alibaba Dragonwell JDK dragonwell-8.7.7_jdk8u292-ga
Bellsoft Liberica JDK jdk-8u292-bellsoft

https://github.com/alibaba/dragonwell11 は Wisp2 に未対応だった

結果

それぞれの条件で5回ずつ計測。値は実行時間の平均値(ミリ秒)。

JDK Wisp2無効(ActiveProcessorCount=1) Wisp2無効(ActiveProcessorCount=4) Wisp2有効(ActiveProcessorCount=1) Wisp2有効(ActiveProcessorCount=4)
Dragonwell 108259.4 80422.6 1054.2 45304.6
Liberica 116458 70475.6 n/a n/a
  • t(Wisp2無効:ActiveProcessorCount=1) > t(Wisp2無効:ActiveProcessorCount=4)
    • Wisp2無効では、ActiveProcessorCount=4の実行時間は、ActiveProcessorCount=1より短い(75%くらい)
    • 👍CPUコア数に応じて処理速度が速くなっている
  • t(Wisp2有効:ActiveProcessorCount=1) > t(Wisp2有効:ActiveProcessorCount=4)
    • Wisp2有効では、ActiveProcessorCount=4の実行時間は、ActiveProcessorCount=1より大幅に長い(4300%くらい)
    • 👎CPUコア数に応じて処理速度が遅くなっている
  • t(Wisp2無効:ActiveProcessorCount=1) > t(Wisp2有効:ActiveProcessorCount=1)
    • ActiveProcessorCount=1では、Wisp2無効の実行時間は、Wisp2有効より大幅に長い(10271%くらい)
    • 👍シングルコアCPUならとても処理速度が速くなっている
  • t(Wisp2無効:ActiveProcessorCount=4) > t(Wisp2有効:ActiveProcessorCount=4)
    • ActiveProcessorCount=4では、Wisp2無効の実行時間は、Wisp2有効より長い(177%くらい)
    • 🤔マルチコアCPUだとそんなに処理速度は速くならない(1.7倍速くなっているから十分かもしれない)
  • t(Dragonwell) == t(Liberica)
    • Dragonwell JDK と Liberica JDK の実行時間はほとんど変わらない

考察

  • Wispの効果
    • 実行環境がシングルコアCPUなら、処理速度の大幅な向上が見込める
    • 実行環境がマルチコアCPUなら、逆にボトルネックになってしまう可能性がある
      • コルーチンスケジューラがマルチコアCPUを想定していないのかもしれない
  • デフォルトのスレッド実装
    • CPUコア数に応じて処理速度は速くなる傾向がある
      • ただしコア数に比例するわけではない

操作

実際に動かすコード。なんかピンポンする。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

public class PingPong {
    static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool();

    public static void main(String[] args) throws Exception {
        BlockingQueue<Byte> q1 = new LinkedBlockingQueue<>(), q2 = new LinkedBlockingQueue<>();
        THREAD_POOL.submit(() -> pingpong(q2, q1)); // thread A
        Future<?> f = THREAD_POOL.submit(() -> pingpong(q1, q2)); // thread B
        q1.put((byte) 1);
        System.out.println(String.format("%s,%s,%s,%d", args[0], args[1], args[2], f.get()));
    }

    private static long pingpong(BlockingQueue<Byte> in, BlockingQueue<Byte> out) throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1_000_000; i++) out.put(in.take());
        return System.currentTimeMillis() - start;
    }
}

Dockerfile はこれ。

FROM bellsoft/liberica-openjdk-alpine:8u292

RUN set -x \
 && wget -O /tmp/jdk8.tar.gz https://github.com/alibaba/dragonwell8/releases/download/dragonwell-8.7.7_jdk8u292-ga/Alibaba_Dragonwell_8.7.7_x64_linux.tar.gz \
 && mkdir -p /usr/lib/jvm/dragonwell8 \
 && tar -xzf /tmp/jdk8.tar.gz --strip-components 1 -C /usr/lib/jvm/dragonwell8 \
 && rm -rf /tmp/jdk8.tar.gz

計測スクリプト。結果は適当に編集する。

$ docker build -t test-wisp -f Dockerfile .
# Git for Windows only
$ export MSYS_NO_PATHCONV=1
$ for c in 1 4; do
docker run --rm -it --volume ${PWD}:/app -e JAVA_HOME=/usr/ilb/jvm/dragonwell8 --cpus ${c} test-wisp sh -c "/usr/lib/jvm/dragonwell8/bin/javac -d /tmp /app/PingPong.java; for _ in 1 2 3 4 5; do /usr/lib/jvm/dragonwell8/bin/java -XX:+UnlockExperimentalVMOptions -XX:-UseWisp2 -XX:ActiveProcessorCount=${c} -cp /tmp PingPong dragonwell8 noWisp2 ${c}; done"
docker run --rm -it --volume ${PWD}:/app -e JAVA_HOME=/usr/ilb/jvm/dragonwell8 --cpus ${c} test-wisp sh -c "/usr/lib/jvm/dragonwell8/bin/javac -d /tmp /app/PingPong.java; for _ in 1 2 3 4 5; do /usr/lib/jvm/dragonwell8/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseWisp2 -XX:ActiveProcessorCount=${c} -cp /tmp PingPong dragonwell8 useWisp2 ${c}; done"
done
$ for c in 1 4; do
docker run --rm -it --volume ${PWD}:/app -e JAVA_HOME=/usr/ilb/jvm/jdk-8u292-bellsoft-x86_64 --cpus ${c} test-wisp sh -c "/usr/lib/jvm/jdk-8u292-bellsoft-x86_64/bin/javac -d /tmp /app/PingPong.java; for _ in 1 2 3 4 5; do /usr/lib/jvm/jdk-8u292-bellsoft-x86_64/bin/java -XX:+UnlockExperimentalVMOptions -XX:ActiveProcessorCount=${c} -cp /tmp PingPong jdk-8u292-bellsoft-x86_64 noWisp2 ${c}; done"
done