AsciiPack を書いている時にやりながら理解していった概念メモ
前提として読めるだけのドキュメント(README.EXT.ja,『Rubyソースコード完全解説』サポートページ,リファレンスなど)は読む。
C言語を書くときに考えなければいけないのがメモリ空間。 特にRubyのC拡張を書くときにはRubyオブジェクトとCのメモリがあることを意識する。
どんなデータもメモリ上にのっているものを使う。
メモリは自動で消えるけど管理しなくていいスタック空間と、ずっと保持してくれるけど自分で管理しないといけないヒープ空間がある。
Rubyオブジェクトは大抵GCのマークが付いたものがメソッドの引数なりに渡されるのでメモリの開放とかは気にしなくていい。もちろん自分で作って自分で保持するのならマークしてGCに消されないように(消していただけるように)する必要がある。
RubyオブジェクトからC側でメモリ内のデータを料理するには大抵の場合はコピーで事足りる。コピーとは値渡しやmemcpyなんかのことを言う。
例えばRubyの文字列を変換して新しいRubyの文字列オブジェクトを作る場合、当然ながらRuby->C->(何らかの加工)->C->Rubyとデータを渡して処理する。
Rubyオブジェクトから自分でヒープに用意したメモリにコピーする。ただのCのメモリ領域はRubyのGC側からは見えない作業なので勝手に開放されることはない。
大抵はこれで上手くいくが文字列が長くなるに連れてただのコピーでもオーバーヘッドが大きくなっていく。
この場合、一旦Ruby文字列の先頭ポインタと文字列の長さを覚えておくオブジェクトを用意すればRuby->Cのコピーの手間は省ける。このオブジェクトはC側でヒープ領域に用意して連結リストにしておくと、長さが予めわからなくても次々に繋げられるし後で一気に(コピー|メモリ開放)することができる。この区切りは「大きい文字列が来たら」にしておくとmallocしすぎない。
あとは適当に文字列を料理してC->Rubyにメモリをコピーすれば変換完了。
できるだけRubyのメソッド呼び出し(rb_funcall()など)を使わないようにする。特に処理回数が大きところでは。
C拡張を書くとテストで頻繁に「[BUG] Segmentation fault」になるけどめげない。どう考えても自分のメモリ管理のバグなのでよく考えて治す。有効打は地道なprintfとまるごと書き直し。
メモリ管理のバグも意識してテストを書く。
ちゃんとメモリが開放されているかとかはテストではわかりにくいので負荷のかかるコードを書いてマシンのメモリ使用量を眺める。もはや科学の実験みたいな気分になる。
そんなこんなでだいぶ早くなりました。