HN 日本語サマリー

← 一覧へ戻る
プログラミング

純粋関数型プログラミングはC言語にどこまで迫れるか?

How close can pure functional programming get to C? (rtfeldman.com)

259 pointsby stmonty89 コメント

要約

Rocという関数型プログラミング言語のDEFLATE圧縮実装(roc-deflate)が、C言語で書かれたlibdeflateに迫るパフォーマンスを示した。平均ではわずかに遅いものの、メモリ安全性や副作用のなさといった強力な安全保証を提供しつつ、純粋関数型プログラミングがネイティブに近いパフォーマンスを達成できる可能性を示唆している。

全文翻訳

純粋関数型プログラミングはC言語にどこまで迫れるか?

2026年10月22日

LeanはRustより高速という記事では、印象的な結果が共有されています。関数型プログラミング言語であり証明支援系でもあるLean 4で書かれたDEFLATE圧縮ライブラリが、crates.ioによると10億回以上ダウンロードされている同等のRustパッケージを上回ったとのことです。記事より引用:

lean-zipはminiz_oxideが提供するすべての圧縮レベルを上回っています。つまり、より高いスループットで同じ圧縮を提供できるということです。最速設定では15%高速で、出力は8.8%小さくなります。

しかし、記事ではRustのminiz_oxideを上回ることはできたものの、DEFLATEパフォーマンスの最終ボスであるC言語で書かれたlibdeflateには及ばなかったとも指摘しています。

lean-zipは確かにここでは最高ではありません。libdeflateは当然ながらそれを圧倒します(これは、アーキテクチャ固有のSIMDを駆使した非常に注意深くチューニングされた実装であり、Leanでは触れることができないため、驚くことではありません)。

ここで登場するのが、実行時パフォーマンスに明確な焦点を当てた関数型プログラミング言語(ただし定理証明系ではない)であるRocです。

Rocのタグラインは「高速、フレンドリー、関数型」であり、「高速」が最初に来るのは、実行速度とコンパイル速度の両方が常に中心的な目標であったためです。Rocは成熟した言語ではありません(最初のナンバリングリリースである0.1.0まであと数週間です)、そしてその初期のパッケージエコシステムはほとんど最適化作業が行われていません。それでも、言語設計、長年の最適化、そして真新しいコンパイラインタプリケーションにより、Rocはすでにチャンピオンであるlibdeflateに挑戦するのに十分な基盤を備えていると私は考えました。

そこで私は、Niclas Ahdenが作成した(そして有料の商用ワークで利用している)DEFLATE圧縮パッケージであるroc-deflateのパフォーマンス最適化に多くの時間を費やしました。libdeflateのパフォーマンスを全体的に上回ることは期待していませんでしたが、非ストリーミングAPI(roc-deflateはまだストリーミングを実装していないため)で、ベンチマークのために古典的なSilesiaコーパスを使用し、同一の出力を生成することを確認しながら、どこまで迫れるかを見たかったのです。

10%未満の遅延

最適化作業を行った2021 M1 MacBook Proと、x64比較のためにAMD Ryzen 9 3900X Ubuntuマシンでのベンチマーク結果は以下の通りです。

roc-deflateは、libdeflateよりも平均で144のベンチマークで遅く実行されました。中央値では、Ryzen 3900Xで8.9%、M1 MacBook Proで2.6%遅かったです。

Silesiaコーパスでの圧縮

M1 MacBook Pro
Ryzen 3900X Ubuntu

MB/s

L1
L2
L3
L4
L5
L6
L7
L8
L9
L10
L11
L12
libdeflate
roc-deflate

圧縮レベル

MB/s

L1
L2
L3
L4
L5
L6
L7
L8
L9
L10
L11
L12
libdeflate
roc-deflate

圧縮レベル

平均的な遅延率が1桁であることは、これほど注意深くチューニングされたSIMD搭載Cライブラリと比較して驚異的です。率直に言って、Rocは自動メモリ管理を備えた関数型言語であり、境界チェックを必要としないコードと競合しているため、事前に達成可能だと知っていた結果ではありません。(境界チェックにはコストがかかりますが、同時に、Cの圧縮コードで境界チェックをスキップしたことが重大なリモートコード実行脆弱性につながった最後の例は昨日でした。)

グラフは中央値のギャップ以上の詳細を示しています。最悪ケースの入力では、roc-deflateはlibdeflateより18%遅かったですが、最良ケースの入力では、roc-deflateは18%以上高速でした!これはRocがCではアクセスできない魔法の最適化を持っているからではなく、単に2つの実装が異なるためであり、平均的にはroc-deflateの方が遅いですが、特定の入力ではより高速になることがあります。したがって、libdeflateからroc-deflateに切り替えることで速度向上が得られる可能性はありますが、平均的には期待すべきではありません。

明確にしておくと、Cのパフォーマンスの天井はRocよりも高いです。roc-deflateが現在libdeflateを上回っているあらゆる入力に対して、Cコードはそのギャップを埋めることができます。逆は真ではありません。Rocコードはメモリ安全であり、自動参照カウントを行い、境界をチェックし、Rustスタイルのunsafeをオプトアウトする手段をサポートしていません。これらの特性は、RocコードにC、Zig、Rust、およびハードウェアへのフルアクセスを許可する他の言語よりも強力な安全保証を与えます。そして、その保証のために支払う代償は、低いパフォーマンスの天井です。

もちろん、この単一の結果から、Rocが一般的なケースでCにどこまで迫れるかを外挿することはできませんが、誰も達成可能だと知らなかったことが実証されました。私の知る限り、これはこれまでRocパッケージに費やされた最適化努力の最大量であり、他の人がこの結果にどれだけ迫れるかは現時点では全く不明です。したがって、この結果に興奮していますが、より広範な実際のRocコードベースのサンプルを最適化するまで、その一般化がどれほどうまくいくかはわからないことを十分に認識しています。

パフォーマンスギャップがもたらすもの

明白な疑問は「なぜ遅くても許容するのか?」ということです。もしCレベルのパフォーマンスが得られるなら、なぜlibdeflate自体を使用せず、その残りのギャップを埋めないのか?

純粋なRoc実装には、純粋なC実装と比較していくつかの利点があります。

  • メモリ安全性。 Rocコードは完全にメモリ安全であり、オプトアウトを許可する言語プリミティブはありません。このコードがCレベルのパフォーマンスに迫れるのは、Rocの最適化パイプラインが多くの境界チェックと参照カウント操作が最適化して削除できると判断したためです。純粋なRocで書かれた圧縮パッケージは、昨日のBrotliリモートコード実行脆弱性を引き起こしたチェックされていないメモリ操作にアクセスできません。
  • 副作用なし。 Rocの型システムは、どの関数が純粋で、どの関数が副作用を実行できるかを推論します。純粋関数の型はBool -> Boolのような細い矢印を使用しますが、副作用のある関数はBool => Boolのような太い矢印を使用します(メモリ安全性と同様に、Rocはこれを強制し、オプトアウトする方法をサポートしていません)。したがって、roc-deflateの関数に細い矢印のみが表示されている場合、それらが本当に純粋関数であることがわかります。
  • サプライチェーンセキュリティ。 Rocには任意のFFIがないため、roc-deflateの純粋関数がI/Oを実行したり、任意のメモリにアクセスしようとしたりできないことを確実に知ることができます。爆発的なサプライチェーン攻撃を考えると、侵害されたRocパッケージ内の純粋関数が悪意のあるアクターが行える損害は、間違った値を返したり、クラッシュしたり、システムリソースを浪費したりすることに限定されることを知るのは価値があります。Rocパッケージはビルドスクリプトもサポートしていませんが、これは2ヶ月前に人気のRust arrayrefパッケージを侵害したベクトルでした。
  • ポータビリティ。 Rocの純粋関数は非常に決定論的になるように設計されています。同じ引数を与えられた場合、同じマシンで実行した場合だけでなく、コンパイルするすべてのターゲットで常に同じ値を返します(関数はクラッシュしたり、終了しなかったりする可能性もありますが、その動作もターゲット間で決定論的であるはずです)。Rocの純粋関数が2つの異なるシステムで実行されたときに異なる動作をすることが期待される唯一の方法は、実行中に一方のシステムがメモリを使い果たす場合です。

これらの保証のいずれかが実際には保持されない場合、それはRocのコンパイラ、アプリケーションが実行されているプラットフォーム、または基盤となるハードウェアの欠陥によるものにすぎません。したがって、roc-deflateのドキュメントでパッケージ全体が100%純粋関数であると表示されている場合、パッケージが提供するすべてのものから上記のすべてのプロパティが得られると確信できます。型システムがそれを強制します!

これらのすべてにこれほど懸命に取り組んできた理由が、逆の命題を見るとより明確になるかもしれません。保証されたメモリ安全性、保証された副作用のなさ、すべてのターゲットでの保証された決定論的な動作、およびパッケージに悪用可能なビルドスクリプトがないことによるセキュリティ上の利点を…平均的なパーセンテージの速度向上と引き換えに手放しますか?

この答えはユースケースによって正当に異なります。例えば、Roc自身のコンパイラはハードウェアが提供できる最大のパフォーマンスを求めていますが、この結果にこれほど興奮している理由を理解できる読者が多いのではないでしょうか。

Roc固有の最適化

roc-deflateの数値を改善した最適化のほとんどは、言語間で広く有益な一般的な最適化技術によるものでした(私は常にCasey MuratoriのPerformance-Aware Programming Seriesを推奨しています。私はこれを購入し、多くのことを学びました)。しかし、最適化技術の中にはRoc固有のものもあり、ここでは参考として言及したいと思います。Rocコードを書いていない場合でも、Rocがパフォーマンスをどのように達成しているかのアイデアを得ることができます。

不要な共有の回避

Rocの最も効果的な最適化の1つは、機会的ミューテーションです(Kokaはこれを「Functional but In-Place」と呼んでいます)。Rocの機会的ミューテーションに関するドキュメントでその仕組みを学ぶことができますが、パフォーマンス最適化の観点からは、値が一意であり、したがってインプレースミューテーションの対象となるように、不要な共有を避けることが非常に重要です。

境界チェックのホイスト

C、Zig、Rustのように境界チェック(例:動的にサイズ変更可能なコレクションから値を取得する)をオプトアウトすることはRocではできませんが、Rocのコンパイラは、それらが確実にパスするとわかっている場合は削除できます。境界チェックはループ内で最もコストがかかるため、ループの開始前にif collection.len() <= highest_index_used { return 0 }のようなものを追加することで多くのメリットを得られます。これは冗長に見える追加のチェックを追加しますが、早期リターン後にコストのかかるチェックが到達不可能であるとコンパイラが判断した場合、ループ内のコストのかかるチェックを安全に削除することを可能にします。

コンパイル時にルックアップテーブルを構築する

Rocのコンパイル時評価は、実行時入力に依存しない限り、コンパイル時にルックアップテーブルを組み立てるために使用できます。さらに良いことに、それらの長さはコンパイル時にわかっているため、Ok(entry) = lookup_table.get(5)のようなことをして、コンパイル時評価のおかげでRocのコンパイラが有効だとわかる網羅的なパターンマッチを行うことができます。これにより、定数でないテーブルが支払う可能性のある実行時境界チェックを回避できます。

ラップ算術を検討する

Rocでは、a + bはa.plus(b)にデシュガーされます。U64のような整数では、U64.plusにはオーバーフローチェックが含まれますが、plus_wrapは含まれません。plusはより安全なデフォルトですが、オーバーフローチェックには実行時コストがかかります。CPUは1つの命令でラップ整数算術を実行できるためです。Rocのコンパイラがオーバーフローが発生しないと証明できる場合、plusとplus_wrapは両方とも同じ命令に最適化されますが、できない場合はplus_wrapを選択することで速度向上が得られる可能性があります。

改善の余地

roc-deflateのパフォーマンス、特にx64 CPUでのパフォーマンス、およびRocコンパイラの最適化パイプラインには、まだ改善の機会があることは確かです。

現時点では、ローエンドの果実はすべて摘み取られたと reasonably に自信がありますが、もし誰かが自分で試したいのであれば、Linuxでベンチマークスイートをセットアップして実行するために使用したスクリプトはこちらです。(Claude Opus 5.5がこのスクリプトを生成したもので、セキュリティについては検証していません。自己責任でサンドボックス外で実行してください!)これはRocコンパイラのリビジョンとroc-deflateのバージョンを両方固定しているため、潜在的な改善を試す場合はこれらのコミットを調整する必要があります。

私自身は、それがリリースされるまで、今後のRoc 0.1.0リリースに集中します。8年間の開発期間を経てきたので、ついにナンバリングリリースを持つことができて非常に嬉しいです!Rocについてもっと知りたい場合は、roc-lang.orgをチェックするか、Roc Zulipで挨拶に来てください!