
2026/01/06 2:19
つまり、とても高速でチャンクしたいということですね。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
Memchunk は Retrieval‑Augmented Generation (RAG) パイプライン向けに設計された高性能なチャンク化ライブラリです。デリミタの数に応じて最適な検索戦略を自動で選択します。1〜3 個のデリミタの場合は SIMD 検索(のmemchr,memrchr,memrchr2を使用)、4 以上の場合は事前に構築された 256 エントリーのルックアップテーブル、そしてチャンク境界直前の最後のデリミタを見つけるために逆方向検索(memrchr3)を行います。この設計により、句点・改行・疑問符など意味的境界で分割しながら、文の断片化を回避し、メモリ確保を最小限に抑えます。memrchr
20 GB の英語 Wikipedia ダンプでベンチマークすると、約 164 GB/s(~120 ms)のスループットを実現し、Rust ライブラリの kiru (4.5 GB/s) や Python ツールの langchain (0.35 GB/s) を大幅に上回ります。Memchunk はオフセットを返すため、呼び出し側はコピーせずにスライスできます。Python では、JavaScript/Node ではmemoryviewを通じてゼロコピービューが提供されます。バインディングは crates.io、PyPI、npm で入手可能です。subarray
元々、Wikipedia スケールのデータに対する以前の Chonkie ライブラリの性能限界から着想を得た Memchunk は、LMM 取得パイプラインで大規模テキストコーパスのスループット最大化のために、デリミタ数に応じて SIMD とルックアップのどちらを選択するかという核心的な革新を持っています。
本文
Chonkie → Memchunk:検索用テキストを分割する最速手段
私たちは RAG パイプライン向けのチャンクライブラリ「chonkie」を開発してきましたが、ある時点で Wikipedia ほどの規模のデータセットでベンチマークを始めました。すると、動作が…遅く感じられました。耐え難いほどではありませんが、十分に遅いため「ここで理論上限はどこまで?」と疑問になりました。抽象化をすべて捨て、メタルレベルでやるとテキスト分割はどれだけ高速化できるでしょうか?
この記事ではその探索過程と、最終的に「memchunk」を作り上げた経緯について書きます。
1. チャンクとは何なのか?
LLM と検索を組み合わせて何かを構築する際は、巨大なテキストを埋め込みモデルやコンテキストウィンドウに収まる小さな単位へ分割しないといけません。
最もシンプルなのは「N 文字ごとに区切る」ことですが、これだと文を途中で切ってしまい検索品質が低下します。
賢い方法:意味的境界(句点・改行・疑問符など)で分割する。
例:
"Hello world. How are you?" → ["Hello world.", " How are you?"]
2. 区切り文字だけで十分なのはなぜ?
-
トークンベースや文字数ベースのチャンクは文末を意識せずに N トークン/N バイトごとに分割します。
例:
と"The quick brown fox jumps over the la"
のように不完全な文が出来上がります。"zy dog." -
区切り文字ベースでは
・.
・?
で切るだけで自然な境界を保てます。NLP パイプラインや埋め込みモデルを使わず、単純にバイト検索で済むため高速化が可能です。\n
3. memchr の登場
Andrew Gallant が開発した memchr クレートは、複数の最適化層を持つバイト検索ライブラリです。
3‑1. フォールバック:SWAR(SIMD Within A Register)
SIMD 命令が無くても memchr は高速です。汎用実装では SWAR を使用し、64 ビット演算で 8 バイトずつ処理します。
fn has_zero_byte(x: usize) -> bool { const LO: usize = 0x0101010101010101; const HI: usize = 0x8080808080808080; (x.wrapping_sub(LO) & !x & HI) != 0 }
has_zero_byte は、ゼロバイトに対して -1 を引くことでキャリーが高ビットへ伝播し、分岐や個別のループを不要にします。
3‑2. 高速経路:AVX2/SSE2
x86_64 で SIMD が利用可能なら、memchr はベクトル命令へ切り替わります。AVX2 では 32 バイトを一度に処理できます。
let needle_vec = _mm256_set1_epi8(needle as i8); let chunk = _mm256_loadu_si256(haystack); let matches = _mm256_cmpeq_epi8(chunk, needle_vec); let mask = _mm256_movemask_epi8(matches); if mask != 0 { // 少なくとも一文字が一致。trailing_zeros() が位置を返す。 }
SSE2(16 バイト)と AVX2(32 バイト)のハイブリッド戦略も採用し、ハヤスタックのサイズに応じて最適なパスを選びます。
3‑3. API
| バリアント | 説明 |
|---|---|
| 単一バイト検索 |
| 2 つのバイトのどちらかを検索 |
| 3 つのバイトのいずれかを検索 |
4 バイト以上はサポートしていません。理由は、追加の比較と OR 演算が増えるためオーバーヘッドが大きくなるからです。
4. 区切り文字が 3 より多い場合
'\n', '.', '?', '!', ';' といった 5 つの区切り文字を扱うと memchr は直接対応できません。そこで 256 エントリのルックアップテーブル を使います。
fn build_table(delimiters: &[u8]) -> [bool; 256] { let mut table = [false; 256]; for &byte in delimiters { table[byte as usize] = true; } table } fn is_delimiter(byte: u8, table: &[bool; 256]) -> bool { table[byte as usize] }
配列アクセスだけで O(1) の処理が可能です。SIMD より速くはありませんが、任意の区切り文字セットを扱え、巨大入力でも数百 GB/s のスループットを維持します。
5. 後ろから検索する理由
フロント側で検索すると、最初に見つかった区切り文字までしか止められません。最適な分割点はさらに近い場所にあるかもしれないため、多くのマッチとインデックス更新が必要になります。
逆に memrchr(memchr の逆)で
chunk_size から後ろへ検索すれば、最初にヒットした区切り文字が分割点となります。操作数を大幅に減らせるため、全体のオーバーヘッドが低くなります。
6. 実装
memchunk は自動で適切な戦略を選択します。
fn find_last_delimiter( window: &[u8], delimiters: &[u8], table: Option<&[bool; 256]>, ) -> Option<usize> { if let Some(t) = table { // 区切り文字が 4 個以上:ルックアップテーブルを使用 window.iter().rposition(|&b| t[b as usize]) } else { match delimiters.len() { 1 => memchr::memrchr(delimiters[0], window), 2 => memchr::memrchr2(delimiters[0], delimiters[1], window), 3 => memchr::memrchr3( delimiters[0], delimiters[1], delimiters[2], window, ), _ => None, } } }
テーブルは最初のイテレーションで遅延構築され、以降は高速に動作します。
7. ベンチマーク
他の Rust チャンクライブラリと比較しました(サイズ・区切り文字数が異なるケースも含む)。
| ライブラリ | スループット |
|---|---|
| memchunk | 164 GB/s |
| kiru | 4.5 GB/s (36×遅い) |
| langchain | 0.35 GB/s (469×遅い) |
| semchunk | 0.013 GB/s (12,615×遅い) |
| llama‑index | 0.0035 GB/s (46,857×遅い) |
| text‑splitter | 0.0017 GB/s (96,471×遅い) |
ファイルサイズと区切り文字数によって多少変動しますが、最悪でも数百 GB/s の範囲に収まります。実際に英語 Wikipedia(約20 GB)を分割するのは 約120 ms で完了しました。
8. Python と WASM バインディング
chonkie は Python ライブラリなので、Python 用バインディングも必要でした。さらにブラウザや Node.js 向けに WASM を追加しています。
from memchunk import chunk text = "Hello world. How are you?" for slice in chunk(text, size=4096, delimiters=".?"): print(bytes(slice))
import { chunk } from "memchunk"; const text = new TextEncoder().encode("Hello world. How are you?"); for (const slice of chunk(text, { size: 4096, delimiters: ".?" })) { console.log(new TextDecoder().decode(slice)); }
両方とも ゼロコピー のビュー(Python では
memoryview、JavaScript では subarray)を返すため、FFI 境界でもほぼ同等のパフォーマンスが得られます。
9. まとめ
ここで紹介したテクニックは新しいものではありません。SIMD 検索やルックアップテーブルは教科書的手法です。しかし、いつどちらを使うかを知ることにこそ勝負があります。
- 区切り文字 1〜3 個 → memchr の SIMD 検索
- 区切り文字 4 個以上 → 256 エントリのテーブル
- 割当はできるだけ最小化し、オフセットを返す
memchunk は crates.io、PyPI、npm に公開されています。ぜひ試してみてください!