web-dev-qa-db-ja.com

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

Duffのデバイスに関するWikipediaの記事 を読みましたが、わかりません。私は本当に興味がありますが、私はその説明を数回読みましたが、ダフのデバイスがどのように機能するかまだわかりません。

より詳細な説明は何ですか?

133
hhafez

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

220
Clinton Pierce

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

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

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

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

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

70
Rob Kennedy

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

「count」バイトをaからbにコピーすると仮定すると、単純なアプローチは次のようにすることです。

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

カウントを比較して、0を超えるかどうかを確認する必要がある回数。 「カウント」回。

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

ここで、duffsデバイスを使用して20バイトをコピーする場合、何回の比較が必要でしょうか?一度に8バイトをコピーするため、3のみ 最終 4つだけをコピーする最初のもの。

更新:8回の比較/ case-in-switchステートメントを実行する必要はありませんが、関数のサイズと速度の間のトレードオフは合理的です。

11
Johan Dahlin

初めて読んだとき、これを自動フォーマットしました

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ステートメントがない場合、制御の流れは、あるケースラベルで制御されるステートメントから次のケースラベルで制御されるステートメントにフォールスルーするという事実に関連して、これは、コードがカウントコピーの連続を指定することを意味しますメモリマップ出力ポートへの順次送信元アドレス。
  • Cのループの途中に合法的にジャンプする機能。
8
Lazer

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-Duffの必要がない場合、これは素晴らしい機能です! そうでない場合は、残りを処理する必要があります-これは苦痛です。

2:Duffsデバイスは、この標準的なループ展開とどのように異なりますか?
Duffsデバイスは、N%n!= 0の場合に残りのループサイクルを処理する賢い方法です。do/ while全体は、標準のループ展開に従ってN/n回実行されます(ケース0が適用されるため) )。ループの最後の実行(「N/n + 1」回目)でケースが開始され、N%nのケースにジャンプして、「残り」の回数だけループコードを実行します。

6
Ricibob

私はあなたが何を求めているのか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回の比較です。これは、わずかですが、場合によっては重要な最適化に相当します。

3
James B

以下は詳細ではない説明で、Duffのデバイスの核心であると感じています。

基本的には、Cは基本的にアセンブリ言語のニースファサードです(具体的にはPDP-7アセンブリです。研究すると、類似点がどれほど印象的かがわかります)。また、アセンブリ言語では、実際にはループはありません。ラベルと条件分岐命令があります。そのため、ループは、ラベルと分岐を含む命令の全体的なシーケンスの一部にすぎません。

        instruction
label1: instruction
        instruction
        instruction
        instruction
        jump to label1  some condition

そして、切り替え命令が多少先に分岐/ジャンプしています:

        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...

Assemblyでは、これら2つの制御構造を組み合わせる方法は簡単に考えられます。そのように考えると、Cでのそれらの組み合わせはもはや奇妙に思えません。

0
einpoklum

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

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