差异执行如何工作?

我在Stack Overflow上看到了一些这样的提及,但是盯着维基百科(相关页面已经被删除了)以及在MFCdynamic对话框演示中并没有给我什么启发。 有人可以解释一下吗? 学习一个根本不同的概念听起来不错。


基于这个答案:我觉得我对它有了更好的感觉。 我想我第一次没有仔细看源代码。 在这一点上,我对差异执行有着复杂的感觉。 一方面它可以使某些任务变得相当容易。 另一方面,启动和运行(也就是用你select的语言来设置它)并不容易(如果我更好地理解它,我相信是这样)…虽然我猜测它的工具箱只需要做一次,然后根据需要进行扩展。 我想为了真正理解它,我可能需要尝试用另一种语言来实现它。

吉恩,布赖恩,我希望我早点看到你的问题。 由于这几乎是我的“发明”(无论好坏),我可能会提供帮助。

插入:我可以做的最简单的解释是,如果正常执行就像在空中抛球并捕捉它,那么差异执行就像是杂耍。

@ windfinder的解释与我的不同,没关系。 这种技术不容易缠绕头脑,我花了20年的时间来find解释。 让我再来一个镜头:

  • 它是什么?

我们都明白一个简单的想法:一个计算机通过一个程序步进,根据input的数据采取条件分支,并做事。 (假设我们只处理简单的结构化goto-less,无返回的代码)。该代码包含语句序列,基本结构化条件,简单循环和子例程调用。 (忘记函数现在返回值。)

现在想象两台计算机彼此执行相同的代码,并能够比较注释。 计算机1以input数据A运行,而计算机2以input数据B运行。它们并排运行。 如果他们遇到像IF(test)…. ENDIF这样的条件语句,并且如果他们对testing是否属实有不同意见,那么说错误testing的人跳到ENDIF并等待其姐姐赶上。 (这就是为什么代码是结构化的,所以我们知道姐姐最终会到达ENDIF。)

由于两台计算机可以互相通话,所以可以比较注释,并对两组input数据和执行历史是如何不同的进行详细说明。

当然,在差分执行(DE)中,用一台计算机完成,模拟两个。

现在,假设你只有一组input数据,但是你想看看它是如何从时间1改变到时间2.假设你正在执行的程序是一个串行器/解串器。 在执行时,您将序列化(写出)当前数据并反序列化(读入)过去的数据(这是上次写入的数据)。 现在你可以很容易地看到上一次数据和现在是什么之间的区别。

您正在写入的文件以及您正在读取的旧文件一起构成队列或FIFO(先进先出),但这不是一个非常深刻的概念。

  • 到底有什么好处呢?

在我开始一个graphics项目的时候,我发现,在这个项目中,用户可以构build一些称为“符号”的显示处理器例程,这些例程可以组装成更大的例程来绘制pipe道,坦克,阀门等图表。 我们希望图表是“dynamic”的,因为他们可以不必重绘整个图表就可以逐步更新自己的图表。 (按照当今的标准,硬件速度很慢)。我意识到(例如)绘制条形图的例程可以记住它的旧高度,只是逐步更新自身。

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

这看起来很愚蠢,当程序包含一个条件的时候,似乎会中断,因为那么新的stream和旧的stream将不同步。 但是后来,我明白了如果他们也将条件testing的布尔值序列化,他们可以重新同步 。 花了一段时间来说服自己,然后certificate,只要遵循一条简单的规则(“擦除模式规则”),这总是可行的。

最终的结果是用户可以devise这些“dynamic符号”并将其组装成更大的图表,而不必担心它们将如何dynamic更新,无论显示器有多复杂或结构可变。

那时候,我确实不得不担心视觉物体之间的干扰,所以擦除不会损害别人。 但是,现在我使用Windows控件的技术,并让Windows处理渲染问题。

那么它成就了什么? 这意味着我可以通过编写一个程序来绘制控件来build立一个对话框,而且我不必担心实际记住控制对象或处理增量更新它们,或者使它们在条件允许的情况下出现/消失/移动。 结果是小得多,简单的对话源代码,大约一个数量级,而像dynamic布局或改变控件的数量或控件的数组或网格是微不足道的。 另外,像编辑字段这样的控件可以简单地绑定到正在编辑的应用程序数据上,而且它总是可以certificate是正确的,我从来不需要处理它的事件。 放入应用程序stringvariables的编辑字段是单行编辑。

  • 为什么很难理解?

我发现最难解释的是它需要对软件进行不同的思考。 程序员和软件的对象 – 行为观牢牢地结合在一起,他们想要知道什么是对象,什么是类,他们如何“build立”显示,以及他们如何处理事件,这需要一个樱桃炸弹爆炸出来。 我想传达的是,真正重要的是你需要说什么? 想象一下,你正在构build一个特定于领域的语言(DSL),你需要做的就是告诉它“我想在这里编辑variablesA,在那里编译variablesB,在那里编译variablesC”,它会为你神奇地照顾它。 例如,在Win32中有定义对话框的“资源语言”。 这是一个非常好的DSL,除了它不够远。 它不会“居住”主要的过程语言,或者为你处理事件,或者包含循环/条件/子程序。 但它意味着很好,dynamic对话框试图完成这项工作。

所以,不同的思维模式是:编写一个程序,你首先find(或发明)一个合适的DSL,并尽可能多地编写你的程序。 让它处理所有为实现而存在的对象和动作。

如果你想真正了解差异化执行并使用它,那么有一些棘手的问题可能会让你感到沮丧。 我曾经在Lispmacros中编写过这些代码,在这些代码中可以为你处理这些棘手的问题,但是在“正常”语言中,它需要程序员的一些训练以避免陷阱。

对不起,这么啰嗦。 如果我没有意义,如果你能指出,我会很感激,我可以尝试修复它。

添加:

在Java Swing中 ,有一个名为TextInputDemo的示例程序。 这是一个静态对话框,占用了270行(不包括50个州的列表)。 在dynamic对话框(在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行定义了编辑单个病人logging的控件。 注意,程序的forms几乎不需要及时通知事件,就好像它只需要创build一次显示一样。 然后,如果添加或删除了主题或者发生了其他结构更改,则只需重新执行主题,就好像从头开始重新创build主题一样,除了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循环使用某种macros或什么东西? – Brian。

@Brian:是的,FOR / END和IF语句是macros。 SourceForge项目有一个完整的实现。 deButton维护一个button控件。 当任何用户input动作发生时,代码运行在“控制事件”模式,其中,deButton检测到它被按下,并通过返回TRUE表示它被按下。 因此,“if(deButton(…)){… action code …}是一种将操作代码附加到button的方法,而不必创build闭包或者编写一个事件处理程序DD_THROW是一个由于动作可能已经修改了应用程序数据,所以在采取行动时终止通行证的方法是无效的,所以继续通过例行程序的“控制事件”过程是无效的,如果将它与写入事件处理程序进行比较,它可以让你有任何数量的控制。

补充:对不起,我应该解释“维护”一词的意思。 当程序第一次被执行时(在SHOW模式下),deButton创build一个button控制并在FIFO中记住它的id。 在后续的传递过程中(在UPDATE模式下),deButton从FIFO中获得ID,必要时修改它,并将其放回到FIFO中。 在擦除模式下,它从FIFO中读取它,破坏它,并不放回,从而“垃圾收集”它。 因此,deButton调用pipe理整个控件生命周期,使其与应用程序数据保持一致,这就是为什么我说它“维护”它。

第四种模式是事件(或控制)。 当用户键入一个字符或点击一个button时,该事件被捕获并logging,然后在EVENT模式下执行deContents过程。 如果这是被点击的控件,deButton从FIFO获得其button控件的id。 如果是,则返回TRUE,以便可以执行操作代码。 如果不是,则返回FALSE。 另一方面, deEdit(..., &myStringVar)检测事件是否适合它,如果是,则将其传递给编辑控件,然后将编辑控件的内容复制到myStringVar中。 在这个和普通的UPDATE处理之间,myStringVar总是等于编辑控件的内容。 这就是“绑定”是如何完成的。 同样的想法适用于滚动条,列表框,combobox,任何可以让你编辑应用程序数据的控件。

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

想想监视器的工作原理:

它以每秒60次更新 – 60次。 闪烁闪烁闪烁60次,但你的眼睛很慢,不能真正说出。 监视器显示输出缓冲区中的内容; 不pipe你做什么,它只是每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真正掌握的东西应该是所有项目devise的基础:通过将计算量最大的操作写成computerbrain-food,减lessdevise的(计算)复杂性或尽可能靠近你。 尊重设备的自然时间。

绘制整个屏幕的重绘方法非常昂贵,还有其他应用程序,这种洞察力是非常有价值的。

我们永远不会在屏幕上“移动”物体。 如果我们要模拟“移动”的物理动作,当我们为计算机显示器devise代码时,“移动”是一个代价高昂的操作。 相反,物体基本上只是闪烁在显示器上。 每当一个对象移动,它现在是一个新的基元集合,旧的基元集合闪烁。

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

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

从来没有一个对象与屏幕(或时间敏感的轮询设备)进行交互。 当它贪婪地要求更新整个屏幕以显示仅针对其本身的改变时,我们可以比对象更智能地处理它。

假设我们有一个我们的程序能够产生的所有可能的graphics基元的列表,并且我们将每个基元绑定到一组条件语句

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

当然,这是一个抽象概念,实际上,代表特定基元开/关的条件集可能很大(可能是数百个必须全部评估为真的标志)。

如果我们运行这个程序,我们可以以基本相同的速度绘制屏幕,​​在这个速率下我们可以评估所有这些条件。 (最坏的情况:评估最大的一组条件语句需要多长时间)

现在,对于程序中的任何状态,我们都可以简单地评估所有的条件并输出到屏幕上 (我们知道我们的形状原语和它们的依赖if语句。)

这就像买一个graphics激烈的游戏。 只需要将其安装到您的硬盘并通过您的处理器运行,您就可以购买一块全新的主板来保存整个游戏,并将其作为input:鼠标,键盘,并将其作为输出:显示器。 令人难以置信的有条件的评估(作为条件最基本的forms是电路板上的逻辑门)。 这自然会非常灵敏,但是它几乎不提供修复bug的支持,因为当您进行微小的devise更改时,整个电路板devise会发生变化(因为“devise”已经远离电路板的本质)。 由于我们在内部代表数据的灵活性和清晰性,我们获得了显着的“响应性”,因为我们不再在计算机上做“思考” 对于基于input的电路板来说,这只是reflection

根据我的理解,这个教训是分工,这样你就可以给系统的每个部分(不一定只是计算机和显示器),它可以做得很好。 “计算机思维”可以用物体这样的概念来完成……计算机大脑会很乐意尝试并为您思考这一切,但是如果您能够让计算机思考data_update和conditional_evals的条款。 我们将概念抽象成代码是理想化的,内部程序抽取方法有点过于理想化。 当你想要的只是一个结果(具有正确的颜色值的像素数组),而且你有一台机器可以很容易地吐出一个每1/60秒大的数组,那么试着从计算机的大脑中尽可能地消除像因此您可以专注于自己真正想要的内容:将graphics更新与(快速)input和显示器的自然行为同步。

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

差异执行是一种基于外部事件改变代码stream的策略。 这通常是通过操纵某种数据结构来logging变化。 这主要在graphics用户界面中使用,但也用于序列化之类的事情,在这种情况下,将更改合并到现有的“状态”中。

基本stream程如下:

 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) 

这个的好处是less数。 第一,这是你的改变的执行的分离,以及支持数据的实际操作。 这对多个处理器来说是很好的。 二,它提供了一个低带宽的方法来沟通你的程序中的变化。

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

下一个输出取决于当前input和前一个输出的机器(您的代码在这里)。 这个电streaminput只是之前的输出+(USER,INTERACT HERE)。

用这样的机器填充曲面,它将是用户交互式的,同时代表一层可变数据。 但在这个阶段,它仍然是愚蠢的,只是反映了用户与底层数据的交互。

接下来,根据(你的代码在这里),把你的表面上的机器互连起来,让他们分享笔记,现在我们变得聪明起来。 它将成为一个交互式计算系统。

所以你只需在上述模型的两个地方提供你的逻辑; 其余的由机器devise本身照顾。 这是好事。

Interesting Posts