web-dev-qa-db-fra.com

Comment fonctionne l'appareil de Duff?

J'ai lu le article sur Wikipedia sur l'appareil de Duff , et je ne le comprends pas. Je suis vraiment intéressé, mais j’ai lu l’explication à plusieurs reprises et je ne comprends toujours pas comment fonctionne le dispositif de Duff.

Quelle serait une explication plus détaillée?

128
hhafez

Il y a quelques bonnes explications ailleurs, mais laissez-moi essayer. (C'est beaucoup plus facile sur un tableau blanc!) Voici l'exemple de Wikipédia avec quelques notations.

Disons que vous copiez 20 octets. Le contrôle de flux du programme pour la première passe est:

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

Maintenant, démarrez le deuxième passage, nous courons juste le code indiqué:

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

Maintenant, commencez le troisième passage:

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 octets sont maintenant copiés.

Remarque: le périphérique d'origine de Duff (illustré ci-dessus) a été copié sur un périphérique d'E/S à l'adresse to. Ainsi, il n'était pas nécessaire d'incrémenter le pointeur *to. Lors de la copie entre deux mémoires tampons, vous devez utiliser *to++.

215
Clinton Pierce

L'explication du Dr. Dobb's Journal est la meilleure que j'ai trouvée sur le sujet.

Ceci étant mon moment AHA:

for (i = 0; i < len; ++i) {
    HAL_IO_PORT = *pSource++;
}

devient:

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

devient:

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);
}
102
Ric Tokyo

L'appareil de Duff comporte deux éléments clés. Premièrement, ce qui, je suppose, est la partie la plus facile à comprendre, la boucle est déroulée. Cela permet d'échanger une taille de code plus importante pour plus de rapidité en évitant une partie de la surcharge liée à la vérification de la fin de la boucle et à son retour en haut. Le processeur peut fonctionner plus rapidement lorsqu'il exécute du code en ligne droite au lieu de sauter.

Le deuxième aspect est l’instruction switch. Cela permet au code de sauter dans le milieu de la boucle la première fois. Ce qui est surprenant pour la plupart des gens, c’est qu’une telle chose est autorisée. Eh bien, c'est permis. L'exécution commence au libellé de la casse calculée, puis elle tombe à travers à chaque instruction d'affectation successive, comme toute autre instruction switch. Après le dernier libellé de cas, l'exécution atteint le bas de la boucle, point auquel elle revient en haut. Le sommet de la boucle est à l'intérieur l'instruction de commutateur; le commutateur n'est donc plus réévalué.

La boucle d'origine est déroulée huit fois. Le nombre d'itérations est divisé par huit. Si le nombre d'octets à copier n'est pas un multiple de huit, il reste quelques octets. La plupart des algorithmes qui copient des blocs d'octets à la fois gèrent les octets restants à la fin, mais le périphérique de Duff les gère au début. La fonction calcule count % 8 pour que l'instruction switch définisse ce que sera le reste, saute à l'étiquette de casse pour autant d'octets et les copie. Ensuite, la boucle continue à copier des groupes de huit octets.

70
Rob Kennedy

Le but du dispositif est de réduire le nombre de comparaisons effectuées dans une implémentation mémoire serrée.

Supposons que vous vouliez copier les octets 'count' de a vers b, l'approche directe consiste à effectuer les opérations suivantes:

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

Combien de fois avez-vous besoin de comparer le nombre pour voir s'il est supérieur à 0? "compter" fois.

Désormais, l’appareil duff utilise l’effet indésirable involontaire d’un boîtier de commutateur, ce qui vous permet de réduire le nombre de comparaisons nécessaires pour compter/8. 

Supposons maintenant que vous souhaitiez copier 20 octets avec un périphérique Duffs. Combien de comparaisons auriez-vous besoin? Seulement 3, puisque vous copiez huit octets à la fois, sauf le dernier le premier où vous copiez seulement 4.

MISE À JOUR: Vous n'avez pas à faire 8 comparaisons/déclarations cas-in-switch, mais il est raisonnable de faire un compromis entre la taille de la fonction et sa vitesse.

11
Johan Dahlin

Quand je le lisais pour la première fois, je le formatais automatiquement en

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

et je n'avais aucune idée de ce qui se passait.

Peut-être pas quand cette question a été posée, mais maintenant Wikipedia a une très bonne explication

Le dispositif est valide, légal C en vertu de deux attributs en C:

  • Spécification assouplie de l'instruction switch dans la définition du langage. Au moment de l'invention du dispositif, il s'agissait de la première édition du langage de programmation C, qui exige uniquement que l'instruction contrôlée du commutateur soit une instruction syntaxiquement valide (composée) dans laquelle des étiquettes de casse peuvent apparaître précédant n'importe quelle sous-instruction. En plus du fait qu'en l'absence d'une instruction break, le flux de contrôle passera d'une instruction contrôlée par une étiquette de cas à celle contrôlée par la suivante, cela signifie que le code spécifie une succession de copies de comptage à partir de adresses source séquentielles sur le port de sortie mappé en mémoire.
  • La capacité de sauter légalement au milieu d'une boucle en C.
8
Lazer

1: Le dispositif Duffs est une implémentation particulière du déroulement de la boucle. Qu'est-ce que le déroulement de la boucle?
Si vous devez effectuer N fois dans une boucle, vous pouvez échanger la vitesse du programme en exécutant la boucle N/n fois, puis dans la boucle, en déroulant le code de la boucle n fois, par exemple. en remplaçant:

for (int i=0; i<N; i++) {
    // [The loop code...] 
}

avec 

for (int i=0; i<N/n; i++) {
    // [The loop code...]
    // [The loop code...]
    // [The loop code...]
    ...
    // [The loop code...] // n times!
}

Ce qui fonctionne bien si N% n == 0 - pas besoin de Duff! Si ce n'est pas vrai, alors vous devez gérer le reste - ce qui est une douleur.

2: En quoi le dispositif Duffs diffère-t-il de ce déroulement de boucle standard?
Le dispositif Duffs n’est qu’un moyen astucieux de gérer les cycles de boucle restante lorsque N% n! = 0. Le nombre total de fois que l’on exécute pendant que N/n exécute N/n nombre de fois selon le déroulement de la boucle standard (car le cas 0 est valable). Lors de la dernière exécution de la boucle (le 'N/n + 1e fois), le cas se déclenche et nous passons au cas N% n et exécutons le code de boucle le nombre de fois' restant '. 

6
Ricibob

Bien que je ne sois pas sûr à 100% de ce que vous demandez, voici ...

Le problème auquel l'adresse de l'appareil de Duff est liée est le déroulement de la boucle (comme vous l'aurez sans doute déjà vu sur le lien Wiki que vous avez posté). En gros, cela équivaut à une optimisation de l'efficacité d'exécution, au dessus de l'empreinte mémoire. Le dispositif de Duff traite de la copie en série, plutôt que de n'importe quel problème, mais constitue un exemple classique de la façon dont des optimisations peuvent être effectuées en réduisant le nombre de fois qu'une comparaison doit être effectuée en boucle.

A titre d’exemple alternatif, qui facilite la compréhension, imaginez que vous souhaitiez boucler une série d’éléments et ajoutez-leur 1 à chaque fois ... normalement, vous pouvez utiliser une boucle for et une boucle environ 100 fois. . Cela semble assez logique et, c’est ... cependant, une optimisation peut être faite en déroulant la boucle (évidemment pas trop loin ... ou vous pouvez aussi bien ne pas utiliser la boucle).

Donc un habitué de la boucle:

for(int i = 0; i < 100; i++)
{
    myArray[i] += 1;
}

devient

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

Le dispositif de Duff met en œuvre cette idée, en C, mais (comme vous l'avez vu sur le wiki) avec des copies en série. Ce que vous voyez ci-dessus, avec l'exemple non traité, correspond à 10 comparaisons par rapport à 100 dans l'original - il s'agit d'une optimisation mineure, mais peut-être importante.

3
James B

Voici une explication non détaillée qui est ce que je ressens être le noeud du dispositif de Duff:

Le fait est que C est fondamentalement une belle façade pour le langage d'assemblage (PDP-7 Assembly pour être spécifique; si vous étudiez cela, vous verrez à quel point les similitudes sont frappantes). Et, en langage Assembly, vous n'avez pas vraiment de boucles, vous avez des étiquettes et des instructions de branche conditionnelle. La boucle ne représente donc qu'une partie de la séquence d'instructions avec une étiquette et une branche quelque part:

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

et une instruction de commutation est un peu ramifiée/avancée:

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

Dans Assembly, on peut facilement imaginer comment combiner ces deux structures de contrôle, et quand on y pense de la sorte, leur combinaison en C ne semble plus aussi étrange.

0
einpoklum

Juste pour expérimenter, j'ai trouvé une autre variante qui s'entend bien sans entrelacement de commutateur et de boucle:

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