几个月前,LABVIEW7I老师(高巍)向我推荐了一份PPT,其中主要讨论了性能与内存管理的问题,这是NI公司内部团队写的,因此非常珍贵。 LABVIEW对用户屏蔽了内存使用的细节,作为用户很难了解内存是如何使用的以及如何提高程序的运行性能。这份PPT是难得一见的性能与内存管理方面的 资料,所以高老师希望我能翻译其中重要的部分,介绍给大家。

对于英文资料,在翻译的同时,不可避免地会参杂一些个人的理解,如果对其中的内容有歧义,欢迎大家留言讨论。

性能与内存管理

目标:

  • 理解LABVIEW执行系统
  • 学习如果通过减少数据拷贝以及减少总的内存使用来改进性能
  • 了解VI的执行特性

LABVIEW执行系统

  • 执行系统是LABVIEW重要的组成部分,负责实际运行我们的代码。
  • 启动自动并行机制
  • 对于LABVIEW来说非常特殊---其它编程语言需要人工进行线程管理。
LABVIEW 执行系统可以使我们的代码自动并行运行。在其它编程语言中,必须通过人工方式进行多线程管理,但是LABVIEW在可能的情况下,编译器和执行系统共同写 作,自动并行运行我们的代码。在大多数情况下,执行系统对我们来说并不是很重要,因为执行系统不需要我们参与就会做的很好。

但是在某些情况下,我们有必要了解执行系统是如何工作的,因为这有助于我们改进程序的运行性能。

  • 执行系统的工作类似一个线程池。一个任务队列和一个线程集合提取任务出队列。
  • 任务(“队列元素”)就是VI代码需要执行的片段。
  • 每个执行系统对应一个队列,包括UI执行系统、标准执行系统、仪器执行系统、数据采集执行系统、其它1执行系统、其它2执行系统和定时循环执行系统
LABVIEW执行系统的工作类似一个线程池。所谓线程池是多个线程的集合,这些线程协同工作,通过从一个共享的队列,共同完成从队列中提取的任务。在LABVIEW中,这些任务称作“队列元素”,每个队列元素代表一小段需要执行的代码。

LABVIEW实际拥有6个通用的执行系统,每个执行系统拥有一个独立的队列。另外,每个定时循环拥有自己独特的执行系统。

  • 每个执行系统具有多个线程
  • UI执行系统是个例外,它只有一个线程。
Picture
上图是LABVIEW执行系统如何工作的模型图。对于每个执行系统(上图中示意了三个执行系统),我们各自创建了一个队列。入队的数据代表了VI代码中需要执行的片段。当这个需要执行的片段准备好要运行时,LABVIEW把它放入到某个执行系统的队列中。

每个执行系统包括一个或者几个线程,这些线程中一个包括一个循环,负责把代码片段出队列并执行之。UI执行系统只拥有一个UI线程,其它执行系统可以拥有多个线程,但是共享 一个队列。当VI代码并行运行时,由执行系统的不同线程进行管理。


Picture
除了操作系统的抢先式多任务系统外,LABVIEW引入了协作式多任务系统。在编译过程中,LABVIEW分析VI,定位那些可以共同执行的节点 组,称作CLUMPS,每个由优先级和执行系统组成的运行数据队列结构把这些可以共同运行的CLUMPS保存在一起。当执行系统激活一个线程时,执行系统 解析这些CLUMPS并执行之。当执行系统执行完后,运行队列存入其它的符合输入条件的CLUMP进队列,这使得程序框图可以在任意的有效线程中运行。如 果程序框图包含了足够的并行机制,就可以在所有的线程中同时运行。

LABVIEW并不是对某个CLUMP分配特定的线程,在VI再次运行 时,同样的CLUMP可能被分配到不同的线程中。每个CLUMP都会产生一小段代码由LABVIEW调度。在CLUMP内部,LABVIEW不会使用并行 机制,在CLUMP之间,LABVIEW利用执行系统执行多任务。



Picture
上图展示的CLUMP代表了程序框图开始到结束的过程,当执行中间两个FOR循环时,CLUMP处于“睡眠”状态。然后被“唤醒”完成VI(执行两个除法运算)。

 当一个节点进入“睡眠”状态时,它把自己放入一个等待队列中,然会再返回到执行系统中。当等待结束后,它脱离等待队列,再次放入到执行队列中。



首选执行系统

  • 某些节点必须运行在U线程中
  • 每个VI都可以指定首选的执行系统,默认情况下选择“与调用者相同”
某 些节点具有首选的执行系统。最为常见的首选执行系统是UI执行系统。有些节点必须运行在UI线程中,比如VI服务器属性节点、VI服务器调用节点和打开 VI引用节点必须工作在UI线程中。DAQ中的属性节点和调用节点不在UI线程中,LABVIEW中的类的属性节点和调用节点也同样不在UI线程中。

另外,每个VI都可以选择首选的执行系统,默认情况下为“与调用者相同”,这意味着VI可以运行在任何执行系统中。



切换执行系统

  • 当代码需要工作在不同于调用VI的执行系统,或者此前运行的执行系统时,就发生了执行系统的切换。
  • 通常发生在与UI操作有关的代码中。
  • 切换执行系统会导致运行性能的问题。需要进入睡眠状态,然后在其它执行系统   中被唤醒。切换会产生耗时。
  • 避免不必要的VI代码
当一个节点需要运行,但是节点需要运行在另外的执行系统(非当前运行)中时,这会导致队列元素处于“睡眠”状态,并在其它的执行系统中被唤醒。这意 味着这段代码将停止运行,把自己放入到希望运行的那个执行系统队列中。当那个执行系统有时间时,执行系统从队列中取出元素并执行之。这种执行系统的切换需 要耗费时间,可能会引起性能的下降。

为了避免这种执行系统的切换,我们应该避免不必要的UI代码(比如服务器属性节点)。对子VI使用“与调用者相同”的执行系统可以有效地避免不必要的执行系统切换。

在下列两种情况下,我们可能会对VI使用特定的首选执行系统.

  1. 如 果VI内部含有必须在UI线程执行的代码,当我们使用“与调用者相同”选项时,VI执行时必须切换到UI线程,然后再返回到调用方的执行系统,以便结束子 VI时,能够继续调用方的执行系统继续运行。假如子VI运行的一个循环时,这会导致子VI内部不断反复切换执行系统,极大影响程序的性能。这种情况下,如 果对子VI选择工作在UI线程,则进入子VI之前切换到UI执行系统,在循环中一直在UI执行系统下运行,在退出之前切换到调用者的执行系统。这样执行系 统的切换只会在进入和退出前发生两次,可以极大提高程序的运行性能。
  2. 如果一个VI内部存在一个长时间的循环运行的任务,如控制数据采 集,我们可以设定VI工作在其它执行系统(仪器执行系统、数据采集执行系统、其它1执行系统或者其它2执行系统)。这样这些任务就不会和其它VI竞争获取 执行系统操作时间。我们需要注意,如果我们不是工作在实时操作系统中,我们还要考虑操作提供本身的调度问题。通过控制定时循环的优先级可以有效地控制我们 代码执行的优先执行次序,在实时操作体统中,定时循环的优先级更为可靠,也更加明显。

优先级

  • 子VI的优先级影响执行系统中队列元素的优先级。
  • 高优先级的队列元素优先出队列。
  • 子VI的优先级高低设置并不影响执行系统线程自身的优先级。

子程序优先级

  • 子程序优先级并非真正的优先级。
  • 对于经常被调用的代码可以减少执行系统的开销。
  • 强制把整个VI放在一个单独的CLUMP中。
  • 阻止VI进入睡眠状态。
  • 没有并行机制
  • 可以被设置为“如果忙,去除子程序设置”。
  • 通常不推荐使用。
子程序优先级并非真正的VI优先级。VI的优先级是通过改变队列中元素的优先级实现的,而子程序优先级是把整个VI放入在一个CLUMP中,这就保证了 VI一旦执行就不会进入睡眠状态。这意味着我们不能在子程序内部调用有可能导致进入睡眠状态的其它子VI,在VI内部也不能引发执行系统的切换。设定为 “子程序优先级”的VI在内部只能调用同样设置为“子程序优先级”的其它子Vi。“子程序优先级”的VI的代码在内部执行时都是按照次序执行,而不是并行 运行。

内嵌(内联)VI

  • 首选用于替代子程序优先级
  • 当主调VI编译完成后,整个设置为内嵌的子VI的全部代码插入到主调VI中。这样就不存在调用的开销了。
  • 依然支持并行运行机制。
  • 允许更多的编译优选项。
  • 存在下列限制:不允许访问前面板,仅支持部分节点,当子VI反生变化时,强迫主调VI重新编译。

封锁线程

  • 在执行调用期间,外部代码(如调用库函数、CIN节点、NET、ACTIVEX等等)会锁定线程。
  • 由于C代码不使用LABVIEW队列元素,所以运行时会锁定线程。
  • 等待期间,其它VI无法运行。
  • 一般情况下,无法中止外部代码的操作。
LABVIEW的执行系统是基于多线程相互协作的基础上,但是其它语言编写的代码并非如此。这意味着当我们从LABVIEW中调用外部代码 时,LABVIEW只有在外部代码操作完成后才能继续线程操作。当外部代码需要等待运行权限或者其它原因进入等待状态时,当前的执行系统必须等待外部操作 代码运行结束,因此等待期间,执行系统内不会有其它VI运行。如果外部代码无法完成,经常会导致出现“重置VI”对话框。如果使用调用库函数节点调用外部 代码,则需要在配置对话框中的“回调”也选择合适的选项,避免这个问题的发生。



连线的含义

  • 每一条连线都代表一个缓冲区。
  • 连线分支会导致数据拷贝。
Picture
LABVIEW的数据流编程模式意味着每一条数据连线都操作着它自身的数据拷贝,在子VI内部同样也需要创建数据的拷贝。由于两个分支创建了数据拷贝,因此两个分支可以同时独立工作,不 会出现数据锁定的情况。

Picture
在上图的示例中,按图索骥需要5个数组的拷贝。但是实际上LABVIEW仔细分析这段代码,发现可以分为上下两个分支,分别执行加法操作和乘法操作。上面 分支中,表明的部位(+)产生一个拷贝,因为加法操作将改变输入部分的数组,对输入的数组具有破坏性。下面的分支LABVIEW进行了优化,乘法输出和加 法输出采用同地址操作,使用相同的数组。可以看出,经过LABVIEW的优化,需要5个拷贝变成了只有一个拷贝。这个优化方案是非常理想的,因为上下两个 分支都需要把数据写回数组,所以不管使用那种编程语言,都需要至少一个拷贝的过程。

同地址算法

未完待续

 
最近GSD论坛上的一个帖子谈到了全局变量的问题:

  • sandan0615:
在保证数据不冲突的情况下可以对全局变量写操作吗?
RTRT,各位高手解释下,我在陈树学老师的宝典里看到说在程序里要避免对全局变量进行写操作。
  • czhen:
当然可以
不行的话,要它干啥

  • wyb4993:
我有一个自动化测试程序,里面有很多LabVIEW全局变量,可以读和写。是前一任离职的兄弟留下的。目前运行很正常。


关于慎用全局变量的问题,很多编程语言方面的书籍都会提及,NI论坛上有一个长达十几页的帖子专门讨论的这个问题,非常详细。
其中不仅仅涉及全局变量,还提及了许多解决问题的技巧。
下面我大概翻译一下其中重要的部分,希望有助于理解如何正确使用全局变量。帖子很长,我需要用几天的时间陆续给大家介绍。
------------------------------------------------------------------------------------------------------------------
TBOB:
不止一次地看到人们在抱怨,全局变量是罪恶之源,根本就不应该使用它们。但是我不认为这个结论是显而易见正确的。我希望能听到一次有关全局变量的严肃认真的讨论。论坛中的朋友们很多都提到了他们都正在编程中使用全局变量。
先从全局变量的有点谈起。一般来说,全局变量是公认的在各个VI之间传递数据的有效方法,比起其它方式的全局变量(个人意见)更容易管理,因为假如我们使用了一个簇作为全局数据,我们没有办法确定在何处使用了它们,可能需要自己创建一个文件记录它们使用的位置。但是全局变量则不然,通过全局变量的右键快捷菜单,我们可以很容找到引用全局变量的位置。

使用全局变量有两个不利之处,其一,引用全局变量需要创建数据的拷贝,这可能会导致潜在的竞争条件或者导致数据的丢失。其二,使用全局变量会中断数据流程。
所以,我对那些反对使用全局变量的人士提出一个问题---你们在应用全局变量时考虑了全局变量是否有效的问题了吗?
如果对全局变量只有一个写入者,而有多处读取者,您仅仅关心变量的最新写入值的情况下,您怎么能断定不能使用全局变量呢?
------------------------------------------------------------------------------------------------------------------
Darren:
全局变量在某些情况下是非常好用的。如果我有一些静态数据,这些静态数据必须在多个VI中共享,这种情况下,我会使用全局变量存储这些静态数据。(所谓静态数据就是不需要改变的数据,常量)。最常见的例子是需要给用户提示的文本字符串。如果我有一个非常复杂的GUI,需要在很多地方向用户提示文本信息。我会创建一个全局变量VI,把这些字符串创建为全局变量,并且按照字母顺序排序(通过设置TAB ORDER)。这样我们需要在程序框图中使用全局变量时,直接拖入并选择我们需要的。这种方法可以很容易使我们的应用程序本地化,因为所有的显示字符串集中在一个VI之中,而不是散布在各个VI之中,很容易集中处理。
我另外一次使用全局变量是在一个子面板应用中,因为我的子面板中的VI不需要和主VI交换信息,所以我将子面板VI中的数据写入全局变量,供应用程序其它部分读取。因为不需要同步化以及只有子面板中的VI写入数据,保证了只有一个写入者,这恰恰是全局变量的最佳工作方式。
------------------------------------------------------------------------------------------------------------------
TBOB:
很高兴能看到同人谈及如何恰当地使用全局变量,而不是简单地说完全不要使用全局变量。我们更应该强调如何恰当地使用全局变量,帮助人们了解数据竞争是如何产生的,以及如何避免竞争情况出现。

我大多数使用全局变量时,是把全局变量作为常量来使用的,比如保存一个GPIB的地址。它们一旦创建后就永远不会再次写入更改,这种情况下,绝对不会出现数据竞争的情况。或者在生产消费者模式中,生产者写入全局变量,而消费者读取全局变量。这种情况下,读的时机是非常重要的。我使消费者不断查询全局变量,是否和原来的值发生变化。换句话说,消费者在数据更新之前可能读取了两次,当然并不很理想。
对于局部变量也是如此,总有它们合适使用的场合,但是必须小心可能会导致的问题。教会人们发现问题和解决问题好于仅仅说避免使用它们。
------------------------------------------------------------------------------------------------------------------
Jasonhill:
我也经常看到要求禁止使用全局变量,在合适的条件下,使用全局变量还是非常有用的。但是程序员还是会不自觉地倾向于滥用它们,任何变量(全局变量、局部变量、LV2全局变量)在使用时需要格外小心,“连线”还是最安全的。
我非常讨厌上下或者左右堆积大量的控件,在程序框图中多达20几个层叠顺序结构中,到处散布一些全局变量或者局部变量。
至于你提及的生产者消费者模式,我还是愿意使用队列来完成,使用队列可以使我们不需要考虑读的时机问题。
------------------------------------------------------------------------------------------------------------------
TBOB
在生产者消费者模式中,使用队列(我也倾向于使用队列,而不是变量)同样存在问题。消费者可能运行速度高于消费者,此时可能读回空数据,必须在编程中检查是否是否读回空数据。
------------------------------------------------------------------------------------------------------------------
TST:
在生产消费者模式使用队列时,我愿意使用超时的默认值-1,这意味着消费者在没有数据时不会执行一个循环,也不需要检查超时是否发生了。
------------------------------------------------------------------------------------------------------------------
TITOU:
真是个好题目!
全局变量是魔鬼吗?------我愿意这样回答:不是,只要你遵循了全局变量的工作规则。
我经常建议避免使用全局变量,但是的确在特定的场合,我还是会使用全局变量,因为使用全局变量的确非常方便。
使用但不要滥用------------------------------------------------------------------------------------------------------------------
 ROBERT:
即使在基于文本的编程语言中,采用封装和抽象本身就倾向于不使用全局变量。理想的结构应该是这样的,如果函数需要一个变量,必须从函数的调用者哪里接收这个变量。尽管如此,即使在这样的编程环境中,还是需要有限度的和合理的利用全局变量。正如上面的帖子中指出的那样,一个写入者,多个读取者。亦或需要在整个程序应用,但是不需要改变的场合。
我经常采样下面的方式。在程序启动时,先运行一个配置函数或者“参数设置”函数,此时没有其它的进程工作,数据采集也尚未进行。此时为程序的其它部分创建全局变量是合理的。
------------------------------------------------------------------------------------------------------------------
KEVIN:
我在程序开发时,习惯于在多个消费者情况下使用“通告”。通常情况下,只有一个生产者。但是像全局变量情况,可能会有几个“潜在”的生产者。
对于使用通告,消费者可以进行选择。通告可以不管消费这是否已经消费了先前的数据,随时查询最新的数据,这类似于全局变量。通告也可以一直等待,直至有最新更新的数据,避免不断的轮询数据,加重CPU的负担,这个是全局变量无法实现的。
-----------------------------------------------------------------------------------------------------------------
BEN:
很抱歉没能早点参与这个话题的讨论。我并非求全责备,但是全局变量存在下列主要问题:
1、数据拷贝
2、利用线程
3、对于一个写入者,多个读取者,OK.但是这要求开发者必须确认只有唯一一个写入者。这对一个拥有800多个VI,有些是动态载入的情况下,是很难做到的。
4、灵活性 。如果你使用一个LV2型全局变量,需要的情况下,你可以很安全地添加新的新的写入者。在编写大型应用时,这的确是令人头疼的问题。
5、性能。LV2全局变量可以很容易重用缓存,全局变量不行。
-----------------------------------------------------------------------------------------------------------------
RAY:
我同意大家的看法。我通常用全局变量保持静态数据,比如IP地址。
-----------------------------------------------------------------------------------------------------------------