
2026/02/03 3:19
**OCaml でのスレッド制御:Delimited Continuations と Lwt の比較** - **Delimited Continuations(境界付き継続)** - 軽量に制御フローを扱える手段。計算を任意の箇所で一時停止・再開したい場合、スレッド全体を生成するオーバーヘッドなしで実装できる。 - **Lwt(Lightweight Threads)** - コオペラティブスレッド用の標準ライブラリ。成熟度が高く、ドキュメントも充実しており、多くの OCaml プロジェクトと自然に統合できる。 --- ## Delimited Continuations を使うケース | 条件 | 理由 | |------|------| | **細粒度な非同期制御** | 生成器(generator)やコルーチンを実装する際、計算を任意のポイントで停止させたいが、スレッドのオーバーヘッドは避けたい場合。 | | **可合成効果(Composable Effects)** | 独自の効果ハンドラを構築し、状態管理や例外処理など他の抽象と組み合わせて使いたい時に便利。 | | **パフォーマンスクリティカルな箇所** | Lwt のプロミスベースモデルではコンテキストスイッチングが発生するため、そのコストを回避したい場合。 | --- ## Lwt が適しているケース | 条件 | 理由 | |------|------| | **標準的なネットワーク I/O** | ソケット、HTTP クライアント/サーバー、ファイル操作など、既に Lwt 用のバインディングが揃っている場合。 | | **大規模コードベース** | 既存のライブラリが `Lwt.t` 型を前提としているとき、継続を導入すると膨大なリファクタリングが必要になる。 | | **コミュニティサポート・ツールチェーン** | デバッグツールや診断機能、豊富なサードパーティ製パッケージが揃っている。 | --- ## 実用的比較 | 特徴 | Delimited Continuations | Lwt | |------|------------------------|-----| | **API 形態** | `unit -> 'a` のコールバックや独自ハンドラ | `('a, 'e) result Lwt.t` のプロミス | | **ボイラープレート** | シンプルな一時停止であれば最小限 | `let%lwt` や `>>=` などチェーンが必要 | | **相互運用性** | 他の非同期ライブラリとの統合は難しい | 多くのライブラリとシームレスに連携 | | **パフォーマンス** | オーバーヘッド低いが複雑さが増すと管理が大変 | やや高めだが予測可能で安定 | --- ## 推奨 - **ドメイン固有言語(DSL)を構築する際**、または「プロミスでは表現しにくい」制御フローが必要な場合は *Delimited Continuations* を採用。 - **一般的なネットワークサービスや I/O 集約型の処理**、あるいはメンテナンス性とコミュニティサポートを重視するプロジェクトでは *Lwt* を選択。 要するに、抽象度に合わせてツールを使い分けることが重要です。 - 低レベルの細かな制御 → **Delimited Continuations** - 高レベルで実運用可能な非同期プログラミング → **Lwt**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
現在の要約はすでに明確で簡潔かつ主要ポイントリストに忠実です。追加の改訂は不要です。
元の要約の繰り返し
MirageOS はイベント駆動型アーキテクチャを中心に構築されているため、開発者は通常 OCaml の Lwt モナドまたはその構文拡張を使用して非同期コールバックを整理します。この記事では、プレーンな Lwt と
delimcc ライブラリを介したデリミテッド継続(fiber 実装)を比較しています。マイクロベンチマークによると、ブロッキング呼び出しが多い場合や深い再帰が使用される場合は fiber がわずかに遅くなることがありますが、スタックの末尾で一つだけブロックが発生するケースでは Lwt を上回るパフォーマンスを示します。しかし、MirageOS のワークロードはこれらの差異に対して十分に感度が高くないため、性能のみでどちらのアプローチを採用すべきか決まるわけではありません。代わりに相互運用性と JavaScript サポート(js_of_ocaml 経由)がより重要になります。この議論は、将来の MirageOS プロジェクトが測定可能な速度向上を必要とする場合に Lwt_fiber や類似の fiber インターフェースを採用するかもしれないことを示唆していますが、現在のところ実行時効率を損なうことなくコードの明瞭さとツール互換性によって選択が導かれる可能性があります。本文
ミラージュOSは完全にイベント駆動型のシステムであり、従来型のプリエンプティブスレッドをサポートしていません。
プログラムは「イベント」(例えば受信したネットワークパケット)によって起床され、対応するコールバックが実行されます。このコールバックは I/O やタイマーなどでブロックしなければならない場合やタスクを完了したときまで実行され続けます。
イベント駆動型システムは実装が簡単で、ネットワーククライアント数に応じてスケールできる上、node.js のようなフレームワークのおかげで人気があります。
しかし、イベントコールバックを直接書くと制御ロジックが多数の小さな関数に散らばってしまうため、イベント登録・待機による中断を隠す抽象化が必要です。
OCaml の Lwt スレッドライブラリはモナド的アプローチを採用しています。
val return : 'a -> 'a Lwt.t val bind : 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t val run : 'a Lwt.t -> 'a
スレッドは型
'a Lwt.t を持ち、終了時に型 'a の値を返します。return は OCaml の値からそのようなスレッドを作成し、bind はスレッドが完了したときに呼び出される関数(=「将来実行する」処理)を組み合わせます。
例えば sleep 操作の場合:
val sleep : int -> unit Lwt.t let x = sleep 5 let y = bind x (fun () -> print_endline "awake!") run y
ここで
x は unit Lwt.t 型です。bind に渡したクロージャは sleep が終了したときに unit を引数として呼び出されます。run は実際に Lwt スレッドの評価を開始します。
懸念事項
ミラージュOS はすでに Lwt を広範囲に利用していますが、ユーザーの中には使いづらさを感じる方もいます。主な問題は次のとおりです。
- コード書き換え – ブロッキング処理を
とreturn
で適応させる必要があり、サードパーティライブラリの統合が複雑になります。bind - 割当コスト – すべての潜在的ブロックポイントでクロージャが生成されます。OCaml では軽量ですが、オーバーヘッドは無視できない場合があります(例:Jun Furuse は自らの Planck パーサで組み込み型ベースシステムより遅いと報告)。
Lwt は
pa_lwt シンタックス拡張を通じて第 1 の問題に対処しています。これにより、以下のようなキーワードが利用できます。
lwt x = sleep 5 in print_endline "awake"
lwt キーワードは自動的に bind を挿入します。
フィーバー(Fiber)
代替手段として delimcc があり、これはデリミテッド継続を実装しています。
Lwt と組み合わせることも可能で(例:Jake Donham の Lwt_fiber ライブラリ)、フィーバーは次のように開始します。
val start : (unit -> 'a) -> 'a Lwt.t val await : 'a Lwt.t -> 'a
実行中にフィーバーは
await で別スレッドをブロックできます。再開可能例外がスタックを
start が呼ばれた位置まで戻し、待機していたスレッドが完了すると再開します。
ベンチマーク
ミクロベンチマークでは Lwt スレッドとフィーバーの性能を比較しています。
フィーバーテスト:
module Fiber = struct let basic fn yields = for i = 1 to 15000 do for x = 1 to yields do Lwt_fiber.await (fn ()) done done let run fn yields = Lwt_fiber.start (fun () -> basic fn yields) end
LWT テスト:
module LWT = struct let basic fn yields = for_lwt i = 1 to 15000 do for_lwt x = 1 to yields do fn () done done let run = basic end
両テストは最初に高速な
Lwt.return () を使用します。ブロッキング関数を次のように置き換えると:
- slow –
(タイムアウト登録)Lwt_unix.sleep 0.0 - medium –
(スケジューラへ投げ込む)Lwt.pause ()
フィーバーは浅い呼び出しスタックでも単なる Lwt より遅くなります。
次のテストでは再帰深度を調べます:
module Fiber = struct let recurse fn depth = let rec sum n = Lwt_fiber.await (fn ()); match n with | 0 -> 0 | n -> n + sum (n-1) in for i = 1 to 15000 do ignore (sum depth) done let run fn depth = Lwt_fiber.start (fun () -> recurse fn depth) end
同等の LWT バージョンは冗長ですが、ロジックは同じです。
Lwt_unix.sleep 0.0 をブロッキング関数にすると、フィーバー実装は再帰深度が増すにつれて性能が低下しますが、単純な Lwt は線形のままです。
再帰に関するフォローアップ
Jake Donham は、コピー/復元コストを大きなスタックで均等化できればフィーバーが速くなる可能性があると指摘しました。
彼はテストを再実行し、
fn が 1 回だけ 長い呼び出しスタックの終端で呼ばれるように変更しました:
module Fiber = struct let recurse fn depth = let rec sum n = match n with | 0 -> Lwt_fiber.await (fn ()) ; 0 | n -> n + sum (n-1) in for i = 1 to 15000 do ignore (sum depth) done let run fn depth = Lwt_fiber.start (fun () -> recurse fn depth) end
このシナリオではフィーバーが速くなります。
違いは、単純な LWT バージョンの各再帰呼び出しでクロージャ(
Lwt.bind)が割り当てられる一方、delimcc バージョンは最終的な yield まで通常の再帰を行うためです。
結論
- MirageOS のワークロードに対しては Lwt とフィーバー間の性能差はほぼ無視できるレベルです。
- 相互運用性 がより重要です:
- Lwt コードは
を通じて JavaScript 上でそのまま動作します。js_of_ocaml - Delimcc コードは既存の同期コードを移行する際に役立ちます。
- Lwt コードは
- 将来的には、JavaScript の yield 演算子が導入されれば、
/shift
と同等の表現力を持つため、両者の収束も可能です。reset
つまり、性能だけでなく統合と移植性に最適なスレッドモデルを選択することが推奨されます。