Previous | ToC | Next |
赤木 :色々忙しいと思わず逃避行動にでているんだけど(だれが?)、Crystal で多次元配列ってどうなのかしら?
学生C:どうなのかしら?といわれましても、、、ないですよね? Ruby にな いし。もちろん、配列の配列つくれば多次元配列だから、速度とか気にしなけ ればそれでいいし、Ruby はそのレベルで速度とか気にするなら使えないし。
赤木 :でもほら、田中さんの narray とか、あと Python の numpy とか、ラ イブラリ使えば速いみたいなのがあるじゃない?そういう感じのが Crystal にもあってもいいと思わない?
学生C:あればいいんでしょうけど、えーと、具体的に何が欲しいと?
赤木 :要するに、普通に多次元配列宣言して、その要素にアクセスしたいわ け。例えば Fortran なら
subroutine matmul(a,b,c,n) integer*4 n real*8 a(n,n), b(n,n), c(n,n) ... endみたいな感じで配列かけて、多次元配列のサイズを関数に渡せるし、 C も C99 ならできるじゃない? C++ は未来永劫これができそうにないけど。
学生C:C++ なら、サイズと、実際のデータエリアへのポインタもった構造体とか作りますよね?
赤木 :まあそうね。
学生C:で、Crystal でどうしたいと?
赤木 :どっちいうとC++ でやるみたいにサイズと、実際のデータエリアへの ポインタもったクラス作って、でも、なるべくそれを自然に使うみたいな。例 えば Ruby の NArray だと
x =Narray.float(10) xy =Narray.float(3,3)とかで、長さ10の1次元配列とか、3x3の2次元配列ができて、2次元配列のほう は
xy[1,2] = 3 pp! xy[1,2]みたいな感じで、 [1][2] みたいな書き方じゃなくてコンマ区切りでいいのと、 あと、Fortran や Cのプログラムに配列のポインタを渡せる、言い換えると、 内部で Ruby のArrayを使ってなくて、連続アドレス領域をとってデータがお いてあるわけ。だから、これ使うと色々なライブラリが簡単に使えるのね。
学生C:えー、でも、Array じゃないんだとどうするんですか?全部 C で書くとか?そんなのできませんよ。
赤木 :あ、Crystal その辺便利で、Pointer ってのがあるの。それで C みた いに malloc とかできるの。で、面白いのは、 free はないのね。プログラム で使わなくなったらガーベージコレクタが自動的にケアするから。なので、 こういうの使っても C/C++ みたいなややこしいバグとかは起こらないの。
学生C:まあそれはよさそうですが、、、多次元配列を a[2,3] みたいに書くっ て、 [] を2つ引数とる関数として定義するみたいなのがいりますよね?それ できるんですか?
赤木 : def [](i,j) とか def[]=(i,j,val) でできるみたいよ。
学生C: それならなんとかなりそうですね。Ruby の narray とか Python の numpy と違って、ちゃんとコンパイラだから演算自体は Crystal で書いて別 に遅くないはずだし。ちょっとかいてみます。
学生C: えーと、Pointer というのがあるんですね。で、赤木さんがいってたみた いに malloc できると。あれ、Slice というのもあると。
While a pointer is unsafe because no bound checks are performed when reading from and writing to it, reading from and writing to a slice involve bound checks. In this way, a slice is a safe alternative to Pointer.えーと、範囲チェックが必要ならこっちを使えということですね。でも、いつもチェックして たら遅い気がします。コンパイラオプションで切換えるとかですかね?まずはそういうの考 えないで最低限だと、2次元までなら
1: class Narray_F64 2: property :data 3: def initialize(nx : Int32,ny : Int32 = 1) 4: @data = Pointer(Float64).malloc(nx*ny) 5: @nx=nx 6: @ny=ny 7: end 8: def [](i) @data[i] end 9: def [](i,j) @data[i*@ny+j] end 10: def []=(i,x) @data[i]=x end 11: def []=(i,j,x) @data[i*@ny+j]=x end 12: end3次元以上にしたければ @nz とかもつければいいと。initialize 最初は配列 の次元数毎に書いたんだけど、中身は要するに使わない次元を1にするだけだっ たのでデフォルト引数ですむ。 一応これ呼ぶプログラム作ると、
1:require "./narray0.cr" 2: 3:x =Narray_F64.new(10) 4:x[1]=2.0 5:pp! x 6:xy =Narray_F64.new(4,4) 7:16.times{|i| xy[i]=i.to_f64} 8:xy[2,3]=-1.0 9:4.times{|i| 10: 4.times{|j|print " ",xy[i,j]} 11: print "\n" 12:} 13:pp! xyこれ実行してみると
gravity> crystal narray0test.cr x # => #<Narray_F64:0x7ff2a772ee60 @data=Pointer(Float64)@0x7ff2a7739f60, @nx=10, @ny=1> 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 xy # => #<Narray_F64:0x7ff2a772e940 @data=Pointer(Float64)@0x7ff2a773dea0, @nx=4, @ny=4>値もはいっているしそれっぽいですね。
赤木 :そうね。でもこれだと Float64 しか使えないから、Float32 も使える ようにできる?
学生C:2つ書けばよくないですか?
赤木 :まあ、そういうことをしなくていいための機能が C++ とかでいうとこ ろのテンプレートで、 Crystal だと Generics というのね。多項式のところ で、 Array から新しいクラス作るとか、あと MathVector でも使ってるけど、
class Fooの代わりに
class Foo(T)として、別にTでなくてもいいはずだけど、クラスが引数として型を持つよう にできるのね。Array も
class Array(T)で、 new は
Array(Float64).new(...)とかするのと同じわけ。
学生C:そうすると、
class Narray(T)で、
def initialize(nx : Int32,ny : Int32 = 1) @data = Pointer(T).malloc(nx*ny) @nx=nx @ny=ny endですね。これを narray1.cr にして
1:require "./narray1.cr" 2: 3:x =Narray(Float64).new(10) 4:x[1]=2.0 5:pp! x 6:xy =Narray(Float32).new(4,4) 7:16.times{|i| xy[i]=i.to_f32} 8:xy[2,3]=-1.0_f32 9:4.times{|i| 10: 4.times{|j|print " ",xy[i,j]} 11: print "\n" 12:} 13:pp! xyこれ実行してみると
gravity> crystal narray1test.cr x # => #<Narray(Float64):0x7f60e559fe60 @data=Pointer(Float64)@0x7f60e55aaf60, @nx=10, @ny=1> 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 xy # => #<Narray(Float32):0x7f60e559f9a0 @data=Pointer(Float32)@0x7f60e55b0f50, @nx=4, @ny=4>なるほど、大丈夫ですね。ちゃんと x と xy で違う型ですね。 あとなんでしたっけ?範囲チェックですね?
赤木 :そう。Slice 使ってみて。
学生C:単に使うだけなら
def initialize(nx : Int32,ny : Int32 = 1) @data = Slice(T).new(Pointer(T).malloc(nx*ny), nx*ny) @nx=nx @ny=ny endですかね。これで @data は Pointer じゃなくて Slice になるんですよね?
赤木 :そのはず。なんかテスト書いて範囲外アクセスとかしてみて。
学生C:ではこんなので
1:require "./narray2.cr" 2: 3:x =Narray(Float64).new(10) 4:x[1]=2.0 5:pp! x 6:xy =Narray(Float64).new(4,4) 7:16.times{|i| xy[i]=i.to_f64} 8:xy[2,3]=-1.0 9:4.times{|i| 10: 4.times{|j|print " ",xy[i,j]} 11: print "\n" 12:} 13:pp! x[11]
gravity> crystal narray2test.cr x # => #<Narray(Float64):0x7f794d130f00 @data=Slice[0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], @nx=10, @ny=1> 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 Unhandled exception: Index out of bounds (IndexError) from /usr/share/crystal/src/indexable.cr:903:8 in '[]' from narray2.cr:8:17 in '[]' from narray2test.cr:13:1 in '__crystal_main' from /usr/share/crystal/src/crystal/main.cr:115:5 in 'main_user_code' from /usr/share/crystal/src/crystal/main.cr:101:7 in 'main' from /usr/share/crystal/src/crystal/main.cr:127:3 in 'main' from /lib/x86_64-linux-gnu/libc.so.6 in '__libc_start_main' from /home/makino/.cache/crystal/crystal-run-narray2test.tmp in '_start' from ??? x[11] # =>学生C:おお、ちゃんと x[11]は範囲外だとでますね。narray1.cr を使うと
1:require "./narray1.cr" 2: 3:x =Narray(Float64).new(10) 4:x[1]=2.0 5:pp! x 6:xy =Narray(Float64).new(4,4) 7:16.times{|i| xy[i]=i.to_f64} 8:xy[2,3]=-1.0 9:4.times{|i| 10: 4.times{|j|print " ",xy[i,j]} 11: print "\n" 12:} 13:pp! x[11]
gravity> crystal narray2test1.cr x # => #<Narray(Float64):0x7f6165ad9e60 @data=Pointer(Float64)@0x7f6165ae4f60, @nx=10, @ny=1> 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 x[11] # => 0.0なんか知らないけど0が戻りますね。
赤木 :これ、Slice にしておくと、範囲チェックができる他に色々いいこと があるのね。ドキュメントみてもらうとわかるけど、Slice にはArray にある ような便利な関数がほとんどあるから、each とか map! とか使えるようにな るわけ。map もあるけどこれは配列になっちゃうからそのままではちょっとね。
学生C:sort! とかもできるわけですか?
赤木 :できるわよ。ただ、今のだと、 x.@data.sort! とかでちょっと見苦し いわね。method_missing 使って
macro method_missing(call) @data.{{call}} endとしておけば、 Narray に対して知らない関数がきたら @data に投げる、と できるから、これで sort! とか map とかなんでも使えるわね。
学生C:ちょっと試しに、、、
1:require "./narray2.cr" 2: 3:xy =Narray(Float64).new(4,4) 4:16.times{|i| xy[i]=i.to_f64} 5:xy[2,3]=-1.0 6:4.times{|i| 7: 4.times{|j|print " ",xy[i,j]} 8: print "\n" 9:} 10:xy.sort! 11:4.times{|i| 12: 4.times{|j|print " ",xy[i,j]} 13: print "\n" 14:} 15:を実行すると
gravity> crystal narray2test2.cr 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 -1.0 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 12.0 13.0 14.0 15.0おおお、確かに実行できてソートもされますね。2次元配列こんなふうにソー トすることはあんまりないとは思いますが、map! とかはいいかもですね。
赤木 :で、これでいいんだけど、「コンパイルオプションによって Pointer か Slice か切換える」「でも、 Slice の機能は使えるようにする」ってでき る?
学生C:まずポインタで領域とって、それに対してSlice作成だから、添字での アクセスでどっち使うかですね。コンパイルオプションで選ぶのは、少なくと もクラス全体をマクロにすればよかったような、、、なので、こんなのですね。
1:# narray.cr 2:# 3:# multi-dimensional array similar to NUMO:Narray 4:# 5:# Copyright 2020- J. Makino 6:# 7:macro use_narray(bound_check = false) 8:class Narray(T) 9: property :data 10: def Narray.range_check? 11: {% if bound_check %} 12: true 13: {% else %} 14: false 15: {% end %} 16: end 17: def initialize(@nx : Int32, @ny : Int32 = 1, @nz : Int32 = 1) 18: n=@nx*@ny*@nz 19: @data = Slice(T).new(Pointer(T).malloc(n), n) 20: @datasl = @data 21: {% if bound_check %} 22: @data = @datasl 23: {% end %} 24: end 25: def [](i) @data[i] end 26: def [](i,j) @data[i*@ny+j] end 27: def [](i,j,k) @data[(i*@ny+j)*@nz+k] end 28: def []=(i,x) @data[i]=x end 29: def []=(i,j,x) @data[i*@ny+j]=x end 30: def []=(i,j,k, x) @data[(i*@ny+j)*@nz+k]=x end 31: macro method_missing(call) 32: @datasl.\{{call}} 33: end 34: def each_index 35: @nx.times{|i| 36: @ny.times{|j| 37: @nz.times{|k| 38: yield i,j,k 39: } 40: } 41: } 42: end 43: def each_index(idim) 44: if idim == 0 45: @nx.times{|i| yield i} 46: elsif idim == 1 47: @ny.times{|j|yield j} 48: elsif idim == 2 49: @nz.times{|k|yield k} 50: end 51: end 52:end 53:end 54: 55:{% if flag?(:range_check) %} 56: use_narray(true) 57: {% else %} 58: use_narray(false) 59:{% end %}まず、クラス定義全体を
macro use_narray(bound_check = false) ... endで囲って、
use_narray(true)で範囲チェックあり、 false か引数なしならチェックなしのコードがコンパ イル時に生成されるようにします。そのなかでは
{% if bound_check %} foo {% else %} bar {% end %}でどっちのコードになるか選べるわけです。チェックするかどうかは、 initializeのほうで
{% if bound_check %} @data = Slice(T).new(Pointer(T).malloc(nx*ny*nz), nx*ny*nz) @datasl = @data {% else %} @data = Pointer(T).malloc(nx*ny*nz) @datasl = Slice(T).new(@data, nx*ny*nz) {% end %}として、チェックするなら data も datasl も Slice、しない時は data は Pointer のままで、としました。
@data = Pointer(T).malloc(nx*ny*nz) @datasl = Slice(T).new(@data, nx*ny*nz) {% if bound_check %} @data = @datasl {% end %}のほうが格好よいですがまあ。あと、range_check? というクラス関数も作っ てみました。で、最後に
{% if flag?(:range_check) %} use_narray(true) {% else %} use_narray(false) {% end %}で、コンパイル時に -Drange_check されるかどうかでどっちかのマクロが生 成されます。
1:require "narray" 2:x =Narray(Float64).new(10) 3:x[1]=2.0 4:xy =Narray(Float64).new(4,4) 5:16.times{|i| xy[i]=i.to_f64} 6:xy[2,3]=-1.0 7:4.times{|i| 8: 4.times{|j|print " ",xy[i,j]} 9: print "\n" 10:} 11:xy.sort! 12:pp! xy 13:pp! Narray.range_check? 14:pp! x[11]を範囲チェックありで実行すると
gravity> crystal run -Drange_check narray_test.cr 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 xy # => #<Narray(Float64):0x7f0fdfae8680 @data= Slice[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 13.0, 14.0, 15.0], @datasl= Slice[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 13.0, 14.0, 15.0], @nx=4, @ny=4, @nz=1> Narray.range_check? # => true Unhandled exception: Index out of bounds (IndexError) from /usr/share/crystal/src/indexable.cr:903:8 in '[]' from lib/narray/src/narray.cr:56:1 in '[]' from narray_test.cr:14:1 in '__crystal_main' from /usr/share/crystal/src/crystal/main.cr:115:5 in 'main_user_code' from /usr/share/crystal/src/crystal/main.cr:101:7 in 'main' from /usr/share/crystal/src/crystal/main.cr:127:3 in 'main' from /lib/x86_64-linux-gnu/libc.so.6 in '__libc_start_main' from /home/makino/.cache/crystal/crystal-run-narray_test.tmp in '_start' from ??? x[11] # =>なしだと
gravity> crystal run narray_test.cr 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 -1.0 12.0 13.0 14.0 15.0 xy # => #<Narray(Float64):0x7f4ac75dced0 @data=Pointer(Float64)@0x7f4ac75e0f30, @datasl= Slice[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 13.0, 14.0, 15.0], @nx=4, @ny=4, @nz=1> Narray.range_check? # => false x[11] # => 0.0で、ちゃんとコンパイルオプションで @data の型が変わって、範囲チェック の結果も変わってるのがわかります。
赤木 :なるほど。最低限のことはこれでできる感じね。あとは、C/C++ に比 べて速度は、とか、 SIMD 化はどうするの?とかまだ色々あるけど、それはお いおいね。
この章は息抜きなので課題はありません。
特になし。Crystal 言語の Pointer, Slice, Macro 等をみておこう。
Previous | ToC | Next |