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