境界検査

現代的なプログラミング言語の多くと同様に、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

この getindexsum にインライン化されると、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 が伝播するのは一つのインライン化レイヤーだけ」という規則を上書きできます。このときアクセスが境界内 (あるいは境界外) とみなされるコンテキストはインライン化レイヤーをもう一つ余計に伝播するようになります。

境界検査の呼び出し階層

配列に対する境界検査の全体的な階層は次の通りです:

ここで 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 は添え字の型に対してだけ特殊化を行います。

広告