一个快速编码技巧以某种方式最终使 Julia 中的代码变慢

ten*_*ten 6 optimization types julia

我听说意识到类型稳定性对 Julia 编程的高性能有很大贡献,所以我尝试测量将类型不稳定的函数重写为类型稳定版本时可以节省多少时间。正如很多人所说,我认为类型稳定的编码当然比类型不稳定的编码具有更高的性能。然而,结果却相反:

# type-unstable vs type-stable

#?type-unstable
function positive(x)
    if x < 0
        return 0.0
    else
        return x
    end
end

# type-stable
function positive_safe(x)
    if x < 0
        return zero(x)
    else
        return x
    end
end

@time for n in 1:100_000_000
    a = 2^( positive(-n) + 1 )
end

@time for n in 1:100_000_000
    b = 2^( positive_safe(-n) + 1 )
end
Run Code Online (Sandbox Code Playgroud)

结果:

0.040080 seconds
0.150596 seconds
Run Code Online (Sandbox Code Playgroud)

我无法相信这。我的代码中是否有一些错误?或者这是事实?

任何信息,将不胜感激。

语境

  • 操作系统和版本:Windows 10
  • 浏览器和版本:Google Chrome 90.0.4430.212?官方版本??64 位)
  • JupyterLab 版本:3.0.14

@btime 结果

只是将@time 替换为@btime 为我上面的代码

@btime for n in 1:100_000_000
    a = 2^( positive(-n) + 1 )
end
# -> 1.500 ns 

@btime for n in 1:100_000_000
    b = 2^( positive_safe(-n) + 1 )
end
# -> 503.146 ms
Run Code Online (Sandbox Code Playgroud)

还是很奇怪。

DNF 给我看的完全相同的代码

using BenchmarkTools

@btime 2^(positive(-n) + 1) setup=(n=rand(1:10^8))
# -> 32.435 ns (0 allocations: 0 bytes)
@btime 2^(positive_safe(-n) + 1) setup=(n=rand(1:10^8))

#-> 3.103 ns (0 allocations: 0 bytes)

Run Code Online (Sandbox Code Playgroud)

按预期工作。

我仍然不明白发生了什么。我觉得我必须更好地了解@btime基准测试过程的使用。

顺便说一句,正如我上面所说,我正在 Jupyterlab 上尝试这种基准测试。

Vit*_*huk 6

您的基准测试的问题,您测试不同的逻辑代码:

2 ^ (integer value)2 ^ (float value)

但最关键的部分,如果ab没有在循环之前定义,Julia 编译器可能会删除该块。你的表现非常依赖是ab定义,并且在全球范围内或没有被定义。

电源是代码中耗时的核心部分(不是类型不稳定的部分)。

positive函数Float在您的情况下positive_safe返回,返回 Int)

类似于您的案例(按逻辑)的代码可能如下所示:

# type-unstable

function positive(x)
    if x < 0
        return 0.0
    else
        return x
    end
end

# type-stable
function positive_safe(x)
    if x < 0
        return 0.0
    else
        return Float64(x)
    end
end

function test1()
    a = 0.0
    for n in 1:100_000_000
        a += 2^( positive(-n) + 1 )
    end
    a
end

function test2()
    b = 0.0
    for n in 1:100_000_000
        b += 2^( positive_safe(-n) + 1 )
    end
    b
end

@btime test1()
@btime test2()  
Run Code Online (Sandbox Code Playgroud)
98.045 ms (0 allocations: 0 bytes)
2.0e8

  97.948 ms (0 allocations: 0 bytes)
2.0e8
Run Code Online (Sandbox Code Playgroud)

结果几乎相同,因为您的类型不稳定不是这种情况的瓶颈。

如果要测试函数(这与未定义 a/b 时的情况类似):

function test3()
    b = 0.0
    for n in 1:100_000_000
        b += 2^( positive_safe(-n) + 1 )
    end
    nothing
end

@btime test3()
Run Code Online (Sandbox Code Playgroud)

基准测试将显示结果:

1.611 ns 
Run Code Online (Sandbox Code Playgroud)

这不是因为我的笔记本电脑每 1.611 ns 进行了 100_000_000 次迭代,而是因为 Julia 编译器足够聪明,可以理解该test3函数可能被替换为nothing.


DNF*_*DNF 2

这是基准测试问题。该@time宏不适合微基准测试。使用 BenchmarkTools.jl 包,并阅读用户手册。基准测试时很容易犯错误。

操作方法如下:

jl> using BenchmarkTools

jl> @btime 2^(positive(-n) + 1) setup=(n=rand(1:10^8))
  6.507 ns (0 allocations: 0 bytes)
2.0

jl> @btime 2^(positive_safe(-n) + 1) setup=(n=rand(1:10^8))
  2.100 ns (0 allocations: 0 bytes)
2
Run Code Online (Sandbox Code Playgroud)

如您所见,类型稳定函数速度更快。

  • @ForceBru BenchmarkTools 包负责编译和预热,选择适当的执行次数,并计算各种统计数据。它还允许您插入变量以避免可能导致类型不稳定的全局问题。“@time”宏不会做这些事情,至少,必须手动将代码包装在函数中,第一次调用基准测试以避免包括编译时间,并使用循环来减少统计噪声。 (2认同)
  • @BenoitPasquier 我真的不知道。显然,编译器能够不断折叠不安全循环的表达式,也许额外的函数调用“zero()”以某种方式阻止了第二个循环中的情况。但这表明微基准测试是多么挑剔,人们应该阅读性能提示,将代码包装在函数中,并且通常要小心过度解释结果。 (2认同)