🗃️

Swift Fluent 4ではレコードの読み取りが遅いのかを調査した結果

に公開

概要

先日、100万行程度のDBのレコードを集計するスクリプトをSwiftで書いたところ、レコードの取得だけで6分ほどかかってしまいました。そこでSwiftのFluentライブラリのレコード取得がほかの言語・ライブラリと比較して遅いのかを試してみたのでその結果をここに残しておきます。

結論

SwiftのCodableは高度な抽象化を行っている分、実行時にMirrorAPIでリフレクションによるマッピングをしているために、他のライブラリよりも遅くなっていることがわかりました。
また、macOSのtimegmが低速なため、Linux上やDocker上で実行することによって2倍以上高速化することが可能なことがわかりました。

調査方法

1000万レコードのtestdataテーブルを含むSQLiteファイルを作成したあと、Rust(rusqlite), Swift(Fluent), Go(GORM), Scala(Slick), TypeScript(Bun)でそれぞれSELECT * FROM testdataを走らせてみました。

コードはSwiftの例をChatGPTに渡して書かせたもので、大体の比較ができれば良いと拘ってはいません。GitHub
macOS 15.3.2上で実行しています。

結果

以下のような結果になりました。

実装 実行時間
Swift Fluent 1908.30s user(約 32 分)
Swift Fluent on Docker 821.585 user(約 14 分)
Go (GORM) 4 時間以上待っても終わらず
TypeScript (Bun) 3.27s user
Rust (rusqlite) 2.62s user
Scala (Slick) 14.78s user

考察

パフォーマンス低下の調査を行ったところ、Swiftではリフレクション(Mirror)APIを多用していることが起因していそうです。実行時にモデルの全フィールドをMirrorで操作して値を設定しており、SwiftのMirrorによるリフレクション処理は高速ではないためにオーバーヘッドになっていそうです。

VaporコミュニティのFluentのGwyyneさんもFluent 5の開発に向けて、こう言っています

Reflection is what makes Fluent so slow. (It also doesn't work very well on enums.)
If you're considering using reflection in Swift, you're probably designing wrong. If you're not designing wrong, reach for macros instead - at least then the overhead is at build time instead of runtime.
However, you can use CaseIterable to iterate enum cases (and the compiler will generate the conformance for you, as long as the enum doesn't have any cases with associated values).
(Not that that helps you with SQLKit, since none of its enums conform to that.)
Fluentが遅い原因はリフレクションです。(しかも、enumではあまりうまく動作しません。)
Swiftでリフレクションを使おうとしているなら、多くの場合は設計が間違っている可能性が高いです。もしそうではないなら、代わりにマクロを使ったほうがいいでしょう。少なくとも、その場合のオーバーヘッドは実行時ではなくビルド時に発生します。
ただし、CaseIterable を使えば enum のケースを反復処理することはできます(enum に関連値付きのケースがなければ、コンパイラが自動的に実装を生成してくれます)。
(もっとも、SQLKit の enum はどれもこれに準拠していないので、この方法は役に立たないのですが。)

また、Issueや結果から、macOSで実行時に遅くなるため、Linux上、あるいはDocker上で行うと2倍以上速くなることがわかります。
https://github.com/vapor/fluent-mysql-driver/issues/183

GoのGORMがSwift以上に遅いのも実行時のリフレクションに依存していることが理由だと推測されます。

対照的に、RustやBun, Scalaが速いのは、リフレクションをせずにコンパイル時にデコード方法を決定しているからだと考えられます。

あとがき

Discordを追ってみるとFluent 5ではリフレクションを使わずにmacroでコンパイル時にdecodeが決定することによるコンパイルの高速化が期待できそうです。
Fluent 4での100万以上のレコード取得ではCodableによるリフレクションを用いたdecodingは使用せずに、生の行を自分でdecodeするコードを書くといいと思います。

nextbeat Tech Blog

Discussion