境界検査
現代的なプログラミング言語の多くと同様に、Julia は配列にアクセスするとき境界検査を行ってプログラムの安全性を確認します。しかしタイトな内部ループなどの性能が重要な状況では、境界検査をスキップして実行時性能を向上させたい場合もあるでしょう。例えばベクトル化された命令 (SIMD 命令) を生成するときはループ本体に分岐が含まれてはいけないので、境界検査は (自動的に) 無効化されます。このような場合、Julia は @inbounds
マクロを挿入して特定のブロックで境界検査をスキップするようコンパイラに伝えます。ユーザー定義の配列型は @boundscheck
マクロを使うことで、文脈に依存した形で境界検査コードを実行できます。
境界検査の省略
@boundscheck(...)
マクロは特定のコードブロックが行う処理が境界検査であることを示す印を付けます。この印の付いたブロックが @inbounds(...)
ブロックにインライン化されるとき、そのブロックの削除がコンパイラに許可されます。コンパイラが @boundscheck
ブロックを削除するのは、そのブロックが呼び出し側の関数にインライン化されたとき、かつそのときだけです。例えば sum
メソッドを次のように書いたとします:
function sum(A::AbstractArray)
r = zero(eltype(A))
for i = 1:length(A)
@inbounds r += A[i]
end
return r
end
さらに配列風の独自型 MyArray
を次のように定義したとします:
@inline function getindex(A::MyArray, i::Real)
@boundscheck checkbounds(A,i)
A.data[to_index(i)]
end
この getindex
が sum
にインライン化されると、checkbounds(A,i)
の呼び出しは削除されます。関数のインライン化が複数のレイヤーに対してまとめて起こるときは、最大でも一つ下の @boundscheck
ブロックだけが削除されます。この規則はスタックのずっと上にあるコードによってプログラムの振る舞いが変わることを防ぐためにあります。
@inbounds
の伝播
コードの設計上の理由で、@inbounds
を伝播させたい場合でも @inbounds
と @boundscheck
の間に複数のレイヤーを入れなくてはならないという状況が存在します。例えばデフォルトの getindex
では getindex(A::AbstractArray, i::Real)
が getindex(IndexStyle(A), A, i)
を呼び、これが _getindex(::IndexLinear, A, i)
を呼びます。
Base.@propagate_inbounds
を関数に付けると、前述した「@inbounds
が伝播するのは一つのインライン化レイヤーだけ」という規則を上書きできます。このときアクセスが境界内 (あるいは境界外) とみなされるコンテキストはインライン化レイヤーをもう一つ余計に伝播するようになります。
境界検査の呼び出し階層
配列に対する境界検査の全体的な階層は次の通りです:
-
checkbounds(A, I...)
-
checkbounds(Bool, A, I...)
-
checkbounds_indices(Bool, axes(A), I)
- 各次元に対する
checkindex
- 各次元に対する
-
-
ここで A
は配列、I
は "要求された" 添え字です。axes(A)
は A
の "許される" 添え字からなるタプルを返します。
添え字が不当なとき checkbounds(A, I...)
はエラーを発生させますが、checkbounds(Bool, A, I...)
は false
を返します。checkbounds_indices
は配列に関する axes
タプル以外の情報を捨て、純粋な添え字同士の比較を行います: この仕組みにより、様々な配列型があったとしてもコンパイルされるメソッドの個数は比較的少なくて済みます。添え字はタプルとして指定され、通常は各次元を一つずつ比較することで行われます。このときに通常使われるのがもう一つの重要な関数 checkindex
です:
checkbounds_indices(Bool, (IA1, IA...), (I1, I...)) =
checkindex(Bool, IA1, I1) &
checkbounds_indices(Bool, IA, I)
checkindex
は単一の次元を確認します。ここまでに出てきた関数はどれも (エクスポートされていない checkbounds_indices
を含めて) docstring を持つので、?
でアクセスできます。
特定の配列型に対する境界検査をカスタマイズするときは checkbounds(Bool, A, I...)
を特殊化するべきです。ただし多くの場合では、axes
をその型に与えることで checkbounds_indices
を使った境界検査が行えるはずです。
新しい添え字型があるときは、まず特定の次元に対する単一の添え字を処理する関数 checkindex
の特殊化ができないかを考えてください。CartesianIndex
のような多次元添え字を表す独自型を使うときは checkbounds_indices
の特殊化が必要な場合もあります。
この階層はメソッドの曖昧性が生じる可能性を抑えるように設計されていることに注目してください。配列型に関する特殊化は checkbounds
であり、このメソッドでは添え字の型に対する特殊化を行いません。逆に checkindex
は添え字の型に対してだけ特殊化を行います。