通过优化内存存取提高代码性能
By Stuart McGarrity
大多数Matlab用户都希望他们的代码能够快速运行,尤其是处理很大数据集时。由于内存性能不像CPU性能一样提高,现在的代码经常受内存制约,整体性能受制于存取内存所需的时间。
幸运的是,通过对Matlab储存和读取数据过程的了解,我们可以避免低效的内存使用,提高代码运行速度。
这篇文章描述了三种提高受内存制约的代码性能的方法:
- 在循环中存取数组前预分配空间
- 按列存储和读取数据
- 避免创建不必要的变量
针对每种情况,我们将对比使用优化技术前后代码片段的执行速度。为确保最好的性能,代码片段都是在M文件的函数中,而不是脚本 。由于内存性能与机器有关,因此我们在两台不同的机器上进行性能测试。
在循环中存取数组前预分配空间
当在循环中创建或者重复多次修改数组时,通常需要预先分配数组空间。在上述三种技术中,最常见的这种性能提高效果最好。
图1中的代码片段使用了一个for循环来计算基于方程 x(k) = 1.05* x(k-1)的数值序列,也就是一个银行账户以5%利息的年度结算。
Figure 1. Preallocating arrays. Code segment 2 executes in 99.8% less time (580 times faster) than segment 1 on machine A, and in 99.7% less time (475 times faster) than segment 1 on machine B.
为什么代码片段2比较快
Matlab语言不需要你在使用变量前声明其类型和大小。因此你可以通过索引大于当前大小的点来增大数组的大小。这种方法对快速完成代码很方便,但是你每次使用它时,Matlab必须分配一个新的更大的数组并将现有数据复制进去。在一个循环中重复多次这一过程的代码速度很慢,效率很低。
代码片段2的第一步为整个数组x分配它所需要的最大空间。在代码执行过程中就不在需要内存分配了。如果M-Lint code checker 发现某处需要预分配空间,就会产生警告信息。
按列存储和读取数据
当处理2-D或N-D数组时,按列读取和存放数据可以使其更容易存取。
图2中的代码片段将2-D数组x中的正数复制到新数组y中。
Figure 2. Storing and accessing data in columns. Code segment 2 executes in 33% less time than segment 1 on machine A, and in 55% less time than segment 1 on machine B.
为什么代码片段2比较快
现代CPU使用高速缓存来减少存取内存的平均时间。当你的代码单调递增的遍历内存位置时可以达到最高的缓存效率。由于Matlab是在单调递增的内存位置中存储矩阵的列,因此逐列处理数据可以达到最高的缓存效率。
代码片段2比较快是因为在内循环中它沿列向下遍历2-D数组的元素。如果算法允许,你也可以通过用一维索引x(k)取代x(r,c)来使缓存效率最佳。
缓存的作用是依赖于数据大小和机器类型的,因此很难预测其性能。例如,当你处理行向量时列的大小影响了内存存取时间,因为它决定了存取内存的步长。 图3表明了在两台机器上对一个有10k个元素的行向量与不同列长度的矩阵进行逐点相乘所用的时间。当列长度为1,也就是1-D向量时运算效率最高。对其他列长度,代码运行较慢。
Figure 3. Time vs. column size for multiplying a 10k row vector.
数组大小也会影响效率。图4表明了在两台机器上对不同大小的1-D数组进行逐点乘法所需的时间。依赖与缓存和内存的特性,处理大数组比处理小数组效率高。
Figure 4. Time per element vs. number of elements for element-wise multiplication.
在有些情况下,大小相差不大的数组所需的处理时间可能会有很大差别,在一台机器上对代码的优化在另一台机器上可能没有同样的效果。
避免创建不必要的变量
当创建新的变量或者已有数据集的函数时,要确保他们对你是必要的,尤其当你的数据集很大的时候。
图5a和5b中的代码片段分别使用片段1、2中的一个算子(图5a)和片段3、4中的M函数(图5b)对矩阵x乘以一个标量。
Figure 5a. Avoiding unnecessary variable creation by calling operators in-place. Code segment 2 executes in 40% less time than code segment 1 on machine A and in 96% less time on machine B.
Figure 5b. Avoiding unnecessary variable creation by calling M-functions in-place. Code segment 4 executes in 40% less time than code segment 3 on machine A and in 59% less time on machine B.
为什么代码片段2和4比较快
在Matlab中,很容易在不注意的时候就对数据进行了复制。对一个大数据集这会使用很多内存,并且分配空间和复制内存都是很耗时的。
代码片段2和4比较快是因为使用原地操作避免产生仅为已有数据修改版本的新变量。这种能力在进行逐点操作(比如.*,+) ,使用某些Matlab函数(如sin,sqrt)以及你自己的M函数时是可用的。为创建一个可以被原地in-place调用的M函数文件,你必须确保输出参数与一个输入参数相匹配。只有当函数本身是由M函数文件而不是脚本调用时调用原地函数才是可行的。The ability to call functions in-place (introduced in MATLAB 7.3) is available only when the function itself is called from a function M-file and not from a script.
如上所述,原地操作可以减少内存浪费,减少计算时间。图6表明了代码片段1和2处理100-million元素数组时Windows任务管理器中内存使用情况(使用了手动单步调试以使内存变化明显)。
Figure 6. Memory usage logged over time as code segments 1 and 2 execute. Click on image to see enlarged view.
当处理大于可用RAM的数据集时减少内存使用尤其重要。操作系统使用交换或硬盘上的文件系统来扩充物理RAM。 然而存取硬盘上的数据要比存取RAM上的数据满得多。为使性能最佳,当Matlab运行时要使内存使用率保持较低以避免系统耗尽RAM。
内存性能术语
Cache.The fast memory on a processor or on a motherboard, used to improve the performance of main memory by temporarily storing data.
RAM. Main physical memory, usually in the range of 1GB to 4GB on 32-bit operating systems.
Memory-bound code. Code whose performance is limited by memory speed, not by CPU speed.
Stride. Memory step size taken by the code when accessing matrix rows.
Vectorized code. Code that operates on arrays and does not use for
loops.
1 - Machine A is a Lenovo T60 ThinkPad, 1.83GHz Intel Core Duo T2400 processor, 2MB L2 cache, 2GB RAM, 32-bit Windows XP. Machine B is a Dual 2.1GHz AMD Opteron 248 processor machine , 1MB L2 Cache, 1GB RAM, 64-bit Linux.