AIにプログラミング作業を奪われている
せっかく10年以上かけて学んだプログラミングだが、人間がコード書くよりChatGPTにやらせた方が早いなということが度々あり、だんだん自分でプログラミングをやる時間が減ってきた。AIにコードを書かせてそれをGitHubにコピペして残りの時間は遊んでるだけで成果が出てお給料ももらえる日は近いし、段々会社もそのことがわかってきて失職する日も近い。
残念ながら現時点では全ての仕事がAIで上手くいくわけではないが、どういう時に使えるかを知っておくと楽をしやすくなるので、僕がどう使っているかをまとめておく。
失職できるケース
簡単なスクリプトを高速に書かせる
僕はRubyが全ての言語の中で一番慣れており、StackOverflowやドキュメントをほぼ見ずに大抵のプログラムを書き切れるため、Rubyを書いている時がプログラマとして一番生産性が高いのだが、それでも最近AIにRubyを書かせたことがあった。
数行で書けるほど単純ではないが複数ファイルが必要なほど複雑でもないみたいな時、欲しい仕様を入力する時間が十分に短ければAIにやらせた方が生産性が高いことになる。人間が次に何を書くかを考えながらキーボードタイプするより、言語モデルがテキストを出力する方が圧倒的に高速なので、当然そういうことになる。
具体的に何に使ったかというと、本番環境で走っているアプリケーションサーバーのワーカーのメモリ使用量に各コンテナ内で偏りがあり、アプリ内から取っているメトリクスはリクエストが来た時にしか飛ばないので、メモリ使用量の分布をプロセスの外から調べて綺麗に整形するスクリプトが欲しいということがあり、それを書かせた。
成果物はこれなのだが、要件を伝えて、何度か出力の整形方法に関してフィードバックするだけで完成した。まあこんなのは書いてても楽しくも何ともないので、AIにやらせたらいいと思う。
書くのが面倒な言語を書かせる
どこでもRubyが使えたら僕は楽なのだが、世の中には主なスクリプト言語としてPythonを採用しているツールがそこそこある。仕事で長い間使っていたこともあるし僕も普通に書ける言語ではあるのだが、たまにStackOverflowを見に行かないといけない程度には不慣れなのでそういう点で面倒くさい。
そういったツールの一つがLinux perfというプロファイリングツールで、これはプロファイル結果をスクリプトに集計させるperf scriptという機能があるのだが、Pythonなどの言語で書かないといけない。perf scriptは何度も書いたことがあるが、Pythonは僕はStackOverflowを開かないと書けないのが面倒くさいのでAIにやらせることにした。
とりあえず書かせてみたら、perf scriptのインターフェースを何故か知らなかったようなので、以前書いたスクリプトをコピペしたところ仕様を理解してもらえた。その後は欲しい仕様を伝えて、必要な仕様が変わる度にそれを伝えたら都度欲しいものができた。成果物はこれ。
自分が詳しくない環境を扱わせる
僕は私用にはLinuxを使い、仕事ではmacOSを使わされているのだが、Windowsはあまり触らないため詳しくない。ruby/rubyのCIでVisual Studio 2019上でVisual Studio 2015相当のC言語環境をセットアップする必要があったのだが、まるでやり方がわからなかったので、当時使っていたGitHub ActionsのYAMLを貼り付け、書き直してもらった。
例えば vcvars64.bat -vcvars_ver=14.0
と書くとVisual Studio 2015相当で、その結果 C/C++ Optimizing Compiler Version 19.00.24247.2と出たら2015で、19.29.30153だと2019なのだが、こんなわけのわからないバージョンスキームを採用しているWindows環境を理解するのは人類には不可能なので、AIにやらせると良い。
他には、会社のKubernetes環境で特定のラベルを持つPodを探して中に入る必要あったのだが、Kubernetesを採用していないインフラを触る期間が続いたため全然使い方がわからず、一連のコマンドをAIに書かせたりした。エラーにも遭遇したが、エラーを貼りつけたらAIがデバッグしてくれた。 Kubernetesも人類には難しすぎるので、AIに使わせると良い。
自分でやった方がいいケース
必要な入力が大きい
僕がLLMを使う時は基本的にChatGPT (GPT-4) を使っているのだが、インターネット上のこのファイルを読んでくれとか、コンテキストに収まらないほどでかいファイルを貼り付けてこれを読んでくれみたいなのは少なくともデフォルトのチャット画面からはできないので、ChatGPTに渡す情報量がある程度小さくないとワークしない。
例えば膨大なRuby処理系のコードを読み込んでそこに最適化を実装するみたいな作業は、それを任せるのに必要なコンテキストが大きすぎて伝えるのが難しいので、自分でやっている。目的のコードに似た実装を持つ関数を1つ渡して書かせてみたこともあるが、自分で書いたヘルパー関数の定義なども渡さないと正しく使ってくれない。
そういう状況だとGitHub Copilotの方がインターフェース的には向いてるような気がするが、なんというか所詮コード補完だなという感じの結果になることが多くて、AIに渡すコンテキストを自分でコントロールしないと、読んで欲しいところを読んでくれてない感じになる。
大体書けていて細かい手直しだけ必要
チャットでAIにコードを書いてもらうと、何か修正が必要になった時に全て書き直しになるため、コードが長ければ長いほど再生成に時間がかかる。何十行もあるコードで、自明な変更を一行足すくらいだと、AIにやらせる方がかえって生産性が低くなる。
また、何度もやり取りを繰り返すと、それ自体大きなコンテキストになってしまい、最初の方に言ったことを忘れてしまう。これ忘れとるやんけ、と伝えると、その前に伝えた別のことを忘れてきたりする。ある程度できてきて簡単な修正で完成する状態になったら、人間がやってしまった方がいいこともある。
まとめ
いかがでしたか? 人類はまだ失職できないことがわかりました!
2023年にやったこと
今年で30歳、社会人9年目、在米5年目になった。今年は
- 趣味でRJITを作り、仕事でYJITを超高速化した
- 初めて論文を国際会議に投稿し、採択された
- 子供とプリスクールに行き始めた
という感じの一年だった。
仕事
大変ありがたいことに、自分が今一番興味のある仕事であるYJITの高速化に集中できた一年だった。 いろいろやったが、代表作は以下の三つかなと思う。
どれもベンチマークがかなり速くなった。 特に二つ目と三つ目は、自分で発案してかつ主に僕が重要性を訴えていた奴で、 それらで大きな成果が出たときはかなり達成感があった。 単独のPRでRailsベンチが7%速くなった時はこりゃ昇給するわと思ったが、実際めちゃくちゃ昇給した。
ベンチマークも速くしている一方、僕は本番アプリの最適化を主戦場にしていて、 最適化のヒントになるメトリクスをひたすら追加し、 毎週のようにRuby masterを本番にデプロイし、効果を計測することを繰り返した。 Ruby 3.2ではYJITによる高速化が10%程度だったのが、 Ruby 3.3では弊社最大サイズのアプリ(モノリス)と最大トラフィックのアプリ(StoreFront Renderer)がどちらも YJITで17-18%くらい高速化する状態になった。 チーム全体での成果であるが、上記の変更は目に見えて効果があった。
論文
YJITに関する論文を業務で書き、MPLRという査読付きカンファレンスで採択された。
僕は去年修士を卒業したばかりで、在学中に論文形式のものは何本も書いたのだが、 そもそも修士論文なしで卒業するプログラムなこともあり、論文をどこかに投稿するようなことはなかった。 チーム内にYJITの作者がいるため僕はファーストオーサーではなかったが、 執筆的にも論文のトピック的にもそこそこ貢献したため、セカンドオーサーであった。 学士でも論文を投稿することはなかったので、これが初めての論文投稿経験となった。
修士で取った授業のうち一つは研究の仕方そのものや論文の書き方を学ぶためのものだったので、 論文の執筆には少なからず興味があったし、個人の時間で書くほどの余裕はなかったので、業務で経験できて良かった。 ポルトガルでのカンファレンスだったが、学部時代にお世話になった先生にばったり会ってご挨拶できたのも良かった。
子供
子供が三歳になり、プリスクールに行き始めた。 三歳だとオムツが外れてないといけないプリスクールがほとんどで、 娘はまだオムツをしていることにより選択肢が限られているのと、 あと単純に費用が安いので、Co-Opのプリスクールを選んだ。 Co-Opというのは親参加型の奴のことで、当番制でお手伝いをすることになっている。
月に一回有給を取って子供と一緒にプリスクールに登園してそのジョブをやっている。 あと不定期に資金調達のためのイベントが行われいて、その手伝いもやっているし、 休日の午前を使って大掃除するのも何回かやっている。 プリスクールに行き始めれば子供を預けられる時間ができると最初は考えていたが、 子供が一人で登園するのを嫌がるため常に妻か僕が一緒に行っていて、 今のところ育児の負担はむしろ増えている。
資産
僕のポートフォリオはThree-fund portfolioという奴で、 資産のほとんどをUS株72%、(US以外)全世界株18%、US債券10%で持つ状態を3年くらい維持している。 US株以外の資産は去年の下がりをようやく取り戻したくらいの状態だが、US株が好調なのでトータルではものすごくプラスになっている。 巷ではいわゆるオルカンが流行っているが、 これもUS株を60%持つインデックスなので良い利益が出るんじゃないだろうか。
USでは総資産が$2M (2億8000万円) 以上あるとグリーンカードを放棄する時にExit Taxという税金が取られることになっていて、 今はその半分くらいまで進捗したのだが、 僕は全資産に対して何割か課税されるという勘違いをしていたのでこれまで結構ビビっており、 $2Mに達する前に日本に本帰国するのを検討していた。 よく調べてみると、実際には資産ではなく未確定のキャピタルゲインに対して課税されるということだった。 自分が覚悟していたほどは課税されなそうなので、帰国は完全に家族の都合だけ見て決めればいいなと思った。
散財
テスラ
ここ数年値上げを続けていたテスラの値段が、電気自動車の税制優遇の関係で今年何度か下がった。 一番最初に大きく下げた直後に、中古の2015 Mazda 3から新車の2023 Tesla Model 3 RWD ($43,990) に買い替えた。 僕は自動運転が欲しくて買っていて、普段は無課金のAutopilotを酷使し、連休中の現在は一時的にFull Self-Driving (月額$199) を試しているのだが、どちらも便利だし、乗り心地も良い。 妻も満足しているようでよかった。
歯科矯正
アメリカのママ友に歯並びの良い人が多いようで、妻が歯科矯正をやり始めた。 僕はあまり興味はなかったのだが、妻がやって欲しいと言っていたのと、 歯並びが良い方が歯磨きがしやすくなるという話に説得され、僕もやり始めた。 一人 $5,000 以上かかっていて高いし痛いのだが、終わったらどうなるのかは楽しみである。
登壇
以下の2回登壇した。 今年は社会人になってから最も登壇数が少ない一年だった。 リモート登壇はもう少し自分から応募するなどしても良かった気がするが、 物理参加枠はRuby Infraチームでニューヨークに集まった奴と、YJIT論文でポルトガルに行っていた分で家族に負担をかけたので、 これが限界と思われる。
ポッドキャストでは初めてRemote Rubyに出させてもらった。 同僚が結構出ているシリーズなので、入社したころに結構聴いたのだが、自分も出れて良かった。
ブログ
今年は日本語では9記事ほど書いた。 書きたいネタを思いついた時に衝動的に書いていたのだが、 おおむね例年くらいのペースで投稿し、ブクマもそこそこ集まった。 どれも楽しく書けたので、そういう感じで続けたい。
- エンジニアが給料を12倍にする方法 - k0kubun's blog
- Mojoは「C言語のように速いPython」なのか - k0kubun's blog
- 自作PC2023: Ryzenをやめた - k0kubun's blog
- Re: OSSで世界と戦うために - k0kubun's blog
- Rustプログラムのデバッグ辛すぎ問題 #Rust - Qiita
- Ruby 3.3でYJITを今すぐ有効にすべき理由 - k0kubun's blog
- YJITの性能を最大限引き出す方法 - k0kubun's blog
- RJIT: RubyでRubyのJITコンパイラを書いた - k0kubun's blog
- RubyKaigiでJITコンパイラの書き方について発表した - k0kubun's blog
Hacker News上でアクティブな同僚が多いので僕もHacker Newsのフロントページに乗るかどうかということを気にするようになったが、 英語で投稿したものは2回ほどランクインし、英語圏でランキングを上げる経験も積めるようになってきた。
- RJIT, a new JIT for Ruby (336 points, 2位)
- Ruby 3.3's YJIT Runs Shopify's Production Code 15% Faster (175 points, 2位)
OSS活動
RJIT
今年の新作はRJITだけである。 最初いくつかのベンチマークでYJITを抜いてしまった結果、 YJITを仕事にしている都合これはconflict of interestになり得ると言われたり、 RJITの話ばかりしているのがお気に召さなかったのかTwitterでリムーブされたりしたため、 その辺を丸く収めるべく、出した直後は意識的に開発速度を落としていた。 とはいえ、やりたいことはいろいろあるプロジェクトなので、来年もほどほどのペースでいろいろやれたらいいなと思う。
RJITとYJITを両方開発していた関係で、 今年は過去1年間のコミット数がnobuさんを超えた瞬間もあったのだが、 上記のRJITの件と、論文執筆やリリース前のバグ修正で失速し、 Ruby 3.3もnobuさんの方がコミットが多かった。 nobuさんはすごい。
sqldef, xremap
去年に引き続き、 個人でやってるOSSではsqldefとxremapが一番盛り上がっている。
sqldefはmssqldefで@odzさん、psqldefで@hokacchaさんが新たにメンテナに就任し、 大変心強い。メンテナ5人体制になったので、 リポジトリも個人アカウント下からsqldef orgに移管しsqldef/sqldefとなった。
xremapもスターが☆1,000を超えた。mrubyで書いていたころはhanachinさんもメンテナだったが、 Rustに書き換えてからずっと僕だけでメンテしている。 普段のissueのやり取りでも、sqldefはGoだから皆書いてくれるが、xremapはRustなので書けないと言われることが多い。 正直sqldefもRustで書き直してenumやパターンマッチを使ったりしたいのだが、 人類のほとんどはRustを書きたがらないため、sqldefの方はGoのままにしておくか…という気持ちである。 「は? Rust書くの簡単やろがい」と思った人には、xremapの共同メンテナとして無双していただければと思う。
2024年は
今年はYJITが大幅に速くなるアイデアを運良く複数思いついたが、 来年はそれに再現性を持たせられるようにして、 誰も想像していなかったような方法でYJITを無限に速くし、無限にお金を稼ぎたいと思う。
過去ログ
Ruby 3.3でYJITを今すぐ有効にすべき理由
Ruby 3.3がリリースされた。YJITには非常に多くの改善が含まれたリリースだったが、 NEWS解説記事やリリースパーティーでは 2点しか触れられなかったので、この記事ではRuby 3.3でYJITがどう改善されたかについて解説する。
YJITは既に実用段階
YJITはRuby 3.1で導入されたが、Ruby 3.2の時点でexperimentalのマークが外れ、実用段階となった。 Ruby 3.2では、以下のような企業で性能改善が報告された。
- DeNA: 40% 高速化
- GMOペバボ: 18% 高速化
- STORES: 6.5-7.5% 高速化
- Timee: 10% 高速化
- メドピア: 2.8% 高速化
- BOOK☆WALKER: 20-30% 高速化
- Discourse: 15.8-19.6% 高速化
- Lobsters: 26% 高速化
- CompanyCam: 20-40% 高速化
弊社Shopifyで最もトラフィックが多いアプリでは、 Ruby 3.2の時点でのYJITによる高速化は10%程度だったが、 Ruby 3.3では17%高速化まで改善した。 YJITを本番で使っている全ての人にRuby 3.3へのバージョンアップをお勧めしたい。
我々はRuby 3.3.0のリリースを安定化させるべく、リリース前からRuby masterを本番のモノリスに全台デプロイしていた。 現在はリリース版Ruby 3.3.0が走っており、YJITも有効になっているが、Ruby masterを使っていた段階で数多くのバグを弊社が発見・修正したため、 Ruby 3.3.0は比較的安定したリリースになっているはずである。
YJIT本番運用のための手引き
ここまで読んで「YJIT使うぞ!」となって使ってみたが思うように性能が改善しなかった人のために、 本番運用のためのドキュメントへのリンクを貼っておく。
- Ruby 3.2
- Ruby 3.3
Ruby 3.3で本番運用に便利なツールが増えたため、バージョンごとに少し内容の異なるドキュメントを管理している。 Ruby 3.2時点での日本語の記事としては YJITの性能を最大限引き出す方法 がある。 Ruby 3.3で便利になった点は以下で解説する。
Ruby 3.3のYJIT改善点
前置きが長くなったが、ここからNEWSで触れられている改善点について解説していく。 運用方法に影響がある点から順に書いていく。
Code GCのデフォルト無効化
YJITが生成するコード量は --yjit-exec-mem-size
でコントロールできる (デフォルト64MiB)。
生成コードのサイズが --yjit-exec-mem-size
に達すると、YJITはデフォルトで以下のような動きをする。
- Ruby 3.2: 全ての生成コードを破棄し、以降呼ばれたメソッドをコンパイルし直す。
- Ruby 3.3: 新たにメソッドをコンパイルしなくなる。未コンパイルのメソッドはインタプリタ実行される。
Ruby 3.2のこの挙動をCode GCと呼んでいる。
数時間に一回Code GCが走る程度なら大した性能影響はないのだが、
--yjit-exec-mem-size
が小さすぎると、頻繁にコンパイルし直すコストによってアプリがむしろ遅くなる場合がある。
こういった問題にヒットしにくくなるよう、Code GCはデフォルトで無効になった。
これの最大の利点は気軽に --yjit-exec-mem-size
が下げられるようになった点で、
RubyVM::YJIT.runtime_stats[:code_region_size]
を参考にしつつ、
--yjit-exec-mem-size=32
のような設定を使うのが現実的な選択肢になった。
もう一つの利点にCopy on Write フレンドリになる点がある。 弊社ではモダンなUnicornフォークであるPitchforkを使っているが、 これは既にリクエストを捌いているワーカーを定期的にフォークし直すことでプロセス間のメモリ共有を目指すリフォーキングという機能が備わっている。 リフォーク対象のサーバーが既にYJITのコンパイルを停止済みなら、 YJITが使うメモリは全ワーカー間で共有し続けられることになる。
RubyVM::YJIT.enable の追加
YJITはこれまでコマンドライン引数 --yjit
や環境変数 RUBY_YJIT_ENABLE=1
で有効化するしかなかったが、
それらを使わずとも、Rubyコード内で RubyVM::YJIT.enable
を呼び出すだけでYJITが有効化できるようになった。
開発中のRails 7.2ではこれを呼び出すイニシャライザがデフォルトで生成されるようになった。 つまりRailsではこれを使ってYJITがデフォルト化されたということになる。
もう一つの利点は、YJITの起動を遅延させることで、
アプリ初期化後は使われないコードのコンパイルを避けメモリ消費量を削減できる点である。
Railsのイニシャライザでも効果はあるが、理想的にはUnicornのafter_fork
やPumaのafter_worker_fork
から呼び出すと良い。
これにより、起動するワーカーの半分だけYJITを有効化し、インタプリタと性能を比較する基盤として利用することもできる。
なお、--yjit-exec-mem-size
などのチューニングオプションも指定するだけでも起動時にYJITが有効化されるため、
その場合も遅延起動するには --yjit-disable
を明示する必要がある。
一部YJIT statsのデフォルト提供
RubyVM::YJIT.runtime_stats[:yjit_alloc_size]
がデフォルトで提供されるようになった。
これはYJITがRustのヒープにアロケートしているメタデータのサイズで、:code_region_size
と合わせると、
YJITが使っているメモリをバイト単位で監視できる。
YJITを運用する際は、この2つだけでも見ておくと --yjit-exec-mem-size
のチューニングの参考になる。
また、RubyVM::YJIT.runtime_stats[:ratio_in_yjit]
が --yjit-stats
時にデフォルトのビルドでも提供されるようになった。
これはRuby VM上で実行される命令のうち何%がYJITで実行されたかを示すもので、
理想的には最大99%くらいが望ましいが、アプリによってはこれが平均90%とかでも18%高速化したりする。
速度を妥協して --yjit-exec-mem-size
を下げる場合は、これがあまり下がり過ぎないように気をつけると良い*1。
NEWSにはないが RubyVM::YJIT.runtime_stats[:compile_time_ms]
も追加されており、デフォルトで使える。
これはYJITがコンパイルに使った累計時間を出すもので、例えばリクエスト前後で呼んで差を取ると、
そのリクエストでのYJITのコンパイルのオーバーヘッドを見ることができる。GC.stat(:time)
に似ている。
YJITのメモリ使用量の削減
Ruby 3.2の時点でRustの省メモリ化の努力はあったが、
Ruby 3.3でもRcのかわりにBoxを使ったり、ひとつのu8に様々な意味のビットを詰めまくるといったチューニングが行なわれ、
YJITが使うメタデータのサイズは大幅に小さくなった。
Ruby 3.2とRuby 3.3で :code_region_size
が同じ程度であれば、:yjit_alloc_size
にあたる部分は大きく削減されるはずである。
それから、--yjit-cold-threshold
という概念が追加され、あまり使われないメソッドのコンパイルをスキップするようになった。
また、--yjit-call-threshold
がデフォルトで30なのが、メソッドやブロックが4万以上あると120に自動で引き上げられるようになった。
これにより、コンパイルしてもあまり性能に貢献しないメソッドがコンパイルされなくなり、メモリ使用量が節約される。
高速化
Ruby 3.2の時点では、YJITがコンパイルに対応していないパスがそこそこあり、
--yjit-exec-mem-size
が十分でも :ratio_in_yjit
が90%程度に留まることがあった。
記事が長くなってしまったので詳細は別の機会に語るが、
Ruby 3.3ではこれらの問題をほぼ解決し、ほとんどのアプリで :ratio_in_yjit
が99%に達するようになった。
Ruby 3.2だと運悪くYJITの対応率が低かったアプリでも、Ruby 3.3なら速くなることが期待できる。
あとは、特別な最適化が実装されたCメソッドの数が増えており、
NEWSで言及している奴のことだが、
それらはインライン化もされる。
また、単に即値を返すRubyメソッドのインライン化も実装され、具体的にはRailsのblank?
とかがmov
命令一発になるのだが、
present?
の方もRails 7.2では同じ最適化が期待できるようになった。
まとめ
あなたとYJIT、今すぐ RUBY_YJIT_ENABLE=1
参考文献
- Ruby 3.3's YJIT Runs Shopify's Production Code 15% Faster
- YJIT Is the Most Memory-Efficient Ruby JIT
- Ruby 3.3's YJIT: Faster While Using Less Memory
*1:--yjit-stats のかわりに RubyVM::YJIT.enable(stats: true) でもstatsが有効化できるので、これを使ってPumaやUnicornのワーカーのうち1つだけstatsを有効にしておくと、比較的楽にratio_in_yjitが監視できて便利かもしれない。
エンジニアが給料を12倍にする方法
はてブの人気エントリーに日本のエンジニア達は海外に出なければいけないという記事があった。 カナダ在住で経験年数4年のソフトウェアエンジニアで年収1600万円の方らしく、 日本より海外の方がソフトウェアエンジニアの給料が一般に高いので海外に行くべきという話が書かれている。
実際僕も居住地域による給与差を利用すべく渡米し、先月の記事 では新卒から数えて8年で年収が12倍になっていた話も紹介した。 一方、年収1600万円であれば海外に出なくても稼げると思っているので、 国内にいてもできそうなものも含め、ソフトウェアエンジニアとして給料を上げる上で過去に活用したハックを紹介していきたい。
昇給履歴
新卒入社
僕が新卒で入社した会社の当時の初年度給与は450万円だった (公開情報)。 大学の4年間はずっとアルバイトとしてソフトウェアエンジニアをやっていて、 3社を渡り歩いて時給は800〜1350円という感じだったが、それに比べると正社員というのはすごい額の給料がもらえる。 経験年数というのはビザの取得とかにも影響してきたりするので、正社員にはさっさとなってしまうのが良い。
外資転職
新卒社員として1年11か月働いた後、外資の会社に転職した。 東京のエンジニアポジションだと、この会社の最低年俸は800万円とかである (公開情報)。 新卒2年目の間に年収800万円に達していたら昇給RTAとしてはまあまあという感じがする。
転職の前に転職ドラフトに参加していたのだが、 ありがたいことに外資ではなくともこれくらいの額の指名が結構いただけ、 1000万円で指名していただけた会社もあり、そのあたりを根拠にこの転職での給与を交渉した。
外資なら、日本にいて年収2000万円の人もざらにいるので、 年収1600万円は外資であれば日本でも割と有り得る話だと思う。
買収
僕はこの会社にシリーズCの資金調達直後に入社したのだが、その1年後に会社が買収された。 この時社員からミリオネア (つまり1億円もらった人) が50人以上生まれたらしい (公開情報) のだが、多分僕もそれにカウントされてそうな程度にはお金をいただいた。 この額がどう支払われたかは公開情報ではないが、買収後は4年間在籍してるわけで、 4年で1億円以上もらえてる場合の年収は…まあそういうことだ。
有望そうなスタートアップを見つけたらなるべく早いうちに入っておくと、 日本でも一発で大金を得られるチャンスになるかもしれない。
渡米
買収で得られる収入はボーナスのようなもので、基本給にあたる部分は東京のエンジニアらしい推移を続けていたが、 渡米した段階で年収が増えて $151,491 になった (公開情報)。 当時の為替で1600万円。アメリカの就労ビザであるH-1Bビザを申請する時、 給与は最低このくらいないといけないというガイドラインがあって、 僕の地域のSenior Software Engineerの当時のPrevailing Wageがこれであったと記憶している。 なので、シニアエンジニアがH-1BでSFベイエリアに渡米すると、最低でもこの年収はもらえることになる。 今の為替だと2200万円になる。
僕の経験上、どこの会社でも従業員の現在の住所に従って給与には傾斜がかかる。 例えアメリカの会社に勤めていても、 例の記事の人のようにカナダ在住であればアメリカのNYやSFから勤めるのに比べたら給与は劣るはずだし、 日本からだとそれより更に低くなる。 逆に僕はカナダの会社にSFベイエリアから勤務しているが、多分カナダの本社周辺の人たちより良い傾斜がかかっている。
つまりニューヨークかサンフランシスコに住みましょうという話になるのだが、 どっちも治安は最悪である。その点、サンフランシスコから少し南のシリコンバレーのあたりは、 田舎なので治安が少しマシで、給料はサンフランシスコより若干劣る程度で、日本食も豊富なのもあり、 いいバランスだなと思ってそこに住んでいる。
昇進
外資だと、マネージャーにならなくてもIndividual Contributorとしてそこそこ昇進し続けられるのが普通である。 Senior, Staff, Principal, Distinguished あたりが割とメジャーなタイトルで、 タイトルがインフレすると間にSenior StaffとかSenior Principalが挟まってくる。 目の前のタスクに集中するのではなく、多くのチームをまたいだデザインやリードをやるようにしていると、 タイトルが上がる。
これらのタイトルはそれぞれ役割が異なるので、単に別の仕事として位置づけて給与に連動させない会社もあるが、 まあ普通はジョブタイトルごとに給与のレンジがあり、それはlevels.fyiを眺めればすぐにわかる。 なので、なるべくビジネスインパクトの大きいタイトルにポジションチェンジを続けると、ついでに給与も上がる。
この会社では僕はStaffまで昇進した。 その際の給与交渉の額の参考にするために他の会社のリクルーターと話したりしていたが、 このタイトルでのSFベイエリアでのスタートアップの基本給の相場は $180,000 みたいな感じだった。 今の為替で2700万円。
大企業転職
この会社で2度目のEXITチャンスが見えてきて、 僕は2億円欲しいと公言していた。 これは在籍し続ければ実現する可能性は十分にあったが、 コンパイラが書きたくなったので転職した。 それができる会社がたまたま社員1万人だっただけなのだが、 スタートアップから一転、上場済みの大企業に移ることとなった。
スタートアップだと一発当てない限りは前述の $180,000 からそれほど年収が増えないイメージだが、 大企業だと2億円みたいな夢はないかわりに、安定して高い年収が得られる。 例えばAmazonのSeniorにあたるポジションは年収 $360,300 くらいらしい (ソース: levels.fyi) ので、大体倍くらいになるということ。 ちなみにこのポジションはリクルーターに話しかけられて受けたのだが、もらったオファーも実際そのくらいの額だった。 実際にはこのオファーは蹴ったわけだが、今の為替だと5400万円で、新卒の450万円の12倍ということになる。 円安じゃなかったとしても8倍はある。
こういった相場感を適切に調べておき、複数の企業からオファーをもらっておくと、 このくらいの額がもらえるような交渉ができる。
この辺は日本にいても当てはまる話で、例えばGoogleでSeniorまで昇進できた場合、 日本オフィスでも $265,266 (4000万円) とかもらえるようだ (ソース: levels.fyi)。
Q&A
お金に執着しすぎでは?
そう思う人は多分お金に困ったことがないのだろう。 学生時代に貯金を全て親の借金の返済に使われ、 大学院進学を経済的理由により諦めたといった経験から、 貧乏に対して常に強い不安を感じ続けている。 実家は家のローンの返済に苦労しているが、 自分の家族は家も教育も不自由なく得られるようにしたい。
起業した方が稼げるのでは?
それがそんな簡単に上手くいくかはおいておいて、 僕はやっぱりコンパイラが書きたいというのが最初にあって、 経営ではなくコードを書くのに集中できるロールのままお金もいっぱいもらえる状況を望んでいる。
海外だと物価も高いのでは?
これはよくあるエアプコメントだと思う。例えば給料と出費が両方4倍になる時、手元に残るお金も4倍になることになる。 実際には、給料が12倍になった期間、例えば家賃はせいぜい4~5倍にしかなってないので、もっと残る。 僕の場合は老後は日本に帰るつもりなので、その残ったお金は低い物価の国で消費することになる。 もっと話を単純にすると、これくらい収入があると1年で数千万資産が増えるのだが、 日本にいたらそもそも額面で数千万受け取るのが大変だと思う。
海外だとレイオフされるのでは?
ビザの状況によっては割と深刻な問題だと思う。 僕の場合はもうグリーンカードを持っているのでそこは安心。これを書いた次の日にレイオフされるみたいなリスクはあるが、 普通は数ヶ月分給料がもらえるし、その間にまともな転職ができそうな程度にはリクルーティングメールは来続けてるので、 少なくとも金銭的な心配はない。個人的にレイオフされたら困るのは、自分がやりたい仕事ができなくなることくらいである。
まとめ
海外に住むと、日本語は使えないし、趣味や食事や医療などの選択肢や治安などが変わってくる。 家庭がある場合は家族にも影響がある。
「日本のエンジニア達は海外に出なければいけない」と結論づける前に、 海外に行くことで得られる給料が本当にその変化に見合うものなのか、 またそれは日本では達成できないものなのか考えておくと今後のためになる。
Re: OSSで世界と戦うために
yusukebe さんの OSSで世界と戦うために を読んで感銘を受けた。 hono の快進撃もさることながら、OSSで日本のコミュニティの外にリーチしたり、 GitHubスター数を伸ばしたりみたいな話は、 自分も10年くらい挑戦し続けているけどあんまり表に出てこない気がするネタなので興奮した。
僕はいくつかの点で上記の記事とは違う方法でOSSで世界と戦っているのだが、 その中でうまく行っているものや、良くないと思っているものなどについて紹介したい。
GitHubのスター数
OSSを始めたばかりの学生時代、GitHubのスターへの執着がもはや煩悩の域であり、 集めたスターの数を合計するCLIツールを作ったり、 同じ計算方法でランキングを作るWebサイトを作ったりした。
このサイトによると、僕の今のスター数は9000を超えている。
自作したOSSの中では、スター数が1600くらいのものが2つ、970くらいものが2つ、700-800くらいのものが3つ、 300くらいのものが2つある。GitHubのアチーブメントでStarstruck x3 (☆512) を達成するプロジェクトは量産しているが、 Starstruck x4 (☆4096) を達成するものは1つもない、という感じになっている。
この10年を振り返ってみて、正直これはあんまり上手くなかったと思っている。 というのも、☆512 のリポジトリを10個持つより、 ☆4096 のリポジトリを1つ持つ方が代表作として人々の記憶に残りやすいし、 メンテの効率も良くなるし、世界に与えるインパクトも大きくなる可能性が高いからだ。
一方で必ずしも多作が悪いということではなく、FluentdやMessagePackといった Starstruck x4クラスのプロジェクトを何度も世に送り出した @frsyuki さんという完全上位互換みたいな存在もいるので、 そういうパスもあることは書いておきたい。 これどうやってたのかというのが気になりすぎて、Rubyist Hotlinksという連載では僕の回の次に古橋さんを指名して、 インタビューは収録済みで公開待ちというステータスなのだけど、とても良い話が聞けたので乞うご期待。
なぜ戦うのか
楽しいからだ。 OSSに限らず一般に、以下のような条件を満たす問題解決に取り組むのは楽しい。 *1
- 自分が最も興味がある分野で
- まだ他の人に十分解決されておらず
- 自分の能力や知識が活きやすい問題
そういった問題を解決するソフトウェアを書く時に、興味が一致するかわからない現在の所属企業で予算や人を集めて始めるよりは、 業務外で個人で作り始める方がよっぽど実現ハードルが低いので、そういう意味で個人開発はありがちな選択肢になる。
僕の場合はコードを秘密にして他者を出し抜きたいみたいな気持ちがなく、 むしろお互いのコードをオープンにして議論を深める環境に身を置く方が知的好奇心が満たされるため、 個人で書いたソフトウェアは基本的にオープンソースにしている。
より多くの人に使ってもらった方がより難しい問題に挑戦する機会が増えるし、当然承認欲求も満たされるので、 せっかくなら可能な限りバズらせてスターを集めて楽しくやりたいと思っている。 あと、純粋に数字を伸ばすことにこだわっていたとしても、 持続性や自分のパフォーマンスの維持のため、どっちにしても楽しさの追求は重要になると思われる。
OSSとインフルエンサー
yusukebe さんの記事にこういう話があった。
声を上げることは昨今のOSSでは強力な戦闘力になる。 我々は、なかなかこの文脈に我々は入れない。 それは英語ができないからではない。日本語で話している環境の中で英語で発信しても力にならないからだと思う。
大筋同意なのだけど、「日本語で話している環境の中で英語で発信しても力になる」ケースは普通にあると思っている。この戦闘力は本質的には「目的の環境でインフルエンサーに気にかけてもらえる力」*2 であると僕は捉えていて、英語を喋っている人の方がよくリーチするのでインフルエンサーになりやすく、そこに一方的に英語で発信するだけでは不十分で、何らかの方法でその人たちの気を引く必要があるがこれが難しい、という構造だと思っている。
やり方はいろいろあるが、海外のカンファレンスに何度も参加して仲の良い関係になるとか、そういう人たちが多く所属する会社やチームに入ってしまうとか、影響力のあるポッドキャストに出るなどして知ってもらうとか、そういう比較的難易度の高い話。*3 一度そういうコネができると、日本のコミュニティに囲まれている場所で(英語で)発信していても、 英語圏のインフルエンサーの会話の輪に入れてもらったり、自然と自分の発信を取り上げてもらえたりする。 その先は発信するネタの質の問題だと思われる。
英語のXアカウント
どちらにしても、英語で発信するのであれば投稿する内容は英語オンリーにした方がフォロワー獲得の効率は当然良くなると思われる。 僕の場合は自分の英語の練習および英語話者のコミュニティや元/現同僚とのコミュニケーションのためにXの投稿をリプライ以外は英語に倒しているのだが、 それでもフォロー/フォロワーの日本人比率は依然として高く、正直まだ日本語圏で活動してるなという感覚が強い。
僕のように英語アカウントにコンバートするデメリットは、 日本語圏で(ブログではなく普通の)ポストをバズらせたりフォロワーを獲得したりみたいなのが明らかに難しくなっている点で、 英語発信用アカウントを例えば @honojs のような形で分離するのに成功した場合はどちらのコミュニティにも効率良く発信できそうでいいなという感じがする。とはいえ、僕の日本語の細かな発信に関しては、日本語のSlackやZulip *4 で割とベラベラ話してるので、まあそれでいいかという気もする。
子ネタとして極端な例を出しておくと、Matzは日本語の投稿が多いけど明らかに英語圏の多くの人が(おそらく翻訳機能を使って)読んでいるし、 Railsコミッター四皇の @kamipo さんはアイドルや食べ物などに関する日本語の投稿が多いけど、 英語圏でかなり影響力があると思われるDHHにフォローされている。 僕にも彼らのようなパワーがあればもっと自由に発信できたのに、と思う。
英語圏へのリーチ
yusukebe さんの記事では名前が登場しなかったが、Redditは比較的敷居が低く、かつターゲット層にリーチさせやすいので便利だと思う。 ある種英語圏のはてなブックマークのような存在だけど、人気投稿に加えて新規投稿もそこそこ露出する仕組みになってるので、 アカウントを作って突然GitHubリポジトリのリンクを張るだけでも割とスター伸ばしに貢献するような印象がある。 何かスターが伸びてるなと思ったら、誰かが自分のプロジェクトをRedditで褒めてくれていた、みたいなこともあった。
まあでもやっぱり本命はHacker Newsかなと思う。 Redditだと良くも悪くもプログラミング言語などでコミュニティが分断されたSubredditを使いがちだが、 Hacker Newsだと全テックコミュニティが混ざるので人の目が多くなるし、 こちらのトップに載る方がインパクトが大きい印象を受ける。 業務時間中にRuby関係のスレに同僚がよくコメントしまくっている様子を見るのだが、 それくらい皆気にかけているということ。
僕が書いたもののうち2つが今年Hacker Newsのトップページに載ることに成功した。
- RJIT, a new JIT for Ruby (336 points, 2位)
- Ruby 3.3's YJIT Runs Shopify's Production Code 15% Faster (175 points, 2位)
どちらも僕自身でHacker Newsに掲載したものではないが、 X上で(上述したような方法でできたRuby界のコネにより)インフルエンサーによってシェアやリポストされた結果、 日本人がはてブでコメントを書くようなモチベーションで、 Hacker News上でコメントしたい人がでてきて伸びたという感じだと思われる。
神対応
issueで機能要望が上がってきた時、即日実装してクローズするみたいな生活を繰り返していると、 それだけでGitHub Sponsorになったよ、という人が出てくる程度にはそういった「神対応」にはOSSを伸ばす力がある。
それはそうなんだけど、「神」という名前がふさわしい程度に希少であるのには理由があって、 例えば複数プロジェクトでアクティブに要望が来続けると、割と簡単に1人の人間のキャパシティを超える。 僕はフルタイムで仕事を続けながら0歳児が生まれてからの1年9か月でCS修士を取るという無茶をしていたことがあるが、3歳児となった今の方が子の睡眠のスケジュールが不安定で作業時間の確保が難しくなっており、 その上で家の事務処理やプリスクールとかで発生する雑務も全てこの作業時間に押し込む結果、 業務以外でOSSをやるのはissueを書いてきた人自身にパッチを書いてもらってマージするのが精一杯という感じになっている *5。
「神対応」が可能になるのは早くても子が小学校に入ってからかな、という感じだが、 まあそもそも子供と時間を過ごす方が他人のOSSのissueを実装するより楽しいし、 一番興味があるOSSは業務時間中に触れるので、現状に何か不満があるわけではない。
コントリビューター
神対応不可能性に対する解としては、それをフルタイムのジョブにしてしまうというのが多分最も持続性がある。 僕は自分が一番興味があるRubyのYJIT開発にそれを充て、 業務外のプロジェクトの開発スピードはある程度諦めることによって(OSS)ワークライフバランスを保っている。
とはいえ、自分の仕事にならなかったとしても、使う人がいるものに関しては可能な限り速く開発された方がいいので、 業務外のOSSはなるべくissueを出した人に自分で実装する手助けをしたり、 力がある人にはメンテナになってもらうなどすることで、可能な限り最大のスループットを出そうとしている。 sqldefはありがたいことにそこそこメンテナがいるけど、 sqldefとxremapあたりは引き続きメンテナを増やしていきたい感じなので、興味がある人は声をかけて欲しい。
英語
東京にいたころ、隣の机にいる上司が英語ネイティブでかつ彼と比較的高頻度で話す必要があった時期があるのだが、 その1年が僕の英会話力は一番伸びた感じがあり、そこで業務に必要なラインも超えた気がする。 そういう環境にどうにか自分を突っ込むというのが一番効率がいい。
プログラミングに関わる語彙が基本的に英語と日本語で共通してる関係上、 業務に必要な会話で語彙に困ることはほぼない。 これは裏を返すと、要求語彙力的には「業務に必要なライン」は「日常会話に必要なライン」 を遥かに下回ることを意味しており、正直仕事以外の雑談は未だに理解に失敗することが多い。 それでも「OSSで世界と戦う」という要件では困ることはない。
時差
北米東海外がメインのタイムゾーンの会社にいるんだけど、 このタイムゾーンは日本とはほぼ真逆なので日本の人がMTGを持つことは実質不可能で、 チームメンバーの構成次第では一人だけ日本にいられても困るみたいなことは普通に有り得る。
まあ…普通に移住するのが良い。 短期的には円安という話もあるが、そうでなくても東京とニューヨーク/サンフランシスコの間には凄まじい給与格差がどの企業でも存在しているため、 移住するだけで少なくとも金銭的には得をする可能性が高い。
OSSで戦うために
僕の中で一貫しているのは「楽しくやる」という点かなと思う。OSSで数字を伸ばしたりお金を稼いだりすることよりも、自分が取り組んでいることを真に楽しめていて、プライベートも楽しく暮らせるみたいなことの方が大事だと思っている。
*1:僕は汎用プログラミング言語の最適化に今最も興味があり、本番環境で満足な性能のものが他になく自分がコミッターである経験が活きてくるCRubyという言語処理系で、YJITというJITコンパイラを開発する仕事に楽しさを感じる、という話。
*2:自分自身がインフルエンサーになれている場合も当然含まれる
*3:なんか当人達に読まれると恥ずかしい気がするので具体名は出してないのだが、この3つの例は全て実践しているつもり。
*4:オープンな奴だと ruby-jp, vim-jp, rust-lang-jp, prog-lang-sys-ja
*5:ところで直近少し返事も滞ってるのがあるんだけど、これは出張の後に風邪を引くというコンボが発生したため。この記事を書きたい気持ちが強すぎて今はこれを書いてるけど、明日は(風邪の症状がマシになっていれば)そのあたりに対応する。
自作PC2023: Ryzenをやめた
Ryzenはゲーム用CPUとしては特に問題ないのだが、 ソフトウェア開発においてはIntelのCPUに比べて不便なポイントがいくつかある。 日々業務で使っていてあまりにもストレスが溜まるので、CPUをIntel Core i7に変更した。
このマシンは8年前に組んだ自作PC なのだが、使っていて不便を感じたパーツを差し替え続けた結果、 今回のアップデートで全てのパーツが当時とは違うものに変わったため、 それぞれ古い方のパーツで不便だったポイントなどを紹介したい。
仕事で使う自作PC
社内のサービスをいじる時は会社から貸与されているM1 MacBook Proを使うのだが、このマシンは不便である。 Rubyのビルドは自分のLinuxのマシンに比べ2倍以上遅いし、Reverse Debuggingができるデバッガが存在しないし、 慣れたツールであるLinux perfも使えないし、Podmanを使う会社なのだが当然Linux上でDockerを使う方が便利だし、 命令幅が32bitな関係で主に64bitの値を扱うRubyのJITコードは読みづらい。
僕はOSSの開発が主業務なのだが、OSS開発はMacから自作のLinuxマシンにsshしてそこで作業している。 ssh越しにtmuxを立ち上げ、マシン間でクリップボードを同期する仕組みも自作した結果、ローカル環境がLinuxかのような快適さで作業ができている。 会社のお金でLinuxマシンを立ち上げることもできるが、 rr-debuggerや安定したベンチマーク環境が必要な関係でベアメタルなマシンが必要になりがちで、 これは高いので使う時間は最小限にする必要があるが、それが不要な手元のマシンは楽である。
使っているパーツ
現在使っているパーツの一覧はこちら。
種類 | 名称 | 値段 | 購入日 |
---|---|---|---|
CPU | Intel Core i7-12700KF | $219 | 2023/10/13 |
CPUクーラー | ID-COOLING ZOOMFLOW 240X ARGB | $64 | 2023/07/16 |
ケース | NZXT H7 Flow | $113 | 2023/10/15 |
マザーボード | ASUS Prime B760-PLUS D4 | $120 | 2023/10/13 |
メモリ | TEAMGROUP Elite DDR4 16GB x 4 | $67 x 4 *1 | 2021/01/11 |
GPU | ZOTAC GeForce RTX 3060 | $550 | 2021/05/28 |
SSD | TEAMGROUP T-FORCE 1TB M.2 | $93 | 2021/08/12 |
電源 | ROSEWILL 80 Plus Gold 750W | $70 | 2019/11/29 |
合計 $1,497 *2。 RTX 3060はDeep Learningの授業で必要になって買った、 当時入手可能な中では安かったRTXで、僕の普段の用途だと過去に持ってた $50 の適当なGPUで置き換えても問題ないし、 メモリの半分もその授業のために盛った本来不要なものなので、実質 $863 で僕の開発環境は再現できそう。
8年前に組んだPCは98,316円で、 当時はこれと家賃と持株会で1か月の給料を使い切ったようだが、 円安の関係で今は年収が当時の12倍になったため、特に無理なく買えるようになった。 生活が苦しくてiMacをヤフオクした時の苦労が嘘のようだ。 円安最高!
CPU
Ryzenをやめた理由
僕の仕事はJITコンパイラの開発なのだが、この業務でよく使うツールがrr-debugger *3 とLinux perfである。 Ryzenでもこれらは使うことが一応可能なのだが、Intel CPUで使う場合に比べると、どちらも少し不便。 rrのために毎日zen_workaround.pyを叩くのだが、 こんな名前のスクリプトを日々叩かされたらRyzenに嫌気が差して当然である。 perfはLBR*4 が動かないし、Event Modifierの:upや:pppが動かなかったりする。
普通は耐えられるレベルの不便さだと思うが、業務でよく使う二大ツールでストレスを溜め続けるのも嫌だし、 Prime Big Deal Daysでかなり安かった割に性能も結構改善しそうな見込みがあったので、乗り換えを決意した。
Ryzen 7 5800X vs Core i7-12700KF
そもそもi7-12700KFは第12世代で、最新の第13世代のCPUに比べると少し古い。 2020/11/05に出たRyzen 7 5800Xに比べて、2021/11/04に出たCore i7-12700KFはたった1年分しか新しくなってないのだが、 PassMark Single Threadを見ると、17%くらい性能が改善していることになっている。
RyzenでPCを組み直したら爆速で最高になった という記事を書いた時は、趣味で開発していたRuby 3.0 MJITをNESエミュレータでベンチマークしたが、 今回も同じベンチマークを使い、今は仕事で開発しているRuby 3.3 YJITで比較してみた。
Ryzen 7 5800X | Core i7-12700KF |
---|---|
201.17fps | 287.57fps |
速い! そもそも前回の記事では142.09fpsだったわけなので、 Ruby自体の性能の進歩にも喜びたいところだけど、実装同じで今回のCPUの差し替えだけで43%速くなったのもすごいなと思う。 これは元々Ruby 2だと20fpsで動く想定のベンチマークで、 Ruby 3ではこれを60fpsで動かせたら3倍速くなってRuby 3x3だねというベンチマークなんだけど、 今はRuby 3x14くらいある。
CPUクーラー
水冷クーラーを使っている。前回の記事では空冷を使っていたのだが、 空冷は良い性能のものはヒートシンクがめちゃくちゃでかく、これがケースの容量をかなり圧迫して取り回しがしづらくなるし、 それで狭くなるのに加えてヒートシンクが鋭利なことで割と手を切ったりする。
これを買い換えたのは、ちょっと負荷の高い使い方をしたらPCがクラッシュするようになったのがきっかけで、 CPUクーラーを水冷に換えたら問題が発生しなくなった。 また、水冷は省スペースな上に元々使っていた空冷クーラーより静かだったので、今後は水冷しか使わないと思う。 元々水冷を避けていたのは機器の寿命や液漏れを心配していたからだが、 このクーラーのポンプの想定寿命は6年弱で、正しく扱えば液漏れの確率も低いようだし、NZXT H1 *5 みたいに発火するのに比べたらマシな気がする。
ケース
8年間唯一同じものを使い続けた、最も長持ちしたパーツがケースである。 本当はCPUとマザボだけ買い替えるつもりだったのだが、 マザボを取り付けるネジがガバガバになってしまい寿命ぽいなと思ったのでケースも買い替えた。
r7kamuraさんや yoshioriさん が使っているのを見て前々からNZXTが気になっていたので、NZXTのケースにした。 YouTubeとかでレビューを結構眺めたんだけど、NZXT H7 Flowは評判がいい。 NZXT H7については、Eliteは穴が少ない分見た目がかっこいい反面Flowと違って冷却性能が悪く音もうるさいらしいので、 穴だらけのFlowが無難と思われる。
最近のケースはマザボ側をガラスにして中身が見えるようにするのが流行っている *6 ようで、これもそうなっている。 中身が見える影響か、配線が綺麗にできるような工夫が随所にあって、それがこのケースを買って一番嬉しかったポイント。 ガラスになっている前面がインスタ映えするのは、背面にケーブルを送りまくっているだけというのがオチのようだが、背面もケーブルをまとめるための仕掛けがいい感じになっていて、背面の方も以下のようにそこそこ綺麗 *7。あとSSDのスロット*8も省スペースで良い。
マザーボード
前のマザボではUSB-Cに対応してなかったのが気になっていたので、今回はUSB-Cをつけた。 ケースも前にUSB-Cのポートがある奴にした。サイズはいつも通り、大きくて取り回しがしやすいATX。 CPUを買い替える度にソケットの互換性の都合でマザボを買い替えるはめになっているのだが、これはどうにかならんかなと思う。 メモリは前回のマザボからDDR4にしているが、次にマザボ替える時はDDR5になってまたメモリ買い直しとかありそうだし、 CPUクーラーもソケットごとの対応なので互換性なくなるリスクがある。 とりあえず単に要件を満たす一番安い奴を買うようにしている。
メモリ
最初16GBで使い始めた。それで十分な気がするが、まあ最近は32GBが人権みたいな風潮があり、何かの拍子に32GBまで増やした。 Deep Learningの授業の時、PyTorchを使っている既存プロジェクトの中に、前処理でやたらメモリを使う奴があって、 それが64GBないと足りないという感じだったので、そこで買い足した。
容量はやたら大きいが、それ以外の性能のところは値段のためにあえて妥協したスペックのものを選んでいる。 まだDDR4だし、2666MHzだし、ECCでもない。 ところで、IntelのCPUはXeonとかにしない限りはECCをサポートしていないというのが長く続いていたらしいのだが、 12世代と13世代ではi5やi7でも普通にECCをサポートしているらしい。現にi7-12700KFにもついている。
GPU
GPU *9 はRTX 3060なのだが、このシリーズが出た直後に買っていて、当時半導体がめちゃくちゃ不足していたので在庫の確保が大変だったし、 値段もめちゃくちゃ高かった。記憶が正しければ、古いシリーズの中古のRTXの方が最新シリーズの新品を買うより高い状態だった。 従って、新品入荷情報にめちゃくちゃアンテナを張ってどうにか新品を掴むというのが、一番安く買う方法だった。 もう少し安い奴もあったが、在庫を確保するのが難しすぎるのと、ある程度急ぎだったのでこれになった。
GPUの性能だけでいうとNVIDIAよりAMDの方がコスパがいいらしいのだが、 Deep Learningという用途の都合CUDAを使いたかったため、実質NVIDIA縛りだった。 NVIDIAのGPUはWaylandの対応も微妙なのだが、僕はアンチWaylandなので関係ない。
ゲームも普通に動く性能だし、これは当分買い替えの必要がなさそう。 グラボを持ってると、CPUのオンボードグラフィックが不要になるので、CPUは少し安く買える。
SSD
今どきはSSDといったらM.2一択と思われる。 このパーツもDeep Learningの授業の時、モデルの保管に1TBという大容量が役に立った。 まあそうじゃなくても、128GBのディスクは結構普通に足りなくなる印象で、最低256GBは欲しいし、 Dockerとかをそこそこ使うような開発用途には512GBくらいがちょうどいいのではと思う。
電源
電源の品質の等級にいろいろあって、80 Plus Goldはまあそこそこいい奴くらいのつもりで買った気がするが、 これが購入日が一番古い奴なので正直よく覚えていない。 買い替えたのは、マシンが起動しなくなってしまった時で、まあ寿命だったのだと思うが、 起動しない理由は完全にエスパーだった中、期待した通りちゃんと直ってよかった。
750Wは割と多めに盛ったつもりだったので、他を買い替える時もいちいち容量を再計算してなかったのだが、 Power Supply Calculator で今見積ってみたら僕の構成は600-699Wだった。十分ぽいけど、めちゃ余裕があるわけでもなさそう。
感想
8年前の記事を見ると、 およそまともなエンジニアとは思えないめちゃくちゃなことを言っている。
刺さりそうなピンに勘で適当にケーブルを挿していき、動作するまでの回数を競うゲーム。 一発では動かなくて、よくわからんけど似たようなとこに適当に付け替えたら動いた。やはりエンジニアに必要なのは運命力。
そんなことはない。 仕組みを理解し、マニュアルの読む必要があるところだけ丁寧に読み、 その通りに配線していくことで、今回は効率良く一発で起動にこぎつけた。
PCの組み立てはそれ自体がパズルのようで楽しいが、このようにスキルの上達が感じられるところも面白いポイントだと思う。
*1:バラバラに買っているので、買ったタイミングごとに値段が異なり、$67というのは平均の値段。2021/01/11: $55, 2021/05/29: $77, 2021/07/17: $68 x 2
*2:なお、複数同時に購入した時の商品あたりの税金の計算が面倒だったため、値段は全て税抜である。
*3:Reverse Debugging機能がついたGDBのフォーク。GDBにはReverse Debuggingが元々ついているのだが、これはあんまりまともに動かないし、 サブコマンドの利便性的にもReverse DebuggingをするならGDBではなくrr-debugger一択である。 言語処理系のデバッグをする時に、とりあえずクラッシュさせてからリバースステップ実行で原因を調査するみたいなのは大変便利で、 これは必須のツールと言える。M1 MacではそもそもGDBすら動かないし、LLDBにはReverse Debuggingはないので、まあMacは全然使う気にならない。
*4:Last Branch Record。僕が開発しているYJITではperf上でフレームのunwindingが動かなかった (それを可能にする変更を最近マージした) のだが、 LBRはハードウェア側でアドレスの履歴を記録することで動くため、YJIT使用時もスタックトレースが問題なく取れるという利点がある。
*5:CPUクーラーじゃないけど、NZXT H1は発火することが原因でリコールされたPCケース
*6:ガラスは割れるらしいので、実用的には普通にマイナスな気もする。 他に不便になった点としては、DVDドライブをつける場所がモダンなケースには基本ないというものがあるが、 まあ必要になったらUSB接続で外付けの奴を使う、で十分な気がする
*7:とかいいつつ大分スパゲッティな絡み方をしているが、全くリファクタリングをしてない状態がこれなので、改善はできるかもしれない。でも当分パーツ差し替える予定ないしやらないかも…
*8:ところでこれはSSDのところで解説しているM.2 SSDとは別のSSD 2つで、まああんま使ってないけどとりあえずつけてるだけなので、パーツ一覧には含めなかった (追記: なお、これにはWindowsとArch Linuxが入っていて、それぞれの環境のサポートの動作確認用に維持しており、使用頻度は大分低いものの、一応開発目的でつけているものではある。)
*9:買っているもの自体はグラフィックボードとかビデオカードとか言うのが正しいのだが、 性能的にGPU以外の部分はどうでも良いことが多いのでGPUとカテゴライズしている。
*10:はてなブログのAmazon商品埋め込み機能を使っているのだが、このパーツだけは日本のAmazonだとヒットしなかった。そのためリンクだけになっている。
YJITの性能を最大限引き出す方法
RubyのJITコンパイラYJITを開発している弊社Shopifyでは、社内で最もトラフィックが多いストアフロントのアプリにRuby 3.3 (master) をデプロイして平均レスポンスタイムが16%高速化、社内で最も大きなアプリであるモノリスにRuby 3.2をデプロイして平均レスポンスタイムが9%高速化している。他の会社でも、YJITを本番で有効にしたら高速化したという事例をちらほら目にした。
一方で必ずしも良い報告ばかりではなく、YJITを有効化したらメモリを使い切ってしまったりだとか、遅くなったみたいな報告も目に入ることがある。こういった問題は我々も多かれ少なかれ経験しており、それぞれ適切に対処することで解決できたため、その知見を共有する。*1
メモリを使い切ってしまった時
YJITを有効化すると、YJITが生成する機械語に加えて、それに関するメタデータもメモリを消費する。機械語の最大サイズは --yjit-exec-mem-size
(デフォルト 64MiB) で制限されるが、メタデータは特にリミットがない。ただし、メタデータサイズは生成コードのサイズに比例する傾向にあり、かつRuby 3.2の時点ではメタデータは生成コードの3~4倍程度メモリを使うと見積っておくと良い*2。従って、デフォルトではメモリは最大で256~320MiB使われることになる*3。
ここで注意しなければならないのは、この値はあくまで各プロセスあたりのメモリ消費量であること。UnicornやPumaで複数プロセスを走らせる場合、ワーカーがforkする時点で存在しているメモリのページのうち、その後更新がされないものは複数プロセス間で共有される*4が、YJITのコードやメタデータに関しては基本的にワーカーのfork後に生成されるため、メモリの共有は期待できない。そのため、例えばUnicornのプロセスが16ある場合は、最悪の場合 16 x 256~320MiB = 4096~5120MiB 使うことを覚悟しなければならない。
--yjit-call-threshold を大きくする
一番簡単に試せるのはこれ。デフォルトでは --yjit-call-threshold は30で、つまり30回呼ばれたメソッドからコンパイルを開始するようになっているのだが、アプリの初期化のロジック次第では、この閾値では初期化時にしか使われないコードがコンパイルされてしまうということが有り得る。それらのメモリ消費は後で無駄になるので、この値を大きくするとメモリ使用量が大幅に改善することもある。
Shopifyのストアフロントではこれを30よりどれだけ大きくしてもそれほどメモリ使用量は変わらなかったが、20から30に上げた時はメモリ使用量が大幅に減った。あなたのアプリでは30よりもう少し大きくしないとその変化は訪れないかもしれない。なお、これを大きくすればするほどウォームアップが遅くなってしまうため、各ワーカープロセスが処理したリクエスト数合計などのメトリクスと見比べながら、程々の大きさに留める必要がある。
--yjit-exec-mem-size を小さくする
Ruby 3.2のYJITにはCode GCという機構が入った*5。これは生成した機械語のサイズが --yjit-exec-mem-size に達したら全てのコードを開放し、その後必要になったコードだけコンパイルして省サイズ化を目指すものだが、ついでにメタデータの方も開放されるため、これを小さくすればするほどメモリ消費量は抑えられることになる。
これを小さくしすぎるとCode GCが頻繁に行なわれるようになってしまうため、Code GCがどれくらい頻繁に行なわれているかモニタリングする必要がある。 RubyVM::YJIT.runtime_stats[:code_gc_count]
の現在の値をRackミドルウェアなどで定期的に記録しておくと良い。Datadogとかだと DataDog/dd-trace-rb#2711 が使えるかもしれない。目安としては、これが0の場合は多少サイズを小さくする余地があり、1で安定する場合や1時間おきに1回走る程度が理想、それより頻繁に値が増える場合はサイズを大きくした方が良い。
肥大化したプロセスをkillする
ワーカー1つあたりに許容するメモリ使用量をあらかじめ決めておき、それを越えたプロセスを止めるようにするという手も有効である。YJITを使っているかに関わらず、本番でメモリリークが発生してしまった時の供えとしても役に立つ。我々は独自の実装を使っているが、OSSのものではunicorn-worker-killerやpuma_worker_killerなど使えばよさそう。一方で、killの頻度が高いと速度的には悪影響であるため、killの回数もモニタリングしておくのが望ましい。
ワーカーをreforkする (上級者向け)
同僚の@byrootがUnicornのフォークであるPitchforkというのを開発した。これはUnicornと比べてレガシーな依存が一つ外れているモダンなUnicornとしても使うことができるが、それに加えて "Reforking" と呼ばれている機能が追加されている。通常、UnicornやPumaのfork時にはアプリのコードがコンパイル済みでないワーカープロセスが作られるが、Reforkingというのはアプリのコードが既にコンパイルされているプロセスを後から定期的にforkし直すことでそのメモリを複数プロセス間で共有することを目指すというもの。YJITの運用でメモリの使用量を最適化しようとしたら、これが一番効果があると思われる。
デメリットとしては、スレッドを扱うgemなどがfork-safeでないことがあり、アプリが使っているコードが全てfork-safeであることをどうにか保証しておく必要がある。具体的にはgrpc.gemがこの問題を抱えており、byrootたちがGoogleとコミュニケーションを取ってこれに対応している。それから、PitchforkはUnicornのフォークであるため、Pumaのように各プロセスでマルチスレッドワーカーを立てることはできない。
一部ワーカーのみ有効化する (Ruby 3.3以降)
Ruby 3.3では新たに --yjit-disable
というフラグと RubyVM::YJIT.enable
というメソッドが追加される。この2つを使うと、Ruby起動時にはJITコンパイルを無効にしておき、アプリの初期化が終わった後に手動でコンパイルを開始するということができる。それが主な想定用途なのだが、byrootの発案で、モノリスではUnicorn (Pitchfork) のワーカープロセスのうち、ワーカー番号の若い一部のみ有効化することによって、メモリ使用量を抑えている。これは、Unicornがリクエストを捌く際、全てのワーカーが均等にリクエストを処理するわけではなく、キャパシティに余裕がある場合はワーカー番号が若いものにリクエストが偏るという性質を利用している。そのため、ワークロード次第ではYJITを有効化しているプロセスの割合以上のリクエストがYJIT有効のワーカーに処理されうることになる。
Ruby 3.2にはこの機能はないのでRuby 3.3を待っていただく必要があるが、我々は独自のRuby 3.2フォークを持っており、これにはこの機能がバックポートされている。
遅くなってしまった時
残念ながら、YJITを本番で有効化したら遅くなったという話もちらほら聞く。
YJIT 有効化すると遅くなってしまったときに確認する・試すことリストどこかにないですかね🤔🤔
— Pin📍AppBrew CTO (@spinute) June 27, 2023
YJITを有効化した結果、レスポンス悪化 #tqrk14
— hirotea (@nifuchi222222) July 29, 2023
--yjit-exec-mem-size を大きくする
YJITを有効化したら遅くなった、と聞いたときに僕が真っ先に疑うのは --yjit-exec-mem-size が小さすぎるというもの。これが小さいとCode GCが頻繁に走りすぎて遅いというリスクが高くなる。その症状になっているかを確認するには、RubyVM::YJIT.runtime_stats[:code_gc_count]
をモニタリングし、この値が頻繁に増加していないかを確認すると良い。これが0か1で安定するところまで上げれば、速度的には影響がない状態にできる。
弊社ストアフロントでは --yjit-exec-mem-size=64 を使っていて、これは十分すぎるサイズなのだが、一方弊社モノリスでは --yjit-exec-mem-size=256 を使っていて、これを大きくしてきたときに大幅に速度が改善された。これに関連して、Ruby 3.3 (master)ではこのオプションのデフォルトが128に変更されている。
ワーカープロセスをなるべく長く走らせる
僕が次に疑うのは、ワーカープロセスが定期的にkillされているような環境で、そのkillがあまりにも頻繁すぎるというもの。プロセスが長く走ればコンパイル済みのコードを再利用する機会が増え、速度的には良い影響が期待できる*6が、頻繁にkillされるとコンパイルのオーバーヘッドがかかり続けるということが有り得る。各ワーカープロセスが過去に処理したリクエスト数合計などをモニタリングしておき、その値が大きくなる前にプロセスがkillされてしまっている場合は、killの閾値の見直したり、OOMの影響を確認して割り当てるメモリを増やしたりする必要がある。
--yjit-call-threshold を調整する
関連した問題として、--yjit-call-thresholdはワーカーのリクエスト処理数に応じて調整する必要がある。--yjit-call-threshold が小さすぎると、起動時に多くのコードがコンパイルされ、ウォームアップのオーバーヘッドが一気にかかりがちになる。その一方で --yjit-call-threshold を例えば1000まで上げた時、ワーカーが頻繁に再起動されていてプロセスあたり1000リクエスト足らずでkillされてしまっている場合、1000回目のリクエストで初めてコンパイルされるパスが有り得ることになる。その場合は閾値を100くらいまで下げると、より早く、一方で早すぎずウォームアップが行なわれ、速度が改善したりする。
ratio_in_yjit を確認する
これはRuby 3.2では少し手間がかかるのだが、Rubyのconfigure時に --enable-yjit=stats
をつけてビルドしておき、Rubyの起動時に --yjit-stats
をつけ、RubyVM::YJIT.runtime_stats[:ratio_in_yjit]
を確認すると、実行されているVM命令のうち何%がYJITで実行されているか確認することができる。ワーカープロセスがピーク性能に達した状態でこれが90%くらいあれば性能が改善していることが期待できるが、これが例えば80%を下回るなどしている場合、アプリのコードかYJITの実装のどちらかに問題があることになる。例えばTracePointがどこかで有効になっていると、ものによってはそれがVM命令を全てtrace命令に書き換えることがあり、その場合YJITはコンパイルを諦めてしまう。いずれにせよ、例えばワーカープロセスが10000リクエスト処理した後もこの値が小さい場合は、--yjit-stats
を使用した状態での RubyVM::YJIT.runtime_stats
の中身を丸ごとYJITチームに共有*7していただけると、よしなに対応できると思う。
Ruby 3.3ではデフォルトのビルドで ratio_in_yjit
が確認できるようになっている。YJITが使える全てのRubyのビルドで、起動時に --yjit-stats
をつけておけば、RubyVM::YJIT.runtime_stats[:ratio_in_yjit]
が利用できる。一方で、Ruby 3.3には ratio_in_yjit
を改善する様々な実装が入っており、以前は92%だったのが現在では97%まで改善していたりするので、そもそもこれを確認しなくても速くなるようになっているかもしれない。
まとめ
簡単にできるアクションのまとめとしては、RubyVM::YJIT.runtime_stats[:code_gc_count]
と各ワーカープロセスが処理したリクエスト数をモニタリングし、それらや速度、メモリ使用量に応じて --yjit-call-threshold
や --yjit-exec-mem-size
を調整したり、割り当てられているメモリのサイズやワーカーのkillの設定などを見直していただくのが良いと思われる。
これを読んで「YJITを使うのは面倒くさい」と思った人もいるかもしれないが、これらの知見を可能な限りデフォルトのパラメータにフィードバックしており、基本的にはデフォルトの設定で有効化しただけで特に苦労なく速くなる状態を目指して開発されている。また、より多くのVM命令の対応が日々コミットされ続けているので、Ruby 3.2で遅くても、Ruby 3.3にしたら速くなった、ということも有り得ると思う。
参考資料
1月に書いた(のを6月にやっと公開した)記事なので若干情報は古いが、会社のブログに書いた以下の記事も参考になるかもしれない。
*1:余談だが、こういった話のプロポーザルをRubyKaigi 2023に出していたのだが、RJITの方の話が勝ったため話しそびれていた。
*2:なお、Ruby 3.3ではメタデータのサイズがより小さくなっており、生成コードの2~3倍程度になる。
*3:メモリは仮想ページごとに必要に応じて割り当てが行なわれるため、アプリのサイズ次第ではメモリ消費量はもっと小さくなる。
*4:Copy on Writeの話をしている
*5:僕が書いた
*6:一方で長く走らせすぎるとメモリが断片化していき若干性能が減衰する問題もあるが、ここで話している問題とは別なので置いておく
*7:https://bugs.ruby-lang.org/ にチケットを起票するか、https://github.com/Shopify/yjit にissueを書くか、gistとかに貼ってX (Twitter) で僕にリプライなど