为什么这两段Julia代码的性能如此不同?
第二次更新:查看hckr答案。比我的好多了 更新:这不是一个全面的答案。正如我所能理解的那样,由于时间的限制,我现在不得不放弃 我可能不是回答这个问题的最佳人选,因为就编译器优化而言,我知道的只是足够危险。希望更好地理解Julia编译器的人能够偶然发现这个问题,并给出更全面的回答,因为从我所看到的,您的为什么这两段Julia代码的性能如此不同?,julia,Julia,第二次更新:查看hckr答案。比我的好多了 更新:这不是一个全面的答案。正如我所能理解的那样,由于时间的限制,我现在不得不放弃 我可能不是回答这个问题的最佳人选,因为就编译器优化而言,我知道的只是足够危险。希望更好地理解Julia编译器的人能够偶然发现这个问题,并给出更全面的回答,因为从我所看到的,您的c2函数正在做大量它不需要做的工作 因此,这里至少有两个问题。首先,目前情况下,c1和c2都将始终返回nothing。出于某种原因,我不明白,编译器能够在c1的情况下解决这个问题,但在c2的情况下
c2
函数正在做大量它不需要做的工作
因此,这里至少有两个问题。首先,目前情况下,c1
和c2
都将始终返回nothing
。出于某种原因,我不明白,编译器能够在c1
的情况下解决这个问题,但在c2
的情况下却不行。因此,编译后,c1
几乎立即运行,因为算法中的循环从未实际执行过。事实上:
@time c1()
0.019102 seconds (40.99 k allocations: 2.313 MiB)
@time c1()
0.000003 seconds (4 allocations: 160 bytes)
@time c2()
9.205925 seconds (47.89 k allocations: 2.750 MiB)
@time c2()
9.015212 seconds (4 allocations: 160 bytes)
@time c3()
0.019848 seconds (39.23 k allocations: 2.205 MiB)
@time c3()
0.000003 seconds (4 allocations: 160 bytes)
@time c4()
0.705712 seconds (47.41 k allocations: 2.719 MiB)
@time c4()
0.760354 seconds (4 allocations: 160 bytes)
您还可以使用
@code\u native c1()
和@code\u native c2()
来查看这一点。前者只有几行,而后者包含更多的指令。还值得注意的是,前者不包含对函数的任何引用,这是关于Julia使用幂次平方法对文本进行编译时优化的。Julia能够优化指数是否可以通过平方仅通过幂来达到,或者幂是0,1,2,3。我认为这是通过将整数p
的x^p
降低到x^Val{p}
并使用编译器专门化来实现的(或者内联加上一种元编程,我不确定这里的正确术语是什么,但它类似于您将在Lisp中找到的;类似的技术用于Julia中的源到源自动微分,请参见)如果p
为0,1,2,3或2的幂,则将代码降低到常数的技术
Julia将10^8
降低为内联literal\u pow
(然后通过平方power\u将其降低为一个常数,然后Julia降低constant*10
以获得另一个常数,然后实现所有while循环都是不必要的,并删除循环等等,所有这些都是在编译时进行的
如果在c1
中将10^8
更改为10^7
,您将看到它将在运行时计算数字和循环。但是,如果将10^8
替换为10^4
或10^2
,您将看到它将在编译时处理所有计算。我认为julia并不是专门设置为compile time OPTIME如果指数是2的幂,但事实证明编译器能够优化(将代码降低到常数)仅适用于这种情况的代码
在Julia中对p
为1,2,3的情况进行了硬编码。通过将代码降低到literal\u pow
的内联版本,然后编译专门化,再次对其进行优化
您可以使用@code\u llvm
和@code\u native
宏查看发生了什么。让我们试试
julia> @code_native c2()
.text
; Function c2 {
; Location: REPL[3]:2
pushq %r14
pushq %rbx
pushq %rax
movq $-1, %rbx
movabsq $power_by_squaring, %r14
nopw %cs:(%rax,%rax)
; Location: REPL[3]:3
; Function literal_pow; {
; Location: none
; Function macro expansion; {
; Location: none
; Function ^; {
; Location: intfuncs.jl:220
L32:
addq $1, %rbx
movl $10, %edi
movl $9, %esi
callq *%r14
;}}}
; Function <=; {
; Location: int.jl:436
; Function >=; {
; Location: operators.jl:333
; Function <=; {
; Location: int.jl:428
testq %rax, %rax
;}}}
js L59
cmpq %rax, %rbx
jbe L32
; Location: REPL[3]:6
L59:
movq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %r14
retq
nopw %cs:(%rax,%rax)
;}
请参阅f()
原来只是一个常量,而g()
将在运行时对内容进行求值
我想如果你想挖掘更多,朱莉娅开始了这个整数求幂的技巧
编辑:让我们在编译时优化c2
我还准备了一个计算整数指数的函数,julia也将使用该函数优化非2次幂指数。不过,我不确定它在所有情况下都是正确的
julia> f() = 10^8*10
julia> g() = 10^7*10
julia> @code_native f()
.text
; Function f {
; Location: In[101]:2
movl $1000000000, %eax # imm = 0x3B9ACA00
retq
nopw %cs:(%rax,%rax)
;}
julia> @code_native g()
.text
; Function g {
; Location: In[104]:1
; Function literal_pow; {
; Location: none
; Function macro expansion; {
; Location: none
; Function ^; {
; Location: In[104]:1
pushq %rax
movabsq $power_by_squaring, %rax
movl $10, %edi
movl $7, %esi
callq *%rax
;}}}
; Function *; {
; Location: int.jl:54
addq %rax, %rax
leaq (%rax,%rax,4), %rax
;}
popq %rcx
retq
;}
现在将c2
中的10^9
替换为ipow(10,9)
,享受编译时优化的强大功能
另请参阅通过平方获得的功率
请不要按原样使用此函数,因为它会尝试内联所有的求幂运算,无论它是否由文字组成。您不会希望这样做。不熟悉Julia,但使用一些y::Uint64=(10^8*10)运行代码需要多少时间
和将while中的表达式替换为变量?whilex@Ghukas:editeddd.第一个问题是,您的函数当前总是返回nothing
,出于某种原因,编译器能够为c1
和c3
计算出这一点,并且基本上跳过了整个例程。如何曾经,一旦你将return x
添加到函数的底部,在计时方面仍然存在差异,我不确定我是否能够解释。我仍在考虑这一点。几乎可以肯定,这是某种编译器优化被触发,我只是看不到它是什么。另外,使用Pkg;Pkg.add(“基准测试工具”);使用BenchmarkTools
。现在使用@btime
来计时例程,而不是@time
。这是一个更好的解决方案,您不必为了避免编译时的复杂性而连续运行两次。我敢打赌,这与编译器逻辑有关,如果x不应该是10^7*10^2,那么它将比10^9快100倍?@oneloop I在发布答案时,我跑出了门,所以一切都有点仓促。事后看来,我不喜欢我以前的想法。我看了一下@code\u native
,键入了一个新的答案,这肯定说明了无
的问题,但归根结底,它完全无法解释为什么c2
比慢c1
即使在添加了return x
语句之后。希望有人了解Julia的内部结构,并回答了我认为非常有趣的问题。这将花费我非常、非常长的时间来解决。谢谢你的回答。我学到了很多。不过,我不确定为什么编译器不能ze代表非2次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂次幂
julia> @code_native c1()
.text
; Function c1 {
; Location: REPL[2]:2
movq $-1, %rax
nopw (%rax,%rax)
; Location: REPL[2]:3
; Function <=; {
; Location: int.jl:436
; Function <=; {
; Location: int.jl:429
L16:
addq $1, %rax
cmpq $1000000001, %rax # imm = 0x3B9ACA01
;}}
jb L16
; Location: REPL[2]:6
retq
nopl (%rax)
;}
julia> @code_native c2()
.text
; Function c2 {
; Location: REPL[3]:2
pushq %r14
pushq %rbx
pushq %rax
movq $-1, %rbx
movabsq $power_by_squaring, %r14
nopw %cs:(%rax,%rax)
; Location: REPL[3]:3
; Function literal_pow; {
; Location: none
; Function macro expansion; {
; Location: none
; Function ^; {
; Location: intfuncs.jl:220
L32:
addq $1, %rbx
movl $10, %edi
movl $9, %esi
callq *%r14
;}}}
; Function <=; {
; Location: int.jl:436
; Function >=; {
; Location: operators.jl:333
; Function <=; {
; Location: int.jl:428
testq %rax, %rax
;}}}
js L59
cmpq %rax, %rbx
jbe L32
; Location: REPL[3]:6
L59:
movq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %r14
retq
nopw %cs:(%rax,%rax)
;}
julia> f() = 10^8*10
julia> g() = 10^7*10
julia> @code_native f()
.text
; Function f {
; Location: In[101]:2
movl $1000000000, %eax # imm = 0x3B9ACA00
retq
nopw %cs:(%rax,%rax)
;}
julia> @code_native g()
.text
; Function g {
; Location: In[104]:1
; Function literal_pow; {
; Location: none
; Function macro expansion; {
; Location: none
; Function ^; {
; Location: In[104]:1
pushq %rax
movabsq $power_by_squaring, %rax
movl $10, %edi
movl $7, %esi
callq *%rax
;}}}
; Function *; {
; Location: int.jl:54
addq %rax, %rax
leaq (%rax,%rax,4), %rax
;}
popq %rcx
retq
;}
@inline function ipow(base::Int, exp::Int)
result = 1;
flag = true;
while flag
if (exp & 1 > 0)
result *= base;
end
exp >>= 1;
base *= base;
flag = exp != 0
end
return result;
end