性能提示
在以下部分中,我们将简要介绍一些可以帮助您的Julia代码尽可能快地运行的技术。
一般建议
性能关键代码应该在函数内部
任何性能关键的代码都应该在函数内部。 由于Julia编译器的工作原理,函数内部的代码往往比顶级代码运行得更快。
函数的使用不仅对性能很重要:函数更具可重用性和可测试性,并明确正在执行的步骤以及它们的输入和输出是什么, 编写函数,而不仅仅是脚本也是Julia的Styleguide的推荐。
函数应该接受参数,而不是直接对全局变量进行操作,请参阅下一点。
避免无类型的全局变量
无类型全局变量的值在任何时候都可能发生变化,可能导致其类型发生变化。 这使得编译器很难使用全局变量优化代码。 这也适用于类型值变量,即全局级别的类型别名。 变量应该是局部的,或者尽可能作为参数传递给函数。
我们发现全局名称通常是常量,将它们声明为常量可以大大提高性能:
const DEFAULT_VAL = 0
如果已知非常量全局始终为同一类型, 类型应注释; 康斯特 全局变量不需要注释,因为它们的类型是从它们的初始化值推断出来的。
无类型全局变量的使用可以通过在使用时注释它们的类型来优化:
global x = rand(1000)
function loop_over_global()
s = 0.0
for i in x::Vector{Float64}
s += i
end
return s
end
将参数传递给函数是更好的风格。 它带来了更多可重用的代码,并澄清了输入和输出是什么。
|
注意REPL中的所有代码都在全局范围内求值,因此在顶层定义和分配的变量将是*global*变量。 在模块内部的顶级作用域中定义的变量也是全局的。 |
在以下REPL会话中:
julia> x = 1.0
相当于:
julia> global x = 1.0
所以前面讨论的所有性能问题都适用。
用 @时间并注意内存分配
测量性能的有用工具是 @时间宏。 我们在这里用上面的全局变量重复这个例子,但是这次删除了类型注释:
julia> x = rand(1000);
julia> function sum_global()
s = 0.0
for i in x
s += i
end
return s
end;
julia> @time sum_global()
0.011539 seconds (9.08 k allocations: 373.386 KiB, 98.69% compilation time)
523.0007221951678
julia> @time sum_global()
0.000091 seconds (3.49 k allocations: 70.156 KiB)
523.0007221951678
在第一次通话(@时间sum_global())函数被编译。 (如果你还没有使用 @时间在这个会话中,它还将编译计时所需的函数。)你不应该认真对待这次运行的结果。 对于第二次运行,请注意,除了报告时间外,它还表示分配了大量内存。 我们在这里只是计算一个64位浮点数向量中所有元素的总和,所以不需要分配(堆)内存。
我们应该澄清什么 @时间 报告特别是_heap_分配,通常用于可变对象或创建/增长可变大小的容器(例如 阵列 或 Dict,Dict、字符串或其类型仅在运行时已知的"类型不稳定"对象)。 分配(或解除分配)这样的内存块可能需要对libc进行昂贵的函数调用(例如通过 马洛克 在C)中,并且必须跟踪它们以进行垃圾收集。 相比之下,不可变的值,如数字(bignum除外),元组和不可变 结构体s可以更便宜地存储,例如在堆栈或CPU寄存器内存中,因此通常不必担心"分配"它们的性能成本。
意外的内存分配几乎总是您的代码出现问题的迹象,通常是类型稳定性问题或创建许多小型临时数组。 因此,除了分配本身之外,为您的函数生成的代码很可能远非最佳。 认真对待这些迹象,并遵循以下建议。
有关Julia中的内存管理和垃圾回收的更多信息,请参阅 内存管理和垃圾回收。
在这种特殊情况下,内存分配是由于使用了类型不稳定的全局变量 x,所以如果我们通过 x 作为函数的参数,它不再分配内存(下面报告的剩余分配是由于运行 @时间 宏在全局范围内),并且在第一次调用后显着更快:
julia> x = rand(1000);
julia> function sum_arg(x)
s = 0.0
for i in x
s += i
end
return s
end;
julia> @time sum_arg(x)
0.007551 seconds (3.98 k allocations: 200.548 KiB, 99.77% compilation time)
523.0007221951678
julia> @time sum_arg(x)
0.000006 seconds (1 allocation: 16 bytes)
523.0007221951678
看到的1分配是从运行 @时间 宏观本身在全球范围内。 如果我们在函数中运行定时,我们可以看到确实没有执行分配:
julia> time_sum(x) = @time sum_arg(x);
julia> time_sum(x)
0.000002 seconds
523.0007221951678
在某些情况下,您的函数可能需要分配内存作为其操作的一部分,这可能会使上面的简单图片复杂化。 在这种情况下,请考虑使用 工具来诊断问题,或者编写一个将分配与其算法方面分开的函数版本(参见 预分配产出)。
|
注意对于更严重的基准测试,请考虑https://github.com/JuliaCI/BenchmarkTools.jl[BenchmarkTools.jl]包,其中除其他事项外,多次评估函数以减少噪音。 |
将函数分解为多个定义
编写一个函数作为许多小的定义允许编译器直接调用最适用的代码,甚至内联它。
下面是一个"复合函数"的例子,它应该被写成多个定义:
using LinearAlgebra
function mynorm(A)
if isa(A, Vector)
return sqrt(real(dot(A,A)))
elseif isa(A, Matrix)
return maximum(svdvals(A))
else
error("mynorm: invalid argument")
end
end
这可以更简洁,更有效地写成:
mynorm(x::Vector) = sqrt(real(dot(x, x)))
mynorm(A::Matrix) = maximum(svdvals(A))
但是应该注意的是,编译器在优化代码中的死分支方面是相当有效的。 梅诺姆 例子。
工具
Julia及其软件包生态系统包括可帮助您诊断问题并提高代码性能的工具:
* 分析允许您测量正在运行的代码的性能,并确定作为瓶颈的行。 对于复杂的项目,https://github.com/timholy/ProfileView.jl[ProfileView]软件包可以帮助您可视化分析结果。
*Https://github.com/aviatesk/JET.jl[JET]包可以帮助你发现代码中常见的性能问题。
*意外-大内存分配-如报告的 @时间, @分配,或者探查器(通过对垃圾回收例程的调用)--暗示你的代码可能有问题。 如果您没有看到分配的其他原因,请怀疑类型问题。 你也可以从Julia开始 --轨道分配=用户 选择并检查结果 *.梅姆 文件以查看有关这些分配发生位置的信息。 见 内存分配分析。
* @code_warntype 生成代码的表示形式,该表示形式有助于查找导致类型不确定性的表达式。 见 @code_warntype下面。
类型推断
在许多具有可选类型声明的语言中,添加声明是使代码运行得更快的主要方法。 这是朱莉娅的情况。 在Julia中,编译器通常知道所有函数参数、局部变量和表达式的类型。 但是,在一些特定的情况下,声明是有用的。
避免使用抽象类型参数的容器
使用参数化类型(包括数组)时,最好尽可能避免使用抽象类型进行参数化。
考虑以下几点:
julia> a = Real[]
Real[]
julia> push!(a, 1); push!(a, 2.0); push!(a, π)
3-element Vector{Real}:
1
2.0
π = 3.1415926535897...
因为 a 是抽象类型的数组 真实的,它必须能够容纳任何 真实的 价值。 自 真实的 对象可以是任意大小和结构, a 必须表示为单独分配的指针数组 真实的 物体。 但是,如果我们只允许相同类型的数字,例如 漂浮64,以存储在 a 这些可以更有效地存储:
julia> a = Float64[]
Float64[]
julia> push!(a, 1); push!(a, 2.0); push!(a, π)
3-element Vector{Float64}:
1.0
2.0
3.141592653589793
将数字分配到 a 现在将它们转换为 漂浮64 和 a 将存储为可以有效操作的64位浮点值的连续块。
如果您无法避免使用抽象值类型的容器,则有时最好使用以下参数进行参数化 任何 以避免运行时类型检查。 例如。 IdDict{Any, Any} 表现优于 IdDict{Type, Vector}
另见下文的讨论 参数类型。
避免使用抽象类型的字段
可以声明类型,而无需指定其字段的类型:
julia> struct MyAmbiguousType
a
end
这允许 a 是任何类型的。 这通常很有用,但它确实有一个缺点:对于类型的对象 MyAmbiguousType的,编译器将无法生成高性能代码。 原因是编译器使用对象的类型,而不是它们的值来确定如何构建代码。 不幸的是,关于类型对象的推断很少 MyAmbiguousType的:
julia> b = MyAmbiguousType("Hello")
MyAmbiguousType("Hello")
julia> c = MyAmbiguousType(17)
MyAmbiguousType(17)
julia> typeof(b)
MyAmbiguousType
julia> typeof(c)
MyAmbiguousType
的价值 b 和 c 具有相同的类型,但它们在内存中数据的底层表示非常不同。 即使您只在字段中存储数字值 a,一个内存表示的事实 UInt8不同于a 漂浮64也意味着CPU需要使用两种不同类型的指令来处理它们。 由于所需信息在类型中不可用,因此必须在运行时做出此类决策。 这会降低性能。
你可以通过声明的类型做得更好 a. 在这里,我们关注的是 a 可能是几种类型中的任何一种,在这种情况下,自然的解决方案是使用参数。 例如:
julia> mutable struct MyType{T<:AbstractFloat}
a::T
end
这是一个更好的选择
julia> mutable struct MyStillAmbiguousType
a::AbstractFloat
end
因为第一个版本指定了 a 从包装对象的类型。 例如:
julia> m = MyType(3.2)
MyType{Float64}(3.2)
julia> t = MyStillAmbiguousType(3.2)
MyStillAmbiguousType(3.2)
julia> typeof(m)
MyType{Float64}
julia> typeof(t)
MyStillAmbiguousType
字段的类型 a 可以很容易地从 m,但不是从类型 t. 的确,在 t 可以更改字段的类型 a:
julia> typeof(t.a)
Float64
julia> t.a = 4.5f0
4.5f0
julia> typeof(t.a)
Float32
相比之下,一次 m 被构造,类型 m.a 不能改变:
julia> m.a = 4.5f0
4.5f0
julia> typeof(m.a)
Float64
事实上, m.a 从…… m它的类型—加上它的类型不能改变中间函数的事实—允许编译器为对象生成高度优化的代码,比如 m 但不适用于像 t.
当然,所有这一切都是真实的,只有当我们构造 m 具有混凝土类型。 我们可以通过用抽象类型显式构造它来打破这一点:
julia> m = MyType{AbstractFloat}(3.2)
MyType{AbstractFloat}(3.2)
julia> typeof(m.a)
Float64
朱莉娅>m.a=4.5f0
4.5f0
朱莉娅>类型(m.a)
漂浮物32
出于所有实际目的,这些对象的行为与 [医]神秘型.
比较为一个简单的函数生成的大量代码是非常有启发性的
func(m::MyType) = m.a+1
使用
code_llvm(func, Tuple{MyType{Float64}})
code_llvm(func, Tuple{MyType{AbstractFloat}})
由于长度的原因,结果不在这里显示,但您可能希望自己尝试一下。 因为类型在第一种情况下是完全指定的,所以编译器不需要生成任何代码来在运行时解析类型。 这导致更短和更快的代码。
还应该记住,不是完全参数化的类型表现得像抽象类型。 例如,即使完全指定 阵列{T,n} 是混凝土, 阵列 没有给出参数的本身并不具体:
julia> !isconcretetype(Array), !isabstracttype(Array), isstructtype(Array), !isconcretetype(Array{Int}), isconcretetype(Array{Int,1})
(true, true, true, true, true)
在这种情况下,最好避免声明 [医]类型 带场 A::数组 而是将字段声明为 A::数组{T,N} 或作为 a::A,在哪里 {T,N} 或 A 是参数 [医]类型.
当结构的字段是函数或更一般的可调用对象时,前面的建议特别有用。 定义一个结构如下是非常诱人的:
struct MyCallableWrapper
f::Function
end
但从那以后 功能 是抽象类型,每次调用 包装器。f 将需要动态调度,由于访问字段的类型不稳定 f. 相反,你应该写这样的东西:
struct MyCallableWrapper{F}
f::F
end
它具有几乎相同的行为,但会更快(因为类型不稳定性被消除)。 请注意,我们不强加 F<:函数:这意味着没有子类型的可调用对象 功能 也允许用于该字段 f.
避免使用抽象容器的字段
同样的最佳实践也适用于容器类型:
julia> struct MySimpleContainer{A<:AbstractVector}
a::A
end
julia> struct MyAmbiguousContainer{T}
a::AbstractVector{T}
end
julia> struct MyAlsoAmbiguousContainer
a::Array
end
例如:
julia> c = MySimpleContainer(1:3);
julia> typeof(c)
MySimpleContainer{UnitRange{Int64}}
julia> c = MySimpleContainer([1:3;]);
julia> typeof(c)
MySimpleContainer{Vector{Int64}}
julia> b = MyAmbiguousContainer(1:3);
julia> typeof(b)
MyAmbiguousContainer{Int64}
julia> b = MyAmbiguousContainer([1:3;]);
julia> typeof(b)
MyAmbiguousContainer{Int64}
julia> d = MyAlsoAmbiguousContainer(1:3);
julia> typeof(d), typeof(d.a)
(MyAlsoAmbiguousContainer, Vector{Int64})
julia>d=MyAlsoAmbiguousContainer(1:1.0:3);
julia>typeof(d),typeof(d.a)
(MyAlsoAmbiguousContainer,Vector{Float64})
为 [医]维护者,对象由其类型和参数完全指定,因此编译器可以生成优化的函数。 在大多数情况下,这可能就足够了。
虽然编译器现在可以很好地完成其工作,但在某些情况下,_you_可能希望您的代码可以根据_element type_执行不同的操作 a. 通常实现这一点的最佳方法是包装您的特定操作(这里, [医]脚)在一个单独的功能:
julia> function sumfoo(c::MySimpleContainer)
s = 0
for x in c.a
s += foo(x)
end
s
end
sumfoo (generic function with 1 method)
julia> foo(x::Integer) = x
foo (generic function with 1 method)
julia> foo(x::AbstractFloat) = round(x)
foo (generic function with 2 methods)
这使事情变得简单,同时允许编译器在所有情况下生成优化的代码。
但是,在某些情况下,您可能需要为不同的元素类型或 [医]文摘 领域的 a 在 [医]维护者. 你可以这样做:
julia> function myfunc(c::MySimpleContainer{<:AbstractArray{<:Integer}})
return c.a[1]+1
end
myfunc (generic function with 1 method)
julia> function myfunc(c::MySimpleContainer{<:AbstractArray{<:AbstractFloat}})
return c.a[1]+2
end
myfunc (generic function with 2 methods)
julia> function myfunc(c::MySimpleContainer{Vector{T}}) where T <: Integer
return c.a[1]+3
end
myfunc (generic function with 3 methods)
julia> myfunc(MySimpleContainer(1:3))
2
julia> myfunc(MySimpleContainer(1.0:3))
3.0
julia> myfunc(MySimpleContainer([1:3;]))
4
注释取自非类型化位置的值
使用可能包含任何类型值(类型数组)的数据结构通常很方便 数组{Any}). 但是,如果您使用这些结构之一并且碰巧知道元素的类型,则与编译器共享这些知识会有所帮助:
function foo(a::Array{Any,1})
x = a[1]::Int32
b = x+1
...
end
在这里,我们碰巧知道 a 将是一个 Int32. 这样做一个注释有一个额外的好处,如果值不是预期的类型,它将引发运行时错误,可能会在更早的时候捕获某些错误。
在该类型的情况下 a[1] 并不确切地知道, x 可通过以下方式声明 x=转换(Int32,a[1])::Int32. 使用 转换/转换功能允许 a[1] 为任何可转换为 Int32 (如 UInt8),从而通过放宽类型要求来增加代码的通用性。 请注意 转换/转换 在这种情况下,它本身需要一个类型注释来实现类型的稳定性。 这是因为编译器无法推导出函数返回值的类型,甚至 转换/转换,除非已知所有函数参数的类型。
如果类型是抽象的,或者在运行时构造,类型注释不会增强(并且实际上会阻碍)性能。 这是因为编译器不能使用注释来专门化后续代码,并且类型检查本身需要时间。 例如,在代码中:
function nr(a, prec)
ctype = prec == 32 ? Float32 : Float64
b = Complex{ctype}(a)
c = (b + 1.0f0)::Complex{ctype}
abs(c)
end
的注释 c 损害性能。 要编写涉及在运行时构造的类型的执行代码,请使用 函数屏障技术,并确保构造的类型出现在内核函数的参数类型中,以便内核操作被编译器适当地专业化。 例如,在上面的片段中,只要 b 被构造,它可以传递给另一个函数 k,内核。 如果,例如,功能 k 声明 b 作为类型的参数 复杂{T},在哪里 T 是一个类型参数,然后是一个出现在赋值语句中的类型注释 k 表格的:
c = (b + 1.0f0)::Complex{T}
不会妨碍性能(但也没有帮助),因为编译器可以确定 c 当时 k 被编译。
注意Julia何时避免专门化
作为启发式,朱莉娅自动避免 专业三种具体情况下的参数类型参数: 类型, 功能,及 瓦拉格. 当参数在方法中使用时,Julia将始终专业化,但如果参数只是传递给另一个函数则不会。 这通常在运行时没有性能影响和 提高编译器性能。 如果在您的情况下发现它在运行时确实会对性能产生影响,则可以通过向方法声明中添加类型参数来触发特化。 以下是一些例子:
这不会专门化:
function f_type(t) # or t::Type
x = ones(t, 10)
return sum(map(sin, x))
end
但这将:
function g_type(t::Type{T}) where T
x = ones(T, 10)
return sum(map(sin, x))
end
这些不会专门化:
f_func(f, num) = ntuple(f, div(num, 2))
g_func(g::Function, num) = ntuple(g, div(num, 2))
但这将:
h_func(h::H, num) where {H} = ntuple(h, div(num, 2))
这不会专门化:
f_vararg(x::Int...) = tuple(x...)
但这将:
g_vararg(x::Vararg{Int, N}) where {N} = tuple(x...)
一个只需要引入一个单一的类型参数来强制特化,即使其他类型是无约束的。 例如,这也将专门化,并且在参数不是全部相同类型时很有用:
h_vararg(x::Vararg{Any, N}) where {N} = tuple(x...)
请注意 @code_typed并且朋友总是会向您展示专门的代码,即使Julia通常不会专门化该方法调用。 你需要检查 方法内部如果您想查看参数类型更改时是否生成特化,即: 基地。专业化(@which f(。..)) 包含有问题的论点的特化。
编写"类型稳定"函数
在可能的情况下,它有助于确保函数始终返回相同类型的值。 考虑以下定义:
pos(x) = x < 0 ? 0 : x
虽然这看起来足够无辜,但问题是 0 是一个整数(类型 Int型)和 x 可能是任何类型的。 因此,取决于 x,此函数可能返回两种类型之一的值。 这种行为是允许的,并且在某些情况下可能是期望的。 但它可以很容易地固定如下:
pos(x) = x < 0 ? zero(x) : x
避免更改变量的类型
对于在函数中重复使用的变量,存在类似的"类型稳定性"问题:
function foo()
x = 1
for i = 1:10
x /= rand()
end
return x
end
局部变量 x 开始作为一个整数,并在一个循环迭代后成为一个浮点数(的结果 /运算符)。 这使得编译器更难以优化循环的主体。 有几种可能的修复方法:
*初始化 x 与 x=1.0
*声明的类型 x 明确为 X::Float64=1
*通过以下方式使用显式转换 x=oneunit(Float64)
*初始化与第一次循环迭代,以 x=1/兰德(),然后循环 对于i=2:10
单独的内核函数(又名,功能障碍)
许多函数遵循执行一些设置工作的模式,然后运行许多迭代来执行核心计算。 在可能的情况下,将这些核心计算放在单独的函数中是一个好主意。 例如,下面的contrived函数返回一个随机选择的类型的数组:
julia> function strange_twos(n)
a = Vector{rand(Bool) ? Int64 : Float64}(undef, n)
for i = 1:n
a[i] = 2
end
return a
end;
julia> strange_twos(3)
3-element Vector{Int64}:
2
2
2
这应该写成:
julia> function fill_twos!(a)
for i = eachindex(a)
a[i] = 2
end
end;
julia> function strange_twos(n)
a = Vector{rand(Bool) ? Int64 : Float64}(undef, n)
fill_twos!(a)
return a
end;
julia> strange_twos(3)
3-element Vector{Int64}:
2
2
2
Julia的编译器专门用于函数边界处的参数类型的代码,因此在原始实现中它不知道 a 在循环期间(因为它是随机选择的)。 因此,第二个版本通常更快,因为内部循环可以重新编译为 fill_twos! 对于不同类型的 a.
第二种形式通常也是更好的风格,可以导致更多的代码重用。
这种模式在Julia Base的几个地方使用。 例如,请参阅 vcat的 和 hcat,hcat 在https://github.com/JuliaLang/julia/blob/40fe264f4ffaa29b749bcf42239a89abdcbba846/base/abstractarray.jl#L1205-L1206[抽象阵列。jl],或 填满!函数,我们本可以用它来代替我们自己写的 fill_twos!.
功能如 奇怪的,奇怪的 处理不确定类型的数据时发生,例如从可能包含整数、浮点数、字符串或其他内容的输入文件加载的数据。
@code_warntype
宏 @code_warntype(或其功能变体 code_warntype)有时可以帮助诊断类型相关的问题。 这里有一个例子:
julia> @noinline pos(x) = x < 0 ? 0 : x;
julia> function f(x)
y = pos(x)
return sin(y*x + 1)
end;
julia> @code_warntype f(3.2)
MethodInstance for f(::Float64)
from f(x) @ Main REPL[9]:1
Arguments
#self#::Core.Const(f)
x::Float64
Locals
y::Union{Float64, Int64}
Body::Float64
1 ─ (y = Main.pos(x))
│ %2 = (y * x)::Float64
│ %3 = (%2 + 1)::Float64
│ %4 = Main.sin(%3)::Float64
└── return %4
解释的输出 @code_warntype`就像它的表兄弟一样 xref:stdlib/InteractiveUtils.adoc#InteractiveUtils.@code_lowered[@code_lowered`, @code_typed, @code_llvm,和 @code_native,需要一点练习。 您的代码正在以生成已编译机器代码的方式被大量消化的形式呈现。 大多数表达式都由一个类型注释,由 ::T (哪里 T 可能是 漂浮64,例如)。 最重要的特征 @code_warntype是非具体类型用红色显示;由于本文档是用Markdown写的,没有颜色,在本文档中,红色文本用大写表示。
在顶部,函数的推断返回类型显示为 主体::Float64. 接下来的几行代表 f 在Julia的SSA IR表格中。 编号框是标签,表示跳跃的目标(通过 后藤)在你的代码中。 看着尸体,你可以看到发生的第一件事就是 pos机 被调用,返回值被推断为 工会 类型 工会{Float64, Int64} 显示为大写,因为它是非具体类型。 这意味着我们无法知道确切的返回类型 pos机 基于输入类型。 然而,结果 y*x是一个 漂浮64 不管如果 y 是一个 漂浮64 或 Int64 最终结果是 f(x::Float64) 即使某些中间计算是类型不稳定的,它的输出也不会是类型不稳定的。
如何使用这些信息取决于您。 显然,这将是遥远的最好的修复 pos机 要保持类型稳定:如果你这样做了,所有的变量都在 f 将是具体的,其性能将是最佳的。 但是,在某些情况下,这种_ephemeral_类型的不稳定性可能不太重要:例如,如果 pos机 永远不会孤立地使用, f输出是类型稳定的(对于 漂浮64inputs)将保护以后的代码免受类型不稳定的传播影响。 这在固定类型不稳定性困难或不可能的情况下尤其相关。 在这种情况下,上面的提示(例如,添加类型注释和/或分解函数)是遏制类型不稳定造成的"损害"的最佳工具。 另外,请注意,即使Julia Base也具有类型不稳定的函数。 例如,函数 [医芬迪斯特]将索引返回到找到键的数组中,或者 什么都没有 如果没有找到,一个明确的类型不稳定。 为了更容易找到可能很重要的类型不稳定性, 工会s包含任一 失踪 或 什么都没有 是用黄色突出显示的颜色,而不是红色。
以下示例可帮助您解释标记为包含非具体类型的表达式:
*函数体从 机构::联合{T1,T2})
**解释:返回类型不稳定的函数
**建议:使返回值类型-稳定,即使你必须注释它
* 调用Main。g(%%x::Int64)::Union{Float64, Int64}
**解释:调用类型不稳定函数 g.
**建议:修复函数,或在必要时注释返回值
* 调用基地。getindex(%%x::数组{Any,1},1::Int64)::任何
**解释:访问类型不佳数组的元素
**建议:使用具有更好定义类型的数组,或者在必要时注释单个元素访问的类型
* 基地。getfield(%%x,:(:data))::数组{Float64,N} 其中N
**解释:获取非具体类型的字段。 在这种情况下,类型的 x,说 ArrayContainer,有一个领域 数据::数组{T}. 但是 阵列 需要尺寸 N 也是混凝土类型。
**建议:使用混凝土类型,如 阵列{T,3} 或 阵列{T,N},在哪里 N 现在是 ArrayContainer
捕获变量的性能
请考虑以下定义内部函数的示例:
function abmult(r::Int)
if r < 0
r = -r
end
f = x -> x * r
return f
end
功能 阿姆特 返回函数 f 它的论点乘以 r. 内部函数分配给 f 被称为"闭包"。 内部函数也被语言用于 做-块和生成器表达式。
这种代码风格给语言带来了性能挑战。 解析器在将其翻译成较低级别的指令时,通过将内部函数提取到单独的代码块来实质上重新组织上述代码。 "捕获"变量,如 r 内部函数及其封闭范围共享的内容也被提取到堆分配的"框"中,内部函数和外部函数都可以访问,因为语言指定 r 在内部范围中必须与 r 在外部作用域中,即使在外部作用域(或另一个内部函数)修改之后 r.
前一段的讨论提到了"解析器",即当模块包含 阿姆特 首先被加载,而不是稍后的阶段,当它第一次被调用。 解析器并不"知道"这一点 Int型 是固定类型,或者该语句 r=-r 变换一个 Int型 到另一个 Int型. 类型推断的魔力发生在编译的后期阶段。
因此,解析器不知道 r 具有固定类型(Int型). 也不是 r 创建内部函数后不会更改值(因此不需要框)。 因此,解析器为包含抽象类型的对象的box发出代码,例如 任何,这需要运行时类型调度的每个出现 r. 这可以通过申请来验证 @code_warntype 到上述功能。 装箱和运行时类型分派都可能导致性能损失。
如果捕获的变量在代码的性能关键部分中使用,那么以下提示有助于确保它们的使用是高性能的。 首先,如果已知捕获的变量不会更改其类型,则可以使用类型注释显式声明(在变量上,而不是右侧):
function abmult2(r0::Int)
r::Int = r0
if r < 0
r = -r
end
f = x -> x * r
return f
end
类型注释部分恢复由于捕获而丢失的性能,因为解析器可以将具体类型关联到框中的对象。 更进一步,如果捕获的变量根本不需要装箱(因为它不会在闭包创建后重新分配),这可以用 让 块如下。
function abmult3(r::Int)
if r < 0
r = -r
end
f = let r = r
x -> x * r
end
return f
end
该 让 块创建一个新变量 r 其范围仅为内函数。 第二种技术在存在捕获变量的情况下恢复完整的语言性能。 请注意,这是编译器的一个快速发展的方面,未来的版本很可能不需要这种程度的程序员注释来获得性能。 同时,一些用户贡献的软件包,如https://github.com/c42f/FastClosures.jl[快速曝光]自动插入 让 声明如 abmult3.
具有值作为参数的类型
假设你想创建一个 N-沿每个轴具有尺寸3的维数组。 这样的数组可以这样创建:
julia> A = fill(5.0, (3, 3))
3×3 Matrix{Float64}:
5.0 5.0 5.0
5.0 5.0 5.0
5.0 5.0 5.0
这种方法工作得很好:编译器可以弄清楚 A 是一个 阵列{Float64,2} 因为它知道填充值的类型(5.0::浮64)和维数((3,3)::NTuple{2,Int}). 这意味着编译器可以为将来的任何使用生成非常有效的代码 A 在相同的功能。
但是现在假设你想写一个创建3×3×的函数。.. 任意维度的数组;你可能会想写一个函数
julia> function array3(fillval, N)
fill(fillval, ntuple(d->3, N))
end
array3 (generic function with 1 method)
julia> array3(5.0, 2)
3×3 Matrix{Float64}:
5.0 5.0 5.0
5.0 5.0 5.0
5.0 5.0 5.0
这是有效的,但是(正如您可以使用 @code_warntype array3(5.0,2))问题是无法推断输出类型:参数 N 是类型的_value_ Int型,并且类型推断不会(也不能)提前预测其值。 这意味着使用此函数输出的代码必须是保守的,检查每个访问的类型 A;这样的代码会很慢。
现在,解决这些问题的一个很好的方法是使用 功能屏障技术。 但是,在某些情况下,您可能希望完全消除类型不稳定性。 在这种情况下,一种方法是将维数作为参数传递,例如通过 瓦尔{T}() (见 "值类型"):
julia> function array3(fillval, ::Val{N}) where N
fill(fillval, ntuple(d->3, Val(N)))
end
array3 (generic function with 1 method)
julia> array3(5.0, Val(2))
3×3 Matrix{Float64}:
5.0 5.0 5.0
5.0 5.0 5.0
5.0 5.0 5.0
朱莉娅有一个专门的版本 n.婴儿 它接受 瓦尔{::Int} 实例作为第二个参数;通过传递 N 作为类型参数,您可以让编译器知道它的"值"。 因此,这个版本的 阵列3 允许编译器预测返回类型。
然而,利用这些技术可能会令人惊讶地微妙。 例如,这将是没有帮助,如果你打电话 阵列3 从这样的功能:
function call_array3(fillval, n)
A = array3(fillval, Val(n))
end
在这里,您再次创建了相同的问题:编译器无法猜测什么 n 是,所以它不知道的_type_ 瓦尔(n). 尝试使用 瓦尔,但这样做不正确,可以很容易地使性能_worse_在许多情况下。 (只有在你有效结合的情况下 瓦尔 使用函数屏障技巧,为了使内核函数更有效率,应该使用像上面这样的代码。)
正确使用的例子 瓦尔 会是:
function filter3(A::AbstractArray{T,N}) where {T,N}
kernel = array3(1, Val(N))
filter(A, kernel)
end
在这个例子中, N 作为参数传递,因此编译器知道它的"值"。 本质上来说, 瓦尔(T) 只有在 T 是硬编码/字面(瓦尔(3))或已在type-domain中指定。
滥用多个调度的危险(又名,更多关于具有值作为参数的类型)
一旦一个人学会欣赏多重调度,就会有一种可以理解的倾向,即过度使用并尝试将其用于所有事情。 例如,您可能想象使用它来存储信息,例如
struct Car{Make, Model}
year::Int
...more fields...
end
然后对像这样的对象进行调度 汽车{:Honda,:Accord}(年,args。..).
当以下任一条件为真时,这可能是值得的:
*您需要对每个CPU密集型处理 汽车,如果你知道 使 和 模型 在编译时和不同的总数 使 或 模型 这将使用不是太大。
*您有相同类型的同质列表 汽车 来处理,这样就可以将它们全部存储在一个 阵列{Car{:Honda,:Accord},N}.
当后者成立时,处理这种同质数组的函数可以高效地专业化:Julia预先知道每个元素的类型(容器中的所有对象都具有相同的具体类型),因此Julia可以在编译函数时"查找"正确的方法调用(避免在运行时检查),从而发出有效的代码来处理整个列表。
如果这些都不成立,那么你很可能没有任何好处;更糟糕的是,由此产生的"类型组合爆炸"将适得其反。 如果 项目[i+1] 有不同的类型 项目[i],Julia必须在运行时查找类型,在方法表中搜索适当的方法,决定(通过类型交集)哪一个匹配,确定它是否已被JIT编译(如果没有,则这样做),然后进行调用。 从本质上讲,您要求完整的类型系统和JIT编译机制基本上在您自己的代码中执行switch语句或字典查找的等价物。
可以找到比较(1)类型分派,(2)字典查找和(3)"switch"语句的一些运行时基准https://groups.google.com/forum/#!msg/julia-users/jUMu9A3QKQQ/qjgVWr7vAwAJ[在邮件列表中]。
也许比运行时影响更糟糕的是编译时的影响:Julia将为每个不同的函数编译专门的函数 汽车{Make, Model};如果你有数百或数千个这样的类型,那么接受这样的对象作为参数的每个函数(来自自定义 get_年 函数你可以自己写,到通用 推! Julia Base)中的函数将有数百或数千个为其编译的变体。 其中每一个都会增加编译代码的缓存大小,方法内部列表的长度等。 对价值作为参数的过度热情很容易浪费巨大的资源。
内存管理和数组
预分配产出
如果你的函数返回一个 阵列 或者其他一些复杂类型,它可能必须分配内存。 不幸的是,分配及其相反的垃圾收集往往是巨大的瓶颈。
有时,您可以通过预分配输出来规避在每个函数调用上分配内存的需要。 作为一个微不足道的例子,比较
julia> function xinc(x)
return [x + i for i in 1:3000]
end;
julia> function loopinc()
y = 0
for i = 1:10^5
ret = xinc(i)
y += ret[2]
end
return y
end;
与
julia> function xinc!(ret::AbstractVector{T}, x::T) where T
for i in 1:3000
ret[i] = x+i
end
nothing
end;
julia> function loopinc_prealloc()
ret = Vector{Int}(undef, 3000)
y = 0
for i = 1:10^5
xinc!(ret, i)
y += ret[2]
end
return y
end;
计时结果:
julia> @time loopinc()
0.297454 seconds (200.00 k allocations: 2.239 GiB, 39.80% gc time)
5000250000
julia> @time loopinc_prealloc()
0.009410 seconds (2 allocations: 23.477 KiB)
5000250000
预分配具有其他优点,例如通过允许调用者控制来自算法的"输出"类型。 在上面的例子中,我们可以通过一个 子阵列 而不是一个 阵列,如果我们如此渴望的话。
发挥到极致,预分配会使您的代码更加丑陋,因此可能需要进行性能测量和一些判断。 但是,对于"矢量化"(元素)函数,方便的语法 x.=f.(y) 可用于具有融合循环且没有临时数组的就地操作(请参阅 矢量化函数的点语法)。
考虑将视图用于切片
在Julia中,数组"切片"表达式如下 阵列[1:5,:] 创建该数据的副本(除了在赋值的左侧,其中 数组[1:5,:]=... 就地分配给 阵列). 如果您在切片上执行许多操作,这对性能有好处,因为使用较小的连续副本比索引到原始数组更有效。 另一方面,如果您只是对切片进行一些简单的操作,则分配和复制操作的成本可能很高。
另一种方法是创建数组的"视图",它是一个数组对象(a 子阵列)实际上是就地引用原始数组的数据,而不进行复制。 (如果您写入视图,它也会修改原始数组的数据。)这可以通过调用单个切片来完成 查看,或者更简单地用于整个表达式或代码块 @意见在那个表情前面。 例如:
julia> fcopy(x) = sum(x[2:end-1]);
julia> @views fview(x) = sum(x[2:end-1]);
julia> x = rand(10^6);
julia> @time fcopy(x);
0.003051 seconds (3 allocations: 7.629 MB)
julia> @time fview(x);
0.001020 seconds (1 allocation: 16 bytes)
请注意3×加速和减少的内存分配 fview,fview 版本的功能。
考虑StaticArrays。用于小固定大小向量/矩阵运算的jl
如果您的应用程序涉及许多小(< 100 元素)固定大小的数组(即在执行之前已知大小),那么您可能需要考虑使用https://github.com/JuliaArrays/StaticArrays.jl[StaticArrays.jl包]。 此包允许您以避免不必要的堆分配的方式表示此类数组,并允许编译器专门化数组的_size_代码,例如通过完全展开向量操作(消除循环)并将元素存储在CPU
例如,如果使用2d几何进行计算,则可能有许多使用2分量矢量的计算。 通过使用 斯维克托 来自StaticArrays的类型。jl,您可以使用方便的矢量表示法和操作,如 规范(3v-w) 关于向量 v 和 w,同时允许编译器将代码展开到相当于最小的计算 @inbounds hypot(3v[1]-w[1],3v[2]-w[2]).
更多点:熔丝矢量化操作
朱莉娅有一个特别的 点语法将任何标量函数转换为"矢量化"函数调用,并将任何运算符转换为"矢量化"运算符,其嵌套"点调用"的特殊属性是_fusing_:它们在语法级别组合成单个循环,而不分配临时数组。 如果您使用 .= 和类似的赋值运算符,结果也可以就地存储在预先分配的数组中(见上文)。
在线性代数上下文中,这意味着即使操作像 向量+向量 和 向量*标量 被定义,它可以是有利的,以代替使用 矢量。+向量 和 矢量。*标量 因为产生的循环可以与周围的计算融合。 例如,考虑两个函数:
julia> f(x) = 3x.^2 + 4x + 7x.^3;
julia> fdot(x) = @. 3x^2 + 4x + 7x^3; # equivalent to 3 .* x.^2 .+ 4 .* x .+ 7 .* x.^3
两者兼而有之 f 和 fdot的 计算同样的事情。 然而, fdot的 (在 @.宏)应用于数组时显着更快:
julia> x = rand(10^6);
julia> @time f(x);
0.019049 seconds (16 allocations: 45.777 MiB, 18.59% gc time)
julia> @time fdot(x);
0.002790 seconds (6 allocations: 7.630 MiB)
julia> @time f.(x);
0.002626 seconds (8 allocations: 7.630 MiB)
那就是, fdot(x) 快十倍,分配1/6的内存 f(x),因为每 * 和 + 运作于 f(x) 分配一个新的临时数组并在单独的循环中执行。 在这个例子中 f.(x) 和……一样快。 fdot(x) 但是在许多情况下,在表达式中添加一些点比为每个矢量化操作定义一个单独的函数更方便。
少点:取消某些中间广播
上面提到的点循环融合使简洁和惯用的代码能够表达高性能的操作. 但是,重要的是要记住,融合操作将在广播的每次迭代中计算。 这意味着在某些情况下,特别是在存在组合或多维广播的情况下,具有点调用的表达式可能比预期计算函数的次数更多。 例如,假设我们要构建一个随机矩阵,其行具有欧几里德范数一。 我们可能会写如下内容:
julia> x = rand(1000, 1000); julia> d = sum(abs2, x; dims=2); julia> @time x ./= sqrt.(d); 0.002049 seconds (4 allocations: 96 bytes)
这行得通。 但是,此表达式实际上会重新计算 sqrt(d[i]) 对于行中的_every_元素 x[i,:],意味着计算的平方根比必要的要多得多。 要准确查看广播将迭代哪些索引,我们可以调用 广播。组合/组合 关于融合表达式的论点。 这将返回一个范围的元组,其条目对应于迭代的轴;这些范围的长度的乘积将是对融合操作的调用总数。
因此,当广播表达式的某些组件沿着一个轴保持不变时—就像 sqrt,sqrt 沿着前面示例中的第二个维度-通过强制"取消使用"这些组件,即预先分配广播操作的结果并沿其恒定轴重用缓存值,可以提高性能。 一些这样的潜在方法是使用临时变量,将点表达式的组件包装在 身份认同,或者使用等效的内在矢量化(但非融合)函数。
julia> @time let s = sqrt.(d); x ./= s end; 0.000809 seconds (5 allocations: 8.031 KiB) 朱莉娅>@时间x。/=身份(sqrt。(d)); 0.000608秒(5次分配:8.031KiB) 朱莉娅>@时间x。/=地图(sqrt,d); 0.000611秒(4分配:8.016KiB)
这些选项中的任何一个都会以分配为代价产生大约三倍的加速;对于大型广播公司来说,这种加速可以渐近地非常大。
按内存顺序访问数组,沿列
Julia中的多维数组按列主顺序存储。 这意味着数组一次堆叠一列。 这可以使用验证 维克 函数或语法 [:] 如下图(注意数组是有序的 [1 3 2 4],不 [1 2 3 4]):
julia> x = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> x[:]
4-element Vector{Int64}:
1
3
2
4
这种对数组进行排序的约定在Fortran,Matlab和R等许多语言中都很常见(仅举几例)。 列主排序的替代方法是行主排序,这是C和Python所采用的约定(n.麻木,麻木)在其他语言中。 在数组上循环时,记住数组的顺序会产生显着的性能影响。 要记住的一个经验法则是,对于列主数组,第一个索引的变化最快。 从本质上讲,这意味着如果最内层循环索引是第一个出现在切片表达式中的循环将更快。 请记住,用 : 是一个隐式循环,迭代访问特定维度内的所有元素;例如,提取列比行更快。
考虑以下做作的例子。 想象一下,我们想写一个函数,接受一个 向量资料并返回一个正方形 矩阵使用填充有输入向量副本的行或列。 假设行或列是否填充这些副本并不重要(也许代码的其余部分可以很容易地相应调整)。 可以想象,我们至少可以用四种方式来做到这一点(除了推荐的内置调用之外 重复一遍):
function copy_cols(x::Vector{T}) where T
inds = axes(x, 1)
out = similar(Array{T}, inds, inds)
for i = inds
out[:, i] = x
end
return out
end
function copy_rows(x::Vector{T}) where T
inds = axes(x, 1)
out = similar(Array{T}, inds, inds)
for i = inds
out[i, :] = x
end
return out
end
function copy_col_row(x::Vector{T}) where T
inds = axes(x, 1)
out = similar(Array{T}, inds, inds)
for col = inds, row = inds
out[row, col] = x[row]
end
return out
end
function copy_row_col(x::Vector{T}) where T
inds = axes(x, 1)
out = similar(Array{T}, inds, inds)
for row = inds, col = inds
out[row, col] = x[col]
end
return out
end
现在我们将使用相同的随机对每个函数进行计时 10000 由 1 输入向量:
julia> x = randn(10000);
julia> fmt(f) = println(rpad(string(f)*": ", 14, ' '), @elapsed f(x))
julia>map(fmt,[copy_cols,copy_rows,copy_col_row,copy_row_col]);
copy_cols:0.331706323
副本:1.799009911
copy_col_row:0.415630047
copy_row_col:1.721531501
请注意 复制/复制 比……快得多。 复制/复制. 这是预期的,因为 复制/复制 尊重基于列的内存布局 矩阵 并一次填充一列。 此外, 复制_col_row 比……快得多。 复制_row_col 因为它遵循我们的经验法则,在切片表达式中出现的第一个元素应该与最内层循环耦合。
复制数据并不总是不好的
阵列连续存储在内存中,使自己依赖于CPU矢量化,并且由于缓存而减少了内存访问。 这些与建议按列主顺序访问数组的原因相同(见上文)。 由于非顺序内存访问,不规则访问模式和非连续视图可能会大大减慢数组的计算速度。
在重复访问之前将不规则访问的数据复制到连续数组中可能会导致很大的加速,如下面的示例所示。 在这里,矩阵在相乘之前被随机洗牌索引访问。 复制到普通数组中可以加快乘法速度,即使增加了复制和分配的成本。
julia> using Random
julia> A = randn(3000, 3000);
julia> x = randn(2000);
julia> inds = shuffle(1:3000)[1:2000];
julia> function iterated_neural_network(A, x, depth)
for _ in 1:depth
x .= max.(0, A * x)
end
argmax(x)
end
julia> @time iterated_neural_network(view(A, inds, inds), x, 10)
0.324903 seconds (12 allocations: 157.562 KiB)
1569
julia> @time iterated_neural_network(A[inds, inds], x, 10)
0.054576 seconds (13 allocations: 30.671 MiB, 13.33% gc time)
1569
如果有足够的内存,将视图复制到数组的成本将超过对连续数组进行重复矩阵乘法所带来的速度提升。
多线程和线性代数
本节适用于多线程Julia代码,该代码在每个线程中执行线性代数操作。 事实上,这些线性代数操作涉及BLAS/LAPACK调用,它们本身就是多线程的。 在这种情况下,必须确保内核不会因两种不同类型的多线程而超额认购。
Julia编译并使用自己的OpenBLAS副本进行线性代数,其线程数由环境变量控制 OPENBLAS_NUM_THREADS. 它可以在启动Julia时设置为命令行选项,也可以在Julia会话期间修改 布拉斯。set_num_threads(N) (子模块 布拉斯 由 使用线性代数). 它的当前值可以访问与 布拉斯。get_num_threads().
当用户没有指定任何内容时,Julia会尝试为OpenBLAS线程的数量选择一个合理的值(例如基于平台,Julia版本等)。). 但是,通常建议手动检查和设置值。 OpenBLAS行为如下:
*如果 OPENBLAS_NUM_THREADS=1,OpenBLAS使用调用Julia线程(s),即它"生活在"运行计算的Julia线程中。
*如果 OPENBLAS_NUM_THREADS=N>1,OpenBLAS创建和管理自己的线程池(N 总共)。 所有Julia线程之间只有一个OpenBLAS线程池共享。
当你在多线程模式下启动Julia时, JULIA_NUM_THREADS=X,一般建议设置 OPENBLAS_NUM_THREADS=1. 鉴于上面描述的行为,增加BLAS线程的数量为 N>1 很容易导致性能下降,特别是当 N<<X. 然而,这只是一个经验法则,设置每个线程数的最佳方法是在您的特定应用程序上进行实验。
执行延迟、包加载和包预编译时间
缩短到第一个绘图的时间等。
第一次调用julia方法(以及它调用的任何方法,或可以静态确定的方法)将被编译。 该 xref:manual/profile.adoc#@time[@时间 宏家族说明了这一点。
julia> foo() = rand(2,2) * rand(2,2) foo (generic function with 1 method) julia> @time @eval foo(); 0.252395 seconds (1.12 M allocations: 56.178 MiB, 2.93% gc time, 98.12% compilation time) julia> @time @eval foo(); 0.000156 seconds (63 allocations: 2.453 KiB)
请注意 @时间@eval 更适合测量编译时间,因为没有 @eval,在计时开始之前可能已经完成了一些编译。
在开发包时,您可能能够使用_precompilation_改善用户的体验,以便在他们使用包时,他们使用的代码已经编译。 为了有效地预编译包代码,建议使用https://julialang.github.io/PrecompileTools.jl/stable/[预编译工具。jl]在预编译期间运行代表典型包使用情况的"预编译工作负载",该工作负载将本机编译代码缓存到包中 [医]pkgimage 缓存,大大减少了这种使用的"首次执行时间"(通常称为TTFX)。
请注意https://julialang.github.io/PrecompileTools.jl/stable/[预编译工具。jl]如果您不想花费额外的时间进行预编译,则可以禁用工作负载,有时可以通过首选项进行配置,这可能是在软件包开发过程中的情况。
减少包装装载时间
保持加载包装所需的时间通常是有帮助的。 软件包开发人员的一般良好做法包括:
-
将您的依赖关系减少到您真正需要的依赖关系。 考虑使用 软件包扩展支持与其他软件包的互操作性,而不会使您的基本依赖关系膨胀。
-
避免使用
__init__()函数,除非没有替代方案,特别是那些可能会触发大量编译,或者只是需要很长时间才能执行的函数。 -
在可能的情况下,修复https://julialang.org/blog/2020/08/invalidations/[失效]在你的依赖项和你的包代码中。
工具 @time_imports可以在回顾上述因素的过程中是有用的。
julia> @time @time_imports using Plots
0.5 ms Printf
16.4 ms Dates
0.7 ms Statistics
┌ 23.8 ms SuiteSparse_jll.__init__() 86.11% compilation time (100% recompilation)
90.1 ms SuiteSparse_jll 91.57% compilation time (82% recompilation)
0.9 ms Serialization
┌ 39.8 ms SparseArrays.CHOLMOD.__init__() 99.47% compilation time (100% recompilation)
166.9 ms SparseArrays 23.74% compilation time (100% recompilation)
0.4 ms Statistics → SparseArraysExt
0.5 ms TOML
8.0 ms Preferences
0.3 ms PrecompileTools
0.2 ms Reexport
... many deps omitted for example ...
1.4 ms Tar
┌ 73.8 ms p7zip_jll.__init__() 99.93% compilation time (100% recompilation)
79.4 ms p7zip_jll 92.91% compilation time (100% recompilation)
┌ 27.7 ms GR.GRPreferences.__init__() 99.77% compilation time (100% recompilation)
43.0 ms GR 64.26% compilation time (100% recompilation)
┌ 2.1 ms Plots.__init__() 91.80% compilation time (100% recompilation)
300.9 ms Plots 0.65% compilation time (100% recompilation)
1.795602 seconds (3.33 M allocations: 190.153 MiB, 7.91% gc time, 39.45% compilation time: 97% of which was recompilation)
请注意,在这个例子中有多个包加载,一些与 __init__() 函数,其中一些导致编译,其中一些是重新编译。 重新编译是由早期的包使方法无效引起的,然后在这些情况下,当以下包运行它们的 __init__() 功能一些命中重新编译之前的代码可以运行。
此外,请注意 统计数字 扩展 [医]麻雀 已被激活,因为 [医]麻雀 在依赖树中。 即见 0.4ms统计→SparseArraysExt.
这份报告提供了一个很好的机会来审查依赖加载时间的成本是否值得它带来的功能。 此外, Pkg,Pkg 公用事业 为什么 可用于报告存在间接依赖关系的原因。
(CustomPackage) pkg> why FFMPEG_jll Plots → FFMPEG → FFMPEG_jll Plots → GR → GR_jll → FFMPEG_jll
或者要查看包带来的间接依赖关系,您可以 pkg>rm 包,看到从清单中删除的deps,然后用 pkg>撤销.
如果加载时间以慢为主 __init__() 具有编译的方法,识别正在编译的内容的一个详细方法是使用julia args --trace-compile=stderr—trace-compile-timing 这将报告一个 预编译每次编译方法时的语句,以及编译花费的时间。 InteractiveUtils宏 @trace_compile提供了一种为特定调用启用这些args的方法。 所以一个完整的报告报告的调用看起来像:
julia> @time @time_imports @trace_compile using CustomPackage ...
注意 --启动-文件=否 这有助于将测试与您可能拥有的软件包隔离开来。 启动。jl.
更多的分析重新编译的原因可以用https://github.com/timholy/SnoopCompile.jl[脧锚脧赂`N.史努比,史努比`]包装。
减少预编译时间
如果包预编译需要很长时间,一种选择是设置以下内部,然后预编译。
julia> Base.PRECOMPILE_TRACE_COMPILE[] = "stderr" pkg>预编译
这具有设置的效果 --trace-compile=stderr—trace-compile-timing 在预编译过程本身中,so将显示哪些方法是预编译的,以及它们需要多长时间来预编译。
还有分析选项,例如 使用外部分析器Tracy对预编译过程进行概要分析。
杂项条文
性能注释
有时您可以通过承诺某些程序属性来实现更好的优化。
*使用 @inbounds消除表达式内的数组边界检查。 在做这件事之前一定要确定。 如果指数超出范围,您可能会遭受崩溃或无声损坏。
*使用 @fastmath允许浮点优化对于实数是正确的,但导致IEEE数字的差异。 这样做时要小心,因为这可能会改变数值结果。 这对应于 -快速数学 叮当的选择。
*写 @simd前面 为 循环以保证迭代是独立的,并且可以重新排序。 请注意,在许多情况下,Julia可以自动矢量化代码,而无需 @simd 宏;它只在这样的转换否则是非法的情况下是有益的,包括允许浮点重新关联性和忽略依赖的内存访问(@simd ivdep). 再次,断言时要非常小心 @simd 因为用依赖的迭代错误地注释循环可能会导致意外的结果。 特别要注意的是 setindex! 在一些 抽象阵列 子类型本质上依赖于迭代顺序。 *此功能是实验性的*,可能会在Julia的未来版本中更改或消失。
如果数组使用非常规索引,则使用1:n索引到AbstractArray的常见习惯用法是不安全的,如果关闭边界检查,则可能导致分段错误。 使用方法 直线(x) 或 每个索引(x) 相反(另请参阅 带有自定义索引的数组)。
|
注意,而 |
这是一个两者兼而有之的例子 @inbounds 和 @simd 标记(我们在这里使用 @noinline 以防止优化器试图过于聪明并击败我们的基准):
@noinline function inner(x, y)
s = zero(eltype(x))
for i in eachindex(x, y)
@inbounds s += x[i]*y[i]
end
return s
end
@noinline函数innersimd(x,y)
s=零(eltype(x))
@simd for i in eachindex(x,y)
@inbounds s+=x[i]*y[i]
结束
返回s
结束
功能时间(n,代表)
x=兰德(Float32,n)
y=兰德(Float32,n)
s=零(Float64)
时间=@经过j在1:代表
s+=内(x,y)
结束
println("GFlop/sec=",2n*reps/time*1E-9)
时间=@经过j在1:代表
s+=innersimd(x,y)
结束
println("GFlop/sec(SIMD)=",2n*reps/time*1E-9)
结束
时间(1000,1000)
在具有2.4ghz英特尔酷睿i5处理器的计算机上,这会产生:
GFlop/sec = 1.9467069505224963 GFlop/sec (SIMD) = 17.578554163920018
(GFlop/秒 衡量性能,数字越大越好。)
下面是一个包含所有三种标记的示例。 该程序首先计算一维数组的有限差分,然后计算结果的L2范数:
function init!(u::Vector)
n = length(u)
dx = 1.0 / (n-1)
@fastmath @inbounds @simd for i in 1:n #by asserting that `u` is a `Vector` we can assume it has 1-based indexing
u[i] = sin(2pi*dx*i)
end
end
function deriv!(u::Vector, du)
n = length(u)
dx = 1.0 / (n-1)
@fastmath @inbounds du[1] = (u[2] - u[1]) / dx
@fastmath @inbounds @simd for i in 2:n-1
du[i] = (u[i+1] - u[i-1]) / (2*dx)
end
@fastmath @inbounds du[n] = (u[n] - u[n-1]) / dx
end
function mynorm(u::Vector)
n = length(u)
T = eltype(u)
s = zero(T)
@fastmath @inbounds @simd for i in 1:n
s += u[i]^2
end
@fastmath @inbounds return sqrt(s)
end
function main()
n = 2000
u = Vector{Float64}(undef, n)
init!(u)
du = similar(u)
deriv!(u, du)
nu = mynorm(du)
@time for i in 1:10^6
deriv!(u, du)
nu = mynorm(du)
end
println(nu)
end
main()
在具有2.7ghz英特尔酷睿i7处理器的计算机上,这会产生:
$ julia wave.jl;
1.207814709 seconds
4.443986180758249
$ julia --math-mode=ieee wave.jl;
4.487083643 seconds
4.443986180758249
在这里,选项 --数学模式=ieee 禁用 @fastmath 宏,这样我们就可以比较结果了。
在这种情况下,加速由于 @fastmath 是约3.7的因子。 这是非常大的-一般来说,加速会更小。 (在这个特定的例子中,基准的工作集足够小以适合处理器的L1高速缓存,使得存储器访问延迟不起作用,并且计算时间由CPU使用率主导。 在许多现实世界的节目中,情况并非如此。)此外,在这种情况下,这种优化不会改变结果-一般来说,结果会略有不同。 在某些情况下,特别是对于数值不稳定的算法,结果可能非常不同。
注释 @fastmath 重新排列浮点表达式,例如更改求值顺序,或假设某些特殊情况(inf,nan)不能发生。 在这种情况下(以及在这个特定的计算机上),主要区别在于表达式 1/(2*dx) 在功能 衍生工具 被吊出循环(即在循环外计算),就好像一个人写了 idx=1/(2*dx). 在循环中,表达式 ... /(2*dx) 然后变成 ... *idx,这是更快的评估。 当然,编译器应用的实际优化以及由此产生的加速在很大程度上取决于硬件。 您可以使用Julia检查生成代码中的更改 编码,编码功能。
请注意 @fastmath 还假设 南在计算过程中不会发生s,这会导致令人惊讶的行为:
julia> f(x) = isnan(x);
julia> f(NaN)
true
julia> f_fast(x) = @fastmath isnan(x);
julia> f_fast(NaN)
false
将非正规数视为零
非正态数字,以前称为https://en.wikipedia.org/wiki/Denormal_number[非规范数字]在许多情况下都很有用,但会对某些硬件造成性能损失。 一个电话 set_zero_subnormals(true)授予浮点运算将非正规输入或输出视为零的权限,这可能会提高某些硬件上的性能。 一个电话 set_zero_subnormals(false)对非正规数强制执行严格的IEEE行为。
下面是一个示例,其中低于正常值会显着影响某些硬件的性能:
function timestep(b::Vector{T}, a::Vector{T}, Δt::T) where T
@assert length(a)==length(b)
n = length(b)
b[1] = 1 # Boundary condition
for i=2:n-1
b[i] = a[i] + (a[i-1] - T(2)*a[i] + a[i+1]) * Δt
end
b[n] = 0 # Boundary condition
end
function heatflow(a::Vector{T}, nstep::Integer) where T
b = similar(a)
for t=1:div(nstep,2) # Assume nstep is even
timestep(b,a,T(0.1))
timestep(a,b,T(0.1))
end
end
heatflow(zeros(Float32,10),2) # Force compilation
for trial=1:6
a = zeros(Float32,1000)
set_zero_subnormals(iseven(trial)) # Odd trials use strict IEEE arithmetic
@time heatflow(a,1000)
end
这给出了类似于
0.002202 seconds (1 allocation: 4.063 KiB) 0.001502 seconds (1 allocation: 4.063 KiB) 0.002139 seconds (1 allocation: 4.063 KiB) 0.001454 seconds (1 allocation: 4.063 KiB) 0.002115 seconds (1 allocation: 4.063 KiB) 0.001455 seconds (1 allocation: 4.063 KiB)
请注意,每个偶数迭代的速度明显更快。
此示例生成许多非正规数,因为以下值 a 成为指数递减的曲线,随着时间的推移慢慢变平。
应谨慎使用将非正规值视为零,因为这样做会破坏某些身份,例如 x-y==0 暗示 x==y:
julia> x = 3f-38; y = 2f-38;
julia> set_zero_subnormals(true); (x - y, x == y)
(0.0f0, false)
julia> set_zero_subnormals(false); (x - y, x == y)
(1.0000001f-38, false)
在某些应用中,将非正规数归零的另一种方法是注入一点噪声。 例如,而不是初始化 a 用零,用:
a = rand(Float32,1000) * 1.f-9
避免i/O的字符串插值
将数据写入文件(或其他I/O设备)时,形成额外的中间字符串是开销的来源。 而不是:
println(file, "$a $b")
使用方法:
println(file, a, " ", b)
第一个版本的代码形成一个字符串,然后将其写入文件,而第二个版本将值直接写入文件。 另请注意,在某些情况下,字符串插值可能更难阅读。 考虑一下:
println(file, "$(f(a))$(f(b))")
对:
println(file, f(a), f(b))
避免急切的弦具体化
在仅有条件地需要对象的字符串表示的设置中(例如在函数的错误路径或条件警告(如弃用)中),建议避免急切地具体化字符串的开销。 自Julia1.8以来,这可以通过以下方式实现 [医懒骨头]和对应的字符串宏 @lazy_str.
例如,代替:
Base.depwarn("`foo` is deprecated for type $(typeof(x))", :bar)
使用方法:
Base.depwarn(lazy"`foo` is deprecated for type $(typeof(x))", :bar)
或等效的无宏版本:
Base.depwarn(LazyString("`foo` is deprecated for type ", typeof(x)), :bar)
通过这种方式,只有在实际显示时才会构造插值字符串。
在并行执行期间优化网络I/O
并行执行远程函数时:
using Distributed
responses = Vector{Any}(undef, nworkers())
@sync begin
for (idx, pid) in enumerate(workers())
Threads.@spawn responses[idx] = remotecall_fetch(foo, pid, args...)
end
end
比……:
using Distributed
refs = Vector{Any}(undef, nworkers())
for (idx, pid) in enumerate(workers())
refs[idx] = @spawnat pid foo(args...)
end
responses = [fetch(r) for r in refs]