@ledsun blog

Hのキーがhellで、Sのキーがslaveだ、と彼は思った。そしてYのキーがyouだ。

Stackprofで時間の掛かる処理を探そうとして上手く行かなかった話

RactorでWoker pool - @ledsun blog で、並列化の効果が芳しくないことがわかりました。 どこに原因があるのか知りたいのでstackprof情報を取得します。

Ractor版だと例外がおきます。 PubAnnotationのアノテーション取り込み処理をRactor化したものをStackprofで計測したらでたエラー · GitHub このエラーメッセージの読み方わからないんですよね。

シリアル処理では計測できました。

Stackprofで取得したフレーム情報を図示したもの

わかったのはテキストアライメント処理に時間が掛かっていることです。 これは事前にわかっている情報です。 うーん、この測り方は期待してたものと違うみたいです。

RactorでWoker pool

Ruby 並行・並列くらべ 2 - @ledsun blog でRactorの並列化とプロセスの並列化が大体同じような性能がでることがわかりました。 Ractorに絞ってもっと性能が出ないか試してみます。 Ractorを作る数を制限していないので、物理コアをこえる数のスレッドを作っています。 それで非効率になっているのでしょうか?

https://github.com/ruby/ruby/blob/master/doc/ractor.md#worker-pool を参考にしてWorker poolを実装して確認してみましょう。

    Ractor.make_shareable(TextAlignment::CHAR_MAPPING)
    Ractor.make_shareable( TextAlignment::LCSMin::PLACEHOLDER_CHAR)
    pipe = Ractor.new do
      loop do
        Ractor.yield Ractor.receive
      end
    end

    workers = (1..4).map do
      Ractor.new pipe do |pipe|
        while msg = pipe.take
          a, d, o = msg
          m = Annotation.prepare_annotations!(a, d, o)
          Ractor.yield [m, a]
        end
      end
    end

    annotations_collection_with_doc = annotations_collection_with_doc.collect do |annotations, doc|
      pipe << [annotations, doc.dup, options]
      doc
    end.map do |doc|
      _r, (error_messages, annotations) = Ractor.select(*workers)
      messages += error_messages
      [annotations, doc]
    end

Worker poolをいれなかったときは次のソースコードでした。

    Ractor.make_shareable(TextAlignment::CHAR_MAPPING)
    Ractor.make_shareable( TextAlignment::LCSMin::PLACEHOLDER_CHAR)
    ractors = annotations_collection_with_doc.collect do |annotations, doc|
      r = Ractor.new do
        a, d, o = Ractor.receive
        m = Annotation.prepare_annotations!(a, d, o)
        Ractor.yield [m, a]
      end
      r.send [annotations, doc.dup, options]
      [r, doc]
    end

    annotations_collection_with_doc = ractors.map do |r, doc|
      error_messages, annotations = r.take
      messages += error_messages

      [annotations, doc]
    end

なかなか複雑になってきました。 実行してみます。

実行結果

1m 26sです。 Worker pool導入前は1m 32sでした。 7%位の差です。 他に何か原因がありそうです。

https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-06-f/dist/browser.script.iife.js のsrc属性対応

ruby.wasm/browser.script.ts at d92d1a82256c12604a1e6e15e1269f6d11a81af3 · ruby/ruby.wasm · GitHub を次のようにすれば良いじゃん簡単じゃん?と思っていました。

        if (tag.type === "text/ruby") {
            if (tag.hasAttribute('src')){
                const response = await fetch(
                    tag.getAttribute('src')
                );
                const rubyScript = await response.text();
                vm.eval(rubyScript);            
            } else if (tag.innerHTML) {
                vm.eval(tag.innerHTML);
            }
        }

ビルドが思っていたより難しいです。

最初 https://github.com/ruby/ruby.wasm/blob/d92d1a82256c12604a1e6e15e1269f6d11a81af3/packages/npm-packages/ruby-wasm-wasi/build-package.sh を使ってビルドすれば良いと思いました。 これでビルドできるのはiife.jsではありませんでした。

iife.jsをビルドしてるのは https://github.com/ruby/ruby.wasm/blob/d92d1a82256c12604a1e6e15e1269f6d11a81af3/packages/npm-packages/ruby-head-wasm-wasi/rollup.config.js#L9-L10 なことに気がつきました。

で、ビルドしてみると ruby.wasm/browser.script.ts at d92d1a82256c12604a1e6e15e1269f6d11a81af3 · ruby/ruby.wasm · GitHubruby.wasmの読込が上手くいかなくなりました。

さて、どうやって動かしたものでしょうか・・・。

Ruby meets WebAssembly

rubykaigi.org

ご本人のブログです。 発表資料もあります。

kateinoigakukun.hatenablog.com

CRubyをruby.wsmにコンパイルしてブラウザ上で動かせるようにした人の発表です。

https://github.com/ruby/ruby.wasm/blob/main/packages/npm-packages/ruby-wasm-wasi/example/hello.html をデモしていました。 次のようなタグの中に書いたRubyスクリプトがブラウザで動かせます。

<script type="text/ruby">
  puts "Hello, world!"
</script>

さらに、https://github.com/ruby/ruby.wasm/blob/main/packages/npm-packages/ruby-wasm-wasi/example/lucky.html を使ったデモでは、 RubyスクリプトからJavaScriptのグローバルオブジェクトを参照しDOMを操作していました。

require "js"
document = JS.global[:document]
button = document.getElementById "draw"

おお。

さらにさらにCDNエッジでWebAssembly化したRubyランタイムを動かすデモ( https://irb-wasm.vercel.app/ )ではirbがうごきます。 gemをrequireできるし、エラー表示も普通にRubyだし、 本当にRubyが動いているかんじです。 というインパクトのあるデモからの、実装上の苦労話が聞けました。

WebAssemblyはポータブルな仕様なので、出来ないことがたくさんあります。 例えば時刻を取ることができません。 つまり時計が存在する環境で動かされているとは限らないからです。 まあでも、それでは不便なので、今ではWASIという、WebAssembly向けのシステムコールインタフェースの仕様があります。 WASIの時刻を取るインタフェースを使えば、例えばブラウザではDate.now()が実行されて時刻が取得できるみたいな感じらしいです。

最近はCDNエッジでもWebAssemblyが動きます。 ただし、大抵のCDNエッジはワンバイナリのWebAssemblyを要求します。 そこで、wasi-vfsというバイナリファイル上に仮想的にRubyスクリプトを埋め込めるファイルシステムをつくって、そこからruby スクリプトを読み込む仕組みを作ったそうです。 作ったって・・・すごいですよね。

リポジトリの コミット履歴 https://github.com/kateinoigakukun/wasi-vfs/commits/v0.2.0 を見ると半年くらいで作ったみたいです。 rustで書かれているんですかね? なんかよくわからないですけど、すごいですね。という感想を抱きました。

つくったruby.wasmはnpmパッケージで公開されているそうです。 ruby-head-wasm-wasi - npm なんですかね? 前出のデモで使っている https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-06-f/dist/browser.script.iife.js との関係がよくわかりません。 バンドルする前のソースコードhttps://github.com/ruby/ruby.wasm/blob/a64de622c5d2a7cbd2147ef789d38a6e6f5cdb18/packages/npm-packages/ruby-wasm-wasi/src/browser.script.tsのようです。

  1. ruby.wasmを読み込む
  2. RubyVMを初期化
  3. HTML上の<script type="text/ruby">を探してきてRubyスクリプトを取得
  4. RubyVM で Rubyスクリプトを実行

という感じでruby.wasmで気持ちよくRubyスクリプトを動かすための仕掛けみたいです。 今の<script type="text/ruby">はsrc属性に対応していないので、外部のRubyスクリプトを読み込めません。 この辺をハックしたら読み込めるように出来そうです。

じつはここからが本題で、WebAssemblyにRubyを移植するための大きな課題が二つあったそうです。

  1. 例外処理
  2. FIber
  3. 保守的GC

これらは、関数の中にはいる外に出るのを呼び出した順には行いません。 例外がおきたら、関数スタックを全部飛ばして、例外処理に入ります。 Fiberも、Fiberの動きは上手くイメージできないのですが、あるFiberの処理から突然別のFiberの処理に切り替わります。 GCは、作ったオブジェクトを全部なめるのが難しいそうです。 どうもこの辺の動きがWebAssemblyでは素直に書けないそうです。 WebAssemblyはスタックマシンだからとかなんか言ってたような気もしますが、なにもわかりません。

これを解決するためにAsyncifyというアルゴリズム?ライブラリ?プログラムを変換するツール?を使ったそうです。

twitter.com

どうやらツールみたいです。

現在のruby.wasmには制限があります。

  • スレッド
  • C拡張ライブラリの実行
  • ファイルサイズ
  • 性能

スレッドとC拡張ライブラリはWASIにいまのところ対応するインタフェースがないようです。 ファイルサイズは25MBくらいあるそうです。 性能はmrubyと同じぐらいで、CRubyの半分くらいだそうです。

いきなりCRubyとおなじことができるとは思ってなかったので、大分動いていて、現時点で相当すごいです・・・。

これらの成果は2021年度Rubyアソシエーション開発助成金成果報告でのもののようです。 助成金出して、スーパーすごい若手のコミッターを獲得する流れになっています。 Rubyアソシエーションの開発助成金の仕組み、すごいですね。 そしてRubyKaigiの基調講演でメジャーデビュー。 おお、なんかすごいエコシステムだ。

参考

静的サイトをホスティングする

RubyスクリプトをWebAssembly化したRubyランタイムで動かす - @ledsun blog でブラウザで動くRubyスクリプトができました。 スマートフォンでみれるように、ホスティングします。

しました。 https://wordle-search.onrender.com/ です。 スマートフォンで見ると「もうちょっとスタイルがんばったほうがいいな。」と思いました。

今回は Cloud Application Hosting for Developers | Render を使ってみました。 GitHub pagesでもいいのですが、それなりに面倒くさいので、もっと簡単なやつを探しました。

最初は How to deploy a static website for free in just 3 minutes straight from your Google Drive, using Fast.io を読んで、Fast.ioを試そうとしました。 サイトを開いたら、ピボットしたのか違うサービスに変わっていました。

Fast.ioのスクリーンショット

次に 8 Best Static Website Hosting for Business and Personal Use を読みました。 今回はindex.htmlを置くだけなので、なるべくリッチなアプリケーション向けでないサービスを選びました。 Renderです。 サインアップして、GitHub上のリポジトリと連携したら、静的サイトがちょちょっと作れました。 便利です。

GitHub - ledsun/wordle_search: Look for wordle answer candidates.ソースコードがあります。 本当にindex.htmlが一つあるだけです。 埋め込んだ辞書データの権利は気になります。

実際に配信されている、レスポンスヘッダを見ると次のことがわかりました。

  • Brotliで圧縮されている
  • cloudflareから配信されている

レスポンスヘッダ

だったら Cloudflare Pages で、よかったのでは?みたいな気もしないでもないです。

RubyスクリプトをWebAssembly化したRubyランタイムで動かす

wordleの候補をgrepするRubyスクリプト - @ledsun blogをブラウザで動くようにしたいと思います。

ruby.wasm/lucky.html at main · ruby/ruby.wasm · GitHub を参考にして実装していきます。

<html>
  <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-06-f/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    require "js"

    dict = %w[
    abaci
    aback
    abaft
        ]

    def search dict, exclude, included, correct_spots
      matched = dict.grep(/^#{correct_spots}$/)
                    .grep_v(/[#{exclude}]/)

      included.to_s.chars.each do |char|
        matched = matched.grep(/#{char}/)
      end
      matched
    end

    document = JS.global[:document]
    search_button = document.getElementById "search_button"
    search_button.addEventListener "click" do
      exclude = document.getElementById("exclude")[:value]
      included = document.getElementById("included")[:value]
      correct_places = document.getElementById("correct_places")[:value]
      matched = search dict, exclude, included, correct_places
      
      html = matched.map do |m|
        "<div>#{m}</div>"
      end.join
      document.getElementById("result")[:innerHTML] = html
    end

  </script>
  <div>
    exclude: <input type="text" id="exclude">
  </div>
  <div>
    included: <input type="text" id="included">
  </div>
  <div>
    corrct_places: <input type="text" id="correct_places" value="\w\w\w\w\w">
  </div>
  <div>
    <button type="button" id="search_button">Search</button>
  </div>
  <div id="result"></div>

</html>

辞書データははしょってあります。 次のコマンドでHTTPサーバーを起動します。

ruby -run -e httpd

ブラウザで実行すると次のような検索が出来ます。

ブラウザで実行した結果

つまづいた点

TODO

  • 結果表示を等幅フォントにする
  • 結果表示に色付けする
  • GitHub page辺りに配置してインターネットからアクセスできるようにする

wordleの候補をgrepするRubyスクリプト

Wordle - The New York Times を解くとき、次のようにgrepしています。

ledsun@MSI:~/wordle_rb►grep -e '^\w\w\wt\w$' /usr/share/dict/words | grep -v '[o
sclh]' | grep a | grep e | grep r
Berta
Greta
Harte
grate
irate
prate

これをRubyスクリプトに書き換えました。

exclude, included, correct_spots = ARGV

correct_spots = if correct_spots
                  correct_spots.gsub(/\*/, '[a-z]')
                else
                  '[a-z]{5}'
                end

dict = File.open('/usr/share/dict/words') do |f|
  f.readlines
end

matched = dict.grep(/^#{correct_spots}$/)
              .grep_v(/[#{exclude}]/)

included.chars.each do |char|
  matched = matched.grep(/#{char}/)
end

matched.each do |value|
  value.chars.each do |char|
    if included.include? char
      print "\e[#{31}m#{char}\e[0m"
    else
      print char
    end
  end
end

実行すると次のように候補が得られます。

ledsun@MSI:~/wordle_rb►ruby main.rb osclh aer '***t*'
grate
irate
prate

色づけもしています。

Rubyスクリプトの実行結果

引数の設計は適当です。 '*'はクォート必須になるから使いにくいかなあ?などと思っています。

grep使えばいいので、べつにRubyスクリプトにしなくても良かったです。 現に今までしていません。 24時過ぎて、wordleの新課題でているのには気がついているが、PCを開くのは面倒くさいことがたまにあります。 スマートフォンでやれば良いんですが、スマートフォンではgrepできません。 RubyスクリプトにするとWebAssemblyでブラウザで動かせます。 ブラウザで動けばスマートフォンからgrepできます。 やった!夜中でも出先でもwordleできます。

実際は目的と手段は逆です。 WebAssemblyを使ってブラウザでRubyスクリプトを動かすネタで、実装が簡単で自慢できそうな奴を探して出てきたアイデアです。

参考

Ruby 並行・並列くらべ 2

Ruby並行・並列くらべ - @ledsun blogRubyの並行・並列の効果を測ってみました。 DBへのIO時間があると、効果がわかりにくかったです。 DBへの書き込み処理をコメントアウトしてリトライしました。

結果

シリアル処理

シリアル処理の実行結果

2m 4s。 DBへの書き込み処理とコメントアウトする前は2m 27sでした。 23秒速くなりました。 ということはIOバウンドな処理時間は23秒しかないみたいです。 思ってたより、大分CPUバウンドな処理でした。

スレッド

スレッドの実行結果

2m 4s。 シリアル処理と同じです。

プロセス

プロセスの実行結果

1m 31s。なんかすごい速い。 DBへの書き込み処理とコメントアウトする前は1m 48sでした。 15秒縮まりました。

Ractor

Ractorの実行結果

1m 32s 。 DBへの書き込み処理とコメントアウトする前は1m 52sでした。 20秒縮まっています。 シリアル処理比で1.35倍です。

Ractorで並列化したときはプロセスで並列化した時と同じぐらいの性能は出るようです。 プロセスと同じ性能がでるなら、ゾンビ化や例外処理の心配する必要があるプロセスよりRactor使えば良いじゃん、て気持ちになってきました。

Ruby並行・並列くらべ

Ractorで並列に動いているっぽい? - @ledsun blog の続きです。 Ractorで並列処理が出来そうなので、他の方法と性能を比べてみました。

比べるのは次の4つの方法です。

  1. シリアル処理
  2. スレッド
  3. プロセス
  4. Ractor

シリアル処理は比較用の逐次処理です。

あるActiveJobを実行して800ファイルを処理し終わる時間を比べます。 並列数は特に制限しません。 コア数は4つですが30並列ぐらい動くと思います。 必ずしも最適な並列化ではありません。

このジョブにはDBへのIOも含まれています。 並列化だけで速くならない時間もあります。 かなり雑に比べると、CPUバウンドな処理時間が1に対して、IOバウンドな処理時間は0.5です。 並列化でCPUバウンドな処理時間が半分になると、全体の処理時間は2/3になるイメージです。 理想的に4コアを使い切って、CPUバウンドな処理時間が1/4になると、全体の処理時間は1/2になります。 ここまでは行かないはずです。

予測

スレッドはスレッド切り替えのオーバーヘッドの分、シリアル処理より遅くなりそうです。 プロセスはGILを回避するので1.5倍くらいには速くなりそうです。 RactorもGILを回避出来ます。プロセスとどちらが早いか気になる所です。

結果

シリアル処理

シリアル処理の実行結果

Eapsed 2m 27sが基準になる値です。

スレッド

スレッドの実行結果

Eapsed 2m 27s。 シリアル処理より全く遅くなっていないのが意外です。

プロセス

プロセスの実行結果

Eapsed 1m 48s。 明確に速くなりました。

  • 20220922 計測結果を間違えていたことに気がつきました。計り直して貼り直しています。。

Ractor

Ractorの実行結果

Eapsed 1m 52s。 明確に速くなりました。 1.3倍程度です。 1.5倍くらいになってくれると、並列化効いてる感がぐっと出てきて、うれしいです。

ソースコード

改変部分のみ抜粋したものをのせます。

シリアル処理

    annotations_collection_with_doc.each do |annotations, doc|
      messages += Annotation.prepare_annotations!(annotations, doc, options)
    end

全く並列化せずに順次処理します。

スレッド

    annotations_collection_with_doc.map do |annotations, doc|
      Thread.new do
        messages += Annotation.prepare_annotations!(annotations, doc, options)
      end
    end.each(&:join)

スレッドは変数は共有できるし、GILがあるのでレースコンディション考えなくて良いし、めちゃくちゃ簡単です。

プロセス

    annotations_collection_with_doc = annotations_collection_with_doc.map do |annotations, doc|
      read, write = IO.pipe
      Process.fork do
        messages = Annotation.prepare_annotations!(annotations, doc, options)
        Marshal.dump([messages, annotations], write)
      end
      [read, doc]
    end.map do |read, doc|
      error_messages, annotations = Marshal.load(read)
      messages += error_messages
      [annotations, doc]
    end

プロセス間のデータのやりとりはparallel gemのソースコードを参考にしました。

データは特に工夫せずにMarshalしています。 これでプロセス間通信出来て、Rubyは便利です。 もしかするとMarshalが遅くなっているかもしれません。

Ractor

    Ractor.make_shareable(TextAlignment::CHAR_MAPPING)
    Ractor.make_shareable(TextAlignment::LCSMin::PLACEHOLDER_CHAR)
    annotations_collection_with_doc.collect do |annotations, doc|
      r = Ractor.new do
        a, d, o = Ractor.receive
        m = Annotation.prepare_annotations!(a, d, o)
        Ractor.yield [m, a]
      end
      r.send [annotations, doc.dup, options]
      [r, doc]
    end.each do |r, doc|
      error_messages, annotations = r.take
      messages += error_messages

      [annotations, doc]
    end

Ractorはデータのやりとりのためのおまじないが色々必要です。

感想

わりとあっさり、各種の並行・並列処理が書けました。 Rubyは便利ですね。

参考

Ractorで並列に動いているっぽい?

次の感じのRubyソースコードがあります。

annotations_collection_with_doc.each do |annotations, doc|
  messages += Annotation.prepare_annotations!(annotations, doc, options)
end

Annotation.prepare_annotations!(annotations, doc, options)の先の処理はテキスト処理でCPUバウンドであることがわかっています。 またannotations_collection_with_docは数十であることもわかっています。 そこでつぎのように、入ってきただけバカスカRactorをつくっって並列かしてみました。

Ractor.make_shareable(TextAlignment::CHAR_MAPPING)
Ractor.make_shareable(TextAlignment::LCSMin::PLACEHOLDER_CHAR)
ractors = annotations_collection_with_doc.collect do |annotations, doc|
  r = Ractor.new do
    a, d, o = Ractor.receive
    m = Annotation.prepare_annotations!(a, d, o)
    Ractor.yield [m, a]
  end

  r.send [annotations, doc.dup, options]
  [r, doc]
end

annotations_collection_with_doc = ractors.each do |r, doc|
  error_messages, annotations = r.take
  messages += error_messages

  [annotations, doc]
end

ざっくり見たかんじでは2倍ぐらいの速度では動きました。 4コアあるので、理想的には4倍の速さになって欲しいです。 スレッドを立てすぎて遅いのか、Ractor間のデータのやりとりで遅いのかは謎です。

たぶんキュー作ってワーカーモデルで、並列数を制限すればいいんですけど、面倒臭いですよね。 なるほどー、MaNyでRactorとネイティブスレッドがM:Nで動いてほしいです。

https://github.com/grosser/parallel を使ったら簡単に並列数を制限できるのでしょうか?

イタリアのメタルバンドのイタリア語のインタビュー記事をDeepLで読む

www.metalhammer.it

僕がDestrageを知ったのは2010年のアルバム「The King Is Fat ‘n’ Old」のすこし前です。 10年前は、イタリア語の記事が日本語で読めるようになるとは思ってもみませんでした。 DeepLは、言語の壁を壊す方向で、世界を変えている気がします。

書くことは経験と共有の産物であり、孤立は作曲を許さないからです。その時期に生まれた唯一の曲が「Everything Sucks And I Think I'm A Big Part Of It」で、それを聴くとわかるんだけど、極端で統合失調症的で脳死状態の曲で、いろんなスタイルやジャンルを変えているんだ。

この曲はとてもヒステリックというかストレスフルです。 初めて聞いたときは、なんでこんな曲を?と思いました。 この言葉を聞くと、なるほど・・・納得です。 てか、コロナ初期のイタリアめちゃヤバかったですね・・・。 コロナとの戦いの一形態だと思うと涙が出そうです。 めっちゃエモ曲やん

Making *MaNy* threads on Ruby

rubykaigi.org

発表資料です。

twitter.com

この記事を書くために発表資料を読み直しました。 MaNyではスレッドがM:Nになるんだと勘違いしていたことに気がつきました。

スレッドは1:Nでした。 順番逆かな?N:1でした。 複数のRubyのスレッドが一つのネイティブスレッドで動くようになります。 要するにRuby1.8までのグリーンスレッドと同じ方式です。 先祖帰りです。

RubyのスレッドにはGVLがあります。 Concurrentには動いてもParallelには動きません。 であれば、Rubyのスレッドひとつひとつにネイティブスレッドを割り当てなくてもいいはずです。 ネイティブスレッド一つにうまいことディスパッチできれば、OSの資源を節約できます。

なぜRuby1.9では、Rubyのスレッドとネイティブスレッドを1:1にしたのでしょうか? C拡張を使ったGemの中で長時間かかる計算を実行すると、RubyVMからは検知できません。 Rubyのスレッドを一定時間で切り替えることができません*1。 一つでも時間を占有するRubyのスレッドがあるとすべてのRubyのスレッドが遅くなります。

イベントループと同じ問題がおきるのが面白いです。 僕はずっとGVLがある理由は、Mutexを使ってとってなくてレースコンディションを起こしちゃうC拡張Gemがある(あり得る)からだと思ってました。 そうじゃない、あるいは、それだけじゃないんですね。

時間を占有するRubyスレッドの問題を解決するために、Dedicated Native thread(占有ネイティブスレッド)というマークを入れるそうです。 そういうマークができるAPIをつくるって意味なんですかね? あんまりよくわかっていないです。 Ruby1.9以降でGVLをリリースするAPIがあって、これはよく使われるようになっているので、同じ要領でAPIを用意すれば、回避出来るだろうということなのでしょうか?

占有ネイティブスレッドがマークされた場合は、Ruby VMがネイティブスレッドを増やします。 占有ネイティブスレッドをマークした以外のRubyスレッドを新しいネイティブススレッドでN:1で動かします。 占有ネイティブスレッドをマークしたRubyスレッドは、元のネイティブスレッドを使って、現在と同じように1:1で動かすそうです。 これで今のRubyのスレッドと同じような動きを担保するそうです。

MaNyでM:NになるのはRactorだそうです。 複数のRuby Ractorに対して、複数のネイティブスレッドが動くそうです。 ここがgolangのgoroutineがM:Nで動くのと同じイメージみたいです。 Concurrentに動かしたければスレッドをつかう、Parallelに動かしたければRactorをつかうという、今のプログラミングパラダイムから変更はないようです。 僕はなぜか、いつの間にかスレッドがParallelに動くって勘違いしてました。

スレッドをつくるプログラムを書くと、並行数に上限を設けたくなります。 リクエストに対して5スレッドつくるプログラムでは、リクエストが100個来ると500スレッドできます。 CPUのコアが16個のときに500スレッドも作っても無駄です。 スレッド数の上限を10とか20とかつけたくなります。 それでスレッドプール作ってキュー使ってワーカーモデルで実装してってなります。 MaNyでは、この部分をRuby VMがやってくれるという話みたいです。

そういえばgoroutineが便利なのも、OSのスレッド数が何個になるか気にせず、バンバンgoroutine作れば、go-langが良い感じに並列に動かしてくれる点でした。 Rubyみたいな歴史のある言語でこれをやろうとすると、どえらく大変なんですね・・・。ひえー

*1:僕は、この原因をよく理解していません。検知できないのか、割り込めるタイミングがわからないのか、それ以外の理由があるのかも知れません

Binding a Lightweight IR and Backend for YJIT

rubykaigi.org

現在のYJITはx86アーキテクチャのCPUだけをサポートしています。 arm64やRISC-Vをサポートしようと考えています。 Rubyのinstructions と機械語の間にYJIT IRという中間層を設けて、CPUアーキテクチャの差異を吸収する作戦だそうです。

つまり、次のものを作りまくるって話ですよね?

そしてつくっている間にRuby自体に機能追加されていく・・・。 やろうとしていることが、壮大すぎてすごいです。 Kevin Newtonのパーサー書き直しといい、Shopifyの人達のアウトプット力ヤバいですね。 会社の仕事としてやっているから分担して進められるとか、そういうことなんですか?

IRという言葉は A Faster CRuby interpreter with dynamically specialized IR - @ledsun blog でも出てきました。 JITを考えるときには一般的な用語なのかもしれません。

Tools for Providing rich user experience in debugger

rubykaigi.org

Google Chromeの開発ツールでRubyデバッグ出来るようにした人の話です。

このOno-sanです。 2021年度Rubyアソシエーション開発助成金成果報告 で着手したもののようです。 コード補完周りはChrome Developer Tool本体にJavaScript用専用のものが組み込まれて上手く動かないそうです。

また、VSCodeからruby/debugを使う VSCode rdbg Ruby Debugger - Visual Studio Marketplace の改良もしているそうです。 Rubyの開発環境が便利なってありがたいです。 普段はRubyMineを使っていますが、RubyMineはそれなり頑張って環境構築しないとつかえないので、 簡単な編集や調査がVSCodeでサクッとできるとそれはそれでいいなあと思います。

この記事を書きながら適当なことを思いつきました。 VSCodeでStackprofで計測するRubyスクリプトを書いて、 実行した結果をしたサクッとVSCode上でグラフで表示できたら*1、 Stackprofを使う人が増えて、速いRubyプログラムを書く人が増えそうです。 たぶんvscode-rdbgのスコープとはズレていて、それっぽいlaunch.jsonを書けば出来そうかな?と考えています。

参考

*1:いま、stackprofのdumpファイルをhtmlに変換して、ブラウザで開く2手なのを、1手にするイメージです。

Towards Ruby 4 JIT

rubykaigi.org

国分さんは英語でも早口でよく聞き取れませんでした。 僕の理解した範囲では

YJITが現れてMJITは役割を終えた。ので、みんながJITコンパイルをハック出来るためのAPI置き場にします。みんなでJITコンパイラをハックしてRuby4を無茶苦茶速くしようぜ。

でした。現地で聞いてたときも「ほんまか?」って思っていたし、今書いてても「ほんまか?」って思っているんですけど、本当なんですか?

国分さんの発表は何を言っているのかわからなくても、エネルギッシュで見ているだけで楽しくなります。 アイドルのコンサートみたいです。