Julia使用splat运算符访问矩阵的性能差异及优化方案
两种索引写法的性能差异来源
性能差距本质是编译器能拿到的类型/长度信息不一样,和splat运算符本身没有关系:
- 你用来存坐标的
point是可变长度的Vector{Int},对编译器来说,这种数组的长度只有运行时才能确定。当你用...展开它做索引的时候,编译器没法提前知道你会传几个索引参数,没法直接匹配二维矩阵专用的、高度优化的索引实现,只能走通用的动态参数处理、逐元素边界检查、动态派发逻辑,过程中会产生临时内存分配,就是你benchmark里看到的4次分配共64字节的开销。 - 写
matrix[point[1], point[2]]的时候,编译器可以明确识别到你传入了2个整数索引,直接命中二维矩阵索引的最优路径:直接根据两个索引值计算目标元素的内存偏移,连栈上临时变量都不需要,所以能跑到1.8ns这种接近直接内存读取的极限速度。
提升splat写法性能的方法
核心思路是让编译器在编译期就能拿到待展开集合的长度,最常用的方案是用固定长度的结构存坐标:
不要用动态长度的Vector存维度固定的坐标,换成元组或者StaticArrays.jl提供的静态长度SVector即可。以下是实测对比的代码:
using BenchmarkTools matrix = Matrix{Float64}(undef, 5000, 4000) # 原动态数组写法 point_vec = [1244, 3353] # 固定长度元组写法 point_tuple = (1244, 3353) @btime $matrix[$point_vec...] # 约70ns,4次内存分配 @btime $matrix[$point_vec[1], $point_vec[2]] # 约1.8ns,0分配 @btime $matrix[$point_tuple...] # 约1.8ns,0分配,和显式索引性能完全一致
如果你非要坚持用动态Vector存坐标,没有任何黑魔法能消除这部分开销——毕竟长度信息只有运行时才能拿到,编译器没法提前做优化。
高性能场景的写法选择
不用一竿子打死splat写法,根据场景选择即可:
- 如果你的坐标维度是写代码时就确定的(比如二维、三维网格点这类固定维度的场景):直接用元组/SVector存坐标就行,这时候splat写法既简洁优雅,又没有任何性能损失,完全没必要硬写冗长的显式索引。
- 如果你的坐标维度本身就是运行时才确定的(比如处理任意维度的数组):不管用不用splat,走的都是通用索引逻辑,splat带来的额外开销占比极低,怎么写方便怎么来。
- 如果你手里只有动态Vector存的固定维度坐标,又不想转成固定长度结构,同时这段代码是跑在循环最内层的热路径,对延迟极度敏感:那直接用显式索引写法确实是最省事儿的性能最优解。
内容的提问来源于stack exchange,提问作者Ewout
相关产品推荐
相关产品推荐

