背景
我的问题是基于一些简单的观察而提出的,这些观察在一定程度上削弱了经验丰富的MATLAB用户通常持有/做出的信仰/假设:
- MATLAB在内置函数和基本语言特性(如向量和矩阵索引)方面进行了非常好的优化。
- 在MATLAB中,循环速度较慢(尽管有JIT),如果算法可以用本机“向量化”的方式表达,则通常应避免使用循环。
结论:核心MATLAB功能高效,试图使用MATLAB代码超越它是难以实现的,如果不是不可能的话。
调查向量索引的性能
下面展示的示例代码是最基本的:我为所有向量条目分配标量值。首先,我分配一个空向量x
:
tic; x = zeros(1e8,1); toc
Elapsed time is 0.260525 seconds.
如果我有一个向量x
,我希望将所有元素都设为相同的值。在实际操作中,您可能会使用不同的方法,例如x = value*ones(1e8,1)
,但是这里的重点是研究向量索引的性能。最简单的方法是编写:
tic; x(:) = 1; toc
Elapsed time is 0.094316 seconds.
我们称其为方法1(根据分配给x
的值)。它似乎非常快(至少比内存分配快)。因为我这里所做的唯一一件事情就是操作内存,所以可以通过计算获得的有效内存带宽并将其与我的计算机的硬件内存带宽进行比较来估算此代码的效率:
eff_bandwidth = numel(x) * 8 bytes per double * 2 / time
在上述示例中,我乘以2
,因为除非使用SSE流式处理,否则在内存中设置值需要同时从向量读取并写入到内存中。eff_bandwidth(1) = 1e8*8*2/0.094316 = 17 Gb/s
我的电脑的STREAM基准内存带宽约为17.9 Gb/s,因此 - 在这种情况下MATLAB确实可以提供接近峰值性能!到目前为止,一切顺利。
如果您想将所有向量元素设置为某个值,则方法1很适合。但是,如果您想每隔step
条目访问元素,则需要用例如1:step:end
替换:
。以下是与方法1的直接速度比较:
tic; x(1:end) = 2; toc
Elapsed time is 0.496476 seconds.
虽然你不会期望方法2表现出任何不同,但显然它造成了巨大的问题:没有理由的5倍减速。我怀疑在这种情况下MATLAB明确地分配了索引向量(1:end
)。这在使用显式向量大小而不是end
时得到了部分确认:
tic; x(1:1e8) = 3; toc
Elapsed time is 0.482083 seconds.
方法2和方法3的表现一样糟糕。
另一种可能性是显式地创建一个索引向量id
并使用它来索引x
。这样可以获得最灵活的索引能力。在我们的情况下:
tic;
id = 1:1e8; % colon(1,1e8);
x(id) = 4;
toc
Elapsed time is 1.208419 seconds.
这真是太厉害了 - 相比方法1,慢了12倍!我知道由于额外的内存使用(用于id
),它应该比方法1表现更差,但为什么比方法2和3要差那么多呢?
让我们试着给循环一次机会 - 尽管听起来可能毫无希望。
tic;
for i=1:numel(x)
x(i) = 5;
end
toc
Elapsed time is 0.788944 seconds.
一个大惊喜——循环比向量化方法快4倍,但仍比方法1、2和3慢。结果发现,在这种特殊情况下,你可以做得更好:
tic;
for i=1:1e8
x(i) = 6;
end
toc
Elapsed time is 0.321246 seconds.
这项研究的最奇怪结果可能是:一个用MATLAB编写的循环明显优于本地向量索引。这肯定不应该是这样的。请注意,JIT编译的循环仍然比方法1几乎获得的理论峰值慢3倍。因此,仍有很大的改进空间。只是出乎意料(更适合的词语是“惊人”)的是通常的“向量化”索引(1:end
)甚至更慢。
问题
- 在MATLAB中,简单索引(方法2、3和4比方法1更慢)非常低效,还是我漏掉了什么?
- 为什么方法4比方法2和3慢那么多?
- 为什么使用
1e8
作为循环边界而不是numel(x)
可以将代码加速2倍?
编辑 阅读了Jonas的评论后,这是另一种使用逻辑索引的方法:
tic;
id = logical(ones(1, 1e8));
x(id) = 7;
toc
Elapsed time is 0.613363 seconds.
比方法4好得多。
为方便起见:
function test
tic; x = zeros(1,1e8); toc
tic; x(:) = 1; toc
tic; x(1:end) = 2; toc
tic; x(1:1e8) = 3; toc
tic;
id = 1:1e8; % colon(1,1e8);
x(id) = 4;
toc
tic;
for i=1:numel(x)
x(i) = 5;
end
toc
tic;
for i=1:1e8
x(i) = 6;
end
toc
end
length(x)
而不是numel(x)
会慢得多。 使用x = x*6;
和方法一一样快。 - R. Schifinitrue(1e8,1)
可能比逻辑转换更快。 - Jonasfor z = 1:1
。在我的机器上(R2012b),对于解决方案1到4没有影响,但是对于5和6会导致一个数量级的减速。换句话说,JIT加速器仍然无法发现双重循环中的低效率,即使外部循环只有一个迭代并且没有任何作用!哦,还有一件事,也许更改您的i
下标。从技术上讲,i
是一个内置函数。出于同样的原因,请避免使用j
。 :-) - Colin T Bowers