世界を動かす技術を、日本語で。

ユーザーが受け入れ基準を最初に定義することでLLMの効果が最大化される

概要

  • LLM生成のRust製データベース は、SQLiteと比べて極端に遅いことが判明
  • コードは 正しく動作し、テストも合格 するが、根本的な設計ミスで実用性に乏しい
  • 性能劣化の原因 は、クエリプランナーやトランザクション管理など複数のバグと安全志向の設計判断
  • LLMは「 もっともらしさ」を優先し、本質的な要件や最適化を見落とす傾向
  • 表面上は正しいが本質的に不適切な実装 が生まれるリスクを検証

LLM生成Rustデータベースの性能問題

  • SQLiteとLLM生成Rust実装 で100件の主キー検索を比較
    • SQLite:0.09 ms
    • LLM Rust版:1,815.43 ms(約20,171倍遅い)
  • Turso/libsql とは無関係なプロジェクトであり、TursoはSQLiteのCコードベースをフォークしたもの
  • Tursoのベンチマーク はSQLiteの1.2倍程度で、成熟したフォークとして妥当な性能

コードの外観と実態

  • Rust実装は動作し、テストも合格
    • SQLiteファイルフォーマットを読み書き可能
    • READMEにはMVCC、C API互換、ファイル互換性を謳う
  • 実際は著しく遅く、運用に耐えない

LLM生成コードの失敗パターン

  • LLMは「もっともらしさ」を最優先
    • 正しく見えるが実用性や最適化が欠如
  • 受け入れ基準を事前に明確化することが重要
  • 他の研究(METR, GitClear)でも同様の傾向 が確認されている

主な性能劣化の原因

  • 主キー検索がB-tree検索にならない
    • id INTEGER PRIMARY KEYがrowidとして認識されず、全件スキャン(O(n²))
    • SQLiteはWHERE id = NでO(log n)のB-tree検索を実現
    • Rust実装はWHERE rowid = ?のみ高速経路
  • INSERT時に毎回fsyncを実行
    • トランザクション外のINSERTごとにフル同期
    • SQLiteはfdatasync(メタデータ同期省略)をデフォルト使用し高速化

細かな性能低下要因

  • ASTクローンの多用 :SQLパース済みASTを毎回複製・再コンパイル
  • ヒープアロケーション乱用 :キャッシュヒットでも4KB Vec<u8>を新規確保
  • スキーマの毎回リロード :autocommitごとに全CREATE TABLEを再パース
  • ホットパスでの不要なフォーマット :毎回SQL文字列へ変換
  • オブジェクトの逐次生成破棄 :トランザクションやVDBEなどを毎回生成・破棄

安全志向の設計判断の弊害

  • Rust所有権や安全性を重視した結果、極端な性能低下
  • SQLiteの高速化はC言語だけでなく、26年の最適化の積み重ね

過剰設計の別事例

  • ビルドアーティファクト削除のための巨大なRustプロジェクト
    • 82,000行、192依存、36,000行のダッシュボードなど
    • 実際にはcronの1行ジョブで十分
    • cargo-sweepやOS標準の回復手段も未活用

LLM生成コードの本質的な問題

  • 「求められたもの」を忠実に生成するが、「本当に必要なもの」とは限らない
  • 表面的には正しく、テストもパスするが、本質的要件や最適化を満たさない
  • 問題の本質は構文や文法の誤りではなく、要件や最適化の見落とし

まとめと教訓

  • LLM活用時は受け入れ基準・ベンチマーク・設計意図の明確化が不可欠
  • 「見た目が正しい」だけではなく、「本当に正しい」かを検証する仕組みの重要性
  • 安全志向や最新技術の導入が、必ずしも実用性や効率性に直結しない事例

Hackerたちの意見

そう、信頼できるテキスト予測ってまさにその通りだよね。でも、著者がプロンプトにベンチマークを含めてたかどうか気になるな。隠れた要件を隠しておくのはちょっとフェアじゃないよ。

これを「隠れた要件」とするのは危険な道だね。Claude Codeや似たようなツールを使った経験から言うと、「隠れた要件」には以下が含まれるかもしれない。* DESIGN.mdが最新であることを確認する * ソースを変更した後はテストを書いたり更新したりして、ちゃんと通るか確認する * ユニットテストだけじゃなくて統合テストも追加する * 現在のタスクに関係ないコードはリファクタリングしない … これらはプロジェクトや言語特有の指示じゃなくて、ソフトウェアエンジニアリングでは常識や良いプラクティスとされてることなんだけど、時々コーディングエージェントにそれを守るようにお願いしなきゃいけなかったこともあったよ。(TypeScriptのコードベースで「any」を使わないように何回強調しなきゃいけなかったか知りたい?)人々は単にこれがコーディングツールの限界だって認めるべきだと思うし、それでも意味のある議論はできるはず。

調べたところ、問題のSQLiteの再実装はFrankensqliteで、数日前にHacker Newsに載ってた(ただしフラグが付いてたけど): https://news.ycombinator.com/item?id=47176209

彼らのデフォルトの解決策は、掘り続けることなんだ。どんどんコードが増えていく悪循環を生むよ。もしあまり良くないアプローチで何かを実装したら、後で制限にぶつかるたびにワークアラウンドや冗長なコードを追加し続けることになる。コードが遅いって言ったら、最適化された高速パス(もっとコード)、専門的なルーチン(もっとコード)、カスタムデータ構造(さらにもっとコード)を追加しようとする。そして、そのコードが生み出した問題を修正するために、さらにフラクタルにコードが増えていく。バグがあるって文句を言ったら、バグごとに10個の特注テストが必要になるよ。さらに、前のフレームワークが目的に合わなかったら、新しいモッキングフレームワークが毎回作られるし。重複を統一してほしいって言ったら、「問題ないよ、新しいメタモック抽象アダプターフレームワークがあって、すべての機能セットのスーパーセットに加えて、古いコードと新しいコード用の2つの新しいメタモックドライバーもあるよ!新しいアダプターのテストを書いてほしいなら教えてね。」って言うんだ。

重複を統一してほしいって言ったら、「問題ないよ、新しいメタモック抽象アダプターフレームワークがあって、すべての機能セットのスーパーセットに加えて、古いコードと新しいコード用の2つの新しいメタモックドライバーもあるよ!新しいアダプターのテストを書いてほしいなら教えてね。」って言うんだ。実際には5つの重複セクションのうち3つしか移行してなくて、今は死んでるコードは一切削除してないのに。

上から下に進めることを強くおすすめするよ。コーディングを始める前に、ちゃんとしたアーキテクチャをアウトラインするようにさせるんだ。それから、もしモジュールの一つが混乱し始めたら、そのモジュールのためにクリーンなコンテキストから始めて、悪い経験から得た注意点や教訓を取り入れる。LLMは、君が指摘した理由から、同じコードを作業したり再作業したりするのがまだ得意じゃない。でも、実装プロセスを何度も繰り返して正しくする「グラウンドホッグデイ」的なアプローチには結構向いてるよ。

コードの削除やクリーンアップのコーパスで訓練されたLLMがあってもいいかもね。

だから、ほとんどのプログラマーの労働力を置き換える準備ができていないって言う人たちに混乱するんだよ。

私の感覚では、コード生成は早いけど、その後に実装が適切で正しいか、しっかりテストされているか、正しい前提に基づいているか、技術的負債を生まないかを確認するのに数時間かかるんだよね。手動でコーディングする時も同じことをやるけど、AIツールが悪いコードを出力するスピードを考えると、もっと重要になってくる。

もし彼らがあまり良くないアプローチで何かを実装したら、後で制限にぶつかるたびにワークアラウンドや冗長なコードを追加し続けることになるよ。プランモード使ってる?昔はあまり良くないアプローチをして問題を掘り下げることが多かったけど、計画を立てることでそれがなくなった気がする。

皮肉を言うつもりはないけど、敬意を表して言うと…これはスキルの問題だよ。ツールなんだから。すごく効果的で能力のあるツールだと思う。なんでこんなに多くの人が似たような体験を語る中で、私の体験がこんなに違うのか分からないけど…ほぼ毎回、入力が出力を決めるって結論に至る。 > もし彼らがあまり良くないアプローチで何かを実装したら、後で制限にぶつかるたびにワークアラウンドや冗長なコードを追加し続けることになる。そう、プロンプトや指示が広すぎて、どうやってやるべきかを示すガードレールやガイドラインがないと、こうなるよね。プランモードを使ってないなら、それはスキルの問題。実装が始まる前に、すべてを整理しておく必要がある。もし実装が「あまり良くない」アプローチで行われたら、それはあなたの責任だよ。 > コードが遅いって言ったら、ふぅ。分かった。コードが遅いって言わないでしょ。自分の同僚に「ねぇ、君のコード遅いよ」って言って、いい結果が期待できる?コードのベンチマークをお願いして、どうやって最適化できるかを聞くんだ。それからその選択肢について話し合う(ここで前の段落の部分をやって、あまり良くないアプローチにならないようにする)まで進めて、気に入ったアプローチにたどり着いて、モデルが状況を理解していることを示したら、計画を受け入れてモデルに作業を始めさせる。この時点で、基本的にはアプローチを指示して、何もバカなことをしないようにしているはず。そうしたら、実行するだけで、あなたが設定した計画のパラメータや範囲内に収まる(もしバグがあるって言う代わりに具体的にバグについて言ったり、どう解決してほしいかを言わないと、脱線することもあるけど)。 > バグごとに10個の特注テストを持つこともできるし、前のフレームワークが目的に合わなかったら新しいモッキングフレームワークを毎回作ることもある。この点では、モデルが非常に無能だということには同意する。テストやテスト環境、モッキングに関して何が問題なのかを誰かが研究する必要がある。これに対する解決策は、掘り下げたり、無限に回ったりする問題の解決策と同じだよ…プロンプトや会話の初めに、最終結果に対する期待を述べる。出力を早めに定義して、その後に文脈を説明したり提供したりする。プロンプトや会話の早い段階で「要件」を設定すればするほど、より強固になる。テストについても全く同じ。自分でテストを書いて、モデルにテストから機能を構築させるか、モデルに計画された出力の一部としてテストを最初に構築させてから、事前定義されたテストから機能を埋め込ませる。テストシステムや環境の設定については非常に具体的に説明し、テスト関連の問題にぶつかった時にはモデルにそのことと解決策をTESTING.mdドキュメントにメモさせる。AGENTS.mdやCLAUDE.mdなどには、モデルがテストを扱う場合はTESTING.mdドキュメントを参照するように指示しておく。個人的には、機能に焦点を当てて、統合して動作する状態まで持っていって、ステージングや本番環境にプッシュする準備ができたら、モデルにその動作するシステムやソリューション、機能について分析させてテストを書かせる。一般的に、モデルに対するテスト環境のメモは、使用中の基本的なテストフローやプロセス、フレームワークを説明する段落のようなもので、どう動いてほしいかを伝える。慣習に従えば従うほど、うまくいくよ。そして、プランモードを使って。

この理由で、GitHub Copilotのチェックポイント復元/フォーク会話機能を多用してる。ほとんどの場合、何かが脱線するよりも巻き戻した方がいいから。

Hacker Newsで議論の続きを見る