我们来看一下正在执行sumtest1
的LLVM代码:
julia> @code_llvm sumtest1(10^9)
define double @julia_sumtest1_21391(i64) {
top:
%1 = icmp sgt i64 %0, 0
%2 = select i1 %1, i64 %0, i64 0
%3 = icmp eq i64 %2, 0
br i1 %3, label %L3, label %L.preheader
L.preheader:
%4 = icmp sgt i64 %0, 0
%smax = select i1 %4, i64 %0, i64 0
br label %L
L:
%lsr.iv = phi i64 [ %smax, %L.preheader ], [ %lsr.iv.next, %L ]
%s.0 = phi double [ %5, %L ], [ 0.000000e+00, %L.preheader ]
%5 = fadd double %s.0, 2.800000e+01
%lsr.iv.next = add i64 %lsr.iv, -1
%6 = icmp eq i64 %lsr.iv.next, 0
br i1 %6, label %L3, label %L
L3:
%s.1 = phi double [ 0.000000e+00, %top ], [ %5, %L ]
ret double %s.1
}
这可能很难理解,但在循环的主体中有一件事情格外突出,L
:
%5 = fadd double %s.0, 2.800000e+01
每次迭代都会将预先计算的常数28.0
加到累加器s
中。编译器可以知道您从未更改任何局部变量,因此它知道每次都添加相同的总和。循环必须执行的唯一原因是重复浮点加法不完全等同于乘法。如果将所有局部变量更改为整数,其中重复加法完全等同于乘法,则循环将完全被消除:
julia> @time sumtest1_int(10^9)
0.000005 seconds (6 allocations: 192 bytes)
28000000000
julia> @code_llvm sumtest1_int(10^9)
define i64 @julia_sumtest1_int_21472(i64) {
top:
%1 = icmp slt i64 %0, 1
br i1 %1, label %L3, label %L.preheader
L.preheader:
%2 = icmp sgt i64 %0, 0
%.op = mul i64 %0, 28
%3 = select i1 %2, i64 %.op, i64 0
br label %L3
L3:
%s.1 = phi i64 [ 0, %top ], [ %3, %L.preheader ]
ret i64 %s.1
}
在 Julia 中,这大致翻译为:
sumtest1_int(N) = N < 1 ? 0 : ifelse(N > 0, N*28, 0)
那是有些冗余的,因为可以将代码简化为 ifelse(N > 1, N*28, 0)
(也可以简化为只有 28N
,因为不关心 N
的负值),但这仍然比执行循环要快得多。
sumtest2
函数无法被轻松地分析或优化。它需要证明数组 A
永远不会改变,这相当困难。所以编译器别无选择,必须做所有工作,这当然比不做更慢。在您的模拟中,使用局部变量仍可能比将值存储在数组中快,但也可能不是。您必须测量执行某些更难完全优化掉的代码才能确定。
sumtest1
,但都没有进行这种乘法优化(它们成功将循环每次缩减为加上 28.0)。你的 Julia 是否仍然可以实现此功能?如果可以,请问你使用的是哪个版本? - Dan Getz