「OCamlで構築する高速・ゼロ割当Webサーバー」

2026/02/02 19:45

「OCamlで構築する高速・ゼロ割当Webサーバー」

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

概要:
著者は httpz を構築しました。これは、ヒープ割り当てを排除し、低レベルの C コードと同等の速度を実現する高性能な HTTP/1.1 パーサで、OxCaml で実装されています。OxCaml の unboxed レコード構文 (

#{}
)、
int16#
型、unboxed タプル (
#(span * int16#)
)、およびスタックのみのローカル変数(
local_
でマークするか
let mutable
で宣言)を使用することで、値はレジスタまたはスタックに保持され、ガーベジコレクションの対象とはなりません。パーサはヘッダー情報を最大 32 KB まで扱い、
Bigarray
の代わりに
bytes
を使用します。公開シグネチャは以下の通りです。

parse : bytes -> len:int16# -> limits:limits ->
  #(Buf_read.status * Req.t * Header.t list) @ local

ベンチマークでは、ヒープ割り当てがゼロで約 6.5 M リクエスト/秒を実現し、一方で従来のパーサはリクエストごとに 100–800 ワードを割り当てるため約 3 M リクエスト/秒となります。パーサは既に稼働中の Eio ベースのウェブサーバに統合され、現在このページを含むトラフィックを処理しています。今後の作業としては、OxCaml の

caml_alloc_local
を活用してゼロコピー io_uring I/O を可能にし、Docker VPNKit のパフォーマンス向上を図ることや、odoc 連携などツールサポートの拡充が挙げられます。著者はまた、OxCaml の迅速な開発/テスト用に Claude スキル のセットも作成しました。この成果は、高トラフィックウェブサーバやコンテナネットワークスタックで、最小限の GC オーバーヘッドで超高速 HTTP パーシングが必要な場面に有益であり、低レベル OCaml エコシステムツールへの影響や他領域での類似最適化のインスピレーションを与える可能性があります。

本文

イントロダクション

昨年ICFPでOxCamlのチュートリアルを手伝った際に、実務でPlanetary Computingの研究インフラ―に導入しようと熱望していました。TESSERAで生成しているペタバイト規模の埋め込みデータを管理するためです。
OxCamlはシステム志向のプログラムで大幅な性能向上をもたらす言語拡張が備わっており、OCamlの慣れ親しんだ関数型スタイルを維持できます。Rustとは違い「通常」コード用にガベージコレクタが利用可能です。また、最近は大規模なPythonスクリプトの保守に疲弊しており、OCamlのモジュラリティと型安全性に渇望しています。

新しい技術を学ぶ私の従来の方法は、最新トレンドでウェブサイトインフラ―を置き換えることです。昨年は自分のライブサイトをOxCamlで構築しましたが、新しい拡張機能を深く統合する時間がありませんでした。そのため、次に紹介したいのはhttpzという新しいウェブサーバーです。これはOCamlで最高性能を追求したものです(Chris Casinghino、Max Slater、Richard Eisenberg、Yaron Minsky、Mark Shinwell、David Allsopp などJane Streetのツール&コンパイラチームに感謝します!)


HTTP/1.1 の「ゼロ割り当て」アプローチ

httpz
は高性能な HTTP/1.1 パーサで、主要なヒープ割り当てをなくし、最小限のマイナー・ヒープ割り当ても実現します。OxCaml のアンボクシング型とローカル割り当てを活用しています。

これが有用なのは?
HTTP 接続全体を呼び出しスタックだけで処理できるため、接続の解放は単にハンドリング関数から戻るだけです。安定した状態ではウェブサーバーはほぼガベージコレクタ活動がありません。直接的な副作用と組み合わせれば、コールバックごちゃごちゃになることもなく書けます。

まずは HTTP/1.1 のみに特化し、入力を単純な 32 KB のバイト列(ヘッダ部分のみ)に限定しました。POST リクエストのボディ処理は比較的シンプルで、本稿では扱いません。

OxCaml と通常の OCaml で高速化できる点を検証します。


アンボクシング型とレコード

パーサで使うコア型を決めることが最初のステップです。OCaml のメモリ表現に慣れたい場合は Real World OCaml を参照してください。

私の通常の OCaml コードでは、2012 年に書いた

cstruct
ライブラリを使ってコピー無しビューを管理しています。

type buffer = (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t
type Cstruct.t = private {
  buffer: buffer;
  off   : int;
  len   : int;
}

レコードで大きなバッファの狭いビューを作り、これらはランタイムのマイナー・ヒープに置かれ高速に回収されます。

OxCaml は小さな数値をレジスタやスタック上で保持するアンボクシング型

int16#
を提供します。今度は
bytes
へ切り替え、32 KB のバッファなら 16‑bit 整数で位置と長さを表せます。

type Httpz.t = #{ off : int16# ; len : int16# }

ここに新機能が二つあります。

  • #{}
    構文でレコードをアンボクシング化できること。
  • フィールド自体も小さい幅(
    int16#
    )になること。

Cstruct の箱詰め版とこの OxCaml 版の違いを詳しく見てみましょう。

utop でアンボクシングを確認

通常は

Obj
モジュールで対話的に調べますが、OxCaml は特殊レイアウトを使うため少し難しいです。

# type t = #{ off : int16# ; len : int16# };;
type t = #{ off : int16#; len : int16# }

# let x = #{ off=#1S; len=#2S };;
val x : t = #{off = <abstr>; len = <abstr>}

# Obj.repr x;;
Error: This expression has type t but an expression was expected of type
         ('a : value)
       The layout of t is bits16 & bits16
         because of the definition of t at line 1, characters 0-41.
       But the layout of t must be a sublayout of value.

エラーは期待した型とレイアウトが合わないことを示しています。ここで「int16# ペアのレイアウト」が通常の OCaml フラット値表現とは異なることが分かります。

ラムダ中間言語で確認

小さなテストプログラムを書いてコンパイラのラムダ出力を調べます。依存を避けるため、OxCaml のソースコードから直接内部 API をバインドします。

external add_int16 : int16# -> int16# -> int16# = "%int16#_add"
external int16_to_int : int16# -> int = "%int_of_int16#"

type span = #{ off : int16#; len : int16# }

let[@inline never] add_spans (x : span) (y : span) : span =
  #{ off = add_int16 x.#off y.#off; len = add_int16 x.#len y.#len }

let () =
  let x = Sys.opaque_identity #{ off = #1S; len = #2S } in
  let y = Sys.opaque_identity #{ off = #100S; len = #200S } in
  let z = add_spans x y in
  Printf.printf "off=%d len=%d\n" (int16_to_int z.#off) (int16_to_int z.#len)

ocaml -dlambda src.ml
で型チェック後の中間形を確認すると、アンボクシングが継承されていることがわかります。

ネイティブコードで確認

最適化されたネイティブコードを

ocamlopt -O3 -S
でビルドし、アセンブリを見ると以下のようになります(ARM64)。

_in_entry_point:
  orr   x0, xzr, #1      ; x.#off = 1
  orr   x1, xzr, #2      ; x.#len = 2
  movz  x2, #100, lsl #0 ; y.#off = 100
  movz  x3, #200, lsl #0 ; y.#len = 200
  bl    _camlX__add_spans_0_1_code

_camlX__add_spans_0_1_code:
  add   x1, x1, x3       ; len: x.#len + y.#len
  sbfm  x1, x1, #0, #15  ; sign-extend to 16 bits (int16# semantics)
  add   x0, x0, x2       ; off: x.#off + y.#off
  sbfm  x0, x0, #0, #15  ; sign-extend to 16 bits
  ret

アセンブリからはボクシングもヒープ割り当てもないことが確認できます。

sbfm
命令で 16‑bit の符号拡張を維持しています。

対照として、通常の OCaml バージョンをコンパイルするとマイナー・ヒープへの割り当てが多数発生します(以下は部分的な出力)。

_camlY__add_spans_0_1_code:
      sub   sp, sp, #16
      str   x30, [sp, #8]
      mov   x2, x0
      ldr   x16, [x28, #0]        ; load young_limit
      sub   x27, x27, #24         ; bump allocator: reserve 24 bytes (3 words)
      cmp   x27, x16              ; check if GC needed
      b.cc  L114                  ; branch to GC if out of space
...
      ret

OCaml のマイナー・ヒープは確かに高速ですが、レジスタ間で直接演算を行うアンボクシング版には到底及びません。


標準モジュールでの整数操作

OxCaml は

int16#
などの特殊型用に通常のモジュールを公開しています。これらを使えば外部関数呼び出しなしに同等の演算が可能です。

module I16 = Stdlib_stable.Int16_u

let[@inline always] i16 x = I16.of_int x
let[@inline always] to_int x = I16.to_int x

let pos : int16# = i16 0
let next : int16# = I16.add pos #1S

アンボクシング文字

整数以外にも、OxCaml はアンボクシング文字操作を提供します。OCaml の

int
を使うよりも高速にパックされた 8‑bit 操作が可能です(ただし未完全)。HTTP 日付タイムスタンプはアンボクシング浮動小数点でも実装できます。


アンボクシングレコードとタプルの返却

一度アンボクシングレコードを宣言すれば、他のアンボクシングレコード内にネストしても問題ありません。HTTP リクエストは以下のようになります。

type request =
  #{ meth : method_
   ; target : span           (* Nested unboxed record *)
   ; version : version
   ; body_off : int16#
   ; content_length : int64#
   ; is_chunked : bool
   ; keep_alive : bool
   ; expect_continue: bool
   }

関数はアンボクシングタプルを返すことで、複数値の返却時に割り当てを発生させずに済みます。

let take_while predicate buf ~(pos : int16#) ~(len : int16#)
    : #(span * int16#) =
  let start = pos in
  let mutable p = pos in
  while (* ... *) do p <- I16.add p #1S done;
  #(#{ off = start; len = I16.sub p start }, p)

let #(result_span, new_pos) = take_while is_token buf ~pos ~len

ローカル割り当てとエグザイル

関数の引数に

local_
を付ければ、呼び出し側からは「逃げない」ことを保証できます。これでスタック上に割り当てられます。

let[@inline] equal (local_ buf) (sp : span) (s : string) : bool =
  let sp_len = I16.to_int sp.#len in
  if sp_len <> String.length s then false
  else Bigstring.memcmp_string buf ~pos:(I16.to_int sp.#off) s = 0

関数がローカル値を返す必要がある場合は

exclave_
キーワードを使います。例として、ヘッダーリストの再帰検索です。

val find : t list @ local -> Name.t -> t option @ local

let rec find_string (buf : bytes) (headers : t list @ local) name = exclave_
  match headers with
  | [] -> None
  | hdr :: rest ->
    let matches =
      match hdr.name with
      | Name.Other -> Span.equal_caseless buf hdr.name_span name
      | known ->
        let canonical = Name.lowercase known in
        String.( = ) (String.lowercase name) canonical
    in
    if matches then Some hdr else find_string buf rest name

exclave_
は再帰関数に対しても有効で、ヒープ割り当てを最小限に抑えられます。


let mutable
でローカル変数

OxCaml の

let mutable
を使えば、スタック上の可変変数を作成できます。これにより
ref
値をヒープに割り当てる必要がなくなります。

let parse_int64 (local_ buf) (sp : span) : int64# =
  let mutable acc : int64# = #0L in
  let mutable i = 0 in
  let mutable valid = true in
  while valid && i < I16.to_int sp.#len do
    let c = Bytes.get buf (I16.to_int sp.#off + i) in
    match c with
    | '0' .. '9' ->
      acc <- I64.add (I64.mul acc #10L) (I64.of_int (Char.code c - 48));
      i <- i + 1
    | _ -> valid <- false
  done;
  acc

対照的に、通常の OCaml では

ref
を使ってヒープ割り当てが発生します。


パーサ全体を構築

トップレベル関数

Httpz.parse
のシグネチャは次のようになります。

val parse : bytes -> len:int16# -> limits:limits ->
  #(Buf_read.status * Req.t * Header.t list) @ local

入力バッファとリソース制限を受け取り、接続ステータス、アンボクシングリクエスト、スタックローカルのヘッダーリストを返します。入力バッファも

local
にすべきです。


注意点と制約

OxCaml にはまだ多くの新機能があり、レイアウト設計に注意が必要です。例えば

or_null
を使って非割り当てオプションを実現したい場合、型推論エラーが長くなることがあります。今はローカル型で代用しています。

また、アンボクシングレコードの可変フィールドについては「箱詰めレコードとは異なる挙動になる」とドキュメントに記載されています。現在は OxCaml 拡張を削除して通常 OCaml へ戻すことが難しいため、今後は OxCaml 専用で開発する方針です。

ツールチェーンもまだ進化中で、

odoc
の統合や
ocamlformat
の新機能(
--erase-jane-syntax
)のサポートが遅れています。今後は改善を期待します。


Claude で作成した OxCaml スキル

小規模な例を通じてアーキテクチャを検証する際、Claude を使ってパーサ全体を書き上げました。その結果をもとに、OxCaml 固有のスキルセットを Claude OCaml マーケットプレイスで公開しています。これらはプロジェクトに簡単に追加でき、機能を学ぶのに便利です。


パフォーマンス結果

実際に Core_bench で測定した結果、httpz は合成ベンチマーク(バッファ転送のみ)で驚異的な性能を示しました。主にヒープ割り当てがほぼゼロになったことで予測可能性と末尾レイテンシが大幅に向上しています。

メトリクスhttpz (OxCaml)従来のパーサ
小さなリクエスト(35 B)154 ns300+ ns
中くらいのリクエスト(439 B)1,150 ns2,000+ ns
ヒープ割り当て0100–800 ワード
スループット6.5 M req/s3 M req/s

新サイトの公開

Eio と組み合わせたフルウェブサーバーを構築し、実際にトラフィックを処理しています。現在もこのページはそのサーバー経由で表示されています。

今後の予定:
caml_alloc_local
で C バインディング

Eio/OxCaml は現状 Bigarray を使用してデータコピーしますが、Thomas Leonard と Patrick Ferris と話し合い、io‑uring のレイヤーから直接

bytes
に切り替える方向へ進めることにしました。Sadiq Jaffer からは、4 KB を超えるバイト列は
mmap
で割り当てられ、ゼロコピーが可能だと教えてもらいました。

重要なのは、OxCaml が FFI で「呼び出し側のスタックに直接 OCaml 値を割り当てる」機能です。これにより、io_uring のリクエストを直接 OCaml コールバックへルーティングでき、ゼロコピーでカーネルへ渡せます。Docker の VPNKit も高速化できそうです。


オープンソースでの開発支援

OxCaml リポジトリは Jane Street の外で実際に稼働中のコードをハックするために新しいモノレポへ移動しました。次週以降、さらに詳細なブログを書きますのでご期待ください。


まとめ

この記事では OxCaml 拡張が実務でどれほど役立つかを示す例として

httpz
パーサとウェブサーバーの開発経緯・実装・パフォーマンスを紹介しました。今後もさらに性能向上や TLS のネイティブ化、他のライブラリへの統合を予定しています。ぜひご活用ください!

同じ日のほかのニュース

一覧に戻る →

2026/02/03 3:02

Codex アプリ

## Japanese Translation: OpenAIは、macOS向けに新しいCodexアプリをリリースしました。このアプリは、開発者が複数のAIエージェントを同時にプロジェクト間で実行できるようにし、Plus、Pro、Business、Enterprise、およびEduプランではレート制限が2倍になります。コマンドセンターインターフェースは、各プロジェクトごとに別々のスレッドでエージェントを実行し、共有リポジトリのビルトインワークツリー編集をサポートし、「スキル」バンドル―Figma翻訳、Linear課題トリアージ、クラウドデプロイメント、画像生成、および文書編集などのタスクに対する数百の内部ツール―を提供します。デモでは、GPT‑Imageとウェブ開発スキルで作成されたレースゲームが1つのプロンプトから700万以上のトークンを消費しました。Codexはまた、スケジュールに従ってバックグラウンドジョブを実行するオートメーション、`/personality`によるパーソナリティ切替、およびセキュリティ用のネイティブサンドボックス機能も備えています。アプリは本日macOSで起動し、すべてのChatGPT購読者に利用可能です。無料/Goユーザーには一時的なアクセスが提供され、追加クレジットを購入することもできます。将来のアップデートでは、Windowsサポート、より高速な推論、拡張されたモデル機能、強化されたマルチエージェントワークフロー、クラウドトリガー型オートメーション、およびコーディング以外の幅広いアプリケーションが追加される予定です。

2026/02/03 5:48

Anki の所有権は AnkiHub に移転されました。

## Japanese Translation: AnkiHub は Anki の新たなリーダーシップ団体として登場し、プラットフォームをコミュニティ所有でオープンソースかつ利益中立に保ちつつ、そのデザイン・エコシステム・ガバナンスを強化することを誓約しています。チームは Damien に協力要請を行い、より大きな役割を受け入れました;ガバナンスの選択肢はまだ決定中ですが、コミュニティからの入力とともに公開される予定です。David Allison はフルタイムで参加し、技術的およびガバナンス上の質問に対応します。 Anki のユーザー主体性と非操作的設計の歴史がこの新方向を支えています。単一開発者への依存から離れ、より広範なボランティア参加へ移行しつつ、ビジネスモデルは公平かつ変更なしに保たれます。近い将来、AnkiHub は透明性と最小限の官僚主義を両立させる正式なガバナンス構造を構築し、UI/UX の再設計を開始し、アドオンエコシステムを拡大し、モバイルアプリを維持し、API をより明確にし、文書化を改善し、開発者向けのリリースサイクルを予測可能にし、非医療ユーザーもサポートします。 利用者にとっては、時間が増え、アクセシビリティが向上し、外部投資家から自由な安定したプラットフォームとなります。アドオン作成者は破壊的変更の減少と強力なサポートを享受でき、広範なコミュニティは一貫した行動、オープンコミュニケーション、約束の遵守により信頼を得ます。ボランティア寄与者は開発を継続する上で不可欠であり、この移行は単一人物以上のエンジニアリング・デザイン・サポートへの帯域幅を徐々に増やしていきます。

2026/02/03 6:28

GitHub での経験―部分的な停止や機能低下の事例

## Japanese Translation: (以下はご提示いただいたテキストの日本語訳です) ``` ## Summary 2026年1月26日(UTC 14:03〜23:42)の間に、GitHub Actions は Windows 標準ホストランナーで失敗を経験しました。これは、新しいランナー構成で D ドライブが欠落していたことによるものでした。すべての Windows ジョブの約2.5 % が影響を受け、22:02 UTC にロールバック完了後でも、パブリックリポジトリにおける 4 コア Windows ランナーで11 % の失敗率が残っていました。 GitHub は問題のある変更を迅速にロールバックし、その構成を持つプロビジョンドランナーを削除、23:45 UTC に残りの影響を受けた容量をオフラインにしました。20:10〜23:51 UTC の間に複数回アップデートが行われ、ユーザーに失敗したワークフローの再実行を促しました;ほとんどのリトライは成功し、変更のロールアウトが限定的だったためです。 再発防止のため、GitHub はランナーのテレメトリー拡張とランナー構成変更の検証改善に取り組みつつ、将来のインシデントに対するより迅速な緩和策を評価しています。このインシデントは、パブリックリポジトリで信頼性の高い CI/CD パイプラインを実現するために堅牢なランナー構成管理が重要であることを示しています。 ```

「OCamlで構築する高速・ゼロ割当Webサーバー」 | そっか~ニュース