Hay algunas buenas explicaciones en otros lugares, pero déjenme intentarlo. (¡Esto es mucho más fácil en una pizarra!) Aquí está el ejemplo de Wikipedia con algunas anotaciones.
Digamos que está copiando 20 bytes. El control de flujo del programa para el primer paso es:
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)
}
Ahora, comience el segundo pase, ejecutamos solo el código indicado:
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)
}
Ahora, comience el tercer pase:
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...
Ahora se copian 20 bytes.
Nota: El dispositivo Duff's original (que se muestra arriba) se copió a un dispositivo de E / S en la to
dirección. Por lo tanto, no fue necesario incrementar el puntero *to
. Cuando copie entre dos buffers de memoria, deberá usar *to++
.
do
tanto. En su lugar, mirada en elswitch
y lawhile
tan anticuada calculadasGOTO
statments o ensamblador dejmp
comandos con un desplazamiento. Laswitch
hace algunos cálculos y luegojmp
s al lugar correcto. Elwhile
hace una comprobación booleano y luego ciegamentejmp
S a la derecha de donde eldo
estaba.La explicación en el Dr. Dobb's Journal es la mejor que encontré sobre el tema.
Este es mi momento AHA:
se convierte en:
se convierte en:
fuente
len%8
fuera 4, ejecutará el caso 4, el caso 2, el caso 2 y el caso 1, y luego saltará hacia atrás y ejecutará todos los casos desde el siguiente ciclo en adelante. Esta es la parte que debe explicarse, la forma en que el bucle y la declaración de interruptor "interactúan".len % 8
no se copiarán los bytes?Hay dos cosas clave para el dispositivo de Duff. Primero, lo que sospecho es la parte más fácil de entender, el ciclo se desenrolla. Esto cambia el tamaño de código más grande por una mayor velocidad al evitar parte de la sobrecarga involucrada en verificar si el ciclo está terminado y saltar de nuevo a la parte superior del ciclo. La CPU puede funcionar más rápido cuando ejecuta código de línea recta en lugar de saltar.
El segundo aspecto es la declaración de cambio. Permite que el código salte a la mitad del ciclo la primera vez. La parte sorprendente para la mayoría de la gente es que tal cosa está permitida. Bueno, está permitido. La ejecución comienza en la etiqueta del caso calculado, y luego cae en cada declaración de asignación sucesiva, como cualquier otra declaración de cambio. Después de la última etiqueta del caso, la ejecución alcanza la parte inferior del bucle, en cuyo punto salta de nuevo a la parte superior. La parte superior del bucle está dentro de la declaración del interruptor, por lo que el interruptor ya no se vuelve a evaluar.
El bucle original se desenrolla ocho veces, por lo que el número de iteraciones se divide por ocho. Si el número de bytes a copiar no es un múltiplo de ocho, entonces quedan algunos bytes. La mayoría de los algoritmos que copian bloques de bytes a la vez manejarán los bytes restantes al final, pero el dispositivo de Duff los maneja al principio. La función calcula
count % 8
la declaración de cambio para calcular cuál será el resto, salta a la etiqueta del caso para esa cantidad de bytes y los copia. Luego, el bucle continúa copiando grupos de ocho bytes.fuente
El objetivo del dispositivo duffs es reducir el número de comparaciones realizadas en una implementación de memcpy estricta.
Suponga que desea copiar bytes 'contar' de a a b, el enfoque directo es hacer lo siguiente:
¿Cuántas veces necesitas comparar el conteo para ver si es superior a 0? 'contar' veces.
Ahora, el dispositivo duff utiliza un desagradable efecto secundario involuntario de una caja del interruptor que le permite reducir la cantidad de comparaciones necesarias para contar / 8.
Ahora suponga que desea copiar 20 bytes usando el dispositivo duffs, ¿cuántas comparaciones necesitaría? Solo 3, ya que copia ocho bytes a la vez, excepto el
últimoprimero donde copia solo 4.ACTUALIZADO: No tiene que hacer 8 comparaciones / declaraciones de cambio de caso, pero es razonable una compensación entre el tamaño de la función y la velocidad.
fuente
Cuando lo leí por primera vez, lo auto formateé para esto
y no tenía idea de lo que estaba pasando.
Tal vez no cuando se hizo esta pregunta, pero ahora Wikipedia tiene una muy buena explicación
fuente
1: el dispositivo Duffs es una implicación particular del desenrollamiento del bucle. ¿Qué es el desenrollado de bucle?
Si tiene una operación para realizar N veces en un bucle, puede cambiar el tamaño del programa por velocidad ejecutando el bucle N / n veces y luego en el bucle en línea (desenrollando) el código del bucle n veces, por ejemplo, reemplazando:
con
Lo que funciona muy bien si N% n == 0 - ¡no necesita Duff! Si eso no es cierto, entonces tienes que manejar el resto, lo cual es un dolor.
2: ¿En qué se diferencia el dispositivo Duffs de este desenrollado de bucle estándar?
El dispositivo Duffs es solo una forma inteligente de lidiar con los ciclos de bucle restantes cuando N% n! = 0. Todo el do / while ejecuta N / n número de veces según el desenrollado de bucle estándar (porque se aplica el caso 0). En la última ejecución del ciclo (la 'N / n + 1' vez), el caso se activa y saltamos al caso N% n y ejecutamos el código del ciclo el número 'restante' de veces.
fuente
Aunque no estoy 100% seguro de lo que estás pidiendo, aquí va ...
El problema que aborda el dispositivo de Duff es el de desenrollar el bucle (como sin duda habrás visto en el enlace Wiki que publicaste). Lo que esto básicamente equivale a una optimización de la eficiencia del tiempo de ejecución, sobre la huella de la memoria. El dispositivo de Duff se ocupa de la copia en serie, en lugar de cualquier problema antiguo, pero es un ejemplo clásico de cómo se pueden hacer optimizaciones al reducir la cantidad de veces que se necesita hacer una comparación en un bucle.
Como un ejemplo alternativo, que puede hacer que sea más fácil de entender, imagine que tiene una serie de elementos que desea recorrer, y agregue 1 a ellos cada vez ... por lo general, puede usar un ciclo for, y recorrer alrededor de 100 veces . Esto parece bastante lógico y, sin embargo, es ... sin embargo, se puede hacer una optimización desenrollando el bucle (obviamente no demasiado lejos ... o bien puede simplemente no usar el bucle).
Entonces, un ciclo for regular:
se convierte
Lo que hace el dispositivo de Duff es implementar esta idea, en C, pero (como viste en el Wiki) con copias en serie. Lo que está viendo arriba, con el ejemplo desenrollado, son 10 comparaciones en comparación con 100 en el original, lo que equivale a una optimización menor, pero posiblemente significativa.
fuente
Aquí hay una explicación no detallada que es lo que siento que es el quid del dispositivo de Duff:
La cuestión es que C es básicamente una buena fachada para el lenguaje ensamblador (el ensamblaje PDP-7 para ser específico; si estudiaras eso, verías cuán llamativas son las similitudes). Y, en lenguaje ensamblador, realmente no tiene bucles, tiene etiquetas e instrucciones de ramificación condicional. Entonces, el bucle es solo una parte de la secuencia general de instrucciones con una etiqueta y una rama en alguna parte:
y una instrucción de cambio se ramifica / salta un poco hacia adelante:
En el ensamblaje es fácilmente concebible cómo combinar estas dos estructuras de control, y cuando lo piensas de esa manera, su combinación en C ya no parece tan extraña.
fuente
Esta es una respuesta que publiqué en otra pregunta sobre el dispositivo de Duff que obtuvo algunos upvaotes antes de que la pregunta se cerrara por duplicado. Creo que proporciona un poco de contexto valioso aquí sobre por qué debería evitar esta construcción.
"Este es el dispositivo de Duff . Es un método de desenrollar bucles que evita tener que agregar un bucle de reparación secundario para lidiar con los momentos en que el número de iteraciones de bucle no se sabe que es un múltiplo exacto del factor de desenrollado".
Como la mayoría de las respuestas aquí parecen ser generalmente positivas al respecto, voy a resaltar las desventajas.
Con este código, un compilador tendrá dificultades para aplicar cualquier optimización al cuerpo del bucle. Si acaba de escribir el código como un bucle simple, un compilador moderno debería ser capaz de manejar el desenrollado por usted. De esta manera, mantiene la legibilidad y el rendimiento y tiene la esperanza de que se apliquen otras optimizaciones al cuerpo del bucle.
El artículo de Wikipedia al que hacen referencia otros incluso dice que cuando este 'patrón' se eliminó del código fuente Xfree86, el rendimiento mejoró realmente.
Este resultado es típico de optimizar a ciegas cualquier código que creas que podría necesitarlo. Impide que el compilador haga su trabajo correctamente, hace que su código sea menos legible y más propenso a errores y generalmente lo ralentiza. Si estaba haciendo las cosas de la manera correcta en primer lugar, es decir, escribiendo código simple, luego creando perfiles para cuellos de botella y luego optimizando, nunca pensaría en usar algo como esto. No con una CPU moderna y un compilador de todos modos.
Está bien entenderlo, pero me sorprendería si alguna vez lo usas ".
fuente
Solo experimentando, encontré otra variante que se lleva bien sin intercalar el interruptor y el bucle:
fuente