2015年01月01日

Juliaの@inboundsを使ってみる

公式サイトの説明によると、@inboundsはこんな意味らしい。

Use @inbounds to eliminate array bounds checking within expressions.
Be certain before doing this.
If the subscripts are ever out of bounds, you may suffer crashes or silent corruption.

http://julia.readthedocs.org/en/latest/manual/performance-tips/

つまり、@inboundsを使うと配列の境界チェックしなくなって、境界の外に対してアクセスするとクラッシュしたりするということらしい。

境界チェックがなくなる分、パフォーマンス的には若干浮くことになる。

試しに適当な配列の要素を取ってくる関数を用意する。

function foo( i::Int )
  x = [1, 2, 3, 4, 5]
  return x[i]
end

要素が境界内であれば、ちゃんと値が返る。

print( foo(3) )
  #=> 3

境界の外にアクセスするとBoundsErrorが起こる。

print( foo(6) )
  #=> ERROR: BoundsError()

これに@inboundsを付けてみる。

function foo( i::Int )
  x = [1, 2, 3, 4, 5]
  @inbounds return x[i]
end

境界内は問題なし。

print( foo(3) )
  #=> 3

これを6にすると

print( foo(6) )
  #=> 0

0が返った。

さらに7にすると

print( foo(7) )
  #=> 70379584

やべえよ。普通に外のメモリ見に行っちゃってるよ。

ちょっとドキドキしながら明らかに割り当ての外になるようなアドレスを見に行ってみる。

println( foo( 1000000 ) )
  #=> signal (11): Segmentation fault

はい、Segmentation fault頂きました。

この@inbounds指定、Juliaの内部のコードではそこそこ使われている。

例えば下記はJuliaのガンマ関数の中身。

function gamma(n::Union(Int8,UInt8,Int16,UInt16,Int32,UInt32,Int64,UInt64))
    n < 0 && throw(DomainError())
    n == 0 && return Inf 
    n <= 2 && return 1.0 
    n > 20 && return gamma(float64(n))
    @inbounds return float64(_fact_table64[n-1])
end

2以下と21以上については個別に処理した上で、それ以外の箇所については _fact_table64 という20個の要素を持つ配列から値を取ってくるようにしている。

このようにちゃんと境界チェックが事前に行われていれば問題はないけど、失敗した時の被害は深刻だし、その割にパフォーマンスが稼げる量も僅かなので、ほとんど使うことはない思われる。

こういうのを使うべきと思えるくらいパフォーマンスと戦うシーンに出会うことがあったらいいなぁ。