当手册谈到@inbounds
通过“一层”传播时,它特别指的是函数调用边界。它只能影响被内联的函数这一事实是次要要求,这使得这特别令人困惑且难以测试,所以让我们稍后再担心内联。
宏注释函数调用,@inbounds
以便它们能够省略边界检查。事实上,宏将对传递给它的表达式中的所有for
函数调用执行此操作,包括任意数量的嵌套循环、begin
块、if
语句等。当然,索引和索引赋值只是降低了函数调用,因此它以相同的方式影响它们。这一切都是有道理的;作为由 包装的代码的作者@inbounds
,您可以看到宏并确保这样做是安全的。
但是@inbounds
宏告诉 Julia 做一些有趣的事情。它改变了在完全不同的地方编写的代码的行为!例如,当您注释呼叫时:
julia> f() = @inbounds return getindex(4:5, 10);
f()
13
宏有效地进入标准库并禁用该@boundscheck
块,允许它计算范围有效区域之外的值。
这是一个令人毛骨悚然的远距离动作……如果没有仔细限制,它最终可能会从库代码中删除不打算或完全安全的边界检查。这就是为什么存在“一层”限制的原因;我们只想在作者明确意识到它可能会发生并选择删除时删除边界检查。
现在,作为库作者,在某些情况下,您可能希望选择允许@inbounds
传播到您在方法中调用的所有函数。这Base.@propagate_inbounds
就是使用的地方。与@inbounds
注释函数调用不同,@propagate_inbounds
注释方法定义以允许调用方法的入站状态传播到您在方法实现中进行的所有函数调用。这在抽象中描述起来有点困难,所以让我们看一个具体的例子。
一个例子
让我们创建一个玩具自定义向量,它只是在它包装的向量中创建一个打乱的视图:
julia> module M
using Random
struct ShuffledVector{A,T} <: AbstractVector{T}
data::A
shuffle::Vector{Int}
end
ShuffledVector(A::AbstractVector{T}) where {T} = ShuffledVector{typeof(A), T}(A, randperm(length(A)))
Base.size(A::ShuffledVector) = size(A.data)
Base.@inline function Base.getindex(A::ShuffledVector, i::Int)
A.data[A.shuffle[i]]
end
end
这很简单——我们包装任何向量类型,创建一个随机排列,然后在索引时,我们只需使用排列索引到原始数组。而且我们知道,基于外部构造函数,对数组子部分的所有访问都应该没问题……所以即使我们自己不检查边界,如果我们索引越界,我们也可以依赖内部索引表达式抛出错误。
julia> s = M.ShuffledVector(1:4)
4-element Main.M.ShuffledVector{UnitRange{Int64},Int64}:
1
3
4
2
julia> s[5]
ERROR: BoundsError: attempt to access 4-element Array{Int64,1} at index [5]
Stacktrace:
[1] getindex at ./array.jl:728 [inlined]
[2] getindex(::Main.M.ShuffledVector{UnitRange{Int64},Int64}, ::Int64) at ./REPL[10]:10
[3] top-level scope at REPL[15]:1
请注意边界错误不是来自对 ShuffledVector 的索引,而是来自对 permutation vector 的索引A.perm[5]
。现在也许我们 ShuffledVector 的用户希望它的访问更快,所以他们尝试关闭边界检查@inbounds
:
julia> f(A, i) = @inbounds return A[i]
f (generic function with 1 method)
julia> f(s, 5)
ERROR: BoundsError: attempt to access 4-element Array{Int64,1} at index [5]
Stacktrace:
[1] getindex at ./array.jl:728 [inlined]
[2] getindex at ./REPL[10]:10 [inlined]
[3] f(::Main.M.ShuffledVector{UnitRange{Int64},Int64}, ::Int64) at ./REPL[16]:1
[4] top-level scope at REPL[17]:1
但是他们仍然遇到边界错误!这是因为@inbounds
注解只是试图@boundscheck
从我们上面写的方法中删除块。它不会传播到标准库以从A.perm
数组或A.data
范围中删除边界检查。这是相当多的开销,即使他们试图删除边界!getindex
所以,我们可以用一个注解来编写上面的方法,Base.@propagate_inbounds
这将允许这个方法“继承”它的调用者的边界状态:
julia> module M
using Random
struct ShuffledVector{A,T} <: AbstractVector{T}
data::A
shuffle::Vector{Int}
end
ShuffledVector(A::AbstractVector{T}) where {T} = ShuffledVector{typeof(A), T}(A, randperm(length(A)))
Base.size(A::ShuffledVector) = size(A.data)
Base.@propagate_inbounds function Base.getindex(A::ShuffledVector, i::Int)
A.data[A.shuffle[i]]
end
end
WARNING: replacing module M.
Main.M
julia> s = M.ShuffledVector(1:4);
julia> s[5]
ERROR: BoundsError: attempt to access 4-element Array{Int64,1} at index [5]
Stacktrace:
[1] getindex at ./array.jl:728 [inlined]
[2] getindex(::Main.M.ShuffledVector{UnitRange{Int64},Int64}, ::Int64) at ./REPL[20]:10
[3] top-level scope at REPL[22]:1
julia> f(s, 5) # That @inbounds now affects the inner indexing calls, too!
0
您可以验证没有带有 的分支@code_llvm f(s, 5)
。
但是,真的,在这种情况下,我认为用它自己的块编写这个 getindex 方法实现会更好@boundscheck
:
@inline function Base.getindex(A::ShuffledVector, i::Int)
@boundscheck checkbounds(A, i)
@inbounds r = A.data[A.shuffle[i]]
return r
end
它有点冗长,但现在它实际上会在ShuffledVector
类型上抛出边界错误,而不是在错误消息中泄漏实现细节。
内联的效果
您会注意到我没有@inbounds
在上面的全局范围内进行测试,而是使用这些小辅助函数。这是因为边界检查删除仅在方法被内联和编译时才有效。因此,简单地尝试在全局范围内删除边界是行不通的,因为它不能将函数调用内联到交互式 REPL 中:
julia> @inbounds getindex(4:5, 10)
ERROR: BoundsError: attempt to access 2-element UnitRange{Int64} at index [10]
Stacktrace:
[1] throw_boundserror(::UnitRange{Int64}, ::Int64) at ./abstractarray.jl:538
[2] getindex(::UnitRange{Int64}, ::Int64) at ./range.jl:617
[3] top-level scope at REPL[24]:1
在全局范围内没有编译或内联,因此 Julia 无法删除这些界限。同样,当类型不稳定时(例如访问非常量全局时),Julia 无法内联方法,因此它也无法删除这些边界检查:
julia> r = 1:2;
julia> g() = @inbounds return r[3]
g (generic function with 1 method)
julia> g()
ERROR: BoundsError: attempt to access 2-element UnitRange{Int64} at index [3]
Stacktrace:
[1] throw_boundserror(::UnitRange{Int64}, ::Int64) at ./abstractarray.jl:538
[2] getindex(::UnitRange{Int64}, ::Int64) at ./range.jl:617
[3] g() at ./REPL[26]:1
[4] top-level scope at REPL[27]:1
一般来说,边界检查删除应该是您在确保其他一切正常工作、经过良好测试并遵循通常的性能提示之后所做的最后一次优化。