Javaでのnullチェックのパフォーマンス

Javaでのプログラムでは、Optionalが入ったとはいえ、nullとのつきあいは依然として重要です。
そんな中で、nullが入ってると困る処理を書くときには、nullチェックを行うほうが安全です。
そのとき、どのようにnullチェックを行うかというのが問題になります。


Java7からは、Objects.requiredNonNullというメソッドが導入されたので、このメソッドを使って、以降の処理でnullじゃないことを保証するということができます。
けど、このrequiredNonNullが遅いんじゃないかという話があるので、どう遅いのか調べてみました。
requireNonNullと同様に、値がnullだったらNullPointerExceptionを吐いて、null以外だったら素通りするという書き方のひとつに、getClassメソッドを呼び出すというものがあるので、これと比べてみます。あと、同様にtoStringを呼び出すというのも書き方としてはgetClassと同等なので、これも比べてみます。


ということで、値がnullになる割合を変えながら1000万回呼び出した時間を計ってみました。ソースは最後に載せてます。
実行環境はWindows 7 64bitで、Core i7 2600K、JDK 8u45です。
結果はこんな感じ

null率 0% 30% 50% 70% 100%
requireNonNull 130.6ms 2771.5ms 4437.5ms 6164.6ms 8649.8ms
toString 1135.1ms 807.6ms 586.1ms 417.8ms 125.7ms
getClass 127.6ms 154.8ms 165.4ms 152.6ms 126.8ms
noOp 126.5ms 153.9ms 164.5ms 152.7ms 126.2ms

みてみると、requireNonNullは、nullがないときは速いのですが、nullの割合が増えていくと遅くなっていきます。
一方で、toStringは、nullではないときは文字列を構築するので遅いのですが、nullの割合が増えると速くなっていきます。30%の時点でrequireNonNullと逆転してますね。
getClassでは、nullの割合に関係なく、だいたい速く、何もしない場合と同等です。


requireNonNullがnullのときに遅いのは、NullPointerExceptionオブジェクトの生成に時間がかかるからのようです。
toStringやgetClassの呼び出しなど、nullに対するメンバアクセスでのNullPointerExceptionの発生では、キャッシュされたオブジェクトを利用しているらしく、時間がかかっていません。
Javaでのnullチェックの話は、ここのJohn Roseのコメントが詳しいです。まだちゃんと読んでないけど。
#JDK-8042127 Performance issues with java.util.Objects.requireNonNull - Java Bug System


ということで、nullが来ない前提ならrequireNonNullでもほぼゼロコストで問題ないけど、nullが高頻度に発生するならgetClass呼び出しでNullPointerExceptionを発生させるのが効率がいい、ってことになります。
まあ、requireNonNullは、名前のとおり、nullが来ない前提であることを示すために書くわけで、目的どおりに使いましょうってことですね。


ところで、getClassやnoOpの速度、おもしろいことになってますね。
nullの場合もnullじゃない場合もだいたい同じ速度なのに、割合が50%に近づくと遅くなってます。コード上の何かの処理が重いのであれば、0%か100%か、どちらかが速くて、比率によって遅くなっていくはずです。requireNonNullやtoStringの場合のように。


JVMでは、分岐予測のようなことを行っていて、条件分岐が毎回同じ側になるのであれば、そちら側が速くなるような最適化を行います。
そのため、計測コード中のパラメータがnullかそうではないか決めるところで、毎回同じ側を通るような、0%や100%の場合にはその最適化がはまるために速くなります。50%に近づくと、そのような最適化が行えなくなっていくので、遅くなっていくのでしょう。
getClassの呼び出し自体はどちらでも処理時間がかわらないような仕組みになっているために、計測コードの最適化の影響が出てきたのだと思います。
こういった最適化の話は、最近出たJavaパフォーマンスに載っています。Javaで書いたプログラムを実行させる人は一度読んでおくといいと思います。

Javaパフォーマンス

Javaパフォーマンス

import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.IntStream;

public class NullCheckPerformance {

    public static void main(String[] args) {
        System.out.println(System.getProperty("java.runtime.version"));
        
        bench("warming  up", NullCheckPerformance::test_toString, 50);
        IntStream.of(0, 30, 50, 70, 100).forEach(rate -> {
            bench("requireNonNull", NullCheckPerformance::test_requireNonNull, rate);
            bench("toString      ", NullCheckPerformance::test_toString, rate);
            bench("getClass      ", NullCheckPerformance::test_getClass, rate);
            bench("no operation  ", NullCheckPerformance::test_NoOp, rate);
        });
    }
 
    /** ベンチ */
    static void bench(String name, Consumer<Object> cons, int nullRatio){
        Object o = new Object();
        //ウォームアップ
        //non null
        for(int i = 0; i < 20; ++i){
            cons.accept(o);
        }
        //null
        for(int i = 0; i < 20; ++i){
            cons.accept(null);
        }
        
        int nullCount = 0;
        
        //乱数初期化
        Random r = new Random(1234);
        //時刻計測
        long start = System.nanoTime();
        //繰り返し
        for(int i = 0; i < 10_000_000; ++i){
            //パラメータがnullかどうか決める
            Object p;
            if(r.nextInt(100) < nullRatio){
                p = null;
                //カウントしておく
                ++nullCount;
            }else{
                p = o;
            }
            //呼び出し
            cons.accept(p);
        }
        //計時
        long time = System.nanoTime() - start;
        //結果表示
        System.out.printf("%s(null:%3d%%) - %.1fms null:%3.1f%% %n", 
                name, nullRatio, time/1000/1000., (double)nullCount / 100_000);
    }
    
    static void test_requireNonNull(Object o){
        try{
            Objects.requireNonNull(o);
        }catch(NullPointerException ex){
            // do nothing
        }
    }
    static void test_toString(Object o){
        try{
            o.toString();
        }catch(NullPointerException ex){
            // do nothing
        }
    }
    static void test_getClass(Object o){
        try{
            o.getClass();
        }catch(NullPointerException ex){
            // do nothing
        }
    }
    static void test_NoOp(Object o){
        // do nothing
    }
}