Rustアプリケーションの実装効率と性能評価 – C言語・Go実装との比較

Rustアプリケーションの実装効率と性能評価 – C言語・Go実装との比較

Rustアプリケーションの実装効率と性能評価 – C言語・Go実装との比較

Rustは、安全性と高速性の両立を目的にデザインされた言語[1]であり、近年では業務に採用されるプロフェッショナル言語としての、更なる飛躍が期待されています[3][8]

ただし、2021年のアンケート結果[3]でも、仕事での利用率は前年の42%から59%に大きく向上している反面、業界での採用実績面の少なさが、Rustの将来性の最大の懸念(38%)に挙げられており、現時点では実績不足な面は否めません。

今回は、Rustの実用性を判断するために、データベース(Redis)とIoT(ECHONET Lite)の2つ分野の同一アプリケーション実装を評価対象とし、Go言語やCなど他プログラミング言語での実装と比較することで、Rust実装の効率と性能評価を試みました。

結論としては、Better Cの観点からはGoがCの後継として最適と考えています。Rustは安全性とスピードを提供しますが、生産性、相互運用性、プログラミングの柔軟性に限界があります。反面、Goは実装の効率性と安定した性能で際立っており、汎用的なアプリケーションに安心して使える選択肢と判断しています。

評価① - Databaseアプリケーション (Redis)

本評価は、データベース分野のRedis[19]仕様を同一アプリケーションとして、C言語、Rust、Goによる実装を評価します。Redisの公式実装[19]はC言語であり、RustおよびGo実装は非公式なサブセット実装になります。

Rust実装はTokio[20]ライブラリの学習用として公開されたmini-redis[21]、Go実装は拙作のRedis互換データベース実装のgo-redis[23]のサンプル実装(go-redis-server)での評価となります。

なお、Redisの公式実装[19]と比較し、mini-redis[21]、go-redis[23]についてはサブセット実装となるためLOC(Lines of code)による実装効率評価はできず、性能評価のみとなります。

評価ベンチマーク

評価したベンチマークプログラムは、Redis公式ベンチマークツールである「redis-benchmark」で実施しました。実行するベンチマークはフルセットではなく、SET/GETコマンドのみに限定し、標準の50スレッドにて10,000回毎繰り返した結果を用いています。

基本的なSET/GETコマンドのみに限定されている理由は、mini-redis[21]がSET/GETコマンドなどの最小限のコマンド実装に留まっており、mini-redis[21]でのベンチマークパラメータ[22]を踏襲したためです。なお、mini-redis[21]は評価時の最新版、環境は「Mac mini (2018) + macOS 12.6」での評価となります。

実行性能評価 - C > Go > Rust

「redis-benchmark」によるSET/GETコマンドの実行結果を以下に示します。評価指標としてはデータベース運用上に指標に用いられる99パーセンタイル(p99(raito))の値を、C言語を基準とした性能比と共に右端に記載しています。

評価ベンチマークでは、公式な実装であるC言語のredis-server[19]、Goのgo-redis[23]、Rustのmini-redis[21]の順に高速な結果が得られました。以下に上記表のグラフを示します。

C言語のredis-server[19]は最適化されたものであり、Go、Rustのサンプル実装と比較すると3倍程度高速な結果が得られました。

Goのgo-redis[23]、Rustのmini-redis[21]共に、サンプルとして実装されたものであり、両者とも改善の余地はあります。その点を加味し、以下に各プログラミング言語の評価結果の寸評を示します。

Rust

公式なC言語のredis-server[19]比でSET:28%、GET:41%の速度に留まり、Go実装のgo-redis[23]比較においても、SET:78%(=2.879/3.663)、GET:88%(=2.167/2.455)の速度に留まりました。

Tokio[20]ライブラリの学習用として公開されている経緯もあり、本格的な最適化は未実施であると想定されます。ただし、ある程度、公式実装のRedis[19]と性能比較しての最適化は実施されているようです[22]

Tokio[20]ライブラリの学習用の位置付けであり、実装は基本的にはTokio[20]ライブラリが用いられており、ネットワーク部はTokio[20]のTCPサーバー(tokio::net::TcpListener)をベースに構築されています。ただし、データベースのキーバリューデータ管理部については、評価②の実装と同様に、標準(std)ライブラリのHashMap、排他制御には標準(std)ライブラリのMutexにて実装されています。性能的な課題については、評価②と同様にHashMap[17]などの、標準(std)ライブラリの性能的課題[18]に拠るものかもしれません。

Go

公式なC言語のredis-server[19]比でSET:35%、GET:46%の速度に留まりましたが、Rustのmini-redis[21]比較においては、SET:127%(=3.663/2.879)、GET:113%(=2.455/2.167)の速度を示しました。

go-redis[23]はRedis互換データベース実装のフレームワークであり、今回の評価対象もmini-redis[21]と同様にサンプル実装的な位置付けであり、実装は簡素です。最適化の余地は少ないものとの思い込みもあり、公式実装との速度差は想定外でした。

go-redis[23]およびサンプル実装の「go-redis-server」は、標準ライブラリのみで実装されており、データベースのキーバリューデータ管理部についてはsync.Mapでの実装となります。公式実装との性能差については、プロファイリングでのボトルネックの特定と並行して、公式の実装を参考にする必要がありそうです。

評価② - IoTアプリケーション (ECHONET Lite)

本評価は、IoT分野の通信プロトコルであるECHONET Lite[9]のクライアント・サーバ機能実装を対象とします。本実装は、対象とする要求仕様(= ECHONET Lite[9]仕様)は同一かつ、設計および実装者が同一であるアプリケーションの評価となります。

評価対象

ECHONET Lite[9]のRust実装[12]を、C言語[11]、Go[13]、Pyton[14]で実装したものを比較しました。基本的なECHONET Lite[9]機能は、可能な限り同一の設計にて実装されていますが、言語により若干の機能実装の差があります。実装機能の差の概要を、以下の図に示します。

本評価対象となったECHONET Lite[9]フレームワークの実装は、C言語、Go、Python、Rustの順番で実装されており、基本的に同一の設計を踏襲しています。

ECHONET Lite[9]仕様に準じて説明すると、基本的なデバイス(Device)機能、デバイスを操作するコントローラー(Controller)機能ならびに、ECHONET Liteで定義されている標準デバイス[10]のデータベース(Database)については、全てのプログラミング言語で実装されています。

ただし、ECHONET Lite[9]仕様のトランスポート層については、必須であるUDP通信機能のみ全てのプログラミング言語で実装、オプション仕様であるTCP通信機については、C言語[11]とGo[13]のみで実装、Rust[12]およびPyton[14]での実装は省略されています。

実装効率評価 (≠実装工数) - Python > Rust ≒ Go > C

Rustでの実装効率をLOC(Lines of code)にて評価しました。LOCの算出は、Tokei[16]を用いており、コメントや空行については除外されています。また、評価時には全てのプログラミング言語版で、LOC削減を意図した実装はされておらず、試験コードについては計測対象外としています。LOCの算出結果を、以下の図に示します。

自動(Auto)については、ECHONET Liteの標準デバイス[10]を定義している自動生成されたコードであり、C言語のヘッダファイルを除いたソース(Source)コードのみを評価対象としました。以下に上記表のグラフを示します。

実装機能範囲を加味すると、LOCでの評価は、Python > Rust ≒ Go > Cの順番に実装されたコードが少ない結果となります。以下に各プログラミング言語について、寸評を示します。

Rust

LOC結果から、(TCP機能の実装はないものの)、Goとは同等、C言語の53%のLOC効率で実装評価となります。今回のRustによる実装は、後述するUDPSocketの課題を除き、Mutexなど標準(std)ライブラリのみで実装されています。

Rustは一般的には生産性を感じるまでに学習期間が必要な言語[5]です。そのため、Rustでは単純に「LOCの少なさ = 実装工数」ではありません。また、自動(Auto)生成されたコードのLOCが極端に多い原因は、Rustのフォーマッター(rustfmt)の標準最大行幅(max_width=100)の指定によるもので、評価からは除外されています。

また、Rust特有の移動(Move)セマンティクスやライフタイムによる制限事項もあり、多言語での設計を持ち込むめず、設計コストが多くなる場合もあります。今回の実装においても、トレイトオブジェクトを対象としたObserverパターンの導入制限や、Mutexのライフタイムと同期したロック区間の制限から、一部設計を変更しての実装となりました。

C

C言語による実装が、最大のLOC結果となりました。要因としては、本実装は外部ライブラリを利用しておらず、多言語では不要な自前ライブラリ(例:文字列やリスト構造、オブジェクト指向構造など)や、移植性のためのラッパークラス(例:Mutex, Threadなど)などの自前ライブラリが含まれているのが原因です。自前ライブラリは、他C言語プロジェクトと共用のものであり、LOC数値ほど、今回のアプリケーションの実装量は多くありません。

Go

LOC結果から、Rustと同等の実装効率で、C言語との比較でも63%のソースコードでの実装評価となります。Rustでは、一部標準ライブラリに課題がありましたが、Go言語では標準ライブラリのみで実装しています。

基本的にはC言語の設計のまま、Go言語に実装されています。Rustでの実装とは違い、C言語での設計を転用して実装できており、柔軟性もあります。また、C言語と比較して標準ライブラリが充実しており、「LOCの少なさ = 実装工数」に直結していると評価できます。

Python

Pythonによる実装が、今回の実装評価で最小のLOC結果となりました。Pythonは、コードブロック表現に括弧({})が不要なスタイルのため、他の括弧が必要な言語(C,Rust,Go)との比較では、差し引いいての評価は必要です。また、Rustと比較においては、実装の継承が可能な点も、LOCの削減に寄与しています。

Pythonも、C言語やGo言語での設計を素直に転用できる柔軟性があります。後述する実行速度の課題を除けば、プロトタイプ言語としては最良かもしれません。

実行性能評価 - Go > C > Rust > Python

評価対象であるECHONET Lite[9]の同水準の機能を、同一の基本設計での実装にて、Rustの実行性能を評価しました。評価したベンチマークプログラムは、ECHONET Lite[9]のコントローラーとオブジェクトが実装されたノードになります。

評価ベンチマーク

評価したベンチマークプログラムは、ECHONET Lite[9]のコントローラーがUDPクライアント、オブジェクトがUDPサーバーとなります。ベンチマークの基本シーケンスとなるメインループは、1回の実行毎にコントローラーからのUDPリクエスト(Request)が12回送信され、そのUDP応答(Response)がオブジェクトより12回送信されるものです。

ECHONET Lite[9]仕様に沿った説明をすれば、ECHONET Liteコントローラー自身をUDPマルチキャスト要求(ESV:0x62)で発見し、発見されたコントローラーノードに含まれるノードプロファイルオブジェクト(0x0EF001)の実装必須プロパティ[10]の値(12個)を、UDPプロトコルでの要求(ESV:0x62)応答(ESV:0x72)動作を10,000回繰り返しをベンチマークとしたものです。また、実装コード評価のため、各環境共に最適化オプションは未使用になります。なお、環境は「Mac mini (2018) + macOS 12.6」での評価となり、評価スクリプトの詳細はこちら[15]で公開しています。

性能評価結果

性能評価は、評価ベンチマークに示した基本シーケンスを10,000回繰り返した実行時間をtimeコマンドで計測しました。各プログラミング言語のコンパイル条件や実装手法の概要とあわせ、評価結果を以下に示します。

評価ベンチマークでは、Goによる実装が最速で、C言語、Rust、Pythonの順に高速な結果が得られました。ECHONET Lite[9]の通信プロトコルはUDPに統一しています。UDP通信は非同期通信となるため、要求(Request)からの応答(Response)は、各プログラミング言語の条件(condition)やチャンネル(channel)機構にてデータの送受信を実装しています。以下に上記表のグラフを示します。

Pythonは極端に実行速度が遅かったため、縮尺的に除外しています。C言語との性能比較のため、C言語を基準とした各プログラミング言語の性能比(ratio)を図とあわせ、各プログラミング言語の評価結果についての寸評を以下に示します。

Rust

Rustによる実装は、GoおよびC言語による実装より性能が劣る結果となりました。C言語比で33%の速度に留まり、Go比では20%(=33/160)程度の速度に留まりました。システム(sys)時間はGoと同等でC言語より高速な値を示しているものの、ユーザー(user)時間がC言語やGoと比較して、大きいのが気になります。

原因については調査が必要ですが「The Rust Performance Book」[17]や「The Rust Language FAQ」[18]によるとHashMapなど、状況により遅くなる標準(std)ライブラリもあるようです。

また、今回の実装で多用している標準(std)ライブラリのMutexは、有効区間が対象オブジェクトと同一となるため扱いにくく、やむを得ずコピー(Copy)セマンティクスで回避している箇所があります。多言語の実装ではゼロコピー(Zero Copy)となるため、プログラム的には(大幅な設計変更が必要となりますが)改善の余地があります。

また、標準(std)ライブラリのUDPSocketは同一ポート番号によるソケット生成には対応できないため、今回の実装対象であるECHONET Lite[9]やmDNSなどのIoT関連プロトコルの実装には、非標準のクレート(crate)を併用する必要があります。また、IPv6機能実装はされているものの、現時点では、エラーのため有効化できていません。

C

基本的に、ユーザー(user)時間は最速ではあるのですが、システム(sys)時間がGoとRustと比較すると2.5倍も遅いのが気になります。今回の評価までC言語の性能についての疑問はありませんでしたが、pthreadなどの標準ライブラリの利用に課題があるのかもしれません。

Go

Goによる実装が、今回の評価ベンチマークで最速の結果となりました。C言語実装と比較すると1.6倍(=160%)、Rustと比較すると5倍(=4.8=160%/33%)の性能結果です。Go標準ライブラリの成熟度もあるのでしょうか、素直にプログラムを書いて、簡単に最良の結果が得られた印象です。

Python

自明かもしれませんが、Pythonによる実装が、今回の評価ベンチマークで最低の結果となりました。システム(sys)時間はC言語比で1/2(=53%)程度の劣化に留まりましたが、ユーザー(user)時間は1/100(<1%)以下の性能です。Pythonによる純粋な実装であるため、インタプリンタ実行の特性となります。

最後に

C言語の後継、Better Cな観点で評価するとすれば、個人的には、Go = (Objective-C) > Rust > C++の順位になるでしょうか。C++は、Better Cといった観点ではオーバースペックですし、Objective-CはARCが導入された2007年(2.0)以降進化が止まっています。後継とされるSwiftについても、マルチプラットフォームの将来性に不透明さがあります[23]

生産性については、Rustは一般的には生産性を感じるまでに学習期間が必要な言語[5]です。Rustは、安全性と高速性を両立させた言語[1]ですが、その制約により、良い面、悪い面、コンパイラとの格闘強いられる言語です。Rustのセマンティクスに沿わない場合、他のプログラミング言語でのデザインパターン(経験)を直接的には持ち込めず[24]、評価②に性能評価の考察に示したような試行錯誤が必要となります。

また、生産性においては、今回の評価のようなRustのみで完結する実装ではなく、既存の開発資産を利用したい場合も多いでしょう。Rustでは、C/C++言語資産の相互運用性の要望は継続して高く、大きな課題として認識されています[5][25][26][4][27]。C言語との相互運用性が言語レベルで担保され、ヘッダファイルをインクルードするだけで利用できる、C++、Objective-CやGoと比較すると、一つの大きな障壁です。rust-bindgen[27]のようなFFIジェネレータも存在はありつつも、既存資産の継承の有無や、相互運用性の課題は、Rustは採用の判断材料となるでしょう。

安全性については、Rustは静的分析や動的な境界チェックなどで担保するプログラミング言語です。ただし、伝統的なC/C++においても、豊富な静的分析ツール(Clang Static Analyzeなど)や動的分析ツール(Valgrindなど)が活用できます。また、Rustも(データ共有を伴う)並行性アプリケーションにおいては、C/C++/Go言語同様な参照(Arc)や排他制御(Mutex)セマンティクスを持ち込まざるを得ず、さらにRustの言語仕様の移動(Move)やライフタイムなどのセマンティクスの制限からの逃れられません。結果的に、他プログラミング言語と比較するとプログラミングの柔軟度が制限され、評価②に性能評価の考察に示したような生産性や性能に影響のある、設計および実装上のトレードオフの判断が必要となります。

最後に、性能評価においては、Rust、C言語、Goの各々の実装において課題が確認できました。本評価において、Goによる実装が実装効率も良く、安定した性能を示しました。汎用的な選択肢としては、Go言語が一番無難な候補かもしれません。いずれにしても、各言語の性能的な課題については、また詳細に調査してみたいと思います。