¿Cómo funciona la ejecución diferencial?

Resuelto Brian asked hace 16 años • 4 respuestas

He visto algunas menciones de esto en Stack Overflow, pero mirar Wikipedia (desde entonces se eliminó la página relevante) y una demostración de diálogo dinámico de MFC no hizo nada para iluminarme. ¿Puede alguien por favor explicar esto? Aprender un concepto fundamentalmente diferente suena bien.


Según las respuestas: Creo que lo estoy sintiendo mejor. Supongo que simplemente no miré el código fuente con suficiente atención la primera vez. En este momento tengo sentimientos encontrados acerca de la ejecución diferencial. Por un lado, puede facilitar considerablemente determinadas tareas. Por otro lado, ponerlo en marcha (es decir, configurarlo en el idioma que prefieras) no es fácil (estoy seguro de que lo sería si lo entendiera mejor)... aunque supongo que la caja de herramientas para ello Sólo es necesario realizarlo una vez y luego ampliarlo según sea necesario. Creo que para entenderlo realmente, probablemente tendré que intentar implementarlo en otro idioma.

Brian avatar Dec 16 '08 23:12 Brian
Aceptado

Vaya, Brian, desearía haber visto tu pregunta antes. Dado que es prácticamente mi "invento" (para bien o para mal), es posible que pueda ayudar.

Insertado: La explicación más breve posible que puedo dar es que si la ejecución normal es como lanzar una pelota al aire y atraparla, entonces la ejecución diferencial es como hacer malabarismos.

La explicación de @windfinder es diferente a la mía y está bien. Esta técnica no es fácil de entender y me ha llevado unos 20 años (de vez en cuando) encontrar explicaciones que funcionen. Déjame darle otra oportunidad aquí:

  • ¿Qué es?

Todos entendemos la idea simple de una computadora avanzando a través de un programa, tomando ramas condicionales basadas en los datos de entrada y haciendo cosas. (Supongamos que estamos tratando sólo con código estructurado simple, sin ir ni devolver). Ese código contiene secuencias de declaraciones, condicionales estructurados básicos, bucles simples y llamadas a subrutinas. (Olvídese de las funciones que devuelven valores por ahora).

Ahora imagine dos computadoras ejecutando el mismo código al mismo tiempo y capaces de comparar notas. La computadora 1 se ejecuta con los datos de entrada A y la computadora 2 se ejecuta con los datos de entrada B. Se ejecutan paso a paso, uno al lado del otro. Si llegan a una declaración condicional como IF(prueba) .... ENDIF, y si tienen una diferencia de opinión sobre si la prueba es verdadera, entonces el que dice que la prueba es falsa salta al ENDIF y espera. su hermana para alcanzarlo. (Es por eso que el código está estructurado, para que sepamos que la hermana eventualmente llegará al ENDIF).

Dado que las dos computadoras pueden comunicarse entre sí, pueden comparar notas y dar una explicación detallada de en qué se diferencian los dos conjuntos de datos de entrada y los historiales de ejecución.

Eso sí, en ejecución diferencial (DE) se hace con un ordenador, simulando dos.

AHORA, suponga que solo tiene un conjunto de datos de entrada, pero desea ver cómo ha cambiado del momento 1 al momento 2. Suponga que el programa que está ejecutando es un serializador/deserializador. A medida que ejecuta, serializa (escribe) los datos actuales y deserializa (lee) los datos pasados ​​(que se escribieron la última vez que hizo esto). Ahora puede ver fácilmente cuáles son las diferencias entre los datos que eran la última vez y los que son esta vez.

El archivo en el que está escribiendo y el archivo antiguo del que está leyendo, en conjunto, constituyen una cola o FIFO (primero en entrar, primero en salir), pero ese no es un concepto muy profundo.

  • ¿Para que sirve?

Se me ocurrió mientras trabajaba en un proyecto de gráficos, donde el usuario podía construir pequeñas rutinas de procesador de pantalla llamadas "símbolos" que podían ensamblarse en rutinas más grandes para pintar cosas como diagramas de tuberías, tanques, válvulas, cosas así. Queríamos que los diagramas fueran "dinámicos" en el sentido de que pudieran actualizarse de forma incremental sin tener que volver a dibujar todo el diagrama. (El hardware era lento para los estándares actuales). Me di cuenta de que (por ejemplo) una rutina para dibujar una barra de un gráfico de barras podía recordar su altura anterior y simplemente actualizarse gradualmente.

Esto suena a programación orientada a objetos, ¿no? Sin embargo, en lugar de "crear" un "objeto", podría aprovechar la previsibilidad de la secuencia de ejecución del procedimiento del diagrama. Podría escribir la altura de la barra en un flujo de bytes secuencial. Luego, para actualizar la imagen, podría simplemente ejecutar el procedimiento en un modo en el que lea secuencialmente sus parámetros antiguos mientras escribe los nuevos parámetros para estar listo para la próxima actualización.

Esto parece estúpidamente obvio y parecería fallar tan pronto como el procedimiento contenga un condicional, porque entonces la nueva secuencia y la antigua secuencia no estarían sincronizadas. Pero luego me di cuenta de que si también serializaban el valor booleano de la prueba condicional, podrían volver a sincronizarse . Me tomó un tiempo convencerme, y luego demostrar, que esto siempre funcionaría, siempre que se siguiera una regla simple (la "regla del modo de borrado").

El resultado neto es que el usuario podría diseñar estos "símbolos dinámicos" y ensamblarlos en diagramas más grandes, sin tener que preocuparse por cómo se actualizarían dinámicamente, sin importar cuán compleja o estructuralmente variable fuera la visualización.

En aquellos días, tenía que preocuparme por la interferencia entre objetos visuales, para que borrar uno no dañara a los demás. Sin embargo, ahora uso la técnica con los controles de Windows y dejo que Windows se encargue de los problemas de renderizado.

Entonces ¿qué logra? Significa que puedo construir un diálogo escribiendo un procedimiento para pintar los controles, y no tengo que preocuparme por recordar los objetos de control o actualizarlos incrementalmente, o hacerlos aparecer/desaparecer/moverse según las condiciones lo justifiquen. El resultado es un código fuente de diálogo mucho más pequeño y simple, aproximadamente en un orden de magnitud, y cosas como el diseño dinámico o la alteración del número de controles o tener matrices o cuadrículas de controles son triviales. Además, un control como un campo Editar puede vincularse trivialmente a los datos de la aplicación que está editando, y siempre será demostrablemente correcto y nunca tendré que ocuparme de sus eventos. Poner un campo de edición para una variable de cadena de aplicación es una edición de una línea.

  • ¿Por qué es difícil de entender?

Lo que me ha resultado más difícil de explicar es que requiere pensar de manera diferente sobre el software. Los programadores están tan firmemente aferrados a la visión objeto-acción del software que quieren saber cuáles son los objetos, cuáles son las clases, cómo "construyen" la visualización y cómo manejan los eventos, para eso se necesita una cereza. bomba para expulsarlos. Lo que intento transmitir es que lo que realmente importa es ¿ qué necesitas decir? Imagine que está creando un lenguaje de dominio específico (DSL) donde todo lo que necesita hacer es decirle "Quiero editar la variable A aquí, la variable B allá y la variable C allá abajo" y mágicamente se encargará de ello por usted. . Por ejemplo, en Win32 existe este "lenguaje de recursos" para definir diálogos. Es un DSL perfectamente bueno, excepto que no llega lo suficientemente lejos. No "vive" en el lenguaje de procedimiento principal, ni maneja eventos por usted, ni contiene bucles/condicionales/subrutinas. Pero tiene buenas intenciones y Dynamic Dialogs intenta terminar el trabajo.

Entonces, el modo diferente de pensar es: para escribir un programa, primero encuentra (o inventa) un DSL apropiado y codifica la mayor cantidad posible de su programa en él. Deje que se ocupe de todos los objetos y acciones que sólo existen por motivos de implementación.

Si realmente desea comprender la ejecución diferencial y utilizarla, hay un par de cuestiones complicadas que pueden hacerle tropezar. Una vez lo codifiqué en macros Lisp , donde estas partes difíciles podrían ser manejadas por usted, pero en lenguajes "normales" se requiere cierta disciplina del programador para evitar los obstáculos.

Lamento ser tan prolijo. Si no tengo sentido, te agradecería que me lo señalaras y puedo intentar solucionarlo.

Agregado:

En Java Swing , hay un programa de ejemplo llamado TextInputDemo. Es un diálogo estático, que ocupa 270 líneas (sin contar la lista de 50 estados). En Dynamic Dialogs (en MFC) son aproximadamente 60 líneas:

#define NSTATE (sizeof(states)/sizeof(states[0]))
CString sStreet;
CString sCity;
int iState;
CString sZip;
CString sWholeAddress;

void SetAddress(){
    CString sTemp = states[iState];
    int len = sTemp.GetLength();
    sWholeAddress.Format("%s\r\n%s %s %s", sStreet, sCity, sTemp.Mid(len-3, 2), sZip);
}

void ClearAddress(){
    sWholeAddress = sStreet = sCity = sZip = "";
}

void CDDDemoDlg::deContentsTextInputDemo(){
    int gy0 = P(gy);
    P(www = Width()*2/3);
    deStartHorizontal();
    deStatic(100, 20, "Street Address:");
    deEdit(www - 100, 20, &sStreet);
    deEndHorizontal(20);
    deStartHorizontal();
    deStatic(100, 20, "City:");
    deEdit(www - 100, 20, &sCity);
    deEndHorizontal(20);
    deStartHorizontal();
    deStatic(100, 20, "State:");
    deStatic(www - 100 - 20 - 20, 20, states[iState]);
    if (deButton(20, 20, "<")){
        iState = (iState+NSTATE - 1) % NSTATE;
        DD_THROW;
    }
    if (deButton(20, 20, ">")){
        iState = (iState+NSTATE + 1) % NSTATE;
        DD_THROW;
    }
    deEndHorizontal(20);
    deStartHorizontal();
    deStatic(100, 20, "Zip:");
    deEdit(www - 100, 20, &sZip);
    deEndHorizontal(20);
    deStartHorizontal();
    P(gx += 100);
    if (deButton((www-100)/2, 20, "Set Address")){
        SetAddress();
        DD_THROW;
    }
    if (deButton((www-100)/2, 20, "Clear Address")){
        ClearAddress();
        DD_THROW;
    }
    deEndHorizontal(20);
    P((gx = www, gy = gy0));
    deStatic(P(Width() - gx), 20*5, (sWholeAddress != "" ? sWholeAddress : "No address set."));
}

Agregado:

A continuación se muestra un código de ejemplo para editar una serie de pacientes del hospital en aproximadamente 40 líneas de código. Las líneas 1 a 6 definen la "base de datos". Las líneas 10 a 23 definen el contenido general de la interfaz de usuario. Las líneas 30 a 48 definen los controles para editar el registro de un solo paciente. Tenga en cuenta que la forma del programa casi no se da cuenta de los eventos en el tiempo, como si todo lo que tuviera que hacer fuera crear la visualización una vez. Luego, si se agregan o eliminan temas o se realizan otros cambios estructurales, simplemente se vuelve a ejecutar, como si se estuviera recreando desde cero, excepto que DE hace que se realice una actualización incremental. La ventaja es que usted, el programador, no tiene que prestar atención ni escribir ningún código para realizar las actualizaciones incrementales de la interfaz de usuario, y se garantiza que serán correctas. Podría parecer que esta reejecución sería un problema de rendimiento, pero no lo es, ya que la actualización de controles que no necesitan ser cambiados toma del orden de decenas de nanosegundos.

1  class Patient {public:
2    String name;
3    double age;
4    bool smoker; // smoker only relevant if age >= 50
5  };
6  vector< Patient* > patients;

10 void deContents(){ int i;
11   // First, have a label
12   deLabel(200, 20, “Patient name, age, smoker:”);
13   // For each patient, have a row of controls
14   FOR(i=0, i<patients.Count(), i++)
15     deEditOnePatient( P( patients[i] ) );
16   END
17   // Have a button to add a patient
18   if (deButton(50, 20, “Add”)){
19     // When the button is clicked add the patient
20     patients.Add(new Patient);
21     DD_THROW;
22   }
23 }

30 void deEditOnePatient(Patient* p){
31   // Determine field widths
32   int w = (Width()-50)/3;
33   // Controls are laid out horizontally
34   deStartHorizontal();
35     // Have a button to remove this patient
36     if (deButton(50, 20, “Remove”)){
37       patients.Remove(p);
37       DD_THROW;
39     }
40     // Edit fields for name and age
41     deEdit(w, 20, P(&p->name));
42     deEdit(w, 20, P(&p->age));
43     // If age >= 50 have a checkbox for smoker boolean
44     IF(p->age >= 50)
45       deCheckBox(w, 20, “Smoker?”, P(&p->smoker));
46     END
47   deEndHorizontal(20);
48 }

Agregado: Brian hizo una buena pregunta y pensé que la respuesta pertenecía aquí en el texto principal:

@Mike: No tengo claro qué está haciendo realmente la declaración "if (deButton(50, 20, “Add”)){". ¿Qué hace la función deButton? Además, ¿tus bucles FOR/END utilizan algún tipo de macro o algo así? – Brian.

@Brian: Sí, las declaraciones FOR/END e IF son macros. El proyecto SourceForge tiene una implementación completa. deButton mantiene un control de botón. Cuando se realiza cualquier acción de entrada del usuario, el código se ejecuta en modo "evento de control", en el que deButton detecta que fue presionado y significa que fue presionado devolviendo VERDADERO. Por lo tanto, "if(deButton(...)){... código de acción...} es una forma de adjuntar código de acción al botón, sin tener que crear un cierre o escribir un controlador de eventos. DD_THROW es un forma de terminar el pase cuando se realiza la acción porque la acción puede haber modificado los datos de la aplicación, por lo que no es válido continuar el paso del "evento de control" a través de la rutina. Si compara esto con escribir controladores de eventos, le ahorra escribirlos, y te permite tener cualquier número de controles.

Agregado: Lo siento, debería explicar lo que quiero decir con la palabra "mantiene". Cuando el procedimiento se ejecuta por primera vez (en modo MOSTRAR), deButton crea un control de botón y recuerda su identificación en FIFO. En pasadas posteriores (en modo ACTUALIZAR), deButton obtiene la identificación del FIFO, la modifica si es necesario y la vuelve a colocar en el FIFO. En el modo BORRAR, lo lee del FIFO, lo destruye y no lo devuelve, por lo que lo "recoge basura". Entonces, la llamada deButton administra toda la vida útil del control, manteniéndolo de acuerdo con los datos de la aplicación, razón por la cual digo que lo "mantiene".

El cuarto modo es EVENTO (o CONTROL). Cuando el usuario escribe un carácter o hace clic en un botón, ese evento se captura y registra, y luego el procedimiento deContents se ejecuta en modo EVENTO. deButton obtiene la identificación de su control de botón del FIFO y pregunta si este es el control en el que se hizo clic. Si así fuera, devuelve VERDADERO para que se pueda ejecutar el código de acción. Si no, simplemente devuelve FALSO. Por otro lado, deEdit(..., &myStringVar)detecta si el evento estaba destinado a él y, de ser así, lo pasa al control de edición y luego copia el contenido del control de edición en myStringVar. Entre esto y el procesamiento UPDATE normal, myStringVar siempre es igual al contenido del control de edición. Así es como se hace la "vinculación". La misma idea se aplica a las barras de desplazamiento, cuadros de lista, cuadros combinados y cualquier tipo de control que le permita editar datos de la aplicación.

Aquí hay un enlace a mi edición de Wikipedia: http://en.wikipedia.org/wiki/User:MikeDunlavey/Difex_Article

Mike Dunlavey avatar Jan 28 '2009 23:01 Mike Dunlavey

La ejecución diferencial es una estrategia para cambiar el flujo de su código en función de eventos externos. Esto generalmente se hace manipulando una estructura de datos de algún tipo para registrar los cambios. Esto se usa principalmente en interfaces gráficas de usuario, pero también se usa para cosas como la serialización, donde se fusionan cambios en un "estado" existente.

El flujo básico es el siguiente:

Start loop:
for each element in the datastructure: 
    if element has changed from oldDatastructure:
        copy element from datastructure to oldDatastructure
        execute corresponding subroutine (display the new button in your GUI, for example)
End loop:
Allow the states of the datastructure to change (such as having the user do some input in the GUI)

Las ventajas de esto son algunas. Uno, es la separación entre la ejecución de sus cambios y la manipulación real de los datos de respaldo. Lo cual es bueno para múltiples procesadores. Segundo, proporciona un método de bajo ancho de banda para comunicar cambios en su programa.

Alex avatar Dec 19 '2008 18:12 Alex

Piense en cómo funciona un monitor:

Se actualiza a 60 Hz, 60 veces por segundo. El parpadeo parpadea 60 veces, pero tus ojos son lentos y realmente no pueden notarlo. El monitor muestra lo que hay en el búfer de salida; simplemente arrastra estos datos cada 1/60 de segundo sin importar lo que hagas.

Ahora bien, ¿por qué querrías que tu programa actualice todo el búfer 60 veces por segundo si la imagen no debería cambiar con tanta frecuencia? ¿Qué pasa si solo cambia un píxel de la imagen, debería reescribir todo el búfer?


Esta es una abstracción de la idea básica: desea cambiar el búfer de salida en función de la información que desea que se muestre en la pantalla. Desea ahorrar tanto tiempo de CPU y tiempo de escritura del búfer como sea posible, para no editar partes del búfer que no necesitan cambiarse para la siguiente extracción de pantalla.

El monitor está separado de su computadora y de la lógica (programas). Lee del búfer de salida a cualquier velocidad que actualice la pantalla. Queremos que nuestra computadora deje de sincronizarse y volver a dibujar innecesariamente. Podemos resolver esto cambiando la forma en que trabajamos con el búfer, lo que se puede hacer de varias maneras. Su técnica implementa una cola FIFO que está en retraso: contiene lo que acabamos de enviar al búfer. La cola FIFO retrasada no contiene datos de píxeles, sino "primitivas de forma" (que pueden ser píxeles en su aplicación, pero también pueden ser líneas, rectángulos, cosas fáciles de dibujar porque son solo formas, no se almacenan datos innecesarios). permitido).

¿Quieres dibujar/borrar cosas de la pantalla? Ningún problema. Según el contenido de la cola FIFO, sé cómo se ve el monitor en este momento. Comparo mi salida deseada (para borrar o dibujar nuevas primitivas) con la cola FIFO y solo cambio los valores que deben cambiarse/actualizarse. Este es el paso que le da el nombre de Evaluación Diferencial.

Dos maneras distintas en las que aprecio esto:

El primero: Mike Dunlavey utiliza una extensión de declaración condicional. La cola FIFO contiene mucha información (el "estado anterior" o el contenido actual en el monitor o dispositivo de sondeo basado en tiempo). Todo lo que tienes que agregar a esto es el estado que deseas que aparezca en la pantalla a continuación.

Se agrega un bit condicional a cada ranura que pueda contener una primitiva en la cola FIFO.

0 means erase
1 means draw

Sin embargo, tenemos el estado anterior:

Was 0, now 0: don't do anything;
Was 0, now 1: add it to the buffer (draw it);
Was 1, now 1: don't do anything;
Was 1, now 0: erase it from the buffer (erase it from the screen);

Esto es elegante, porque cuando actualiza algo en realidad sólo necesita saber qué primitivas desea dibujar en la pantalla; esta comparación descubrirá si debe borrar una primitiva o agregarla/mantenerla en/en el búfer.

El segundo: Este es sólo un ejemplo, y creo que lo que Mike realmente quiere decir es algo que debería ser fundamental en el diseño de todos los proyectos: reducir la complejidad (computacional) del diseño escribiendo sus operaciones computacionalmente más intensas como alimento para el cerebro de la computadora. o lo más cerca que puedas llegar. Respete la sincronización natural de los dispositivos.

Un método de redibujado para dibujar la pantalla completa es increíblemente costoso y existen otras aplicaciones en las que esta información es increíblemente valiosa.

Nunca estamos "moviendo" objetos por la pantalla. "Mover" es una operación costosa si vamos a imitar la acción física de "mover" cuando diseñamos código para algo como un monitor de computadora. En cambio, los objetos básicamente parpadean con el monitor. Cada vez que un objeto se mueve, ahora es un nuevo conjunto de primitivas y el antiguo conjunto de primitivas se apaga.

Cada vez que el monitor extrae del búfer tenemos entradas que parecen

Draw bit    primitive_description
0           Rect(0,0,5,5);
1           Circ(0,0,2);
1           Line(0,1,2,5);

Un objeto nunca interactúa con la pantalla (o el dispositivo de sondeo sensible al tiempo). Podemos manejarlo de manera más inteligente que un objeto cuando solicita con avidez actualizar toda la pantalla solo para mostrar un cambio específico solo para él.

Digamos que tenemos una lista de todas las primitivas gráficas posibles que nuestro programa es capaz de generar y que vinculamos cada primitiva a un conjunto de declaraciones condicionales.

if (iWantGreenCircle && iWantBigCircle && iWantOutlineOnMyCircle) ...

Por supuesto, esto es una abstracción y, en realidad, el conjunto de condicionales que representa que una primitiva particular esté activada/desactivada podría ser grande (quizás cientos de indicadores que deben evaluarse como verdaderos).

Si ejecutamos el programa, podemos dibujar en la pantalla esencialmente a la misma velocidad con la que podemos evaluar todos estos condicionales. (En el peor de los casos: cuánto tiempo lleva evaluar el conjunto más grande de declaraciones condicionales).

Ahora, para cualquier estado del programa, podemos simplemente evaluar todos los condicionales y enviarlos a la pantalla a la velocidad del rayo. (Conocemos nuestras primitivas de forma y sus declaraciones if dependientes).

Esto sería como comprar un juego con gráficos intensos. Solo que en lugar de instalarlo en tu disco duro y ejecutarlo a través de tu procesador, compras una placa nueva que contiene todo el juego y toma como entrada: mouse, teclado y toma como salida: monitor. Evaluación condicional increíblemente condensada (ya que la forma más fundamental de un condicional son las puertas lógicas en las placas de circuito). Esto, naturalmente, respondería muy bien, pero casi no ofrece soporte para corregir errores, ya que todo el diseño de la placa cambia cuando se realiza un pequeño cambio de diseño (debido a que el "diseño" está muy alejado de la naturaleza de la placa de circuito ). A expensas de la flexibilidad y claridad en la forma en que representamos los datos internamente, hemos ganado una "capacidad de respuesta" significativa porque ya no "pensamos" en la computadora; Todo es simplemente un reflejo para la placa de circuito basado en las entradas.

The lesson, as I understand it, is to divide labor such that you give each part of the system (not necessarily just computer and monitor) something it can do well. The "computer thinking" can be done in terms of concepts like objects... The computer brain will gladly try and think this all through for you, but you can simplify the task a great deal if you are able to let the computer think in terms of data_update and conditional_evals. Our human abstractions of concepts into code are idealistic, and in the case of internal program draw methods a little overly idealistic. When all you want is a result (array of pixels with correct color values) and you have a machine that can easily spit out an array that big every 1/60th of a second, try and eliminate as much flowery thinking from the computer brain as possible so that you can focus on what you really want: to synchronize your graphical updates with your (fast) inputs and the natural behavior of the monitor.

How does this map to other applications? I'd like to hear of other examples, but I'm sure there are many. I think anything that provides a real-time "window" into the state of your information (variable state or something like a database... a monitor is just a window into your display buffer) can benefit from these insights.

sova avatar Nov 10 '2010 11:11 sova