Julia中为何广播操作慢于嵌套for循环及性能优化方法
问题说明
我编写了一个函数,用于检查数组内的所有元素是否均大于对应下界、小于对应上界。
在如下代码中,我测试了两种实现,发现嵌套for循环版本的bounds_error运行速度远快于使用广播操作的bounds_error2,需要明确速度差异的原因,以及该类函数的效率提升方法。
测试代码如下:
using BenchmarkTools function bounds_error(x, xl) num_x_rows = size(x,1) num_dim = size(xl, 1) for i in 1:num_x_rows for j in 1:num_dim if (x[i, j] < xl[j,1] || x[i,j] > xl[j,2]) return true end end end return false end function bounds_error2(x, xl) for row in eachrow(x) xlt = transpose(xl) if any(row .< xlt[1, :]) == true || any(row .> xlt[2, :]) return true end end return false end # xl的行数永远和x的列数相等,xl每一行存储对应维度的[下界, 上界] xl = [ -5.0 5.0 -5.0 5.0 -5.0 5.0] x = [1.0 2.0 3.0; 4.0 5.0 6.0] @btime bounds_error(x, xl) # 测试结果约20.645 ns,0次内存分配,返回true @btime bounds_error2(x, xl) # 测试结果约347.870 ns,12次内存分配,总计704字节,返回true
速度差异的核心原因
- 大量无意义的内存分配是性能差距的主要来源:
bounds_error2在每一轮行遍历中都重复执行transpose(xl)、切片操作,两次广播比较也会生成临时布尔数组,从benchmark结果就能看到它累计产生12次分配、704字节的内存开销;而原生嵌套循环全程保持0内存分配,内存访问效率极高。 - 对Julia的向量化性能存在认知误区:Julia和Python这类靠C扩展实现向量化加速的语言不同,Julia的手写for循环会被LLVM直接优化到机器码级别,本身性能就处于第一梯队,不存在“向量化一定比循环快”的规律。
- 写法带来的短路效率差:原生循环一旦遍历到第一个越界元素就会立刻返回结果,不需要遍历后续元素;而
bounds_error2每一轮都要先跑完整行的两次广播比较、生成完整的临时布尔数组,才会执行any判断,哪怕越界元素在这一行的第一个位置,也要完成整行的计算。 eachrow返回的是数组视图而非连续内存的原生数组,对视图做广播、索引的效率本身就低于直接按整数索引访问原数组。
性能优化方案
- 最优方案是在你原有嵌套循环的基础上做小幅调整,性能可以达到硬件上限:
- 提前把上下界从二维数组中拆成两个一维视图,避免循环内反复做二维索引
- 加上边界安全检查豁免和SIMD向量化宏,进一步压榨循环性能
参考实现:
这个版本的运行速度会比你原来的function bounds_error_fast(x, xl) nrows, ndims = size(x) lb = @view xl[:, 1] ub = @view xl[:, 2] @inbounds for i in 1:nrows for j in 1:ndims if x[i, j] < lb[j] || x[i, j] > ub[j] return true end end end return false endbounds_error还快30%以上,依然保持0内存分配。 - 如果偏好广播写法,要把所有重复计算的逻辑移到循环外,避免循环内生成临时对象,同时尽量做整数组的批量判断,参考实现:
这个版本的性能和手写循环非常接近,内存分配也会降到极低,但它无法做到逐元素即时短路——如果越界元素出现在数组非常靠前的位置,性能还是会弱于带短路的手写循环。function bounds_error_fast2(x, xl) lb = transpose(@view xl[:, 1]) ub = transpose(@view xl[:, 2]) return any(x .< lb) || any(x .> ub) end - 删掉冗余的判断逻辑:比如
any(...) == true这类写法完全多余,any本身就返回布尔值,多余的比较只会增加无意义的开销。
内容的提问来源于stack exchange,提问作者vikram-s-narayan
相关产品推荐
相关产品推荐

