Come funziona il dispositivo Duff?


Risposte:


240

Ci sono alcune buone spiegazioni altrove, ma lasciami provare. (Questo è molto più semplice su una lavagna!) Ecco l'esempio di Wikipedia con alcune notazioni.

Supponiamo che tu stia copiando 20 byte. Il controllo di flusso del programma per il primo passaggio è:

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

Ora, inizia il secondo passaggio, eseguiamo solo il codice indicato:

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

Ora inizia il terzo passaggio:

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 byte sono ora copiati.

Nota: il dispositivo Duff originale (mostrato sopra) copiato su un dispositivo I / O toall'indirizzo. Pertanto, non è stato necessario aumentare il puntatore *to. Quando si copia tra due buffer di memoria, è necessario utilizzare *to++.


1
Come si può saltare la clausola case 0: e continuare a controllare le altre clausole che si trovano all'interno del ciclo do while che è l'argomento della clausola skpped? Se l'unica clausola al di fuori del ciclo do while viene saltata, perché l'interruttore non finisce qui?
Aurelio,

14
Non guardare le parentesi graffe così duramente. Non guardare docosì tanto. Invece, guarda le switche le dichiarazioni whilecalcolate come vecchio stile GOTOo le jmpistruzioni assembler con un offset. La switchfa un po 'di matematica e poi jmps al posto giusto. La whilefa un controllo booleano e poi ciecamente jmps a destra su dove il doera.
Clinton Pierce,

Se è così buono, perché non lo usano tutti? Ci sono degli svantaggi?
AlphaGoku,

@AlphaGoku Readability.
LF

108

La spiegazione nel Dr. Dobb's Journal è la migliore che ho trovato sull'argomento.

Questo è il mio momento AHA:

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

diventa:

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

diventa:

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

buon post (in più devo trovare una buona risposta da te per votare;) 2 down, 13 to go: stackoverflow.com/questions/359727#486543 ). Goditi il ​​bel badge di risposta.
VonC,

13
Il fatto cruciale qui, e che ha reso il dispositivo di Duff incomprensibile per me per il tempo più lungo, è che per una stranezza di C, dopo la prima volta che raggiunge il tempo, salta indietro ed esegue tutte le dichiarazioni. Pertanto, anche se len%8fosse 4, eseguirà il caso 4, il caso 2, il caso 2 e il caso 1, quindi salterà indietro ed eseguirà tutti i casi dal ciclo successivo in poi. Questa è la parte che deve essere spiegata, il modo in cui il ciclo e l'istruzione switch "interagiscono".
ShreevatsaR,

2
L'articolo del Dr. Dobbs è buono ma a parte il link la risposta non aggiunge nulla. Vedi la risposta di Rob Kennedy di seguito, che in realtà fornisce un punto importante sul resto della dimensione di trasferimento che viene gestita per prima seguita da zero o più blocchi di trasferimento di 8 byte. Secondo me questa è la chiave per comprendere questo codice.
Richard Chambers,

3
Mi sto perdendo qualcosa o nel secondo frammento di codice i len % 8byte non verranno copiati?
novizio

Ero bloccato, dimenticando che se non scrivi una dichiarazione di rottura alla fine dell'elenco delle dichiarazioni di un caso, C (o qualsiasi altra lingua) continuerà a eseguire le dichiarazioni. Quindi, se ti stai chiedendo perché il dispositivo di Duff funzioni affatto, questa è una parte cruciale di esso
goonerify

75

Ci sono due cose chiave nel dispositivo di Duff. In primo luogo, che sospetto sia la parte più semplice da capire, il ciclo viene srotolato. Ciò consente di scambiare codice di dimensioni maggiori per una maggiore velocità, evitando alcune delle spese generali coinvolte nel controllo del completamento del loop e nel salto in cima al loop. La CPU può funzionare più velocemente quando esegue il codice lineare invece di saltare.

Il secondo aspetto è l'istruzione switch. Consente al codice di saltare al centro del loop per la prima volta. La parte sorprendente della maggior parte delle persone è che una cosa del genere è consentita. Bene, è permesso. L'esecuzione inizia dall'etichetta del caso calcolato e quindi passa a ciascuna istruzione di assegnazione successiva, proprio come qualsiasi altra istruzione di commutazione. Dopo l'ultima etichetta del caso, l'esecuzione raggiunge la parte inferiore del loop, a quel punto torna alla parte superiore. La parte superiore del ciclo si trova all'interno dell'istruzione switch, quindi l'interruttore non viene più rivalutato.

Il ciclo originale viene svolto otto volte, quindi il numero di iterazioni viene diviso per otto. Se il numero di byte da copiare non è un multiplo di otto, allora ci sono alcuni byte rimasti. La maggior parte degli algoritmi che copiano blocchi di byte alla volta gestirà i rimanenti byte alla fine, ma il dispositivo di Duff li gestisce all'inizio. La funzione calcola count % 8per l'istruzione switch per capire quale sarà il resto, passa all'etichetta del caso per quel numero di byte e li copia. Quindi il ciclo continua a copiare gruppi di otto byte.


5
Questa spiegazione ha più senso. la chiave per me per capire che il resto viene copiato prima il resto poi in blocchi di 8 byte, il che è insolito poiché, come menzionato la maggior parte delle volte, copieresti in blocchi di 8 byte e poi copi il resto. fare prima il resto è la chiave per comprendere questo algoritmo.
Richard Chambers,

+1 per menzionare il posizionamento pazzo / annidamento di switch / while loop. Impossibile immaginare di venire da una lingua come Java ...
Parobay,

13

Lo scopo del dispositivo duff è ridurre il numero di confronti effettuati in un'implementazione memcpy stretta.

Supponiamo di voler copiare i byte "count" da a a b, l'approccio diretto è quello di fare quanto segue:

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

Quante volte devi confrontare il conteggio per vedere se è superiore a 0? "conta" volte.

Ora, il dispositivo Duff utilizza un brutto effetto collaterale non intenzionale di una custodia switch che consente di ridurre il numero di confronti necessari per contare / 8.

Ora supponiamo che tu voglia copiare 20 byte usando il dispositivo duff, di quanti confronti avresti bisogno? Solo 3, poiché si copiano otto byte alla volta tranne l' ultimo primo in cui si copiano solo 4.

AGGIORNATO: Non è necessario eseguire 8 confronti / istruzioni case-in-switch, ma è ragionevole un compromesso tra dimensione della funzione e velocità.


3
Si noti che il dispositivo di Duff non è limitato a 8 duplicazioni nell'istruzione switch.
Strager

perché non puoi semplicemente usare invece di --count, count = count-8? e utilizzare un secondo ciclo per gestire il resto?
hhafez,

1
Hhafez, puoi usare un secondo loop per gestire il resto. Ma ora hai il doppio del codice per realizzare la stessa cosa senza aumento di velocità.
Rob Kennedy,

Johan, ce l'hai all'indietro. I restanti 4 byte vengono copiati sulla prima iterazione del ciclo, non sull'ultimo.
Rob Kennedy,

8

Quando l'ho letto per la prima volta, l'ho formattato automaticamente in questo

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

e non avevo idea di cosa stesse succedendo.

Forse non quando è stata posta questa domanda, ma ora Wikipedia ha un'ottima spiegazione

Il dispositivo è valido, C legale in virtù di due attributi in C:

  • Specifica rilassata dell'istruzione switch nella definizione della lingua. Al momento dell'invenzione del dispositivo questa era la prima edizione di The C Programming Language che richiede solo che l'istruzione controllata dello switch sia un'istruzione sintatticamente valida (composta) all'interno della quale le etichette del caso possono apparire come prefissi a qualsiasi subistruzione. Insieme al fatto che, in assenza di un'istruzione break, il flusso di controllo passerà da un'istruzione controllata da un'etichetta di caso a quella controllata da quella successiva, ciò significa che il codice specifica una successione di copie di conteggio da indirizzi sorgente sequenziali alla porta di uscita mappata in memoria.
  • La capacità di saltare legalmente nel mezzo di un ciclo in C.

6

1: il dispositivo Duff è una particolare applicazione di srotolamento ad anello. Che cos'è lo svolgimento di loop?
Se hai un'operazione per eseguire N volte in un ciclo, puoi scambiare le dimensioni del programma per la velocità eseguendo il ciclo N / n volte e quindi nel ciclo inline (srotolando) il codice del ciclo n volte, ad esempio sostituendo:

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

con

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

Che funziona alla grande se N% n == 0 - non c'è bisogno di Duff! Se ciò non è vero, devi gestire il resto, il che è un dolore.

2: In che modo il dispositivo Duff differisce da questo srotolamento standard?
Il dispositivo Duff è solo un modo intelligente di gestire i cicli di loop rimanenti quando N% n! = 0. L'intero do / while esegue N / n numero di volte come da srotolamento di loop standard (perché si applica il caso 0). Nell'ultimo giro del ciclo (la 'N / n + 1'esima volta) il caso entra in funzione e passiamo al caso N% n ed eseguiamo il codice del ciclo il numero' rimanente 'di volte.


Mi sono interessato al dispositivo Duff seguendo questa domanda: stackoverflow.com/questions/17192246/switch-case-weird-scoping quindi ho pensato di provare a chiarire Duff - non sono sicuro che si tratti di un miglioramento sulle risposte esistenti ...
Ricibob,

3

Anche se non sono sicuro al 100% di ciò che stai chiedendo, ecco qui ...

Il problema che il dispositivo di Duff affronta è quello di svolgersi in modo circolare (come sicuramente vedrai sul link Wiki che hai pubblicato). Ciò a cui equivale sostanzialmente è un'ottimizzazione dell'efficienza di runtime, oltre l'impronta della memoria. Il dispositivo di Duff si occupa della copia seriale, piuttosto che di qualsiasi vecchio problema, ma è un classico esempio di come è possibile effettuare ottimizzazioni riducendo il numero di volte in cui è necessario eseguire un confronto in un ciclo.

Come esempio alternativo, che può rendere più semplice la comprensione, immagina di avere una serie di elementi su cui desideri eseguire il loop e di aggiungerne uno ogni volta ... di solito, potresti usare un ciclo for e un loop circa 100 volte . Questo sembra abbastanza logico ed è ... tuttavia, è possibile effettuare un'ottimizzazione svolgendo il loop (ovviamente non troppo lontano ... oppure potresti non usare il loop).

Quindi un ciclo regolare per:

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

diventa

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

Quello che fa il dispositivo di Duff è implementare questa idea, in C, ma (come hai visto nel Wiki) con copie seriali. Quello che stai vedendo sopra, con l'esempio svolto, sono 10 confronti rispetto a 100 nell'originale - questo equivale a un'ottimizzazione minore, ma forse significativa,.


8
Ti manca la parte chiave. Non si tratta solo di svolgersi in loop. L'istruzione switch passa al centro del loop. Questo è ciò che rende il dispositivo così confuso. Il tuo ciclo sopra esegue sempre un multiplo di 10 copie, ma Duff esegue qualsiasi numero.
Rob Kennedy,

2
È vero, ma stavo tentando di semplificare la descrizione dell'OP. Forse non l'ho chiarito abbastanza! :)
James B,

2

Ecco una spiegazione non dettagliata che è quello che sento essere il punto cruciale del dispositivo Duff:

Il fatto è che C è fondamentalmente una bella facciata per il linguaggio assembly (l'assemblaggio PDP-7 per essere specifici; se studiassi vedresti quanto sono sorprendenti le somiglianze). E, nel linguaggio assembly, in realtà non hai loop: hai etichette e istruzioni sul ramo condizionale. Quindi il loop è solo una parte della sequenza generale di istruzioni con un'etichetta e un ramo da qualche parte:

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

e un'istruzione switch si sta ramificando / saltando un po 'avanti:

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

Nell'assemblaggio è facilmente concepibile come combinare queste due strutture di controllo e, se ci pensate in quel modo, la loro combinazione in C non sembra più così strana.


1

Questa è una risposta che ho inviato a un'altra domanda sul dispositivo di Duff che ha ottenuto alcuni upvaotes prima che la domanda fosse chiusa come duplicato. Penso che fornisca un po 'di contesto prezioso qui sul perché dovresti evitare questo costrutto.

"Questo è il dispositivo di Duff . È un metodo per srotolare i loop che evita di dover aggiungere un ciclo di correzione secondario per gestire i momenti in cui il numero di iterazioni del ciclo non è noto per essere un multiplo esatto del fattore di srotolamento.

Dal momento che la maggior parte delle risposte qui sembrano essere generalmente positive al riguardo, ho intenzione di evidenziare gli aspetti negativi.

Con questo codice un compilatore farà fatica ad applicare qualsiasi ottimizzazione al corpo del loop. Se hai appena scritto il codice come un semplice ciclo, un compilatore moderno dovrebbe essere in grado di gestire lo srotolamento per te. In questo modo si mantengono la leggibilità e le prestazioni e si ha qualche speranza che altre ottimizzazioni vengano applicate al corpo del loop.

L'articolo di Wikipedia a cui fanno riferimento altri dice anche quando questo "modello" è stato rimosso dalle prestazioni del codice sorgente di Xfree86 effettivamente migliorato.

Questo risultato è tipico dell'ottimizzazione cieca della mano di qualsiasi codice che ritieni possa averne bisogno. Impedisce al compilatore di svolgere correttamente il proprio lavoro, rende il codice meno leggibile e più soggetto a bug e in genere lo rallenta. Se prima facessi le cose nel modo giusto, cioè scrivendo un codice semplice, quindi profilando per i colli di bottiglia, quindi ottimizzando, non penseresti mai di usare qualcosa del genere. Non con una moderna CPU e compilatore comunque.

Va bene capirlo, ma sarei sorpreso se lo usassi davvero. "


0

Solo sperimentando, ho trovato un'altra variante che va d'accordo senza switch e loop interleaving:

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

Dov'è la tua condizione finale?
user2338150
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.