Skip to main content

编译器优化与内嵌子 VI

从提高程序的可读性、可维护性、可重用性的角度来说,在设计 LabVIEW 程序时,应当经可能多的使用子 VI。基本上,每个相对比较独立的功能都应当被做成子 VI,而子 VI 最大也不应到超过 30 个节点。

但子 VI 过多,可能会对程序的运行效率带来一定影响。

首先,调用子 VI 是有一定的开销的,比如调用子 VI 时需要把参数压栈等,但是这些开销是非常小的,可以忽略不计。

造成嵌入式子 VI 提高整个程序的性能的主要原因是在于 LabVIEW 编译器的优化工作。LabVIEW 编译器是可以比较智能的做一些优化工作的,在不改变程序逻辑的前提下,提高生成代码的执行效率。比如下面列出了其中几种常见的编译器优化方法:

  • 去除死代码:把永远的不会被执行到的代码删除。
  • 转移循环中的不变量:若循环每次迭代都做某些相同的运算,编译器会把这个运算挪到循环之外,制作一次就可以了。
  • 相同代码合并:编译器自动发现程序中对同一数据进行的重复运算,把重复的运算去掉。
  • 常量合并:编译器会发现程序中对常量进行的运算,在编译时就计算他们的结果,把结果直接保存在程序中,这样就不需要每次程序运行都对其进行计算了。

LabVIEW 编译器的优化有一个局限性,就是这些优化措施只能在一个 VI 上进行,不能应用于全局。当把一个子 VI B 的代码合并到上层 VI A 中去,编译器可能就会发现合并后的代码有很多可以优化的地方;若 VI A 和 B 的代码分别在不同的 VI 中,编译器分开查看每个 VI 中的代码,可能就找不出太多可以优化的地方。

LabVIEW 中有一个解决方案,可以兼顾可读性与运行效率:在编写程序时,可以多划分一些子 VI;而编译程序时,又把子 VI 的代码合并到上层 VI 中,使得编译器可以做最大程度的优化。这个解决方案就是 “内嵌子 VI”。

在 VI 属性对话框的 "执行" 页面上有个选项是 "在调用 VI 中内嵌子 VI",英文叫 "Inline SubVI into calling VIs"。内嵌子 VI 有点类似与 C 语言中的 inline 函数。

当这个选项被选中,这个 VI 就变成了内嵌子 VI。当内嵌子 VI 被拖拽到其它 VI 上,从编辑代码的角度上看,它与一般的子 VI 没有什么区别;但是在程序编译的角度来看,它与普通子 VI 是不同的:内嵌子 VI 在编译时,并不是独立存在的,它的代码被全部复制到了调用它的 VI 中。用一个实际的例子来讲,假如一个程序中有两个 VI,A 和 B,A 调用了 B。假如 B 是一个普通的 VI,这个程序便编译成可执行代码后,代码中还是有两个 VI,A 和 B;若 B 是内嵌子 VI,编译好的程序就只剩下一个 VI 了,被扩充了的 A,被扩充的 A 中包含原来 A 和 B 两个 VI 的代码。

需要注意的是:内嵌子 VI 这个选项并不是用的越多程序效率就越高。不恰当的使用内嵌子 VI 也会给效率带来负面影响,比如:内嵌子 VI 会在每一处调用它的地方都插入自己的代码,使得程序体积膨胀,占用过多的内存。因此,使用内嵌子 VI,应该把调用不频繁,输入参数常常为常量的 VI 设为内嵌子 VI;而被程序在多出调用的子 VI 则不需设置成内嵌模式。

下面用一个具体的示例来看一下 LabVIEW 编译器是如何优化程序的:

首先,我们编写一个子 VI,这个 VI 有三个输入;其中两个输入是数据,另一个输入表示对两个输入数据进行何种运算,是相加还是相减等;让后把运算结果输出。

这个 "Inline sub VI.vi" 被设置为内嵌子 VI。一个内嵌子 VI 必须是可重入的。内嵌子 VI 的代码在每个子 VI 被调用的地方都会有一个副本,数据空间就更是要每个调用地方都有自己的副本了。LabVIEW 2011 还不支持嵌入式子 VI 的调试和自动错误处理。所以,在 VI 属性对话框中设置嵌入式子 VI 时,要把其它的设置做相应改动,否则 LabVIEW 会在其它设置项上打个叹号,提示这里的设置有问题。

接下来我们在下面的程序中调用的这个子 VI:

下面我们一起看一下,LabVIEW 的编译器是如何对这个程序进行优化的。为了更直观的展示给读者,我们用一些虚构的程序框图来解释每一步优化过程。这些用于示意的程序框图并不是 LabVIEW 产生的,或优化出来的,而是笔者手工制作的,纯粹用于演示。LabVIEW 的优化只针对编译好的可执行代码,它并不会修改 VI 的源代码(程序框图)。但是经过 LabVIEW 的优化,main.vi 生成的可执行代码,与我们制作的示意程序框图编译成的可执行代码是完全等效的。

因为 "Inline sub VI.vi" 是嵌入式子 VI,对于编译器来说,它的代码是被拷贝到 main.vi 上来的,所以对于编译器来说,它看到的代码是这样的:

在这段代码中,条件结构分支选择器的输入是一个常量 "Add",这就意味着程序每次都只会进入 "Add" 这一分支,而其它分支永远不会被执行到。编译器会把那些执行不到的分支移除,因此,优化后的程序代码等效如下:

程序中循环内所作的运算,在每次迭代中都是相同的,因此它可以被挪到循环之外,只运行一次。优化后代码等效如下:

程序中的平方运算的输入值是一个常量,因此这一运算会在编译时就完成,不必每次运行时再计算,等效优化后代码如下:

程序中,对 data 这个输入数据进行两次完全相同的运算,这是没有必要的,编译器也会将其合并,于是优化后的代码最终等效与如下:

可见,一个看似复杂的程序,经过 LabVIEW 编译器的层层优化,最终运行效率可以媲美一段极其简单的代码。当然这并不是说程序员可以不再关心代码的效率了。编译器毕竟还是能力有限,它只能做简单的优化,程序效率的决定因素还是在于程序员是如何编写代码的。