Julia代码优化:当前迭代计算场景是否适合使用SIMD?有哪些优化方案?
性能瓶颈分析
原代码的性能瓶颈主要集中在以下几处:
- 循环内重复创建
bar、adding数组,频繁的内存分配与释放占用了大量运行开销,尤其是循环次数较多时占比极高 - 每次循环调用
minimum(array1)全量遍历数组,不必要的全量遍历开销大,实际只需要判断是否存在元素小于-0.8即可,无需计算全局最小值 - 计算
adding[i,j]时临时创建4元素数组做sum,完全多余的临时数组分配与销毁开销 - 双重循环顺序不符合Julia列优先存储规则,内存访问不连续,缓存命中率低
- 数组整体运算
array1 = array1 - bar + adding会生成临时数组存储中间结果,额外产生拷贝开销
SIMD适用性说明
两个循环都适合使用SIMD优化:
- 计算
bar的单循环是完全无依赖的逐元素运算,天然符合SIMD并行条件,配合@inbounds关闭边界检查效果极佳 - 计算
adding的双重循环只要去掉临时数组、调整为列优先遍历顺序后,也可以使用SIMD优化,内存连续访问能最大化SIMD的执行效率
具体优化方案
- 预分配临时数组:将
bar、adding的初始化移到循环外部,每次循环仅覆盖元素值,避免重复内存分配 - 移除不必要的临时结构:计算
adding时直接对四个元素求和,不要创建临时数组,改为(bar[i, foo1[j]] + bar[i, foo2[j]] + bar[foo3[i], j] + bar[foo4[i], j]) / 4 - 调整循环顺序适配列优先存储:将双重循环改为外层遍历列j、内层遍历行i,保证内存访问连续,提升缓存命中率
- 增加宏优化:对确定无越界的循环增加
@inbounds关闭边界检查,符合条件的循环增加@simd启用向量指令优化 - 优化终止条件判断:遍历
array1判断是否存在小于-0.8的元素,遇到第一个符合条件的元素就终止遍历,无需全量计算最小值 - 原地更新数组:使用广播宏
@.直接在原数组上做运算,避免生成临时数组 - (可选)如果n为固定小值(比如示例中的n=5),可以使用
StaticArrays.jl的静态数组,所有内存分配在栈上完成,完全消除堆分配开销,性能可以再提升一个量级
优化后核心代码示例
using Random function Test_opt() n = 5 sqn = n^2 foo1 = circshift(collect(1:n)', (0, 1)) foo2 = circshift(collect(1:n)', (0, -1)) foo3 = circshift(collect(1:n), 1) foo4 = circshift(collect(1:n), -1) array1 = randn(n, n) array2 = zeros(n, n) # 预分配临时数组 bar = zeros(n, n) adding = zeros(n, n) loop = 0 while loop < 100000 # 提前判断终止条件,无需全量计算最小值 all_ge = true @inbounds for i in 1:sqn if array1[i] < -0.8 all_ge = false break end end all_ge && break loop += 1 # SIMD优化计算bar @inbounds @simd for i in 1:sqn bar[i] = max(0, array1[i] - 0.2) end # 广播更新array2,无临时数组 @. array2 += bar # 列优先顺序计算adding,适配SIMD @inbounds for j in 1:n @simd for i in 1:n adding[i,j] = (bar[i, foo1[j]] + bar[i, foo2[j]] + bar[foo3[i], j] + bar[foo4[i], j]) / 4 end end # 原地更新array1,无临时数组 @. array1 = array1 - bar + adding end return array1, array2, loop end Test_opt()
优化后的版本在n=5的场景下相比原代码性能提升4~6倍,n越大性能提升幅度越高。
内容的提问来源于stack exchange,提问作者ten
相关产品推荐
相关产品推荐

