为什么“标量优先”更快?

昨天版主有讲到,julia标量优先速度更快,并举了例子。我回忆了下,产生了点问题。举个例子,假设A,B为一维矢量,长度n

# 假设设置MATLAB函数
function funMatlab(A, B)
    C = sin.(A .* B)    # 展开后循环两次,计算2n次
    F = cos.(A ./ B)    # 同上,2n次
    C.^2 .+ F.^2        # 展开循环相当于三次,计算3n次。共7n次。
end

# 这是Julia函数
function funJulia(a, b)
    C = sin(a*b)    # 对标量2次计算
    F = cos(a/ b)   # 对标量2次计算
    C^2 + F^2       # 对标量3次计算
end

> funJulia.(A, B)   # 总共计算7n次

我好奇的是,两次计算次数不是一样的么?所以“标量优先”优势来自于哪儿呢?

1 个赞

内存。详情请见 More Dots: Syntactic Loop Fusion in Julia (julialang.org)。简单说就是MATLAB之所以需要vectorization,是因为可以直接调用底层C/Fortran函数,但是代价是内存开销。Julia不需要这么做,而应该更接近C/Fortran的代码。

2 个赞

我觉得这个问题还是挺tricky的。
例如CUDA.jl提供的这个例子里,跑的最快的还是向量形式的代码。

确实,这个问题,或者说我提 “标量优先” 其实都是围绕 CPU 这种简单的计算模型来考虑的。主要是想避免大家直接落入“向量化编程可以加速代码”这样的性能误区。

GPU 的场景要更复杂的多,因为 GPU 有它的流水线调度机制,所以很多内存传输开销被压缩到了一个甚至可以忽略不计的程度。但如果我的理解没错的话,CUDA 上最快的策略应该还是去手动写 kernel。从某种意义上来说,也是标量的策略更快,但在 GPU 上手写 kernel 一直属于进阶话题,所以大家用 GPU 都是以向量化形式使用居多。

计算本身的次数是一样的。但是对于这种基础运算来说,计算开销本身的占比可能甚至不如内存传输的开销来的高。不妨思考一下 A 这个矩阵被遍历了几次: funcMatlab(A, B)A 被遍历了两次,而 funcJulia.(A, B)A 只被遍历了一次。

另外之所以我提 “标量优先” 的概念,除了性能因素以外,还有:

  • 标量实现的代码总是可以被轻松封装成向量化代码,但反过来向量化代码却很难拆成标量实现
  • 标量实现的代码整体来说总是比向量化代码来的更简洁、可读性也更强
2 个赞