回答:
他にも良い説明がありますが、試してみましょう。(これはホワイトボードでの方がはるかに簡単です!)Wikipediaの例といくつかの表記を示します。
20バイトをコピーするとします。最初のパスのプログラムのフロー制御は次のとおりです。
int count; // Set to 20
{
int n = (count + 7) / 8; // n is now 3. (The "while" is going
// to be run three times.)
switch (count % 8) { // The remainder is 4 (20 modulo 8) so
// jump to the case 4
case 0: // [skipped]
do { // [skipped]
*to = *from++; // [skipped]
case 7: *to = *from++; // [skipped]
case 6: *to = *from++; // [skipped]
case 5: *to = *from++; // [skipped]
case 4: *to = *from++; // Start here. Copy 1 byte (total 1)
case 3: *to = *from++; // Copy 1 byte (total 2)
case 2: *to = *from++; // Copy 1 byte (total 3)
case 1: *to = *from++; // Copy 1 byte (total 4)
} while (--n > 0); // N = 3 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it is)
}
次に、2番目のパスを開始し、指定されたコードのみを実行します。
int count; //
{
int n = (count + 7) / 8; //
//
switch (count % 8) { //
//
case 0: //
do { // The while jumps to here.
*to = *from++; // Copy 1 byte (total 5)
case 7: *to = *from++; // Copy 1 byte (total 6)
case 6: *to = *from++; // Copy 1 byte (total 7)
case 5: *to = *from++; // Copy 1 byte (total 8)
case 4: *to = *from++; // Copy 1 byte (total 9)
case 3: *to = *from++; // Copy 1 byte (total 10)
case 2: *to = *from++; // Copy 1 byte (total 11)
case 1: *to = *from++; // Copy 1 byte (total 12)
} while (--n > 0); // N = 2 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it is)
}
次に、3番目のパスを開始します。
int count; //
{
int n = (count + 7) / 8; //
//
switch (count % 8) { //
//
case 0: //
do { // The while jumps to here.
*to = *from++; // Copy 1 byte (total 13)
case 7: *to = *from++; // Copy 1 byte (total 14)
case 6: *to = *from++; // Copy 1 byte (total 15)
case 5: *to = *from++; // Copy 1 byte (total 16)
case 4: *to = *from++; // Copy 1 byte (total 17)
case 3: *to = *from++; // Copy 1 byte (total 18)
case 2: *to = *from++; // Copy 1 byte (total 19)
case 1: *to = *from++; // Copy 1 byte (total 20)
} while (--n > 0); // N = 1 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it's not, so bail)
} // continue here...
20バイトがコピーされます。
注:元のDuffのデバイス(上記を参照)は、to
アドレスのI / Oデバイスにコピーされます。したがって、ポインタをインクリメントする必要はありませんでした*to
。2つのメモリバッファ間でコピーする場合は、を使用する必要があります*to++
。
do
そんなに見ないでください。代わりに、見switch
やwhile
昔ながらの計算されたようGOTO
たstatmentsまたはアセンブラjmp
オフセットを持つ文。switch
その後、いくつかの数学を行い、jmp
適切な場所にね。while
ブールチェックを行い、その後、盲目的jmp
場所について右によdo
でした。
ドブ博士の日記の説明は、このトピックで私が見つけた中で最高です。
これは私のAHAの瞬間です。
for (i = 0; i < len; ++i) {
HAL_IO_PORT = *pSource++;
}
になる:
int n = len / 8;
for (i = 0; i < n; ++i) {
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
}
n = len % 8;
for (i = 0; i < n; ++i) {
HAL_IO_PORT = *pSource++;
}
になる:
int n = (len + 8 - 1) / 8;
switch (len % 8) {
case 0: do { HAL_IO_PORT = *pSource++;
case 7: HAL_IO_PORT = *pSource++;
case 6: HAL_IO_PORT = *pSource++;
case 5: HAL_IO_PORT = *pSource++;
case 4: HAL_IO_PORT = *pSource++;
case 3: HAL_IO_PORT = *pSource++;
case 2: HAL_IO_PORT = *pSource++;
case 1: HAL_IO_PORT = *pSource++;
} while (--n > 0);
}
len%8
、4の場合でも、ケース4、ケース2、ケース2、ケース1を実行し、ジャンプして次のループ以降のすべてのケースを実行します。これは、ループとスイッチステートメントが「相互作用する」方法を説明する必要がある部分です。
len % 8
バイトはコピーされませんか?
Duffのデバイスには2つの重要な点があります。まず、理解しやすい部分だと思いますが、ループは展開されています。これにより、ループが終了したかどうかを確認し、ループの先頭に戻ることに伴うオーバーヘッドの一部を回避することで、コードサイズが大きくなり、速度が向上します。ジャンプする代わりに直線的なコードを実行している場合、CPUはより高速に実行できます。
2番目の側面は、switchステートメントです。これにより、コードは最初からループの途中にジャンプできます。ほとんどの人にとって驚くべきことは、そのようなことが許されるということです。まあ、それは許可されています。実行は計算されたcaseラベルから始まり、他のswitchステートメントと同様に、後続の各代入ステートメントに進みます。最後のケースラベルの後、実行はループの最下部に達し、その時点で最上位に戻ります。ループの先頭はswitchステートメント内にあるため、スイッチは再評価されません。
元のループは8回ほど解かれるので、反復回数は8で除算されます。コピーするバイト数が8の倍数でない場合は、いくつかのバイトが残っています。一度にバイトのブロックをコピーするほとんどのアルゴリズムは、最後に残りのバイトを処理しますが、Duffのデバイスは最初にそれらを処理します。この関数count % 8
は、switchステートメントを計算して残りを計算し、その数のバイトのケースラベルにジャンプして、それらをコピーします。その後、ループは8バイトのグループをコピーし続けます。
duffsデバイスのポイントは、タイトなmemcpy実装で行われる比較の数を減らすことです。
「count」バイトをaからbにコピーするとします。簡単な方法は、次のとおりです。
do {
*a = *b++;
} while (--count > 0);
カウントが0より大きいかどうかを比較するために何回比較する必要がありますか?'count'回。
現在、ダフデバイスは、スイッチケースの意図しない厄介な副作用を使用しています。これにより、カウントに必要な比較の数を減らすことができます/ 8。
ここで、duffsデバイスを使用して20バイトをコピーするとします。いくつの比較が必要でしょうか。最後の 1つを除いて一度に8バイトをコピーするため、3最初の。
更新:8回の比較/切り替え時のステートメントを実行する必要はありませんが、関数のサイズと速度のトレードオフは妥当です。
初めて読んだとき、これをオートフォーマットしました
void dsend(char* to, char* from, count) {
int n = (count + 7) / 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);
}
}
そして、私は何が起こっているのか分かりませんでした。
多分この質問がされた時ではありませんが、今ウィキペディアは非常に良い説明をしています
Cの2つの属性により、デバイスは有効で正当なCです。
- 言語の定義におけるswitchステートメントの緩和された仕様。デバイスの発明の時点で、これはCプログラミング言語の最初のエディションであり、スイッチの制御されたステートメントが構文的に有効な(複合)ステートメントである必要があり、その場合、ラベルはサブステートメントの前に付けることができます。breakステートメントがない場合、制御のフローは1つのケースラベルによって制御されるステートメントから次のケースラベルによって制御されるステートメントにフォールスルーするという事実に関連して、これは、コードがからのカウントコピーの連続を指定することを意味しますメモリマップされた出力ポートへの順次ソースアドレス。
- Cのループの真ん中に合法的にジャンプする機能。
1:Duffsデバイスは、ループ展開の特別な実装です。ループ展開とは何ですか?
ループでN回実行する操作がある場合は、ループをN / n回実行してからループでループコードをn回インライン化(展開)することで、プログラムサイズと速度をトレードオフできます。たとえば、次のように置き換えます。
for (int i=0; i<N; i++) {
// [The loop code...]
}
と
for (int i=0; i<N/n; i++) {
// [The loop code...]
// [The loop code...]
// [The loop code...]
...
// [The loop code...] // n times!
}
N%n == 0-ダフの必要がない場合、これはうまく機能します! それが真実でない場合は、残りを処理する必要があります-これは苦痛です。
2:Duffsデバイスは、この標準ループ展開とどのように異なりますか?
Duffsデバイスは、N%n!= 0の場合、残りのループサイクルを処理する巧妙な方法にすぎません。全体のdo / whileは、標準のループのアンロールに従ってN / n回実行されます(ケース0が適用されるため)。ループの最後の実行(「N / n + 1」回)でケースが開始し、N%nのケースにジャンプして、ループコードを「残り」の回数実行します。
あなたが何を求めているのか100%わかりませんが、これは...
Duffのデバイスが対処する問題は、ループの巻き戻しの1つです(投稿したWikiリンクで見たことがあることは間違いありません)。これは基本的に、メモリフットプリントに対する実行時の効率の最適化に相当します。Duffのデバイスは、古い問題だけでなくシリアルコピーを処理しますが、ループで比較を行う必要がある回数を減らすことによって最適化を行う方法の典型的な例です。
別の例として、理解しやすくするために、ループする項目の配列があり、毎回1を追加するとします。通常、forループを使用して、約100回ループします。 。これはかなり論理的に見えますが、それは...しかし、ループを巻き戻すことによって最適化を行うことができます(明らかにそれほど遠くない...またはループを使用しないこともできます)。
したがって、通常のforループ:
for(int i = 0; i < 100; i++)
{
myArray[i] += 1;
}
なる
for(int i = 0; i < 100; i+10)
{
myArray[i] += 1;
myArray[i+1] += 1;
myArray[i+2] += 1;
myArray[i+3] += 1;
myArray[i+4] += 1;
myArray[i+5] += 1;
myArray[i+6] += 1;
myArray[i+7] += 1;
myArray[i+8] += 1;
myArray[i+9] += 1;
}
Duffのデバイスが行うことは、Cでこのアイデアを実装することですが、(Wikiで見たように)シリアルコピーを使用します。上に表示されているのは、巻き戻しの例では、元の100と比較した10の比較です。これは、マイナーですが、おそらく重要な最適化です。
これが私がダフのデバイスの核心であると感じるものである詳細ではない説明です:
ことは、Cは基本的にアセンブリ言語の優れたファサードです(PDP-7アセンブリは具体的です。これを研究すると、類似点がいかに印象的かがわかります)。そして、アセンブリ言語では、実際にはループはありません-ラベルと条件付き分岐命令があります。したがって、ループは、ラベルと分岐がどこかにある命令シーケンス全体の一部にすぎません。
instruction
label1: instruction
instruction
instruction
instruction
jump to label1 some condition
そして、switch命令はいくらか先に分岐/ジャンプしています:
evaluate expression into register r
compare r with first case value
branch to first case label if equal
compare r with second case value
branch to second case label if equal
etc....
first_case_label:
instruction
instruction
second_case_label:
instruction
instruction
etc...
アセンブリでは、これらの2つの制御構造を組み合わせる方法は簡単に想像できます。そのように考えると、Cでのそれらの組み合わせは、それほど奇妙に思えなくなります。
これは、質問が重複としてクローズされる前にいくつかの賛成を得たダフのデバイスに関する別の質問に投稿した回答です。ここでは、なぜこの構造を避けるべきかについて、少し価値のあるコンテキストを提供していると思います。
「これはダフのデバイスです。です。、ループの反復数がアンロール係数の正確な倍数であることがわからない場合に対処するために二次修正ループを追加する必要を回避する、ループをアンロールする方法です。
ここでのほとんどの回答は一般的に肯定的であるように思われるため、マイナス面を強調します。
このコードでは、コンパイラーはループ本体に最適化を適用するのに苦労します。単純なループとしてコードを記述した場合、最新のコンパイラーが展開を処理できるはずです。このようにして、可読性とパフォーマンスを維持し、ループ本体に他の最適化が適用されることを期待しています。
他の人が参照しているウィキペディアの記事では、この「パターン」がXfree86ソースコードから削除されたときにパフォーマンスが実際に向上したとさえ述べられています。
この結果は、たまたまそれを必要とする可能性があると思われるコードを盲目的に最適化する典型的なものです。これにより、コンパイラが適切に機能しなくなり、コードが読みにくくなり、バグが発生しやすくなり、通常は速度が低下します。最初に正しい方法で物事を行っていた場合、つまり単純なコードを記述し、ボトルネックをプロファイリングしてから最適化した場合、このようなものを使用することすら考えていません。とにかく、最近のCPUとコンパイラではできません。
理解しても大丈夫ですが、実際に使ってみて驚かれることでしょう。」
実験してみると、スイッチとループをインターリーブせずにうまくいく別のバリアントが見つかりました:
int n = (count + 1) / 8;
switch (count % 8)
{
LOOP:
case 0:
if(n-- == 0)
break;
putchar('.');
case 7:
putchar('.');
case 6:
putchar('.');
case 5:
putchar('.');
case 4:
putchar('.');
case 3:
putchar('.');
case 2:
putchar('.');
case 1:
putchar('.');
default:
goto LOOP;
}