配列がビットごとの and を介して計算されるときに境界チェックを排除できるかどうかを調べるために、簡単なベンチマークを作成しました。これは基本的に、ほぼすべてのハッシュ テーブルが行うことです。
h & (table.length - 1)
へのインデックスとして、table
はh
またはhashCode
派生値です。結果は、境界チェックが排除されていないことを示しています。
私のベンチマークの考え方は非常に単純です。2 つの値i
と を計算j
します。両方とも有効な配列インデックスであることが保証されています。
i
ループカウンターです。配列インデックスとして使用されると、境界チェックがなくなります。j
として計算されます。x & (table.length - 1)
ここで、x
は反復ごとに変化する値です。配列インデックスとして使用される場合、境界チェックは排除されません。
関連する部分は次のとおりです。
for (int i=0; i<=table.length-1; ++i) {
x += result;
final int j = x & (table.length-1);
result ^= i + table[j];
}
他の実験では
result ^= table[i] + j;
代わりは。タイミングの違いはおそらく 15% です (私が試したさまざまなバリアントでほぼ一貫しています)。私の質問:
- これには、バウンドチェックの排除以外に考えられる理由はありますか?
- バウンドチェックの削除がない理由がわからない複雑な理由があります
j
か?
回答の要約
MarkoTopolnik の答えは、それがすべてより複雑であり、境界チェックの排除が勝利であるとは限らないことを示しています。特に彼のコンピューターでは、「通常の」コードは「マスクされた」コードよりも遅くなります。これは、この場合実際に有害であることが示されている追加の最適化を許可しているためだと思います(現在のCPUの複雑さを考えると、コンパイラーは確実に知ることさえほとんどありません)。
leventovの答えは、配列の境界チェックが「マスク」で行われ、それを排除することでコードが「通常」と同じくらい高速になることを明確に示しています。
x & (0-1)
Donal Fellows は、長さが 0 のテーブルではマスキングが機能しないという事実を指摘していますx
。したがって、コンパイラが実行できる最善の方法は、バウンド チェックを長さ 0 のチェックに置き換えることです。しかし、長さゼロのチェックはループから簡単に移動できるため、これはまだ価値があります。
提案された最適化
a[x & (a.length - 1)]
if and only ifの等価スローa.length == 0
により、コンパイラは次のことを実行できます。
- 配列アクセスごとに、インデックスがビットごとの and を介して計算されているかどうかを確認します。
- その場合、いずれかのオペランドが長さから 1 を引いたものとして計算されたかどうかを確認してください。
- その場合は、境界チェックを長さゼロのチェックに置き換えます。
- 既存の最適化に任せましょう。
このような最適化は、SSAグラフの親ノードのみを参照するため、非常にシンプルで安価です。多くの複雑な最適化とは異なり、1 つのチェックをわずかに単純なチェックに置き換えるだけなので、有害になることはありません。そのため、ループの外に移動できなくても問題はありません。
これを hotspot-dev メーリング リストに投稿します。