torutkのブログ

ソフトウェア・エンジニアのブログ

マルチスレッド下におけるログ出力性能測定

一つのJavaプログラムについて、処理をマルチスレッドで並行性を持つように記述し、複数CPU(マルチコア)上でそのプログラムを実行することにより並列処理を実現しようとした際、ログ出力が実行性能にどれだけ影響を及ぼすのかを把握したい、と考えています。

プログラムの開発(特にデバッグ)においては、ログが使えないと苦労します。ただし、ログを埋め込むと、性能に影響を及ぼします。また、ログレベルを抑制し、実際にはログ出力がなかったとしても、ログレベルの判定処理が動くことで、多少の影響はあります。
最近のログ出力ライブラリはスレッドセーフに作られていますが、それは内部で排他区間を持つことになるので、並列処理においては排他による性能への影響は無視できません。

そこで、同一プロセス内でマルチスレッドにより並行実行するプログラムを作成・実行し、複数スレッドからログを出力すると、実行性能にどれだけ影響を及ぼすかを調査してみました。

プログラムは、CPUバウンドが主となるものとして「エラトステネスのふるい」を作成し、それをマルチスレッドで複数同時に開始し、各スレッドでの処理が完了するまでの時間を測定します。並行数は1つのとき、CPU(コア)数から1を引いた数のときを計ります。次に、処理の中で繰り返し実行される箇所にログ出力呼び出しを埋め、ログレベルを制御して実際にログを吐くとき、吐かないときの実行時間を測定します。

測定条件

項目 内容 備考
計算機 AMD Phenom II X6 1055T 2.8GHz
OS Windows 7 64bit
JavaVM Java 6 Up24 HotSpot Client VM build 1.9.1-b02
計測手段 System.nanoTime()
log4jバージョン 1.2.16
slf4jバージョン 1.6.1
logbackバージョン 0.9.28
素数検索範囲 〜10万
ログ呼び出し回数 10万+素数発見個数

測定結果

並行数が5のときの測定値は、各スレッドでの素数判定処理に要した時間を平均した値です。

Java標準ロギングAPIjava.util.logging)
並行数 ログコードなし ログレベルを出力抑制 ファイル出力(1) ファイル出力(2)
1 16.681ms 18.930ms 4401.680ms 4654.211ms
5 19.991ms 24.832ms 16747.193ms 19248.895ms
  • ファイル出力は、(1)FileHandler、(2)FileHandlerでローテーションあり(100MB)
slf4j & logback
並行数 ログコードなし ログレベルを出力抑制 ファイル出力(1) ファイル出力(2)
1 19.221ms 21.188ms 1022.111ms 6239.505ms
5 20.005ms 24.414ms 8802.492ms 38424.939ms
  • ファイル出力は、(1)FileAppender、(2)RollingFileAppender
log4j
並行数 ログコードなし ログレベルを出力抑制 ファイル出力(1) ファイル出力(2) ファイル出力(3)
1 17.118ms 38.703ms 4952.577ms 5190.590ms 40.658ms
5 18.776ms 64.124ms 38251.261ms 42264.759ms 12776.841ms
  • ファイル出力は、(1)FileAppender、(2)RollingFileAppender、(3)AsyncAppender経由FileAppender
注記1

ここでファイル出力時の処理時間も記載していますが、ファイル出力はI/Oバウンドが主であり、マルチスレッド性能とは別要因が支配的となるため、あくまで参考です。また、ログローテーションは実施していません。(ここではlogbackのファイル出力が秀逸に見えますが、手元の環境ではログローテーション実施時は逆にJava標準より遅くなっていました)

検討

並行数が1つのとき、ログ呼び出しコードがないときとログ呼び出しを追加(レベルは抑制)したときを比べると、Java標準ロギング、logbackともに2ミリ秒程度の負荷ですが、log4jについては20ミリ秒の負荷となっていました。
これは、ログAPIの設計の違いによるもので、ログ呼び出しメソッドの引数指定において、Java標準およびlogback(slf4j)では文字列と変数の結合は行っていませんが、log4jでは行っているからと思われます。

java.util.loggingのログメソッドは以下のように記述できます。メッセージ文字列と数値データをそのまま渡しています。

   LOGGER.log(Level.FINEST, "found prime {0}", i);

一方、log4jの場合、以下のようにログメソッドの引数に数値から文字列化および文字列結合結果を渡す必要があります。

   LOGGER.trace("found prime " + i);

なお、log4jのFAQには性能を気にするならif文をかませて、ログレベルが抑制される場合、文字列処理を回避するとありますが、ログメソッドを呼ぶ場所で以下のように書くのは、手間もあるし、何より主要機能ではないログ処理にコードが占められ、煩雑(可読性劣化)になります。

    if (LOGGER.isTraceEnabled()) {
        LOGGER.trace("checked number " + number);
    }

次に、並行数が5のとき、同じくログ呼び出しコードがないときとログ呼び出しコードを追加(レベルは抑制)したときを比べると、Java標準ロギング、logbackともに4-5ミリ秒程度の負荷ですが、log4jについては45ミリ秒の負荷となっていました。
並行数1のときに比べていずれもログ出力を追加したことによるオーバーヘッドが増えていますが、log4jについては顕著に増えています。log4jはスレッドセーフであるとFAQにはありましたが、マルチスレッド下での性能はいまいちのようです。

なお、log4jはSLF4Jと組み合わせることができます。この場合、SLF4JのAPIでは前述のように文字列結合を遅延しているので、log4j単独よりも性能向上が期待できます。

サンプルコード

エラトステネスのふるいを実装するクラス
public class Eratosthenes {
    private static final Logger LOGGER = Logger.getLogger(Eratosthenes.class.getName());
    
    /**
     * エラトステネスのふるい。
     * @param max 素数を探す整数範囲の最大値。2以上Integer.MAX_VALUE以下。
     * @return 素数のリスト(昇順)
     */
    public static List<Integer> sieve(int max) {
        BitSet numberBits = new BitSet(max + 1);
        numberBits.set(2, max + 1);  
        int number = 2;
        do {
            for (int i = number * 2; i <= max; i += number) {
                numberBits.clear(i);
            }
            LOGGER.log(Level.FINEST, "checked number {0}", number);
        } while (++number < max);
        
        List<Integer> primes = new ArrayList<Integer>();
        for (int i = 0; i <= max; i++) {
            if (numberBits.get(i)) {
                primes.add(i);
                LOGGER.log(Level.FINEST, "found prime {0}", i);
            }
        }
        return primes;
    }
}
マルチスレッドで呼び出すメインクラス(抜粋)
    private static void launch(int numThread) {        
        ExecutorService executor = Executors.newFixedThreadPool(numThread);
        Collection<Callable<Integer>> tasks = new ArrayList<Callable<Integer>>();
        for (int i = 0; i < numThread; i++) {
            tasks.add(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    long start = System.nanoTime();
                    List<Integer> results = Eratosthenes.sieve(100000);
                    long stop = System.nanoTime();
                    return (int) TimeUnit.NANOSECONDS.toMicros(stop - start);
                }
            });
        }

        List<Future<Integer>> futures = null;
        try {
            futures = executor.invokeAll(tasks);
            executor.shutdown();
        } catch (InterruptedException ex) {
            Logger.getLogger(RunWithJul.class.getName()).log(Level.SEVERE, "割り込み例外発生", ex);
            return;
        }
        for (Future<Integer> future : futures) {
            try {
                int takes = future.get();
                Logger.getLogger("").log(Level.INFO, "It takes {0} [us]", takes);
            } catch (InterruptedException ex) {
                Logger.getLogger(RunWithJul.class.getName()).log(Level.SEVERE, "割り込み例外発生", ex);
                Thread.currentThread().interrupt();
            } catch (ExecutionException ex) {
                Logger.getLogger(RunWithJul.class.getName()).log(Level.SEVERE, "タスク実行中に例外発生", ex);
            }
        }            
    }