差异执行如何工作?


83

我已经在Stack Overflow上看到了一些提及,但盯着Wikipedia(相关页面已被删除)和 MFC动态对话框演示并没有启发我。有人可以解释一下吗?学习根本不同的概念听起来不错。


根据答案:我认为我对此感觉更好。我想我只是第一次没有仔细看一下源代码。在这一点上,我对差异执行有不同的感觉。一方面,它可以使某些任务变得相当容易。另一方面,要使其启动并运行(即以您选择的语言进行设置)并不容易(我相信如果我对它的理解更好的话就可以了)……尽管我猜想它的工具箱只需要制作一次,然后根据需要进行扩展。我认为,为了真正理解它,我可能需要尝试用另一种语言来实现它。


3
感谢您的关注,Brian。对我而言,有趣的是一些简单的事情似乎令人失望。对我来说,最漂亮的事情很简单。照顾自己。
Mike Dunlavey

1
我想我缺少了一些重要的东西。现在我在想,“这很简单”。如果我真的理解的话,我想我会在想:“这很简单。而且确实很棒而且有用。”
布赖恩2009年

6
...我仍然看到人们展示MVC就像是最伟大的事情,我想我宁愿退休也不必再次这样做。
Mike Dunlavey,2009年

1
...对于“撤消”,您需要对数据进行序列化/反序列化,然后分出一个文件,该文件是两者的XOR,该文件的大多数为零,因此很容易压缩。使用它来恢复先前的数据。现在概括为任意数据结构。
Mike Dunlavey,2009年

1
@MikeDunlavey不想增加您的工作量,但是如果您错过了它,则Source Forge因可疑的Biz做法而失宠。Github.com是当今酷孩子们垂死的地方。他们在desktop.github.com上
Falken教授

Answers:


95

e,布莱恩,我希望我早日看到您的问题。由于这几乎是我的“发明”(无论好坏),我也许可以提供帮助。

插入:我可以做的最简短的解释是,如果正常执行就像将球扔向空中并接住球,那么差异执行就像玩杂耍。

@windfinder的解释与我的不同,没关系。这项技术很难缠住别人的头,我花了20年的时间(反复)才能找到可行的解释。让我再给它一个镜头:

  • 它是什么?

我们都了解计算机逐步执行程序,根据输入数据进行条件分支并执行操作的简单想法。(假设我们只处理简单的结构化goto-less,less-return代码。)该代码包含语句序列,基本结构化条件,简单循环和子例程调用。(现在就不用考虑返回值的函数了。)

现在,假设两台计算机彼此同步执行同一代码,并且能够比较注释。计算机1使用输入数据A运行,计算机2使用输入数据B运行。它们并排运行。如果他们遇到诸如IF(test).... ENDIF之类的条件语句,并且如果对测试是否为真有不同意见,那么说测试是否为false的人将跳到ENDIF并等待它的妹妹要赶上。(这就是代码结构化的原因,因此我们知道姐妹最终将进入ENDIF。)

由于两台计算机可以相互交谈,因此它们可以比较注释并详细说明两组输入数据和执行历史记录之间的区别。

当然,在差分执行(DE)中,它是由一台计算机完成的,模拟两台计算机。

现在,假设您只有一组输入数据,但是您想查看它在时间1到时间2之间的变化。假设您正在执行的程序是串行器/解串器。在执行时,您既要序列化(写出)当前数据,又要反序列化(读入)过去的数据(上次执行时写入的数据)。现在,您可以轻松查看上一次数据与这次数据之间的区别。

您要写入的文件和要读取的旧文件一起构成一个队列或FIFO(先进先出),但这不是一个很深的概念。

  • 到底有什么好处呢?

这是我在图形项目中工作时发生的,用户可以在其中构建很少的显示处理器例程,即“符号”,这些例程可以组装成较大的例程,以绘制诸如管道,储罐,阀门等之类的东西。我们希望这些图是“动态的”,因为它们可以不断更新自己而不必重绘整个图。(按照今天的标准,硬件速度很慢。)我意识到(例如)绘制条形图条形图的例程可以记住它的旧高度,并且可以进行增量更新。

这听起来像是OOP,不是吗?但是,我可以利用图过程的执行顺序的可预测性,而不是“使”成为“对象”。我可以在一个连续的字节流中写出小节的高度。然后,要更新映像,我可以以一种模式运行该过程,在该模式下,它依次读取其旧参数,同时写入新参数,以便为下一次更新做好准备。

这看起来非常愚蠢,并且似乎在过程包含条件条件后立即中断,因为这样新的流和旧的流将不同步。但是后来我想到,如果他们还对条件测试的布尔值进行序列化,他们可以重新同步。花了一段时间说服自己,然后证明这会一直工作,提供了一个简单的规则(以下简称“擦除模式规则”)之后。

最终结果是,用户可以设计这些“动态符号”并将它们组装成更大的图表,而不必担心它们将如何动态更新,而不管显示器的复杂程度或结构可变性如何。

在那些日子里,我确实不得不担心视觉对象之间的干扰,这样擦除一个对象就不会损坏其他对象。但是,现在我将这种技术与Windows控件一起使用,并且让Windows处理渲染问题。

那么它能实现什么呢?这意味着我可以通过编写一个绘制控件的过程来建立一个对话框,而我不必担心实际记住控件对象或逐步更新它们,或者在条件允许的情况下使其出现/消失/移动。结果是对话框源代码更小,更简单,大约减少了一个数量级,并且诸如动态布局或更改控件的数量或具有控件的数组或网格之类的事情微不足道。另外,诸如Edit字段之类的控件可以被微不足道地绑定到它正在编辑的应用程序数据上,并且它将始终被证明是正确的,而且我不必处理它的事件。放置应用程序字符串变量的编辑字段是单行编辑。

  • 为什么很难理解?

我发现最难解释的是,它需要对软件进行不同的思考。程序员如此坚定地与软件的对象操作视图结合在一起,以至于他们想知道什么是对象,什么是类,如何“构建”显示以及如何处理事件,这花了很多时间。炸弹将他们炸开。我试图传达的是真正重要的是您需要说什么?想象一下,您正在构建一种特定于域的语言(DSL),您需要做的就是告诉它“我要在此处编辑变量A,在此处编辑变量B,然后在此处编辑变量C”,它将神奇地为您处理。例如,在Win32中,存在用于定义对话框的“资源语言”。这是一个非常好的DSL,但距离还不够远。它不会“驻留”在主要过程语言中,也不会为您处理事件,也不会包含循环/条件/子例程。但这意味着很好,并且“动态对话框”尝试完成这项工作。

因此,不同的思维方式是:编写程序,首先找到(或发明)合适的DSL,然后在其中尽可能多地编写程序代码。让处理所有的对象和动作只存在于执行的缘故。

如果您想真正理解并执行差异执行,则有一些棘手的问题可能会使您绊倒。我曾经用Lisp宏对其进行编码,在这些宏中可以为您处理这些棘手的问题,但是在“普通”语言中,它需要一些程序员的规程来避免陷阱。

抱歉,这么牵强。如果我没有任何道理,请您指出来,我们将不胜感激,我可以尝试修复它。

添加:

Java Swing中,有一个名为TextInputDemo的示例程序。这是一个静态对话框,占用270行(不计算50个州的列表)。在动态对话框(在MFC中)中,大约有60行:

#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."));
}

添加:

这是示例代码,以大约40行代码来编辑一组医院患者。第1-6行定义了“数据库”。第10-23行定义了UI的总体内容。第30-48行定义了用于编辑单个患者记录的控件。请注意,程序的形式几乎不及时通知事件,就好像它要做的只是一次创建显示。然后,如果添加或删除主题或进行其他结构更改,则只需重新执行它,就像从头开始重新创建一样,除了DE导致进行增量更新外。这样做的好处是,程序员不必付出任何精力或编写任何代码即可进行UI的增量更新,并且可以保证它们是正确的。似乎重新执行将是性能问题,但事实并非如此,

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 }

补充:Brian提出了一个很好的问题,我认为答案属于此处的正文:

@Mike:我不清楚“ if(deButton(50,20,“ Add”)){”语句的实际作用。deButton函数的作用是什么?另外,您的FOR / END循环是使用某种宏还是某种宏?–布赖恩。

@Brian:是的,FOR / END和IF语句是宏。SourceForge项目具有完整的实现。deButton维护按钮控件。当发生任何用户输入操作时,代码将在“控制事件”模式下运行,在该模式下,deButton检测到已按下该代码,并通过返回TRUE表示已按下它。因此,“ if(deButton(...)){...操作代码...}是一种将操作代码附加到按钮的方法,而无需创建闭包或编写事件处理程序。DD_THROW是一个执行操作时终止传递的方法,因为该操作可能已修改了应用程序数据,因此继续执行“控制事件”传递例程是无效的。如果将此与编写事件处理程序进行比较,可以节省编写代码的过程,它可以让您拥有任意数量的控件。

补充:对不起,我应该解释“维护”一词的含义。首次执行该过程时(在SHOW模式下),deButton创建一个按钮控件,并在FIFO中记住其ID。在随后的遍历中(在UPDATE模式下),deButton从FIFO获取ID,并在必要时对其进行修改,然后将其放回FIFO。在ERASE模式下,它从FIFO中读取,销毁它并且不将其放回去,从而“垃圾回收”它。因此,deButton调用可管理控件的整个生命周期,使其与应用程序数据保持一致,这就是为什么我说它“保持”了它的原因。

第四种模式是事件(或控制)。当用户键入字符或单击按钮时,将捕获并记录该事件,然后在事件模式下执行deContents过程。deButton从FIFO获取其按钮控件的ID,并询问是否单击了该按钮。如果是,则返回TRUE,因此可以执行操作代码。如果不是,则仅返回FALSE。另一方面,deEdit(..., &myStringVar)检测该事件是否是针对该事件的,如果是,则将其传递给编辑控件,然后将编辑控件的内容复制到myStringVar。在此过程与正常的UPDATE处理之间,myStringVar始终等于编辑控件的内容。这就是完成“绑定”的方式。相同的想法适用于滚动条,列表框,组合框以及任何允许您编辑应用程序数据的控件。

这是我的维基百科编辑的链接:http : //en.wikipedia.org/wiki/User : MikeDunlavey/Difex_Article


4
很抱歉为您提供答案,但是,如果我对这方面的理解正确,那么您基本上是在将计算越来越接近处理器,远离输出硬件。这是一个令人难以置信的见解,因为我们投入了大量的资金来支持这样的想法,即通过对对象和变量进行编程,可以很容易地将它们转换为最佳机器代码,以实现相同的输出,但事实并非如此!尽管我们可以在编译时优化代码,但是无法优化与时间有关的动作。拒绝时间依赖,让原始函数来工作
sova,2010年

2
@Joey:既然您提到了它,即基于FIFO运行的控制结构和基于作业队列运行的并行协同例程的想法,那儿有很多共同点。
Mike Dunlavey 2010年

2
我想知道差异执行与react.js库使用的方法有多接近。
布莱恩(Brian)

2
@Brian:从浏览信息中,react.js使用diff函数将增量更新发送到浏览器。我无法确定diff函数是否实际上与差分执行一样。它可以处理任意更改,并且声称可以简化绑定。是否完成了我不知道的程度。无论如何,我认为这是在正确的轨道上。几个视频在这里。
Mike Dunlavey 2014年

2
@MikeDunlavey,我用OpenGL / IMGUI和在Model,Model-View和View层上的反应式编程的结合来编写我的工具。我现在永远不会回到旧样式。感谢您的视频链接。
Cthutu

13

差异执行是一种用于根据外部事件更改代码流的策略。通常通过操纵某种数据结构来记录更改来完成此操作。这主要用于图形用户界面,但也用于诸如序列化之类的事情,在序列化中您将更改合并到现有的“状态”中。

基本流程如下:

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)

这样做的好处是很少的。一是将更改的执行与支持数据的实际处理分开。这对于多个处理器来说很好。第二,它提供了一种低带宽的方法来传达程序中的更改。


12

考虑一下监视器的工作原理:

它以60 Hz更新-每秒60次。闪烁闪烁60次,但您的眼睛很慢,无法真正分辨。监视器显示输出缓冲区中的内容。无论您做什么,它都会每1/60秒拖出一次数据。

现在,如果映像不应该经常更改,为什么还要让程序每秒更新整个缓冲区60次呢?如果仅更改图像的一个像素怎么办,应该重写整个缓冲区吗?


这是基本思想的抽象:您要根据要在屏幕上显示的信息来更改输出缓冲区。您希望节省尽可能多的CPU时间和缓冲区写入时间,因此您无需编辑缓冲区的某些部分,而这些部分不需要为下一次屏幕提取而更改。

监视器与计算机和逻辑(程序)是分开的。它以任何更新屏幕的速率从输出缓冲区读取。我们希望我们的计算机停止不必要的同步和重绘。我们可以通过更改缓冲区的使用方式来解决此问题,这可以通过多种方式完成。他的技术实现了一个延迟的FIFO队列-它保存了我们刚发送到缓冲区的内容。延迟的FIFO队列不保存像素数据,而是保存“形状图元”(可能是应用程序中的像素,但也可能是线条,矩形,易于绘制的东西,因为它们只是形状,没有多余的数据是允许)。

因此,您想从屏幕上绘制/擦除内容吗?没问题。根据FIFO队列的内容,我知道监视器的当前状态。我将所需的输出(以擦除或绘制新的基元)与FIFO队列进行比较,仅更改需要更改/更新的值。这是将其命名为差异评估的步骤。

我欣赏这两种不同的方式

第一: Mike Dunlavey使用条件语句扩展。FIFO队列包含许多信息(“先前状态”或监视器或基于时间的轮询设备上的当前信息)。您需要添加的所有内容就是您想要在下一个屏幕上显示的状态。

将一个条件位添加到每个可以在FIFO队列中保存原语的插槽中。

0 means erase
1 means draw

但是,我们有以前的状态:

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

这很优雅,因为当您更新某些内容时,您实际上只需要知道要在屏幕上绘制哪些图元即可—此比较将发现是应该删除图元还是将其添加/保留在缓冲区中。

第二个: 这只是一个例子,我认为Mike真正了解的东西应该是所有项目设计的基础:通过将最计算密集的操作编写为计算机大脑食品来减少设计的(计算)复杂性或尽可能接近。尊重设备的自然时机。

重新绘制方法来绘制整个屏幕非常昂贵,在其他应用程序中,这种洞察力也非常有价值。

我们绝不会在屏幕上“移动”对象。如果要在为计算机监视器之类的代码设计代码时模仿“运动”的物理动作,则“运动”是一项昂贵的操作。相反,对象基本上只是在监视器上闪烁。每次对象移动时,它现在都是一组新的基元,而旧的基元会闪烁。

每当监视器从缓冲区中拉出时,我们都会看到类似

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

对象从不与屏幕(或时间敏感的轮询设备)进行交互。当对象贪婪地要求更新整个屏幕以仅显示特定于自身的更改时,我们可以比对象更智能地处理它。

假设我们有一个程序可以生成的所有可能的图形原语的列表,并将每个原语与一组条件语句绑定

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

当然,这是一个抽象,实际上,代表一个特定的原语处于打开/关闭状态的条件集可能很大(也许数百个必须全部评估为true的标志)。

如果运行程序,我们可以以与评估所有这些条件相同的速率绘制到屏幕上。(最坏的情况:评估最大数量的条件语句需要多长时间。)

现在,对于程序中的任何状态,我们都可以简单地评估所有条件并输出到屏幕闪电般的快速!(我们知道形状图元及其相关的if语句。)

这就像购买图形密集型游戏。您只是购买了一块全新的主板,而不是将其安装到HDD上并通过处理器运行,才可以容纳游戏的全部内容,并以输入为例:鼠标,键盘,以输出为例:显示器。简明扼要的条件评估(因为条件的最基本形式是电路板上的逻辑门)。自然,这会非常敏感,但是它几乎不提供修复错误的支持,因为当您进行微小的设计更改时,整个电路板的设计都会更改(因为“设计”与电路板的性质相去甚远) )。以我们在内部表示数据时的灵活性和清晰度为代价,我们获得了显着的“响应能力”,因为我们不再在计算机中进行“思考”。 基于输入的电路板。

据我了解,该课程是分工,以便为系统的每个部分(不一定只是计算机和显示器)提供可以很好完成的工作。可以用诸如对象之类的概念来完成“计算机思维”……计算机大脑会很乐意尝试并一直为您思考,但是如果您能够让计算机思考,则可以大大简化任务。 data_update和conditional_evals的条款。我们对概念的人类抽象到代码中是理想主义的,而在内部程序绘制方法的情况下,则过于理想化了。当您想要的只是一个结果(具有正确颜色值的像素阵列)并且您拥有一台可以轻松实现的机器 每1/60秒要吐出一个大数组,并尝试从计算机大脑中消除尽可能多的繁琐思考,以便您可以专注于自己真正想要的:将图形更新与(快速)输入保持同步。监视器的自然行为。

这如何映射到其他应用程序? 我想听听其他例子,但我敢肯定有很多例子。我认为,任何提供实时“窗口”信息状态(可变状态或类似数据库……监视器只是显示缓冲区的窗口)的东西都可以从这些见解中受益。


2
++非常感谢您的支持。对我而言,最初是尝试在速度较慢的设备(例如9600波特远程文本终端)上进行程序描述的显示,而在该设备上基本上会进行自动差异显示并传输最少的更新。然后,我被迫问为什么不仅仅通过蛮力编写代码。答案是:因为如果代码的表面形式就像是简单的颜料,那么它更短,几乎没有错误,因此只需开发时间的一小部分。(这就是我认为DSL的好处。)
Mike Dunlavey 2010年

...释放出的开发精力可以再投资于更复杂,更动态的显示,用户可以从中得到响应和愉悦。因此,您可以为开发人员带来更多的用户界面。
Mike Dunlavey 2010年

...示例:大约10年前的这个应用程序:pharsight.com/products/prod_pts_using_dme.php
Mike Dunlavey 2010年

1
当您谈论计算机游戏时,这使我明白了。实际上,许多游戏的编码方式都类似于Mike的用户界面处理方式。每帧循环更新的更新列表。
Falken教授

一个与您所说的某些内容看似相关的示例是检测某个按键/按钮是否处于按下状态或刚刚被释放。很容易知道是否按下了按钮。这是您的底层API中的true / false值。要知道某个键是否被按下,您必须知道它以前处于什么状态。如果它是从0-> 1的值,那么它只是被按下了。如果是1-> 1,则表示被按住;如果是1-> 0,则表示您刚刚释放。
约书亚树篱

3

我发现这个概念与经典数字电子的状态机非常相似。特别是那些记得他们以前的输出的人。

根据(您的代码在这里)下一个输出取决于当前输入和上一个输出的机器。当前输入仅是先前的输出+(用户在此处交互)。

用这样的机器填充表面,它将是用户交互的,同时表示一层可变数据。但是在现阶段,它仍然是愚蠢的,仅反映用户与基础数据的交互。

接下来,根据(您的代码在这里)将您表面上的机器互连起来,让它们共享笔记,现在我们使它变得智能了。它将成为一个交互式计算系统。

因此,您只需要在上述模型的两个地方提供逻辑即可;其余的由机器设计本身负责。那就是它的好处。


1
我似乎记得当我想到这时,我确实想到了一个硬件模型。
Mike Dunlavey
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.