RubyKaigi 2026でPure Ruby Apache Arrow reader/writerという話をした須藤です。
なお、クリアコードはシルバースポンサーとしてRubyKaigi 2026を応援しました。
また、アンドパッドさんが開催するコード懇親会のお手伝いもしました。
関連リンク:
内容
Apache Arrowフォーマットを読み書きできるpure Rubyライブラリーを作ったので、そこで得られたpure Rubyでバイナリーファイルを高速に読み書きするための知見を共有しました。また、既存のRubyの機能では足りない部分もまとめました。今後はこのあたりに興味がある人たちとRuby本体を改良していきたいです。
事前情報の方にもっと詳しく書いているのでそちらも読んでみてください。
改良項目案:高速な組み込みのビット操作
ビット操作はFeature #20163 - Introduce #bit_count method on Integerなど、なんどか提案されていますが、まだ実現されていません。
Integerのメソッドとして提案されていますが、Integerはimmutableなので破壊的な変更をできないことが懸念点の1つになっています。RubyKaigi 2026中にまつもとさんや田中さんに聞いてみた感じでは、IntegerではなくStringの機能として提案するとワンチャンあるかも?という気持ちになりました。
PicoRubyのためにこのあたりが強化されて欲しいということで、はすみさんが取り組んでくれそうです。(私もお手伝いするつもりです。)
なお、この話題はRuby Committers and the Worldでもでていました。そして、おそらくそれを受けてIO::Buffer#bit_countが追加されていました。
それはそうと、IO::Bufferからexperimentalを取るためには全体的に実装を確認してコーナーケースも含めて品質を上げる必要がありそうです。あとでやろうかなぁという気持ちになりました。
改良項目案:ゼロコピーなIO::Buffer#get_string
IO::Buffer#sliceとかはゼロコピーでいけるのですが、一部のデータをStringとして取り出すためのIO::Buffer#get_stringはゼロコピーではありません。
RubyKaigi 2026中にまつもとさんに相談した感じでは、効率よく実装できるならアリでした。実装をなかださんに相談した感じではゴニョゴニョしたらいけるかも?という感じでした。
Stringにはすでに外部のデータを参照してString自体はデータを持たないようにする機能があります。IO::Bufferが持っているデータを参照するStringを返すことができればIO::Buffer#get_stringでゼロコピーできます。ただ、IO::Buffer#get_stringが返すStringはIO::Bufferを参照していないといけません。IO::Bufferの方が先にGCされるとStringは無効なデータを参照してしまうからです。ということで、ここをいかに効率よく実現できるかがポイントになります。
インスタンス変数とかRString(StringのCでの表現)に場所を作ってそこから参照するのが簡単ですが、そうすると使用メモリーがちょっと増えてしまいます。せっかくゼロコピーをするなら現状から使用メモリーを増やさずに実現したいです。なかださんからはStringが埋め込まれていないときにprecomputed hash用のフラグの値を再利用してゴニョゴニョすればいけそうな気がするけど。。。みたいな話を聞きました。
全然動かしていないですが、こんな感じかなぁという気持ちでいます。
diff --git a/gc.c b/gc.c
index 0784010d68..c3dacc7a53 100644
--- a/gc.c
+++ b/gc.c
@@ -3437,6 +3437,9 @@ rb_gc_mark_children(void *objspace, VALUE obj)
gc_mark_internal(RSTRING(obj)->as.heap.aux.shared);
}
}
+ else if (STR_EXTERNAL_PARENT(obj)) {
+ gc_mark_internal(RSTRING(obj)->as.heap.aux.parent);
+ }
break;
case T_DATA: {
diff --git a/include/ruby/internal/core/rstring.h b/include/ruby/internal/core/rstring.h
index 35175ea94a..09ff39ba92 100644
--- a/include/ruby/internal/core/rstring.h
+++ b/include/ruby/internal/core/rstring.h
@@ -238,6 +238,13 @@ struct RString {
* control such properties.
*/
VALUE shared;
+
+ /**
+ * Parent of the string. If the parent is also string, "shared"
+ * should be used not this. This is for non-string parent
+ * such as IO::Buffer.
+ */
+ VALUE parent;
} aux;
} heap;
diff --git a/internal/string.h b/internal/string.h
index 02e708d341..600622f5dc 100644
--- a/internal/string.h
+++ b/internal/string.h
@@ -21,6 +21,7 @@
#define STR_CHILLED (FL_USER2 | FL_USER3)
#define STR_CHILLED_LITERAL FL_USER2
#define STR_CHILLED_SYMBOL_TO_S FL_USER3
+#define STR_EXTERNAL_PARENT FL_USER4
enum ruby_rstring_private_flags {
RSTRING_CHILLED = STR_CHILLED,
@@ -105,6 +106,7 @@ void rb_warn_unchilled_symbol_to_s(VALUE str);
static inline bool STR_EMBED_P(VALUE str);
static inline bool STR_SHARED_P(VALUE str);
+static inline bool STR_EXTERNAL_PARENT_P(VALUE str);
static inline VALUE QUOTE(VALUE v);
static inline VALUE QUOTE_ID(ID v);
static inline bool is_ascii_string(VALUE str);
@@ -162,6 +164,12 @@ STR_SHARED_P(VALUE str)
return FL_ALL_RAW(str, STR_NOEMBED | STR_SHARED);
}
+static inline bool
+STR_EXTERNAL_PARENT_P(VALUE str)
+{
+ return FL_ALL_RAW(str, STR_NOEMBED | STR_EXTERNAL_PARENT);
+}
+
static inline bool
CHILLED_STRING_P(VALUE obj)
{
diff --git a/string.c b/string.c
index 9c6cfb700a..ba02efa7f9 100644
--- a/string.c
+++ b/string.c
@@ -99,9 +99,12 @@ VALUE rb_cSymbol;
* 3: STR_CHILLED_SYMBOL_TO_S (will be frozen in a future version)
* The string was allocated by the `Symbol#to_s` method.
* It emits a deprecation warning when mutated for the first time.
- * 4: STR_PRECOMPUTED_HASH
+ * 4: STR_PRECOMPUTED_HASH (this is only used by an embedded string)
* The string is embedded and has its precomputed hashcode stored
* after the terminator.
+ * 4: STR_EXTERNAL_PARENT (this is only used by an non-embedded string)
+ * The string is not embedded and refers data managed by a
+ * non-string object such as `IO::Buffer`.
* 5: STR_SHARED_ROOT
* Other strings may point to the contents of this string. When this
* flag is set, STR_SHARED must not be set.
@@ -1196,6 +1199,30 @@ rb_str_new_static(const char *ptr, long len)
return str_new_static(rb_cString, ptr, len, 0);
}
+VALUE
+rb_str_new_external(VALUE klass, const char *ptr, long len, int encindex, VALUE parent)
+{
+ VALUE str;
+
+ if (len < 0) {
+ rb_raise(rb_eArgError, "negative string size (or size too big)");
+ }
+
+ if (!ptr) {
+ str = str_enc_new(klass, ptr, len, rb_enc_from_index(encindex));
+ }
+ else {
+ RUBY_DTRACE_CREATE_HOOK(STRING, len);
+ str = str_alloc_heap(klass);
+ RSTRING(str)->len = len;
+ RSTRING(str)->as.heap.ptr = (char *)ptr;
+ RSTRING(str)->as.heap.aux.parent = parent;
+ RBASIC(str)->flags |= STR_NOFREE | STR_EXTERNAL_PARENT;
+ rb_enc_associate_index(str, encindex);
+ }
+ return str;
+}
+
VALUE
rb_usascii_str_new_static(const char *ptr, long len)
{
実は他のオブジェクトが持っているデータを参照しているStringを返すというのはglib2 gemでもやっているのですが、Stringにインスタンス変数を追加して実現しているので、IO::Buffer#get_string以外でも使える汎用的なAPIになるといいなぁと思っています。
だれか興味ある人います?お手伝いしますよ。
改良項目案:一時オブジェクトが不要なC互換バイナリーの書き出し
Array#packでC互換のバイナリーを書き出すことができますが、普通にやるとoutput.append_as_bytes([value].pack(format))という感じになり、[value]のためのArrayとArray#packの戻り値のStringが作ってすぐ捨てるオブジェクトになってもったいないです。一時オブジェクトを作らずにC互換のバイナリーを書き出したいです。
まつもとさんとかなかださんとか田中さんに相談してみました。私は今まで知らなかったのですが、Array#packにbuffer:引数があって、出力先を切り替えられました。ということで、[value].pack(format, buffer: output)とすると一時Stringを減らせそうです。
ただ、デフォルトではbuffer:に指定した内容を上書きするので[value].pack("@#{output.bytesize}#{format}", buffer: output)として追記するようにしないといけません。なお、私はフォーマット文字列の@で書き込み場所を書き換えられることも知りませんでした。
buffer:でArray#packの戻り時の一時Stringは減らせますが、その代わり"@#{output.bytesize}#{format}"という一時Stringできてしまいます。また、[value]の一時Arrayは解消できません。
IO#write_value(value, format)とかString#append_packed(value, format)とか話しましたが、これだ!というAPIは見つけられませんでした。
だれか興味ある人います?お手伝いしますよ。
改良項目案:JITでSIMD
RubyのMemoryViewでsumを高速化をJITでできないかなーというやつもまつもとさんに聞いてみました。MemoryViewじゃないかもという感じでした。どうするといいのかなー。いつもと同じようにRubyで書くだけで(特別なアノテーションとかなしで)Cで最適化したくらい速くなるのはかっこいいと思うので実現できるといいとおもうんだよなー。
だれか興味ある人います?お手伝いしますよ。
RubyKaigi 2026の他のこと
コード懇親会
昨年に引き続き、アンドパッドさんがコード懇親会を開催してくれました。私は企画とか進行とか運営としてお手伝いしました。
今回はRubyKaigi 2026の会場のひと部屋を使わせてもらったので広いしネットワークもあるしですごく助かりました。また、PicoPicoRubyのミートアップも同じ場所で開催しました。コード懇親会で60名強、PicoPicoRubyで20名強で90名弱くらいでの開催になりました。
アンケート結果を眺める感じだと今年も多くの人に楽しんでもらえたようです。
今回の課題はこんな感じです。
- 私が英語を話せないので英語でのアナウンスが手薄になってしまって、日本語が得意ではない人たちが楽しみにくかった気がする
- よくあるお酒を飲みながら懇親するイベントではないことを伝えきれなかった
- 例年、勘違いして参加してしまう人たちがいるので、今回はイベント名に「(Social Coding)」と書いてアピールしたつもりだったのですが、1人勘違いした人がいました
- 勘違いする人を0にするのはきびしそうな気がしてきました
- ワイワイ懇親しているのはよいが、グループ全体になにか話そうとすると(たとえば自己紹介するとか)聞き取りにくそうだった
- 静かにしてーというのはなんか違う気がするので、マイクとかなにか声量をあげられるものがあるといいのかも
- 10人ちょいの1グループを1人で見るのはきびしそうだった
- 例年、Rubyグループは野良コミッターが紛れ込んだりしていたので今年も紛れ込んでいるかなーと楽観的に考えていたが、そんなことはなく、前田さんだけに任せる形になってしまったので、事前に体制を整備したほうがよかった
あぁ、お絵描き用に各グループに配布したnu boardの使い勝手を聞くのを忘れちゃった。あれはあって助かった?必要なかった?
今回はフリーテーマをやめてすべて事前にテーマを決めました。フリーテーマがあると進行をしている私が回らなくなってしまうので、今回はフリーテーマをやめて進行とサポートに注力しました。サポートとしてなにをやっているかというと、困っていることありませんかー?と歩きまわったり、今なにやってるんすかー?と声をかけて他の人との交流を促したりとかです。なにやってるんすかー?は@kei_sにもやってもらいました。一人だと回りきれないので助かりました。
最後の方はまつもとさんがぶらぶらしていたので一緒にRubyグループの人たちに声をかけていました。何人かに一緒に声をかけたあとはまつもとさんが1人で他の人たちにも声をかけ始めてくれたのでおまかせしました。せっかくのRubyKaigiなので参加してくれたみんなにはまつもとさんとも懇親して欲しいんですよねぇ。
これまでは事前テーマはすべてこちらからお願いしていたのですが、今回は1テーマだけ公募しました。ruby-jpのSlackで@udzuraさんと@ahogappaさんに手を挙げてもらってRustテーマをやってもらえました。@udzuraさんは資料を準備してくれたりしました。すごい!
dRubyグループとRubyGems/Bundlerグループはあまり見にいけなかったのですが、例年通りいい感じにやってくれていた気がします。mruby/PicoRubyまわりもそんなに見にいけなくて、困っていることはない?くらいしかできなかったのですが、机いっぱいにハードウェアを広げてわちゃわちゃやっている人たちもいて楽しんでいそうでした。
test-unitとRactorとBox
2年くらい前からかつべさんとtest-unitの並列実行対応を進めています。毎週水曜日の12:15-12:45にオンラインでやっていてYouTube Liveで配信しています。
そんな活動の成果をかつべさんがRubyKaigi 2026で発表しました!感慨深い。現在はRactor対応を進めているのですが、発表ではマルチプロセス対応のところを紹介しました。
Ractor対応にあたり、Boxとうまいこと組み合わせられると楽そうだなぁとかつべさんと話していたので、そのあたりをたごもりさんとささださんとまつもとさんに相談しました。
Ractorは、Ractorローカルなオブジェクトあるいは共有可能なリソースだけを触れるようにすることで並列プログラミングが難しい問題を軽減させる設計(あってるよね?)のため、test-unitをRactorに対応させるためには共有できないリソースを触らないようにしないといけません。Ractor対応を進めていく中で、test-unit内では共有できないオブジェクトを触っている箇所が散在していることがわかってきました。各Ractor内でtest-unitのコピーを持つことができれば、たとえtest-unitが共有できないオブジェクトを触っていても問題にならないはずです。Ractorローカルなオブジェクトとして扱えるからです。そのためにBoxを使えるといいなぁというアイディアです。各Ractor内でtest-unitそのものと実行するテストをロードし、そのRactor内でだけ使うことで、test-unitそのものから共有できないオブジェクトを触っている処理を変えなくてもRactor対応できるんじゃない?ということです。
この提案はFeature #21953 Allow accessing unshareable objects within a Ractor-local Ruby Boxで進めているので興味がある人はコメントしてください。
StringScanner#integer_at
RubyKaigi 2026 冒険譚 - 虚無庵にも書いてあるStringScanner#integer_atなんですが、メンテナンスのことを考えてもっとシンプルにするか、あんまり速くならないならMatchData#integer_atもろともrevertするのがいいんじゃないかと思うんですよねぇ。
とりあえず、どのくらい速くなるのかのベンチマークをとってから再検討かなぁという気持ち。
本屋さん
ささださんから「コーナーゲスト」というやつ(たぶん、本屋にいて、本をきっかけにしたりしなかったりしながらおしゃべりしてくれる人)として私が本屋さんに来てくれるといいなぁと言っている人がいたよという連絡をもらったので、3日目の休み時間はずっと本屋さんのまわりにいました。だれがリクエストしてくれたのかは聞いていないので知らないのですが、リクエストしてくれた人は活用してくれたのでしょうか。
3日目でもリーダブルコードが1冊も売れていないということだったので、リーダブルコードをオススメしていました。なぜか、みんな「もう持っているんですよー」と言って買ってくれないんですよねぇ。それでも、サインするんでーとか言いながらなんやかんや5人の人が買ってもらえました。近くにまつもとさんがいたのでまつもとさんにもサインしてもらいました。
ただ、興味がない人に売るのはさすがに気が引けるので、どんな分野に興味があるんですかー?とか聞きながら私だったらこのあたりの本がいいと思いますよーとかお話していました。
そういえば、@taketo1113さんとお話したのも本屋さんの近くだった気がします。csvを速くした話がよくて自分もああいうやつを書きたいとか言ってもらえたような気がします。覚えてもらえる文章を書けていることがわかってうれしかったです。
他の人も感想も伝えてね!
あ、そうそう。本屋さんでは取り寄せられなくなっている私物の本2冊(「Rubyのしくみ」と「Effective Ruby」)を持ち込んで希望者にプレゼントしました。欲しい人が数人いたのでなんで欲しいかをアピールしてもらって私が一番ぐっときたアピールをしてくれた人にプレゼントしました。「Rubyのしくみ」にはしまださんとかくたにさんがサインしてくれました。欲しい人がいてよかったです。
Speeeさんの若者
もうだいぶ長いことSpeeeさんのOSS活動サポートをしているのですが、今年はなんやかんやあって新卒2年目の若者が1人RubyKaigi 2026に参加できることになりました。できるだけ(成果も出しつつ)楽しめるように、事前イベントに行ってもらったりとか、いろんな人を紹介して話をしてもらったりとかしました。もう少ししたらSpeeeさんのブログで参加記事が公開されると思うので楽しみです。
クリアコード20周年
今年でクリアコードは20周年を迎えます。お祝いイベントをしようと思っているので、発表内でお知らせしたりしました。お祝いメッセージを待っているよ!
これからのOSS Gate
今、OSS Gateは10周年中なのですが、もっとばーん!と盛り上がって欲しいなぁと思っているので、OSS Gateのイベントもなんかやろうと思っています。RubyKaigi 2026でいろいろ相談したのでなんやかんや進めていきます。たぶん、秋ぐらいにやると思います。
まとめ
RubyKaigi 2026のことをいろいろ書きました。
オフィシャルパーティーで、ぺんさん、そんなことを考えていたの!?という話をきかせてもらったりとか、やんちゃbarでいろんな人とお話できたりとか、リンクアンドモチベーションさんのDrinkupでもいろんな人とお話できたり(1時間ごとのシャッフル、いいっすね)とか、他にもいろいろあるのですが、このへんで。