プログラミング
すみませんが、あなたはまだ考える必要があります
I'm sorry, but you still have to think (itsallaboutthebit.com)
要約
DHHがAIエージェントを用いてCampfire OnceをRust、Elixir、Goで書き直そうとした試みについて論じています。プロンプトが具体的でない場合、AIは任意に決定を下し、元のコードと比較して機能やパフォーマンスに大きな違いが生じることを指摘しています。著者は、AIはソフトウェア開発におけるトレードオフの理解や人間の思考を代替できないと主張しています。
全文翻訳
DHH、Ruby on Railsフレームワークの作成者は、Campfire OnceをRuby on RailsからRustに書き直すことを決定しました。あるいは、彼自身がRustコードを読むことも書くこともできないため、AIにそれをやらせたのです。その後、ElixirやGoのような他の言語での書き直しも追加しました。これは私にとって非常に興味深いことです。なぜなら、コードを読むことなくAIエージェントを使用した場合の結果について、非常に良い洞察を与えてくれるからです。この場合、その人物はすでに多くのプログラミング経験を持っています。そしてその結果は…まあ、あなたがコードを読むのをやめたり、すぐに完全に考えをやめたりできると期待しているなら、あまり励みになるものではありません。
書き直しには複数の問題がありますが、まず非機能的な違いから始めましょう。なぜなら、それらは多くの人が気づいていないことをうまく示しているからです。つまり、プロンプトが十分に具体的でない場合、多くの決定はコイン投げのようなものになるということです。なぜなら、多くの質問には単一の正解がないからです。後方互換性を維持したいですか?レイテンシーとスループットのどちらをより重視しますか?システムは負荷の下でどれだけのメモリを使用できますか?クラッシュ後に新しいコンテンツ通知を失っても許容できますか?あなたはそれらを気にしないかもしれませんが、少なくともそのうちのいくつかは、LLMがあなたが望んでいると思っていることを実装する際に暗黙的に答えられます。
書き直しを詳しく見ると、後方互換性やその他の制約が異なるように扱われていることがすぐにわかります。例えば、RustバージョンはCSRFトークンを削除してキャッシュを容易にするなど、100%の後方互換性を維持していません。また、例えば通知のために、Redisの代わりにインプロセスキューを採用しています。Elixirの書き直しは、Railsのオリジナルにかなり近いです。これはすでに比較をほとんど無意味なものにしています。なぜなら、これらの違いは言語に依存しないからです。AIが「Elixirで書き直せ」と聞いて、使用されている言語のために100%の後方互換性を維持することを選んだわけではありません。しかし、さらに良いことがあります!コードを見たなら、そしてええ、私も知っています、私たちはもうコードを読むべきではありませんが、多くのことがそれほど良くないことにすぐに気づくでしょう。
例えば、Elixirのバージョンでは、読み取りであっても並行して実行できるにもかかわらず、すべてのSQLクエリを順番に処理する単一のプロセスがあることに人々が不満を言っているのを見ました。それはもっともな不満ですが、DHHはAIがパフォーマンスの高いコードを書けなかったことがElixirにとって最良の姿ではないと考えているようです。Rustバージョンを見ると、私が同意するかどうかはわかりません。なぜなら、Rustバージョンでは、一部のデータベース操作は非同期ではなく、場合によってはさらに悪い可能性があります。
ご存知のように、Rustでは、非同期ランタイムを使用する場合、スケジューリングはプリエンプティブではなく協調的です。タスクが譲歩しないと、同じワーカー・スレッドで他のタスクを実行できません。実際には、これはタスクに費やされる時間が可能な限り短くなければならないことを意味します。例えば、100ミリ秒かかるSQLクエリを実行する場合、クエリを発行しているタスクはすでに待機しているため、他のすべてのタスクを待たせることは避けたいでしょう。したがって、理想的には、データベースからの応答を待っている間にランタイムに譲歩する非同期I/O操作を使用する必要があります。Rustの書き直しでは、一部のクエリはワーカー・スレッドで実行されますが、一部は非同期タスク内のブロッキング操作として実行されます。ロックも同様です。非同期ランタイムを使用する場合、最も安全なのはtokio::sync::Mutexのような非同期ロックを使用することです。ロックが非常に短時間保持されることが確実な場合は、非同期でないバージョンを使用しても問題ありませんが、同期ロックを10ミリ秒保持すると、同じスレッド上のすべてのタスクがそれを待機し、ブロッキングタスク自体は何も作業を行っていません。したがって、残念ながら、コーディングは「解決」された可能性は低く、あなたはまだ自分が何をしているのかを知る必要があることをお伝えしなければなりません。
さらに進んで、スロップベンチマークも、結局のところスロップであることが判明しました。なぜなら、それらはシステムの他のプロパティを無視して、スループットのみを測定するからです。例えば、Zach Danielsは負荷下での新しい投稿通知の配信率を測定し、重い負荷下でのRustバージョンで1%の成功率を発見しました。これはあまり良い結果ではありません。ベンチマークは難しいものです。しかし、改善されたベンチマークでさえ、システムの特性によっては、実際にテストしたいものをテストしていない可能性があります。システムをテストする場合、負荷の下で異なるプロパティを強調したい場合があります。DHHとZachの両方のベンチマークはクローズドループベンチマークでした。つまり、「システムは特定の時間内にどれだけの要求を処理できるか?」をテストしていました。テストではN個のクライアントを使用し、各クライアントは前の応答を受け取るとすぐに新しいメッセージを送信しました。しかし、多くの場合、システムへの負荷の増加は、他のユーザーの操作が完了するのを待たない、多数のユーザーが同時にアクションを実行することから来る可能性があります。この場合、定数到着率を好むでしょう。つまり、システムが応答できる速度に依存するのではなく、一定のレートで要求を送信したいのです。イベント配信の信頼性を確認したい場合は、理想的には同じ数のイベントを比較したいでしょう。ここで、結果を見ると、Rustは6〜7kのうち1%の通知を配信しましたが、Elixirは〜1.7kのうち100%の通知を配信しました。これは、Elixirバージョンがバックプレッシャーをより良く処理することを示していますが、これは要求の数がシステムを過負荷にしない限り有効です。
しかし、ベンチマークは一時的に置いておいて、トレードオフについて話しましょう。なぜなら、プログラミングはトレードオフがすべてだからです。確かに、ツールやソリューションが明らかに優れており、欠点がない状況もありますが、特に信頼性が必要な複雑なシステムになると、それはかなりまれです。Elixirコミュニティの多くの人々が、Rustバージョンの1%の配信率を見た後、その理由を本当に理解せずに様々な結論に達しているのを見ました。一般的なコンセンサスは?Elixirは並行処理に優れている!Rustではスケジューラが協調的である、どうやってそんな生活ができるんだ?私はElixirが好きで、本番環境で成功裏に使用しましたが、それは銀の弾丸ではありません。はい、Elixir(または他のBEAMベースの言語)は並行処理に非常に優れており、確かにElixirで並行コードを書くことは、一般的にRustよりも簡単ですが、低レベルの制御ができない、メモリ使用量が高い、そしてしばしば速度が犠牲になります。Rustlerが存在するのには理由があります。そして、根本原因を知らずに単一のメトリックに基づいて言語の優位性を宣言することは、誤解を招く可能性があります。
クローズドループのストレス・テストがシステムのプロパティの1つしか示していないと述べたことを覚えていますか?Rustは〜4倍の要求を処理し、それゆえWebSocketを通じてクライアントに送信されるより多くのイベントを処理する必要がありました。DHHのRustバージョンとZachのElixirバージョンに様々な修正を加えて、定数配信率でテストを再実行しました。100 POST/秒で、クライアントは約14%のイベントをRustで受信しました。Elixirでは60%でした。まだ良いですよね?そうは問屋が卸さない!このトラフィックレベルでは、RustはHTTPエラーを一切発生させませんでした。Elixirは約23%のHTTP POST要求でタイムアウトしました。レイテンシーはどうでしょうか?最悪のイベント配信レイテンシーは180秒近くでした。Rustでは、配信が遅延すると、クライアントは切断されます。再接続すると、ブラウザクライアントは最新のメッセージを取得し、これは失われたイベントの必要性を大幅に無効にします。あなたは、クライアントがバックグラウンドでサイレントに再接続して新しいアップデートを取得するのと、3分間新しいメッセージのアップデートを待つのと、どちらがより良いUXだと思いますか?これは、再び、単一のメトリックが全体像を語らないことを示しています。
トレードオフに戻りましょう。重い負荷の下でRustバージョンがなぜそれほど多くのメッセージをドロップするのか知っていますか?接続されているクライアントにイベントをブロードキャストするためにtokio::sync::broadcastチャネルを使用しています。新しいチャットメッセージを通知するイベントは、複数の接続に送信する必要がある場合があるため、これは理にかなっています。ブロードキャストチャネルのプロパティの1つは、それがバウンドされているということです