Lwan ハッシュテーブルを刷新
Lwan ハッシュテーブル刷Lwan のハッシュテーブルが刷新され、Rust の hashbrown や Abseil の SwissTable に触発された新しい実装となった。
SIMD 指令を使わない C 標準関数 memchr() を活用し、単一のグループで効率的な探索を実現。
移植性と簡潔性を重視し、パフォーマンス測定は後回し。
ユニットテストも導入され、安定性の向上も図られた。
ネットワークプロトコル「Lwan」の開発において、ハッシュテーブルの刷新が行われたとのことです。Lwanは、ネットワークトラフィックの分析や制御などに用いられるソフトウェアで、これまで利用していたハッシュテーブルの実装に課題があり、パフォーマンス改善のため、新たな方式を採用したそうです。本記事では、Lwanにおけるハッシュテーブルの刷新について、その背景や採用された技術、そしてその構造について解説します。
Lwanにおけるハッシュテーブルの課題
Lwanは、これまでkmodプロジェクト由来のハッシュテーブルをベースに、独自に改良を重ねて使用していたとのことです。しかし、長年の改修によって複雑性が増し、パフォーマンス上の問題が発生していました。そのため、より効率的なハッシュテーブルの導入が検討されていたようです。特に、パフォーマンスの改善と、コードの保守性の向上が課題として挙げられています。
RustのhashbrownとAbseilのSwissTableからの影響
新たなハッシュテーブルの設計において、Rustの標準ライブラリに含まれる`hashbrown`や、Googleが開発したAbseilの`SwissTable`が参考になったとのことです。これらの実装は、線形探索(リニアプロービング)を利用しつつ、キャッシュ効率やハードウェアの機能を最大限に活用することで、高いパフォーマンスを実現しています。特に、`SwissTable`はSIMD(Single Instruction, Multiple Data)命令を活用することで、並列処理を効率的に行うことができる点が注目されました。
Lwanの新ハッシュテーブルの構造
Lwanの新ハッシュテーブルでは、SIMD命令の利用を避けることで、移植性を高めることを優先したとのことです。代わりに、標準Cライブラリの`memchr()`関数を使用することで、同様の効果を実現しています。`memchr()`は通常、SIMD命令で実装されていることが多いそうです。構造としては、複数のグループに分割するのではなく、単一のグループで構成され、ハッシュ値を2つの部分(`tophash`と`startpos`)に分割して処理することで、効率的な探索を実現しています。
まとめ
Lwanにおけるハッシュテーブルの刷新は、パフォーマンス向上と移植性の両立を目指した結果と言えるでしょう。今回の変更によって、Lwanの安定性と効率性が向上することが期待されます。今後のLwanの進化に注目が集まります。
原文の冒頭を表示(英語・3段落のみ)
A new hash table for Lwan2026-05-06For a long time, Lwan used to use a heavily modified version of the hash table from the kmod project. I was lightly involved with that project during its inception, so this seemed like a natural choice. Over the years, that hash table proved inneficient for the use cases in Lwan, so I began patching it to try and improve its performance; I succeeded in some cases, but the added complexity and years of technical debt eventually caught up with me, so things started failing.I've wanted to try a different way of implementing a hash table, possibly using some kind of linear probing method, for a good while, so that's what I did.InspirationI've been inspired by Rust's hashbrown, which has been the default hash table in its standard library for a good while. That hash table, in turn, was inspired by Abseil's SwissTable. Even the Go programming language began using a variation of this hash table since 2025.I wanted to make Lwan's new hash table as portable as possible; however, Swiss Tables require the use of SIMD instructions to perform acceptably. As you can see in the following diagram, a swiss table is laid out like so:It can have multiple groups, chained together;Each group contains a Control Word and the slots containing the actual items;The nth byte in the Control Word represents a Control Byte for the nth slot.The result of the hash function is split in two: H1, which chooses the first group where an item – either empty, unused, or deleted; and H2, which is stored in the Control Word. SIMD is used to determine which slots in a group matches the required H2 (or an empty or a deleted slot), in parallel, with minimal instructions. Even though it performs a linear probe, it does so in a very efficient manner that's not only cache efficient, it's also using hardware capabilities that are rarely exploited in data structures.NoteThe H2 hash has only 7 bits, with one bit left to signal if an item is empty, in use, or deleted. The distinction between deleted and empty is used for a few different things, including reducing fragmentation (or requiring constant rehashes), or straying too far from the original H1 % number_of_groups-th group.The hash table in LwanUsing SIMD directly was ruled out due to it not being portable enough; even though SWAR could be used where SIMD was not available (or in architectures I wasn't familiar with or had the hardware to test the implementation). In its place, the standard C function memchr() was used instead. This function is usually implemented using SIMD instructions anyway (although with higher fixed costs that wouldn't be an issue if SIMD was used directly, as it'll become clear with code snippets down below).As a result, instead of having multiple groups like Swiss Tables, the table in Lwan has only one group:With this structure, the hash H is split into two:uint8_t tophash = no_tombstone(H & 0xff), with no_tombstone() returning a value that's not used for a tombstone (\0);uint32_t startpos = (H >> 8) & (capacity - 1), with capacity being always a power of two.With both tophash and startpos in hand, the table is effectively split in two:Making it possible to define the internal hash table probing functions like this:static bool hash_probe_half(const struct hash *ht,
const void *key,
uint32_t *out_slot,
※ 著作権に配慮し、引用は冒頭3段落までです。続きは元記事をご覧ください。