好奇心から、JDKでのThreadLocal
の実装を検討してきましたが、次のことがわかりました。
_/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
_
これが単純なreturn (i + 1) % len
で実装できることはかなり明白に見えますが、これらの人は自分たちのことを知っていると思います。彼らがこれをした理由は何ですか?
このコードは、スレッドローカルマッピングを保持するためのカスタムマップ、GCを巧妙にするための弱参照など、パフォーマンスを重視しているため、これはパフォーマンスの問題だと思います。 Javaでモジュロが遅いですか?
この例では、パフォーマンス上の理由から%
は避けています。
div
/rem
の操作は、CPUアーキテクチャレベルでも遅くなります。 Javaだけではありません。たとえば、Haswellでのidiv
命令の最小レイテンシは約10サイクルですが、add
では1サイクルしかありません。
[〜#〜] jmh [〜#〜] を使用してベンチマークを行いましょう。
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class Modulo {
@Param("16")
int len;
int i;
@Benchmark
public int baseline() {
return i;
}
@Benchmark
public int conditional() {
return i = (i + 1 < len) ? i + 1 : 0;
}
@Benchmark
public int mask() {
return i = (i + 1) & (len - 1);
}
@Benchmark
public int mod() {
return i = (i + 1) % len;
}
}
結果:
Benchmark (len) Mode Cnt Score Error Units
Modulo.baseline 16 avgt 10 2,951 ± 0,038 ns/op
Modulo.conditional 16 avgt 10 3,517 ± 0,051 ns/op
Modulo.mask 16 avgt 10 3,765 ± 0,016 ns/op
Modulo.mod 16 avgt 10 9,125 ± 0,023 ns/op
ご覧のとおり、%
の使用は、条件式よりも約2.6倍遅くなります。除数(table.length
)は可変であるため、JITは説明したThreadLocal
コードでこれを自動的に最適化できません。
mod
はnotJavaでは遅くなります。これは、整数と浮動小数点数に対してそれぞれバイトコード命令irem
とfrem
として実装されます。 JITはこれを最適化するのに良い仕事をします。
私のベンチマーク( 記事 を参照)では、JDK1.8でのirem
呼び出しには約1ナノ秒かかります。それはかなり速いです。 frem
呼び出しは約3倍遅いので、可能な場合は整数を使用してください。
Natural Integers(例:配列インデックス)と2の累乗(例:8スレッドローカル)を使用している場合は、少し調整するトリックを使用して20%のパフォーマンス向上を得ることができます。