VI内存使用
- 更新时间2025-08-27
- 阅读时长45分钟
LabVIEW可处理大量在文本编程语言中必须由用户处理的细节。文本编程语言的一大挑战是内存的使用。在文本编程语言中,编程者必须在内存使用的前后分配及释放内存。同时,编程者必须注意所写入数据不得超过已分配的内存容量。因此,对于使用文本编程语言的编程者来说,最大问题之一是无法分配内存或分配足够的内存。内存分配不当也是很难调试的问题。
LabVIEW的数据流模式解决了内存管理中的诸多难题。在LabVIEW中无需分配变量或为变量赋值。用户只需创建带有连线的程序框图来表示数据的传输。
生成数据的函数将分配用于保存数据的空间。当数据不再使用,其占用的内存将被释放。向数组或字符串添加新数据时,LabVIEW将自动分配足够的内存来管理这些新数据。
这种自动的内存处理功能是LabVIEW的一大特色。然而,自动处理的特性也使用户无法控制其开始的时间。在程序处理大宗数据时,用户也应了解内存分配的发生时机。了解相关的原则有利于用户编写出占用内存更少的程序。同时,由于内存分配和数据复制会占用大量执行时间,了解如何尽可能降低内存占用也有利于提高VI的执行速度。
虚拟内存
操作系统通过虚拟内存使应用程序能够调用比实际物理内存更多的内存。操作系统将物理内存分为不同的块,称为分页。当应用程序或进程将某个地址指定到一块内存时,地址并不直接引用物理内存中的那个内存,而是引用一个分页中的内存。操作系统可再物理内存和硬盘的分页之间切换。
如应用程序或进程需使用的某一块或某一分页不在物理内存中,操作系统可将目前尚未被应用程序或进程使用的一个物理内存分页移动至硬盘并将其替换为所需分页。操作系统将跟踪内存中的分页,并在应用程序或进程需要使用该内存时,将分页的虚拟地址转换为物理内存中的实际地址。
下图显示了两个进程如何在物理内存中交换分页。本例中进程A与进程B同时运行。
| | ||
| 1 进程A | 3 进程B | 5 进程B的内存分页 |
| 2 物理内存 | 4 虚拟内存分页 | 6 进程A的内存分页 |
应用程序或进程所使用的分页的数量取决于可用的硬盘控件而不是可用的物理内存,故应用程序可使用的内存比物理内存实际可提供的更多。应用程序可使用多少虚拟内存取决于应用程序可使用的内存地址的大小。
LabVIEW 虚拟内存使用
LabVIEW(32位)使用32位地址且可访问大地址。
LabVIEW(64位)可访问8 TB或128 TB虚拟内存,具体取决于Windows操作系统的版本。LabVIEW(64位)可访问的实际虚拟内存大小也取决于物理RAM的大小和最大页面文件大小。
VI组件内存管理每个VI均由以下四部分构成:
- 前面板
- 程序框图
- 代码(编译为机器码的框图)
- 数据(输入控件和显示控件值、默认数据、框图常量数据等)
当一个VI加载时,前面板、代码(如代码与操作平台相匹配)及VI的数据都将被加载到内存。如VI由于操作平台或子VI的界面发生改变而需要被编译,则程序框图也将被加载到内存。
如其子VI被加载到内存,则VI也将加载其代码和数据空间。在某些条件下,有些子VI的前面板可能也会被加载到内存。例如,当子VI使用了操纵着前面板控件状态信息的属性节点时。
组织VI组件的重要一点是,由VI的一部分转换而来的子VI通常不应占用大量内存。如创建一个大型但没有子VI的VI,内存中将保留其前面板、代码及顶层VI的数据。然而,如将该VI分为若干子VI,则顶层VI的代码将变小,而代码和子VI的数据将保留在内存中。有些情况下,可能会出现更少的运行时内存使用。
大型VI的编辑时间较长。可通过下列方式避免上述问题:
- (建议)将VI分割为多个子VI。LabVIEW处理小型VI效率较高,VI按层次组织也更易于维护和阅读。
- 根据大型VI的编译代码复杂度配置LabVIEW响应速度和执行速度的优先级。
在数据流编程中,一般不使用变量。数据流模式通常将节点描述为消耗数据输入并产出数据输出。机械地照搬该模式将导致应用程序的内存占用巨大而执行性能迟缓。每个函数都要为输出的目的地产生数据副本。LabVIEW通过同址机制改进了上述模式的不足。
LabVIEW通过元素同址操作机制决定何时重用内存,以及是否在每个输出端创建数据副本。如LabVIEW没有在输入和输出之间复制数据,输入和输出的数据就是同一段内存。
显示缓冲区分配窗口可显示LabVIEW可创建数据副本的位置。
例如,以下程序框图采用了较为传统的编译器方式,即使用两块数据内存,一个用于输入,另一个用于输出。

输入数组和输出数组含有相同数量的元素,且两种数组的数据类型相同。将进入的数组视为数据的缓冲区。编译器并没有为输出创建一个新的缓冲区,而是在输出缓冲区上重复使用了输入缓冲区的内存。同址操作无需在运行时分配内存,故节省了内存,执行速度也得以提高。
然而,编译器无法做到在任何情况下重复使用内存,如下列程序框图所示。

一个信号将一个数据源传递到多个目的地。替换数组子集函数修改了输入数组,并产生输出数组。在该情况下,一个函数的数据是同址数据,一个输出端重用输入数组的数据,其他输出端未重用输入数据。编译器将为这两个函数创建新的数据缓冲区并将数组数据复制到缓冲区中。本程序框图使用约12 KB内存(原始数组使用4 KB,其他两个数据缓冲区各使用4 KB)。
现有如下程序框图。

与前例相同,输入数组至三个函数。但是,本例中的索引数组函数并不对输入数组进行修改。如将数据传递到多个只读取数据而不作任何修改的地址,LabVIEW便不再复制数据。这样,所有的数据都是同址数据。本程序框图使用约4 KB内存。
最后,考虑以下程序框图。

本例中,输入数组至2个函数,其中一个用于修改数据。这两个函数间没有依赖性。因此,可以预见的是至少需要复制一份数据以使“替换数据子集”函数正常对数据进行修改。然而,本例中的编译器将函数的执行顺序安排为读取数据的函数最先执行,修改数据的函数最后执行。“替换数组子集”函数便可重复使用输入数组的缓冲区而不生成一个相同的数组,使该函数的数据为同址数据。如节点排序至为重要,可通过一个序列或一个节点的输出作为另一个节点的输入,令节点排序更为明了。
事实上,编译器对程序框图作出的分析并不尽善尽美。有些情况下,编译器可能无法确定重复使用程序框图内存的最佳方式。
条件输入控件和数据缓冲区特定的程序框图可阻止LabVIEW重复使用数据缓冲区。在子VI中通过一个条件显示控件能阻止LabVIEW对数据缓冲区的使用进行优化。条件显示控件是一个置于条件结构或For循环中的显示控件。如将显示控件放置于一个按条件执行的代码路径中,将中断数据在系统中的流动,同时LabVIEW也不再重新使用输入的数据缓冲区而将数据强制复制到显示控件中。如将显示控件置于条件结构或For循环外,LabVIEW将直接修改循环或结构中的数据,将数据传递到显示控件而不再复制一份数据。可为交替发生的条件分支创建常量,避免将显示控件置于条件结构内。
监控内存使用可通过若干种方法确定内存的使用情况。
如需查看当前VI的内存使用,可选择文件»VI属性并从顶部下拉式菜单中选择内存使用。注意该结果并不包括子VI所占用的内存。通过性能和内存信息窗口可监控内存中所有VI的内存占用情况,并查看子VI的运行速度。VI性能和内存信息窗口可就一个VI每次运行后所占用的字节数及块数的最小值、最大值和平均值进行数据统计。
![]() | 注:要查看精确的VI运行时性能,使用当前版本LabVIEW保存所有VI,不要分离编译代码。原因如下:
|
使用显示缓冲区分配窗口指出程序框图上的特定位置,在这些位置上,LabVIEW以缓冲区形式分配内存。
![]() | 注:只有LabVIEW完整版和专业版开发系统才有显示缓冲区分配窗口。 |
选择工具»性能分析»显示缓冲区分配可打开显示缓冲区分配窗口。勾选需要查看缓存的数据类型,单击刷新按钮。程序框图上可显示黑色小方块,表明LabVIEW在程序框图上创建的数据缓存的位置。
LabVIEWw为每个缓冲区分配的内存容量等于最大数据类型的大小。运行VI时,LabVIEW可能不使用缓冲区存储数据。LabVIEW可在运行时确定是否创建数据副本,当VI依赖动态数据时,无法预知LabVIEW是否使用数据缓存。
如VI需要分配缓冲区,LabVIEW可在缓冲区中创建数据副本。如LabVIEW无法确定缓冲区需要数据副本,LabVIEW仍可在缓冲区中创建数据副本。
![]() | 注:显示缓冲区分配窗口只显示执行缓冲区,在程序框图上使用黑色方括号标出。该窗口不标出前面板的操作缓冲区。 |
一旦确认了LabVIEW缓冲区的位置,便可编辑VI以减少运行VI所需内存。LabVIEW必须为运行VI分配内存,因此不可将所有缓冲区都删除。
如一个必须用LabVIEW对其进行重新编译的VI被更改,则黑色方块将由于缓冲区信息错误而消失。单击显示缓冲区分配窗口中的刷新按钮可重新编译VI并使黑色方块显现。关闭显示缓冲区分配窗口后,黑色方块也随之消失。
![]() | 注:也可使用LabVIEW Desktop Execution Trace工具包检测LabVIEW编程中的线程使用、内存泄漏以及其他问题。 |
选择帮助»关于LabVIEW可查看应用程序的内存使用总量。该总量包括了VI及应用程序本身所占用的内存。在执行一组VI前后查看该总量的变化可大致了解各VI总体上对内存的占用。
高效使用内存的规则上节内容的要点在于编译器智能地作出重复使用内存的决策。编译器何时能复用内存何时不能复用的规则更为复杂。以下规则有助于在实际操作中创建能高效使用内存的VI:
- 将VI分为若干子VI一般不影响内存的使用。在多数情况下,内存使用效率将提高,这是由于子VI不运行时执行系统可取回该子VI所占用的数据内存。
- 只有当标量过多时才会对内存使用产生负面影响,故无须太介意标量值数据副本的存在。
- 使用数组或字符串时,请不要滥用全局变量和局部变量。读取全局或局部变量时,LabVIEW都会生成数据副本。
- 如无必要,不要在前面板上显示大型的数组或字符串。前面板上的输入控件和显示控件会为其显示的数据保存一份数据副本。
![]() | 提示:如要用到图表显示控件,需注意图表历史记录将保留其显示的数据副本。图表历史中存满历史数据后,LabVIEW将停止占用内存。VI重新运行时,LabVIEW不会自动清除图表历史。所以,在程序执行过程中,需清除图表的历史数据。可将空数组写入图表的历史数据属性节点。 |
- 延迟前面板更新属性。将该属性设置为TRUE时,即使控件的值被改变,前面板显示控制器的值也不会改变。操作系统无须使用任何内存为输入控件填充新的值。
![]() | 注:LabVIEW通常不会在调用子VI时打开子VI的前面板。 |
- 如并不打算显示子VI的前面板,那么不要将未使用的属性节点留在子VI上。属性节点将导致子VI的前面板被保留在内存中,造成不必要的内存占用。
- 设计程序框图时,应注意输入与输出大小不同的情况。例如,如使用创建数组或连接字符串函数而使数组或字符串的尺寸被频繁扩大,那么这些数组或字符串将产生其数据副本。
- 在数组中使用一致的数据类型并在数组将数据传递到子VI和函数时监视强制转换点。当数据类型被改变时,执行系统将为其复制一份数据。
- 不要使用复杂和层次化的数据结构,如含有大型数组或字符串的簇或簇数组。这将占用更多的内存。应尽可能使用更高效的数据类型。
- 如无必要,不要使用透明或重叠的前面板对象。这样的对象可能会占用更多内存。
关于优化VI性能的详细信息,见LabVIEW Style Checklist。
前面板的内存问题前面板打开时,输入控件和显示控件会为其显示的数据保存一份数据副本。
下列程序框图显示的是“加1”函数及前面板输入控件和显示控件。

运行该VI时,前面板输入控件的数据被传递到程序框图。“加1”函数将重新使用输入缓冲区。显示控件则复制一份数据用于在前面板上显示。于是,缓冲区便有了三份数据。
前面板输入控件的这种数据保护可防止用户将数据输入输入控件后运行相关VI并在数据传递到后续节点时查看输入控件的数据变化。同样,显示控件的数据也受到保护,以保证显示控件在收到新数据前能准确地显示当前的内容。
子VI的存在使得输入控件和显示控件能作为输入和输出使用。在以下条件下,执行系统将为子VI的输入控件和显示控件复制数据:
- 前面板保存于内存中。可能引起该结果的原因有:
- 前面板已打开。
- VI已更改但未保存(VI的所有组件将保留在内存中直至VI被保存)。
- 前面板使用数据打印。
- 程序框图使用属性节点。
- VI使用局部变量。
- 前面板使用数据记录。
如要使一个属性节点能够在前面板关闭状态下读取子VI中图表的历史数据,则输入控件或显示控件需显示传递到该属性节点的数据。由于大量与其相似的属性的存在,如子VI使用属性节点,执行系统将会把该子VI面板存入内存。
如前面板使用前面板数据记录或数据打印,输入控件和显示控件将维护其数据副本。此外,为便于数据打印,前面板被存入内存,即前面板可以被打印。
如设置子VI在被VI属性对话框或子VI节点设置对话框调用时打开其前面板,那么当子VI被调用时,前面板将被加载到内存。如设置了如之前未打开则在运行后关闭,一旦子VI结束运行,前面板便从内存中移除。
可重复使用数据内存的子VI通常,子VI可轻松地从其调用者使用数据缓冲区,就像其程序框图已被复制到顶层一样。多数情况下,将程序框图的一部分转换为子VI并不占用额外的内存。正如上节内容所述,对于在显示上有特殊要求的VI,其前面板和输入控件可能需要使用额外的内存。
了解何时内存被释放考虑以下程序框图。

平均值VI运行完毕,便不再需要数据数组。在规模较大的程序框图中,确定何时不再需要这些数据是一个十分复杂的过程,因此在VI的运行期间执行系统不释放VI的数据缓冲区。
在macOS上,如内存不足,执行系统将释放任何当前未运行的VI的数据缓冲区。执行系统不会释放前面板输入控件、显示控件、全局变量或未初始化的移位寄存器所使用的内存。
现在将本VI视为一个较大型VI的子VI。数据数组已被创建并仅用于该子VI。在macOS上,如该子VI未运行且内存不足,执行系统将释放子VI中的数据。本例说明了如何利用子VI节省内存使用。
在Windows和Linux平台上,除非VI已关闭且从内存中移除,一般不释放数据缓冲区。内存将按需从操作系统中分配,而虚拟内存在上述平台上亦运行良好。由于碎片的存在,应用程序看起来可能比事实上使用了更多的内存。内存被分配和释放时,应用程序会合并内存以把未使用的块返回给操作系统。
通过请求释放内存函数可在含有该函数的VI运行完毕后释放未用的内存。该函数仅用于高级的性能优化。重新分配未使用的内存在某些情况下可提高性能。但是,过度的内存重新分配可能导致LabVIEW进行反复的内存重新分配而不是重新使用一个分配后的内存。如VI为数量巨大的数据分配内存但从未重新使用过该内存,则可使用该VI。当顶层VI调用一个子VI时,LabVIEW将为该子VI的运行分配一个内存数据空间。子VI运行完毕后,LabVIEW将在直到顶层VI完成运行或整个应用程序停止后才释放数据空间,这将造成内存用尽或性能降低。将“请求释放”函数置于需要释放内存的子VI中。设置标志布尔输入为TRUE,LabVIEW可释放该子VI的数据空间,使内存使用降低。
确定何时输出可重复使用输入缓冲区如输出与输入的大小和数据类型相同且输入暂无它用,则输出可重复使用输入缓冲区。如前所述,在有些情况下,即使一个输入已用于别处,编译器和执行系统仍可对代码的执行顺序进行排序以便在输出缓冲区中重复使用输入。但其做法比较复杂。故不推荐经常使用该法。
显示缓冲区分配窗口可查看输出缓冲区是否重复使用了输入缓冲区。以下程序框图中,如在条件结构的每个分支中都放入一个显示控件,LabVIEW会为每个显示控制器复制一份数据,这将导致数据流被打断。LabVIEW不会使用为输入数组所创建的缓冲区,而是为输出数组复制一份数据。

如将显示控件移出条件结构,由于LabVIEW不必为显示控件显示的数据创建数据副本,故输出缓冲区将重复使用输入缓冲区。在此后的VI运行中,LabVIEW不再需要输入数组的值,因此“递增”函数可直接修改输入数组并将其传递到输出数组。在此条件下,LabVIEW无需复制数据,故输出数组上将不出现缓冲区。如下图所示。

如使用一致的数据类型,LabVIEW可为输出值复用之前为输入值分配的缓冲区。这减少了内存使用和VI的执行时间。如输入和输出不是一种数据类型,输出就无法复用输入的缓冲内存。
例如,如将32位整数与16位整数相加,LabVIEW将把16位整数强制转换为32位整数。编译器为转换后的数据创建一个新的缓冲区,LabVIEW在加函数上显示一个强制转换点。在该例中,假设输入满足其他要求,LabVIEW可使用32位整数输入作为输出的缓冲区。LabVIEW强制转换了16位整数,所以无法复用该输入的内存。
尽可能使用一致的数据类型可避免占用内存。使用统一的数据类型可以避免占用数据类型强制转换时消耗的内存。在一些程序中,可考虑使用较小的数据类型。例如,用4字节单精度数取代8字节双精度数。但是,子VI预期的数据类型和实际连接至子VI的数据类型应尽量保持一致。
生成特定类型数据的内存优化生成某种特定类型的数据时,可能要在程序框图上转换数据类型。可在LabVIEW创建大型数组之前使用转换函数转换数据类型,以优化内存使用。
在下例中,为了与另一个VI的输入数据类型相匹配,输出数据必须为单精度浮点型。LabVIEW创建一个包含1,000个随机数的数组,然后将数组与一个标量值相加。加函数上有一个强制转换点,将单精度浮点标量转换为双精度浮点数。

为了避免出现强制转换点,可使用转换为单精度浮点数函数将双精度浮点数转换为单精度浮点数。

转换函数在数组创建之后转换数组的数据类型,VI使用的内存与使用强制转换点时相同。
下列程序框图演示了在数组创建之前改变数据的类型,从而优化内存使用和提高执行速度。

数据类型转换无法避免时,可在LabVIEW数组创建之前使用转换函数,避免为大型数组分配一块缓冲区。在LabVIEW创建数组之前转换数据类型优化了VI的内存使用。
避免频繁地调整数据大小如输出与输入的大小不同,则输出无法重复使用输入的数据缓冲区。这种情况常见于创建数组、连接字符串及数组子集等改变数组或字符串大小的函数。使用上述函数时,程序将由于频繁复制数据而占用更多数据内存而导致执行速度降低。因此在使用数组及字符串时应避免经常使用上述函数。
范例1:创建数组考虑以下用于创建数据数组的程序框图。该程序框图中创建了一个置于循环中的数组,通过调用“创建数组”函数来连接新的数组元素。输入数组被“创建数组”函数重复使用。VI不断地在每轮循环中根据新数组重新调整缓冲区的大小,以便加入新的数组元素。于是,运行速度将减缓,当循环多次运行时尤为突出。
![]() | 注:对数组、簇、波形和变体进行操作时,可使用元素同址操作结构,以改善VI中的内存使用。 |

如需使每轮循环都有值添加到数组,可在循环边框上使用自动索引功能,便可达到最佳运行性能。对于For循环,VI可预先确定数组的大小(基于连接到总数接线端的值),仅需一次操作便可重新调整缓冲区的大小。

对于While循环,由于数组的大小未知,故自动索引并不十分有效。但是,在While循环中使用自动索引能以较大的递增量增加输出数组的大小,从而避免每循环一次便需调整输出数组的大小。当循环执行完毕,输入数组便被调整为合适的大小。自动索引在While循环和For循环中的运行原理大体相同。

自动索引假定在每轮循环有一个值添加到数组。如必须有条件地将值添加到数组,而数组大小却有其上限,可考虑预先分配数组并使用替换数组子集来填充数组。
数组值填充完毕后,可将数组调整至合适的大小。数组仅创建一次,“替换数组子集”可将输入缓冲区重复用于输出缓冲区。这与在循环中使用自动索引的运行原理极为相似。由于“替换数组子集”无法重新调整数组大小,故在执行该操作时,应注意进行值替换的数组的大小是否足以装入所有数据。
本例的过程如下列程序图所示:

匹配模式函数用于搜索字符串以获取一个模式。如使用不当,该函数可能会由于创建不必要的字符串数据缓冲区而使运行性能降低。
现假设要匹配一个字符串中的整数,可使用[0–9]+作为该函数的正则表达式输入。要在一个字符串中创建一个完全为整数的数组,可使用一个循环并重复调用“匹配正则表达式”直至返回的偏移值为–1。
以下程序框图显示了如何在字符串中扫描其中所有的整数。首先创建一个空数组,令每次循环在上次循环后余下的字符串中搜索合适的数值模式。如模式找到(即偏移值不是–1时),该程序框图将使用“创建数组”函数,把数字添加到一个显示最后结果的数字数组中。当字符串中没有尚未检索过的值时,“匹配正则表达式”将返回–1,程序框图运行完毕。

该程序框图的问题在于,循环中使用了“创建数组”将新值与上一个值连接。一个替代做法是在循环边框上添加自动索引,将数值累加起来。最后数组中将出现一个多余无用的值,该值产生于“匹配正则表达式”函数无法找到匹配值的最后一次循环。使用“数组子集”可清除该值。该过程如下列程序框图所示。

该程序框图的另一问题是,循环每运行一次都会为余下的字符串复制一份不必要的数据。匹配正则表达式
VI组件内存管理
每个VI均由以下四部分构成:
- 前面板
- 程序框图
- 代码(编译为机器码的框图)
- 数据(输入控件和显示控件值、默认数据、框图常量数据等)
当一个VI加载时,前面板、代码(如代码与操作平台相匹配)及VI的数据都将被加载到内存。如VI由于操作平台或子VI的界面发生改变而需要被编译,则程序框图也将被加载到内存。
如其子VI被加载到内存,则VI也将加载其代码和数据空间。在某些条件下,有些子VI的前面板可能也会被加载到内存。例如,当子VI使用了操纵着前面板控件状态信息的属性节点时。
组织VI组件的重要一点是,由VI的一部分转换而来的子VI通常不应占用大量内存。如创建一个大型但没有子VI的VI,内存中将保留其前面板、代码及顶层VI的数据。然而,如将该VI分为若干子VI,则顶层VI的代码将变小,而代码和子VI的数据将保留在内存中。有些情况下,可能会出现更少的运行时内存使用。
大型VI的编辑时间较长。可通过下列方式避免上述问题:
- (建议)将VI分割为多个子VI。LabVIEW处理小型VI效率较高,VI按层次组织也更易于维护和阅读。
- 根据大型VI的编译代码复杂度配置LabVIEW响应速度和执行速度的优先级。
| 注:如给定VI前面板或程序框图的规模超过了屏幕可显示的范围,将其分为子VI更便于使用。 |
数据流编程和数据缓冲区
在数据流编程中,一般不使用变量。数据流模式通常将节点描述为消耗数据输入并产出数据输出。机械地照搬该模式将导致应用程序的内存占用巨大而执行性能迟缓。每个函数都要为输出的目的地产生数据副本。LabVIEW通过同址机制改进了上述模式的不足。
LabVIEW通过元素同址操作机制决定何时重用内存,以及是否在每个输出端创建数据副本。如LabVIEW没有在输入和输出之间复制数据,输入和输出的数据就是同一段内存。
显示缓冲区分配窗口可显示LabVIEW可创建数据副本的位置。
例如,以下程序框图采用了较为传统的编译器方式,即使用两块数据内存,一个用于输入,另一个用于输出。
输入数组和输出数组含有相同数量的元素,且两种数组的数据类型相同。将进入的数组视为数据的缓冲区。编译器并没有为输出创建一个新的缓冲区,而是在输出缓冲区上重复使用了输入缓冲区的内存。同址操作无需在运行时分配内存,故节省了内存,执行速度也得以提高。
然而,编译器无法做到在任何情况下重复使用内存,如下列程序框图所示。
一个信号将一个数据源传递到多个目的地。替换数组子集函数修改了输入数组,并产生输出数组。在该情况下,一个函数的数据是同址数据,一个输出端重用输入数组的数据,其他输出端未重用输入数据。编译器将为这两个函数创建新的数据缓冲区并将数组数据复制到缓冲区中。本程序框图使用约12 KB内存(原始数组使用4 KB,其他两个数据缓冲区各使用4 KB)。
现有如下程序框图。
与前例相同,输入数组至三个函数。但是,本例中的索引数组函数并不对输入数组进行修改。如将数据传递到多个只读取数据而不作任何修改的地址,LabVIEW便不再复制数据。这样,所有的数据都是同址数据。本程序框图使用约4 KB内存。
最后,考虑以下程序框图。
本例中,输入数组至2个函数,其中一个用于修改数据。这两个函数间没有依赖性。因此,可以预见的是至少需要复制一份数据以使“替换数据子集”函数正常对数据进行修改。然而,本例中的编译器将函数的执行顺序安排为读取数据的函数最先执行,修改数据的函数最后执行。“替换数组子集”函数便可重复使用输入数组的缓冲区而不生成一个相同的数组,使该函数的数据为同址数据。如节点排序至为重要,可通过一个序列或一个节点的输出作为另一个节点的输入,令节点排序更为明了。
事实上,编译器对程序框图作出的分析并不尽善尽美。有些情况下,编译器可能无法确定重复使用程序框图内存的最佳方式。
条件输入控件和数据缓冲区
特定的程序框图可阻止LabVIEW重复使用数据缓冲区。在子VI中通过一个条件显示控件能阻止LabVIEW对数据缓冲区的使用进行优化。条件显示控件是一个置于条件结构或For循环中的显示控件。如将显示控件放置于一个按条件执行的代码路径中,将中断数据在系统中的流动,同时LabVIEW也不再重新使用输入的数据缓冲区而将数据强制复制到显示控件中。如将显示控件置于条件结构或For循环外,LabVIEW将直接修改循环或结构中的数据,将数据传递到显示控件而不再复制一份数据。可为交替发生的条件分支创建常量,避免将显示控件置于条件结构内。
监控内存使用
可通过若干种方法确定内存的使用情况。
如需查看当前VI的内存使用,可选择文件»VI属性并从顶部下拉式菜单中选择内存使用。注意该结果并不包括子VI所占用的内存。通过性能和内存信息窗口可监控内存中所有VI的内存占用情况,并查看子VI的运行速度。VI性能和内存信息窗口可就一个VI每次运行后所占用的字节数及块数的最小值、最大值和平均值进行数据统计。
| 注:要查看精确的VI运行时性能,使用当前版本LabVIEW保存所有VI,不要分离编译代码。原因如下:
|
使用显示缓冲区分配窗口指出程序框图上的特定位置,在这些位置上,LabVIEW以缓冲区形式分配内存。
| 注:只有LabVIEW完整版和专业版开发系统才有显示缓冲区分配窗口。 |
选择工具»性能分析»显示缓冲区分配可打开显示缓冲区分配窗口。勾选需要查看缓存的数据类型,单击刷新按钮。程序框图上可显示黑色小方块,表明LabVIEW在程序框图上创建的数据缓存的位置。
LabVIEWw为每个缓冲区分配的内存容量等于最大数据类型的大小。运行VI时,LabVIEW可能不使用缓冲区存储数据。LabVIEW可在运行时确定是否创建数据副本,当VI依赖动态数据时,无法预知LabVIEW是否使用数据缓存。
如VI需要分配缓冲区,LabVIEW可在缓冲区中创建数据副本。如LabVIEW无法确定缓冲区需要数据副本,LabVIEW仍可在缓冲区中创建数据副本。
| 注:显示缓冲区分配窗口只显示执行缓冲区,在程序框图上使用黑色方括号标出。该窗口不标出前面板的操作缓冲区。 |
一旦确认了LabVIEW缓冲区的位置,便可编辑VI以减少运行VI所需内存。LabVIEW必须为运行VI分配内存,因此不可将所有缓冲区都删除。
如一个必须用LabVIEW对其进行重新编译的VI被更改,则黑色方块将由于缓冲区信息错误而消失。单击显示缓冲区分配窗口中的刷新按钮可重新编译VI并使黑色方块显现。关闭显示缓冲区分配窗口后,黑色方块也随之消失。
| 注:也可使用LabVIEW Desktop Execution Trace工具包检测LabVIEW编程中的线程使用、内存泄漏以及其他问题。 |
选择帮助»关于LabVIEW可查看应用程序的内存使用总量。该总量包括了VI及应用程序本身所占用的内存。在执行一组VI前后查看该总量的变化可大致了解各VI总体上对内存的占用。
高效使用内存的规则
上节内容的要点在于编译器智能地作出重复使用内存的决策。编译器何时能复用内存何时不能复用的规则更为复杂。以下规则有助于在实际操作中创建能高效使用内存的VI:
- 将VI分为若干子VI一般不影响内存的使用。在多数情况下,内存使用效率将提高,这是由于子VI不运行时执行系统可取回该子VI所占用的数据内存。
- 只有当标量过多时才会对内存使用产生负面影响,故无须太介意标量值数据副本的存在。
- 使用数组或字符串时,请不要滥用全局变量和局部变量。读取全局或局部变量时,LabVIEW都会生成数据副本。
- 如无必要,不要在前面板上显示大型的数组或字符串。前面板上的输入控件和显示控件会为其显示的数据保存一份数据副本。
| 提示:如要用到图表显示控件,需注意图表历史记录将保留其显示的数据副本。图表历史中存满历史数据后,LabVIEW将停止占用内存。VI重新运行时,LabVIEW不会自动清除图表历史。所以,在程序执行过程中,需清除图表的历史数据。可将空数组写入图表的历史数据属性节点。 |
- 延迟前面板更新属性。将该属性设置为TRUE时,即使控件的值被改变,前面板显示控制器的值也不会改变。操作系统无须使用任何内存为输入控件填充新的值。
| 注:LabVIEW通常不会在调用子VI时打开子VI的前面板。 |
- 如并不打算显示子VI的前面板,那么不要将未使用的属性节点留在子VI上。属性节点将导致子VI的前面板被保留在内存中,造成不必要的内存占用。
- 设计程序框图时,应注意输入与输出大小不同的情况。例如,如使用创建数组或连接字符串函数而使数组或字符串的尺寸被频繁扩大,那么这些数组或字符串将产生其数据副本。
- 在数组中使用一致的数据类型并在数组将数据传递到子VI和函数时监视强制转换点。当数据类型被改变时,执行系统将为其复制一份数据。
- 不要使用复杂和层次化的数据结构,如含有大型数组或字符串的簇或簇数组。这将占用更多的内存。应尽可能使用更高效的数据类型。
- 如无必要,不要使用透明或重叠的前面板对象。这样的对象可能会占用更多内存。
关于优化VI性能的详细信息,见LabVIEW Style Checklist。
前面板的内存问题
前面板打开时,输入控件和显示控件会为其显示的数据保存一份数据副本。
下列程序框图显示的是“加1”函数及前面板输入控件和显示控件。
运行该VI时,前面板输入控件的数据被传递到程序框图。“加1”函数将重新使用输入缓冲区。显示控件则复制一份数据用于在前面板上显示。于是,缓冲区便有了三份数据。
前面板输入控件的这种数据保护可防止用户将数据输入输入控件后运行相关VI并在数据传递到后续节点时查看输入控件的数据变化。同样,显示控件的数据也受到保护,以保证显示控件在收到新数据前能准确地显示当前的内容。
子VI的存在使得输入控件和显示控件能作为输入和输出使用。在以下条件下,执行系统将为子VI的输入控件和显示控件复制数据:
- 前面板保存于内存中。可能引起该结果的原因有:
- 前面板已打开。
- VI已更改但未保存(VI的所有组件将保留在内存中直至VI被保存)。
- 前面板使用数据打印。
- 程序框图使用属性节点。
- VI使用局部变量。
- 前面板使用数据记录。
如要使一个属性节点能够在前面板关闭状态下读取子VI中图表的历史数据,则输入控件或显示控件需显示传递到该属性节点的数据。由于大量与其相似的属性的存在,如子VI使用属性节点,执行系统将会把该子VI面板存入内存。
如前面板使用前面板数据记录或数据打印,输入控件和显示控件将维护其数据副本。此外,为便于数据打印,前面板被存入内存,即前面板可以被打印。
如设置子VI在被VI属性对话框或子VI节点设置对话框调用时打开其前面板,那么当子VI被调用时,前面板将被加载到内存。如设置了如之前未打开则在运行后关闭,一旦子VI结束运行,前面板便从内存中移除。
可重复使用数据内存的子VI
通常,子VI可轻松地从其调用者使用数据缓冲区,就像其程序框图已被复制到顶层一样。多数情况下,将程序框图的一部分转换为子VI并不占用额外的内存。正如上节内容所述,对于在显示上有特殊要求的VI,其前面板和输入控件可能需要使用额外的内存。
了解何时内存被释放
考虑以下程序框图。
平均值VI运行完毕,便不再需要数据数组。在规模较大的程序框图中,确定何时不再需要这些数据是一个十分复杂的过程,因此在VI的运行期间执行系统不释放VI的数据缓冲区。
在macOS上,如内存不足,执行系统将释放任何当前未运行的VI的数据缓冲区。执行系统不会释放前面板输入控件、显示控件、全局变量或未初始化的移位寄存器所使用的内存。
现在将本VI视为一个较大型VI的子VI。数据数组已被创建并仅用于该子VI。在macOS上,如该子VI未运行且内存不足,执行系统将释放子VI中的数据。本例说明了如何利用子VI节省内存使用。
在Windows和Linux平台上,除非VI已关闭且从内存中移除,一般不释放数据缓冲区。内存将按需从操作系统中分配,而虚拟内存在上述平台上亦运行良好。由于碎片的存在,应用程序看起来可能比事实上使用了更多的内存。内存被分配和释放时,应用程序会合并内存以把未使用的块返回给操作系统。
通过请求释放内存函数可在含有该函数的VI运行完毕后释放未用的内存。该函数仅用于高级的性能优化。重新分配未使用的内存在某些情况下可提高性能。但是,过度的内存重新分配可能导致LabVIEW进行反复的内存重新分配而不是重新使用一个分配后的内存。如VI为数量巨大的数据分配内存但从未重新使用过该内存,则可使用该VI。当顶层VI调用一个子VI时,LabVIEW将为该子VI的运行分配一个内存数据空间。子VI运行完毕后,LabVIEW将在直到顶层VI完成运行或整个应用程序停止后才释放数据空间,这将造成内存用尽或性能降低。将“请求释放”函数置于需要释放内存的子VI中。设置标志布尔输入为TRUE,LabVIEW可释放该子VI的数据空间,使内存使用降低。
确定何时输出可重复使用输入缓冲区
如输出与输入的大小和数据类型相同且输入暂无它用,则输出可重复使用输入缓冲区。如前所述,在有些情况下,即使一个输入已用于别处,编译器和执行系统仍可对代码的执行顺序进行排序以便在输出缓冲区中重复使用输入。但其做法比较复杂。故不推荐经常使用该法。
显示缓冲区分配窗口可查看输出缓冲区是否重复使用了输入缓冲区。以下程序框图中,如在条件结构的每个分支中都放入一个显示控件,LabVIEW会为每个显示控制器复制一份数据,这将导致数据流被打断。LabVIEW不会使用为输入数组所创建的缓冲区,而是为输出数组复制一份数据。
如将显示控件移出条件结构,由于LabVIEW不必为显示控件显示的数据创建数据副本,故输出缓冲区将重复使用输入缓冲区。在此后的VI运行中,LabVIEW不再需要输入数组的值,因此“递增”函数可直接修改输入数组并将其传递到输出数组。在此条件下,LabVIEW无需复制数据,故输出数组上将不出现缓冲区。如下图所示。
匹配数据类型优化内存使用
如使用一致的数据类型,LabVIEW可为输出值复用之前为输入值分配的缓冲区。这减少了内存使用和VI的执行时间。如输入和输出不是一种数据类型,输出就无法复用输入的缓冲内存。
例如,如将32位整数与16位整数相加,LabVIEW将把16位整数强制转换为32位整数。编译器为转换后的数据创建一个新的缓冲区,LabVIEW在加函数上显示一个强制转换点。在该例中,假设输入满足其他要求,LabVIEW可使用32位整数输入作为输出的缓冲区。LabVIEW强制转换了16位整数,所以无法复用该输入的内存。
尽可能使用一致的数据类型可避免占用内存。使用统一的数据类型可以避免占用数据类型强制转换时消耗的内存。在一些程序中,可考虑使用较小的数据类型。例如,用4字节单精度数取代8字节双精度数。但是,子VI预期的数据类型和实际连接至子VI的数据类型应尽量保持一致。
生成特定类型数据的内存优化
生成某种特定类型的数据时,可能要在程序框图上转换数据类型。可在LabVIEW创建大型数组之前使用转换函数转换数据类型,以优化内存使用。
在下例中,为了与另一个VI的输入数据类型相匹配,输出数据必须为单精度浮点型。LabVIEW创建一个包含1,000个随机数的数组,然后将数组与一个标量值相加。加函数上有一个强制转换点,将单精度浮点标量转换为双精度浮点数。
为了避免出现强制转换点,可使用转换为单精度浮点数函数将双精度浮点数转换为单精度浮点数。
转换函数在数组创建之后转换数组的数据类型,VI使用的内存与使用强制转换点时相同。
下列程序框图演示了在数组创建之前改变数据的类型,从而优化内存使用和提高执行速度。
数据类型转换无法避免时,可在LabVIEW数组创建之前使用转换函数,避免为大型数组分配一块缓冲区。在LabVIEW创建数组之前转换数据类型优化了VI的内存使用。
避免频繁地调整数据大小
如输出与输入的大小不同,则输出无法重复使用输入的数据缓冲区。这种情况常见于创建数组、连接字符串及数组子集等改变数组或字符串大小的函数。使用上述函数时,程序将由于频繁复制数据而占用更多数据内存而导致执行速度降低。因此在使用数组及字符串时应避免经常使用上述函数。
范例1:创建数组
考虑以下用于创建数据数组的程序框图。该程序框图中创建了一个置于循环中的数组,通过调用“创建数组”函数来连接新的数组元素。输入数组被“创建数组”函数重复使用。VI不断地在每轮循环中根据新数组重新调整缓冲区的大小,以便加入新的数组元素。于是,运行速度将减缓,当循环多次运行时尤为突出。
| 注:对数组、簇、波形和变体进行操作时,可使用元素同址操作结构,以改善VI中的内存使用。 |
如需使每轮循环都有值添加到数组,可在循环边框上使用自动索引功能,便可达到最佳运行性能。对于For循环,VI可预先确定数组的大小(基于连接到总数接线端的值),仅需一次操作便可重新调整缓冲区的大小。
对于While循环,由于数组的大小未知,故自动索引并不十分有效。但是,在While循环中使用自动索引能以较大的递增量增加输出数组的大小,从而避免每循环一次便需调整输出数组的大小。当循环执行完毕,输入数组便被调整为合适的大小。自动索引在While循环和For循环中的运行原理大体相同。
自动索引假定在每轮循环有一个值添加到数组。如必须有条件地将值添加到数组,而数组大小却有其上限,可考虑预先分配数组并使用替换数组子集来填充数组。
数组值填充完毕后,可将数组调整至合适的大小。数组仅创建一次,“替换数组子集”可将输入缓冲区重复用于输出缓冲区。这与在循环中使用自动索引的运行原理极为相似。由于“替换数组子集”无法重新调整数组大小,故在执行该操作时,应注意进行值替换的数组的大小是否足以装入所有数据。
本例的过程如下列程序图所示:
范例2:字符串搜索
匹配模式函数用于搜索字符串以获取一个模式。如使用不当,该函数可能会由于创建不必要的字符串数据缓冲区而使运行性能降低。
现假设要匹配一个字符串中的整数,可使用[0–9]+作为该函数的正则表达式输入。要在一个字符串中创建一个完全为整数的数组,可使用一个循环并重复调用“匹配正则表达式”直至返回的偏移值为–1。
以下程序框图显示了如何在字符串中扫描其中所有的整数。首先创建一个空数组,令每次循环在上次循环后余下的字符串中搜索合适的数值模式。如模式找到(即偏移值不是–1时),该程序框图将使用“创建数组”函数,把数字添加到一个显示最后结果的数字数组中。当字符串中没有尚未检索过的值时,“匹配正则表达式”将返回–1,程序框图运行完毕。
该程序框图的问题在于,循环中使用了“创建数组”将新值与上一个值连接。一个替代做法是在循环边框上添加自动索引,将数值累加起来。最后数组中将出现一个多余无用的值,该值产生于“匹配正则表达式”函数无法找到匹配值的最后一次循环。使用“数组子集”可清除该值。该过程如下列程序框图所示。
该程序框图的另一问题是,循环每运行一次都会为余下的字符串复制一份不必要的数据。在“匹配正则表达式”上有一个输入可用于表示搜索的起点。如仍记得前一次循环的偏移值,可用该值表示下一次循环搜索的起点。该过程如下列程序框图所示。
开发高效的数据结构
在上一范例中已提到层次化数据结构,如包含大型数组或字符串的簇或簇数组等无法被高效地使用。本节将就其原因和如何选择高效的数据类型展开讨论。
对于复杂的数据结构而言,在访问和更改数据结构中元素的同时,难以不生成被访问元素的数据副本。如这些元素本身很大,如数组或字符串,那么生成其数据副本将占用更多内存和时间。
使用标量数据类型通常效率颇高。同样地,使用其元素为标量的小型字符串或数组也十分高效。以下代码表示如何在一个元素为标量的数组中将其中的一个值递增。
| 注:对数组、簇、波形和变体进行操作时,可使用元素同址操作结构,以改善VI中的内存使用。许多LabVIEW操作要求LabVIEW对数据复制并保存在内存中,因此降低了执行速度且增加到了内存占用。元素同址操作结构可执行常见的LabVIEW操作而无需LabVIEW在内存中复制多份数据。相反,元素同址操作结构对数据元素在内存的同一个位置进行操作,再将元素返回到其在数组、簇、变体或波形中的相同位置。正因为LabVIEW将数据元素在内存的相同位置返回,LabVIEW编译器便无需在内存中额外保留数据的副本。 |
这样做避免了生成整个数组的副本,故十分高效。以索引数组函数所生成的元素为一个标量,可十分高效地创建和使用。
对于簇数组,假定其中的簇仅含有标量,那么也可高效地创建和使用。在以下程序框图中,由于解除捆绑和捆绑函数的使用,元素操作稍嫌复杂。但是,簇可能非常小(标量使用极少内存),因此访问簇元素并将元素替换回原先的簇并不占用大量的系统开销。
下列程序框图显示的是解除捆绑、运算和重新捆绑的高效模式。数据源的连线应仅有两个目的地:“解除捆绑”函数的输入端和“捆绑”函数的中间接线端。LabVIEW将识别出这个模式并生成性能更佳的代码。
在一个簇数组中,每个簇含有大型的子数组或字符串,那么对簇中各元素的值进行索引和更改将占用更多的内存和时间。
对整个数组中的某个元素进行索引将会生成一份该元素的数据副本。这样,簇及其庞大的子数组或字符串都将产生各自的副本。由于字符串和数组的大小各异,复制过程不仅包括实际复制字符串和子数组的系统开销,还包括创建适当大小的字符串和子数组的内存调用。若干次这样的操作不会造成太大影响。然而,如果应用程序频繁地执行这样的操作,内存和执行的系统开销将迅速上升。
解决办法是寻求数据的其他表示形式。以下三个实例分析分别代表了三种不同的应用程序,并就取得各自最佳数据结构提出了建议。
实例分析1:避免复杂的数据类型
现有一应用程序用于记录若干测试的结果。在结果中,需有一个描述测试的字符串和一个存放测试结果的数组。可考虑使用下列前面板所示的数据类型。
要改变数组中的某个元素,须对整个数组中的该元素进行索引。对于簇,则必须对其中的元素解除捆绑以便使用该数组。然后,须替换数组中的一个元素,接着将数组保存到簇中。最后,将簇保存到原来的数组中。本例如下列程序图所示。
每一级解除捆绑或索引的操作所产生的数据都可能生成一份副本。但副本未必一定会生成。复制数据十分占用内存和时间。解决的办法是令数据结构尽可能的扁平。例如,可将本实例中的数据结构分为两个数组。第一个数组是字符串数组。第二个数组是一个二维数组,数组中的每一行代表了某个测试的结果。结果如下列前面板所示。
在此数据结构中,可通过“替换数据子集”函数直接替换一个数组元素,如下列程序框图所示。
实例分析2:混合数据类型全局表
现有一个进行表格信息维护的应用程序。在这个应用程序中,所有数据需可全局访问。表格包含了仪器的设置信息,包括其增益、低压极限、高压极限以及通道的名称。
要使这些数据成为可供全局访问的数据,可考虑创建一组用于访问表格中数据的子VI,如下所示的Change Channel Info VI和Remove Channel Info VI。
以下为实现上述VI的三种不同方案。
常规方案
要实现这个表格,需考虑几种数据结构。首先,使用一个含有一个簇数组的全局变量,数组中的每个簇代表了增益、低压极限、高压极限和通道名称。
如前所述,在这样的数据结构中,通常须经过若干级索引和解除捆绑的操作方可访问数据,因此难以高效地实施。同时,由于这种数据结构聚集了若干不同的信息,因此无法使用搜索1维数组函数来搜索通道。“搜索1维数组”函数可在一个簇数组内搜索一个特定的簇,但无法搜索数个与某个簇元素相匹配的元素。
替代方案1
对于上述实例分析,可将数据保存在两个不同的数组中。一个数组包含了通道名称。另一个数组包含了通道数据。对通道名称数组中的某个通道名称进行索引,使用该索引在另一个数组中找到该通道名相应的通道数据。
注意,字符串数组与数据是分开的,故可通过“搜索1维数组”函数来搜索通道。
在实践中,如果以Change Channel Info VI创建一个含有1000路通道的数组,其执行速度将是上一个方法的两倍。但由于没有其他影响性能的系统开销,因此二者的区别并不明显。
从某个全局变量读取数据时,将会为其生成一份数据副本。这样,每访问一个数组的元素便会生成一份完整的数组数据副本。下一个方法可更有效地避免占用系统开销。
替代方案2
还有一种保存全局数据方法,即使用一个未初始化的移位寄存器。本质上,如不为移位寄存器连接一个初始值,它将在每次调用时记住每个值。
LabVIEW编译器可高效地处理对移位寄存器的访问。读取移位寄存器的值并不一定会生成数据副本。事实上,可对一个保存在移位寄存器中的数组进行索引,甚至改变和更新数组中的值,同时不会生成多余的整个数组的数据副本。移位寄存器的问题在于,只有包含了移位寄存器的VI可访问移位寄存器的数据。但从另一方面来说,移位寄存器的优势在于其模块化。
可指定一个具有模式输入的子VI来读取、改变或清除一个通道,或指定其是否将所有通道的数据清零。
子VI包含了一个While循环,该循环中有两个移位寄存器:一个用于通道数据,另一个用于通道名称。上述移位寄存器都未初始化。接着,在While循环中,可放入一个与模式输入相连的条件结构。视模式的不同,可对移位寄存器中的数据进行读取甚至更改。
下图为一个子VI,其界面能够处理上述三种不同模式。图中仅显示了Change Channel Info的代码。
如元素多达1000个,这个方案的执行速度将是上一个方案的两倍,比常规方案快四倍。
实例分析3:字符串静态全局表
上述实例分析所涉及的应用程序为一个含有混合数据类型且更改频繁的表格。而许多的应用程序中的表格信息往往是静态的。表格能够以电子表格文件的格式读取。一旦载入内存后,该表格可用于查找信息。
在此情况下,实施方案由以下两个函数组成,即Initialize Table From File和Get Record From Table。
实施该表格的方法之一是使用一个二维的字符串数组。注意,编译器将每个字符串保存在位于另一独立内存块中的字符串数组中。如果字符串数量庞大(如超过5000个字符串),那么可将其载入内存管理器。这样的加载可能会由于对象的增多而导致性能的明显下降。
保存大型表格的另一方法是按照单个字符串读取表格。接着创建一个独立的数组,其中含有字符串中每个记录的偏移值。这种做法改变了数据的组织,避免占用上千个相对较小的内存块,而以一个较大的内存块(即字符串)和一个独立的较小内存块(即偏移值数组)来取代。
这种方法在实施时可能较为复杂,但对于大型的表格来说其执行速度将快得多。

