ダフのデバイスはどのように機能しますか?


回答:


240

他にも良い説明がありますが、試してみましょう。(これはホワイトボードでの方がはるかに簡単です!)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++


1
どのようにしてcase 0:句をスキップし、スキップされた句の引数であるdo whileループ内にある他の句を引き続きチェックできますか?do whileループの外にある唯一の句がスキップされる場合、なぜスイッチがそこで終了しないのですか?
Aurelius

14
中括弧をそれほど強く見ないでください。doそんなに見ないでください。代わりに、見switchwhile昔ながらの計算されたようGOTOたstatmentsまたはアセンブラjmpオフセットを持つ文。switchその後、いくつかの数学を行い、jmp適切な場所にね。whileブールチェックを行い、その後、盲目的jmp場所について右によdoでした。
クリントンピアス

これが良ければ、なぜ誰もがこれを使用しないのですか?欠点はありますか?
AlphaGoku

@AlphaGokuの読みやすさ。
LF

108

ドブ博士の日記説明は、このトピックで私が見つけた中で最高です。

これは私の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);
}

良い投稿(プラス私はあなたから1つの良い答えを見つけて賛成投票する必要があります;)2ダウン、13に移動:stackoverflow.com/questions/359727#486543)。素敵な回答バッジをお楽しみください。
VonC、2009

13
ここで重要な事実であり、ダフのデバイスを私が最も長い間理解できなくなったのは、Cの癖により、最初にwhileに達した後、ジャンプしてすべてのステートメントを実行するということです。したがってlen%8、4の場合でも、ケース4、ケース2、ケース2、ケース1を実行、ジャンプして次のループ以降のすべてのケースを実行ます。これは、ループとスイッチステートメントが「相互作用する」方法を説明する必要がある部分です。
ShreevatsaR 2012年

2
ドブス博士の記事は良いですが、リンクを除いて答えは何も追加しません。下記のRob Kennedyの回答を参照してください。これは、最初に処理される残りの転送サイズと、その後に続く0バイト以上の8バイトの転送ブロックについて重要なポイントを提供します。私の意見では、それがこのコードを理解するための鍵です。
Richard Chambers

3
私は何か不足していますか、または2番目のコードスニペットではlen % 8バイトはコピーされませんか?
初心者は2014年

ケースのステートメントリストの最後にbreakステートメントを記述しないと、C(または他の言語)がステートメントを実行し続けることを忘れてしまいました。だから、なぜダフのデバイスがまったく機能しないのか疑問に思っている場合、これはその重要な部分です
goonerify

75

Duffのデバイスには2つの重要な点があります。まず、理解しやすい部分だと思いますが、ループは展開されています。これにより、ループが終了したかどうかを確認し、ループの先頭に戻ることに伴うオーバーヘッドの一部を回避することで、コードサイズが大きくなり、速度が向上します。ジャンプする代わりに直線的なコードを実行している場合、CPUはより高速に実行できます。

2番目の側面は、switchステートメントです。これにより、コードは最初からループの途中にジャンプできます。ほとんどの人にとって驚くべきことは、そのようなことが許されるということです。まあ、それは許可されています。実行は計算されたcaseラベルから始まり、他のswitchステートメントと同様に、後続の各代入ステートメントに進みます。最後のケースラベルの後、実行はループの最下部に達し、その時点で最上位に戻ります。ループの先頭はswitchステートメント内にあるため、スイッチは再評価されません。

元のループは8回ほど解かれるので、反復回数は8で除算されます。コピーするバイト数が8の倍数でない場合は、いくつかのバイトが残っています。一度にバイトのブロックをコピーするほとんどのアルゴリズムは、最後に残りのバイトを処理しますが、Duffのデバイスは最初にそれらを処理します。この関数count % 8は、switchステートメントを計算して残りを計算し、その数のバイトのケースラベルにジャンプして、それらをコピーします。その後、ループは8バイトのグループをコピーし続けます。


5
この説明はもっと理にかなっています。最初に残りがコピーされ、残りが8バイトのブロックにコピーされることを理解するための鍵です。これは、ほとんどの場合、8バイトのブロックにコピーしてから残りをコピーするため、珍しいことです。残りを最初に行うことが、このアルゴリズムを理解するための鍵です。
Richard Chambers

スイッチ/ whileループのクレイジーな配置/ネストについて言及するための+1。Javaのような言語から来ることを想像することはできません...
パロベイ14

13

duffsデバイスのポイントは、タイトなmemcpy実装で行われる比較の数を減らすことです。

「count」バイトをaからbにコピーするとします。簡単な方法は、次のとおりです。

  do {                      
      *a = *b++;            
  } while (--count > 0);

カウントが0より大きいかどうかを比較するために何回比較する必要がありますか?'count'回。

現在、ダフデバイスは、スイッチケースの意図しない厄介な副作用を使用しています。これにより、カウントに必要な比較の数を減らすことができます/ 8。

ここで、duffsデバイスを使用して20バイトをコピーするとします。いくつの比較が必要でしょうか。最後の 1つを除いて一度に8バイトをコピーするため、3最初の。

更新:8回の比較/切り替え時のステートメントを実行する必要はありませんが、関数のサイズと速度のトレードオフは妥当です。


3
duffのデバイスは、switchステートメントの8つの重複に限定されないことに注意してください。
ストレージャー、2009

--count、count = count-8の代わりに使用できないのはなぜですか?残りを処理するために2番目のループを使用しますか?
hhafez 2009

1
ハーフェズ、残りを処理するために2番目のループを使用できます。しかし、速度を上げることなく、同じことを2倍のコードで実行できるようになりました。
ロブ・ケネディ

ヨハン、後ろ向きです。残りの4バイトは、ループの最後ではなく、最初のループでコピーされます。
ロブ・ケネディ

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のループの真ん中に合法的にジャンプする機能。

6

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のケースにジャンプして、ループコードを「残り」の回数実行します。


私はこの質問に続いて、この ダフスデバイスに興味を示しました:stackoverflow.com/questions/17192246/switch-case-weird-scopingなので、ダフの明確化に取り掛かったので、既存の回答が改善されているかどうかわかりません...
リシボブ2013年

3

あなたが何を求めているのか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の比較です。これは、マイナーですが、おそらく重要な最適化です。


8
重要な部分がありません。ループの巻き戻しだけではありません。switchステートメントはループの途中にジャンプします。これが、デバイスを非常に混乱させている理由です。上記のループは常に10の倍数のコピーを実行しますが、Duffは任意の数を実行します。
ロブ・ケネディ

2
それは本当です-しかし、私はOPの説明を簡略化しようとしました。おそらく、それを十分にクリアできなかったのかもしれません。:)
ジェームズB

2

これが私がダフのデバイスの核心であると感じるものである詳細ではない説明です:

ことは、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でのそれらの組み合わせは、それほど奇妙に思えなくなります。


1

これは、質問が重複としてクローズされる前にいくつかの賛成を得たダフのデバイスに関する別の質問に投稿した回答です。ここでは、なぜこの構造を避けるべきかについて、少し価値のあるコンテキストを提供していると思います。

「これはダフのデバイスです。です。、ループの反復数がアンロール係数の正確な倍数であることがわからない場合に対処するために二次修正ループを追加する必要を回避する、ループをアンロールする方法です。

ここでのほとんどの回答は一般的に肯定的であるように思われるため、マイナス面を強調します。

このコードでは、コンパイラーはループ本体に最適化を適用するのに苦労します。単純なループとしてコードを記述した場合、最新のコンパイラーが展開を処理できるはずです。このようにして、可読性とパフォーマンスを維持し、ループ本体に他の最適化が適用されることを期待しています。

他の人が参照しているウィキペディアの記事では、この「パターン」がXfree86ソースコードから削除されたときにパフォーマンスが実際に向上したとさえ述べられています。

この結果は、たまたまそれを必要とする可能性があると思われるコードを盲目的に最適化する典型的なものです。これにより、コンパイラが適切に機能しなくなり、コードが読みにくくなり、バグが発生しやすくなり、通常は速度が低下します。最初に正しい方法で物事を行っていた場合、つまり単純なコードを記述し、ボトルネックをプロファイリングしてから最適化した場合、このようなものを使用することすら考えていません。とにかく、最近のCPUとコンパイラではできません。

理解しても大丈夫ですが、実際に使ってみて驚かれることでしょう。」


0

実験してみると、スイッチとループをインターリーブせずにうまくいく別のバリアントが見つかりました:

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;
}

終了条件はどこにありますか?
user2338150
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.