为什么这两段Julia代码的性能如此不同?

时间:2018-10-31 23:18:14

标签: julia

function c1()
        x::UInt64 = 0
        while x<= (10^8 * 10)
                x+=1
        end
end

function c2()
        x::UInt64 = 0
        while x<= (10^9)
                x+=1
        end
end

function c3()
        x::UInt64 = 0
        y::UInt64 = 10^8 * 10
        while x<= y
                x+=1
        end
end

function c4()
        x::UInt64 = 0
        y::UInt64 = 10^9
        while x<= y
                x+=1
        end
end

应该一样吧?

@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)

2 个答案:

答案 0 :(得分:5)

这是关于Julia使用逐次幂运算对字面量进行编译时的优化。 Julia可以优化是否可以仅通过平方幂或幂为0、1、2、3来达到指数。我相信这是通过将整数x^p的{​​{1}}降低到x^Val{p}并使用编译器专业化(或内联以及某种元编程来完成的,我不确定这里的正确术语是什么,但是就像您在Lisp中会发现的一样;在Julia中使用相似的技术进行源到源的自动区分,请参见Zygote.jl)技术,以在p为0,1时将代码降低为常数,2,3或2的幂。

Julia将p降低到内联 10^8(然后是literal_pow),然后降低到一个常数,然后julia将power_by_squaring降低到得到另一个常量,然后意识到所有的while循环都是不必要的,并删除循环,等等,全部在编译时

如果将constant * 10中的10^8更改为10^7,您将看到它将在运行时评估数字和循环。但是,如果将c1替换为10^810^4,您会发现它将在编译时处理所有计算。我认为,如果指数是2的幂,julia并没有专门针对编译时优化进行设置,而是证明编译器能够针对这种情况优化代码(将代码降低为常数)。

10^2是1,2,3的情况下,Julia进行了硬编码。通过将代码降低到p的内联版本,然后进行编译专用化,再次对此进行了优化。

您可以使用literal_pow@code_llvm宏来查看发生了什么。试试吧。

@code_native

看到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 ;} 只是一个常数,而f()将在运行时评估内容。

我想,如果您想进一步挖掘,茱莉亚(Julia)在this commit附近开始了这种整数幂运算。

编辑:让我们在编译时优化g()

我还准备了一个计算整数整数指数的函数,julia还将使用该函数针对非2幂幂指数进行优化。不过,我不确定在所有情况下都正确。

c2

现在将@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 中的10^9替换为c2,并享受编译时优化的强大功能。

另请参见this question进行逐次平方运算。

请不要按原样使用此函数,因为它会尝试内联所有指数,无论它是否包含文字。您不会想要的。

答案 1 :(得分:1)

第二次更新:查看hckr答案。比我好得多。

更新:这不是一个全面的答案。尽我所能解决,但由于时间限制,我不得不暂时放弃。

我可能不是回答这个问题的最佳人选,因为就编译器优化而言,我知道足够危险。希望能更好地了解Julia的编译器的人会偶然发现这个问题,并能给出更全面的答复,因为从我所看到的情况来看,您的c2函数正在做很多不必要的工作。

因此,这里至少有两个问题在起作用。首先,按现状,c1c2都将始终返回nothing。由于某种原因,我不明白,对于c1,编译器可以解决此问题,而对于c2,则无法解决。因此,在编译之后,c1几乎立即运行,因为从未真正执行算法中的循环。确实:

julia> @btime c1()
  1.535 ns (0 allocations: 0 bytes)

您还可以使用@code_native c1()@code_native c2()看到它。前者只有几行,而后者则包含更多指令。同样值得注意的是,前者不包含对函数<=的任何引用,表明while循环中的条件已被完全优化。

我们可以通过在两个函数的底部添加一个return x语句来处理第一个问题,这将迫使编译器实际解决x的最终值是什么的问题。 。

但是,如果这样做,您会注意到c1仍然比c2快10倍,这是有关示例的第二个令人困惑的问题。

在我看来,即使使用return x,一个足够聪明的编译器仍具有完全跳过循环所需的所有信息。也就是说,它在编译时就知道x的起始值,循环内转换的确切值以及终止条件的确切值。出乎意料的是,如果运行@code_native c1()(在底部添加return x之后),您会注意到它确实已经在本地代码(cmpq $1000000001中计算出了函数返回值。 ):

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)
;}

所以我不太确定为什么它仍在做任何工作!

作为参考,以下是@code_native c2()的输出(在添加return x之后):

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)
;}

显然,c2上还有很多其他工作对我来说没有多大意义。希望更熟悉Julia内部的人能对此有所启发。