Duff's Device(ループ展開の妙技)
switch文とループを交差させる衝撃のCイディオムを解剖し、なぜ動くか・ループ展開の本質・可読性と性能の綱引きまで一気に理解でき、言語仕様の隙間を突く発想力が身につく。
- Duff's Deviceはswitch文のcaseラベルをdo-whileループの途中に置き、フォールスルーでコピー処理を8回展開しつつ端数もひとつのループで捌くCの技法。剰余を先頭ではなく途中に飛び込んで処理する点が肝。
- 1983年、LucasfilmのTom DuffがVAX上のリアルタイムアニメで、メモリマップドレジスタへの連続書き込み(memcpyではない)を高速化するため考案。ループのオーバーヘッド(分岐と回数判定)を減らすのが目的だった。
- 現代ではコンパイラが自動でループ展開やベクトル化を行い、通常のmemcpyの方が速いことも多い。教育的価値と『言語仕様の隙間』の象徴として残る。C言語の緩さ、switchとループの直交性がこの技を許した。
何のコードか(背景と何が有名か)
Duff's Device(ダフのデバイス)は、C言語で書かれた驚愕のループ展開イディオムです。何が驚愕かというと、switch 文の case ラベルを、do-while ループの本体の途中に割り込ませて書いている点にあります。制御構造どうしが構文上「重なって」いて、初見では文法エラーにしか見えません。しかしこれは完全に正しいCコードで、ループのオーバーヘッド(各反復での回数判定と分岐)を減らす目的で機能します。
有名になった理由は3つあります。第一に、見た目のインパクト。2つの独立した制御構造を絡み合わせるという発想が、多くのプログラマの常識を破壊しました。第二に、それが実用目的で書かれた本物の最適化だったこと。単なるパズルではありません。第三に、C言語という言語の「緩さ」——switch の飛び先とループ本体が構文的に直交しているという仕様の隙間——を象徴する事例として、長く語り継がれてきたことです。
1回の反復で1要素だけ処理する代わりに、複数要素をまとめて処理し、反復回数そのものを減らす最適化です。反復ごとに走る「カウンタの更新」「終了条件の比較」「ループ先頭への分岐」というオーバーヘッドが、展開した分だけ薄まります。課題は、要素数が展開幅で割り切れないときの端数(剰余)処理。Duff's Device はこの端数を、別ループを足さず1つのループの中で吸収する点が巧妙です。
コードと仕組みの解説
横にスクロール
Tom Duff 本人が書いた原型は、配列ではなく1つの固定アドレス(メモリマップドI/Oレジスタ)へ連続で値を書き込むものでした。まずその形を示します。
send(to, from, count)
register short *to, *from;
register count;
{
register n = (count + 7) / 8; /* 8で割った切り上げ回数 */
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
注目すべきは *to = *from++; で、to は増えません。これは to がハードウェアレジスタのアドレスで、そこへ count 個の値を次々に送り込む用途だからです。通常のバッファ間コピーなら *to++ = *from++; となり、これも同じ骨格で機能します。
動作の鍵はCの2つの仕様です。1つは switch が「ラベルへのジャンプ」でしかなく、飛び込んだ後は break に当たるまで下のcaseへ流れ落ちるフォールスルー。もう1つは、C言語では switch の飛び先ラベルを、その内側にあるループなど別の制御構造の途中に置いてよいという直交性です。
具体的に count が 11 の場合を追います。n = (11 + 7) / 8 = 2、count % 8 = 3 なので、実行はまず case 3: へジャンプします。そこから case 3, 2, 1 の3文が実行され(端数3個を処理)、while (--n > 0) に到達。n は 2 から 1 になり真なので、ループ先頭 do の直後へ戻り、今度は case 0 相当の位置から8文すべてを実行します(残り8個)。再び while で n が 0 になり終了。合計 3 + 8 = 11 個、ちょうど処理し切りました。
| count | 初回ジャンプ先 | 初回で処理する個数 | ループ反復回数(n) | 合計 |
|---|---|---|---|---|
| 8 | case 0 | 8 | 1 | 8 |
| 11 | case 3 | 3 | 2 | 11 |
| 16 | case 0 | 8 | 2 | 16 |
| 1 | case 1 | 1 | 1 | 1 |
一般化すると、剰余 count % 8 が最初の(短い)反復で処理される端数になり、以降は毎回きっちり8個ずつ。剰余処理のためだけの独立したループを書かずに済むのが、この技法の眼目です。素朴に書けば「8個ずつのループ」と「端数用のループ」の2本が必要なところを、フォールスルーで1本にまとめています。
このコードは count が 1 以上であることを暗黙に前提としています。count が 0 だと n = (0 + 7) / 8 = 0、count % 8 = 0 で case 0 に入って1個書き込んでしまい、その後 while (--n > 0) は n が -1 で偽となり抜けますが、既に1個余計に処理済みです。ゼロ件を正しく扱うには呼び出し側でのガードが要ります。有名コードだからといって無条件に安全なわけではありません。
逸話と経緯
考案者は Tom Duff。1983年11月、彼が Lucasfilm(ジョージ・ルーカスのスタジオ内でのちに Pixar につながるコンピュータ部門)に在籍していたときの産物です。当時 DEC の VAX 上でリアルタイムアニメーションのコードを書いており、ある実時間処理のループを速くする必要がありました。処理内容は前述のとおり、データをひとつの出力レジスタへ次々に送り出すもので、ループ制御のオーバーヘッドが無視できなかったのです。
Duff はこの発見を Usenet に投稿し、そこから広まりました。名称の「device」は「装置」ではなく「うまい手・工夫」の意で、Duff 自身が皮肉と誇りを込めて名付けたものです。彼は投稿で、この構造を認めるC言語について「これがCへの賛辞なのか非難なのか判断がつかないが、とにかく認めざるを得ない」という趣旨の、両義的なコメントを残しています。技巧の凄みと、それを許してしまう言語の緩さへの当惑が同居した名言として知られます。
なお、この技法にまつわる俗説には注意が必要です。「memcpy を高速化する典型例」としてよく紹介されますが、Duff の原型はメモリ間コピーではなく、固定アドレスへの連続書き込みでした。バッファ間コピーへの一般化は後年の応用であり、Duff 自身の動機とは区別すべきです。また「C標準はこんな書き方を想定していなかった」と言われがちですが、switch の飛び先が内側のブロックのラベルでよいことは仕様上明確に許されており、未定義動作ではありません。あくまで仕様の範囲内の離れ業です。
Duff's Device は基本的にC++でも合法ですが、ジャンプがコンストラクタを持つオブジェクトの初期化を飛び越える場合はコンパイルエラーになります。C++ は switch のジャンプが非トリビアルな初期化をスキップすることを禁じているためです。純粋な算術やポインタ操作だけなら問題ありません。
遺産と教訓
Duff's Device が現役の高速化テクニックだった時代は、もう過ぎています。理由は明快で、現代のコンパイラは最適化としてループ展開を自動で行い、さらに SIMD 命令によるベクトル化まで施すからです。素直に while ループや標準の memcpy を書けば、コンパイラや標準ライブラリが CPU・データ量に応じた最適なコードを生成します。手で展開した Duff's Device は、むしろ分岐予測やキャッシュ、命令パイプラインとの相性で通常の memcpy に負けることさえあるのが実情です。事実、ある有名OSS(XFree86)ではこの技法を撤去したところ性能が向上した、という報告も残っています。手動の巧妙さが、ハードウェアとコンパイラの進歩に追い抜かれた典型例です。
では今なぜ学ぶ価値があるのか。第一に、ループ展開という最適化の原理を、これほど鮮烈に体得できる教材は他にありません。剰余をどう捌くか、オーバーヘッドがどこに潜むかが、コード1つで腑に落ちます。第二に、これは「言語仕様の隙間」を突く発想の記念碑です。switch は本質的にラベルへのジャンプでしかなく、ループ本体とは直交する——この制御構造の分解された理解があって初めて、2つを重ねる発想が生まれます。仕様を額面どおりでなく構造として捉える力は、あらゆる言語で通用します。
- caseラベルをdo-whileループの途中に置き、フォールスルーで8回分を展開する。剰余 count % 8 の位置へ初回ジャンプして端数を吸収し、以降は8個ずつ処理する
- 動作の根拠はCの2仕様——switchのフォールスルーと、飛び先ラベルが別の制御構造の途中に置けるという直交性。未定義動作ではなく仕様の範囲内
- Tom Duffが1983年Lucasfilmで、固定レジスタへの連続書き込み(memcpyではない)を高速化するため考案。バッファコピーへの一般化は後年の応用
- 現代はコンパイラの自動ループ展開・ベクトル化により不要。教育的価値と『言語仕様の隙間』の象徴として残る
最大の教訓は、性能の常識には賞味期限があるということです。1983年に理にかなっていた最適化が、コンパイラとCPUの進化で不要になりました。だからこそ、手で凝ったトリックに走る前にまず素直に書き、プロファイルで測り、必要なときだけ武器を抜く——この順序が普遍の原則です。Duff's Device は、C言語の緩さと switch の本質を映す鏡として、また「言語仕様の隙間には妙技が眠る」という発想の教材として、これからも語り継がれるでしょう。
C言語や制御構造の原理をより深く知るにはプログラミングを、メモリマップドI/Oやハードウェアに近い最適化の文脈はOSを参照してください。
コード遺産の記事ガイド
Duff's Device(ループ展開の妙技)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
C言語
比較で見る軸
難易度: advanced / カテゴリ: コード遺産 / タグ数: 5
導入後に効く点
1983年、LucasfilmのTom DuffがVAX上のリアルタイムアニメで、メモリマップドレジスタへの連続書き込み(memcpyではない)を高速化するため考案。ループのオーバーヘッド(分岐と回数判定)を減らすのが目的だった。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- コード遺産
- タグ数
- 5
判断チェックリスト
- 自社の用途が「C言語 / 最適化」に近いか確認する。
- 強みである「Duff's Deviceはswitch文のcaseラベルをdo-whileループの途中に置き、フォールスルーでコピー処理を8回展開しつつ端数もひとつのループで捌くCの技法。剰余を先頭ではなく途中に飛び込んで処理する点が肝。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。