なぜC++からRustへ移行したか (生成AI時代の開発体験)

まとめ

  • C++は統合されたパッケージシステムが弱く, 依存関係の管理と成果の再利用が難しい.
  • Rustはcargoを中心に成果をcrateとして分割・共有しやすく, エコシステムを協力して育てやすい.
  • Python/Julia/Fortranから呼び出す用途では, RustはC-API経由で連携しやすく, バックエンドをメモリ安全に保ちやすい.
  • 生成AI支援を前提にすると, Rustの学習コストや設計変更時の修正負担を下げられる.
  • Rustは堅牢な基盤実装, Juliaは試行錯誤に向き, C-FFIを介して組み合わせるとよい.

まえがき

前回の記事 C++を使った数値計算ライブラリの整備で辛かったこと の続編である. C++で開発していた libsparseirRustへ移行した. その経験を基に, なぜC++からRustへ移行したかを述べる.

背景

libsparseirはPython, Juliaで書かれた前身のライブラリ (sparse-ir, SparseIR.jl) をC++で実装したものである. C++はABI互換性がないため, 他の言語からの利用は困難である. そこでC-FFIのみを公開し, 他言語からの利用を可能にした. これによりsparse-irやSparseIR.jlのバックエンドを統一し, メンテナンスコストを削減することを狙った.

しかし前回記事に書いたような理由で, 標準的なパッケージマネージャー, コンパイラー, ビルドシステムの不在が辛い状況であった.

ちょうど他のプロジェクトでRustに触れる機会がありそうだったので, 練習として libsparseirのRust版 をCursorで実装した. 単なる練習のつもりだったが, 想定以上に開発体験が良かったため, libsparseirを廃止しRust版へ完全移行した. 出来上がったコードはテストやコード例を含めて4万行近いが, 私の自然言語による指示の下で, ほぼ2ヶ月で大部分をAIが生成している.

libsparseirに特有の話, RustとC連携などの技術的な話は別記事に譲り, 本記事ではRustによる開発体験の観点から移行理由を整理する.

前回記事の要点(抜粋)

以下は前回記事の要点を抜粋し, 本記事向けに再構成したものである.

前回記事の主張は, 次の1行に集約される.

特に, 標準的なパッケージマネージャー, コンパイラー, ビルドシステムの不在が辛い.

具体例として, 次が挙げられる.

  • パッケージ管理: ライブラリ分割と再利用, 配布時の依存関係管理が難しい.
  • ビルドとABI: ビルド設定の分断, コンパイラ差分, ABI非互換が配布を難しくする.
  • メモリ安全性: 危険な挙動を容易に書けてしまい, 機械的に潰しにくい.
  • 行列・配列とドキュメント: 事実上の標準やドキュメント生成の標準が弱く, 運用コストが増える.

Rustへ移行した理由(開発体験)

パッケージシステムによる成果共有が容易になることが, Rustを使う最大のメリットだと思う. 計算強相関物理の分野ではALPS, TRIQSなどのC++プロジェクトがあるが, エコシステムが分断しやすく互いに成果を共有しづらい. 例えば, 巨大なライブラリに含まれる特定の機能を使おうとしたとき, 巨大なライブラリ全体をビルドすることになることが多い. それが依存するライブラリの入手・ビルドから始まって, 多大な労力が必要である. 因果関係は逆で, C++に統合されたパッケージシステムがないからこそ, これらのライブラリはモノリシックな構成をとらざるを得なくなっているのである. 一方, Rustでは依存関係が複雑になっても, cargoが自動的に依存関係を解決してくれるので, 手動で依存関係を管理する必要がない. その結果, 機能ごとに独立したcrateを公開することが容易になり, いろんなコミュニティから成果を再利用しやすい. 力を合わせてエコシステムを作っていきやすいところが良い.

また, JuliaやPython, Fortranでのラッパーライブラリを作る際, RustとはC-APIで容易に連携が可能である. ラッパーライブラリをビルドする際に, 内部でcargoを呼べば, 再現性高く共有ライブラリをビルドできるのはありがたい. C++の場合, CMakeなどを使った複雑怪奇なビルド設定が必要になる上, ネットワークアクセスが許されない環境では手動で全依存パッケージをダウンロードすることになりかねない. 一方, Rustの場合, 私はまだ試したことがないが, cargo vendor で依存パッケージを事前にダウンロードし, ソースコードツリー中に同梱することも可能らしい.

特に, FFIを通してPythonやJulia, Fortranから呼び出す場合, バックエンド側がメモリ安全であることは重要である. C++はバックエンド内部でsegmentation faultを起こしやすく, 一旦起きてしまうと解析もしづらい. Rustでは unsafe とFFI境界を重点的に確認すればよい.

Rustは学習曲線が急峻であることで有名である. 私はRustの所有権システムの概略は理解しているが, 誤りなく, 複雑な構文を書くのは難しい. というよりは, そこはAIに積極的に任せた方が良いと感じる. CursorなどのAI支援エディターを使う場合, AIが生成したコードに構文エラーが出ても即時にAIが修正できる. また, 構造体のメンバーの変更などの, 小さな設計変更に伴って, コードベース全体に大規模な修正が必要な場合, AIが自律的に修正できる. AIによるコーディングが主流になってこそ, Rustのメリットが生きてくると感じる.

課題

一方, 課題はある.

  • JuliaやPythonの自動微分システムとどう連携するか?
  • ScalapackなどのHPCライブラリのラッパーの整備

ただ, 生成AIの助けがあれば, これらの解決はそれほど難しくないと今は感じる. C++で新規コードを書いて, どうロバストに配布しようか, 解決できない問題に労力を使うより, 協力してRustでエコシステムを作ってしまったのが良いのではないかと感じる.

Juliaとの使い分け

Rustは優秀な言語だが, 計算物理をすべてRustで行うのには向いていない. Rustは堅牢な基盤ライブラリの実装に向いている. 一方, Juliaは人間が書きやすく, 様々なアイデアを素早く実装する用途に向く. 両者は全く異なる長所と設計を持っている. C-FFIを通した連携が容易であるからこそ, その組み合わせで互いの長所を活かすことができる.

個人的な限られた体験であるが, Rustを推進している人がJuliaを攻撃してくることを見たことがある.

  • 再現性のないノートブックを使う場合, 計算再現性を保つことが難しい.
  • Juliaは動的な言語で, 依存するライブラリの挙動をハックすることもできる. 計算の正しさを保証できない.

などである. それらは言語の性質によるものであり, 言語の選択はその用途に合わせて行うべきである.

一方, Juliaだけを使って, 計算物理エコシステム全体を構築することも現実的ではないと感じる. Juliaは試行錯誤に向いているが, 計算物理のような, 大規模なコードベースを書く際には困難が増えていく. 特に, 型関係の誤りを即座に検出して, AIで修正できるかは, 生成AI時代のコーディングにおいては効率を大きく左右する.

個人的には, Juliaを使って計算物理ライブラリを作成し, アルゴリズム等が比較的固まった段階で, 堅牢性, 再現性, 多言語からの再利用性を確保するために, 一部をRustへ移行するのが良いのではないかと感じる.

Updated: