我正在寻找 bounds checking rules 的一些说明在 Julia 。这是否意味着如果我把 @inbounds
在 for 循环的开始,
@inbounds for ... end
那么只有“一层”入站传播,所以如果里面有一个 for 循环,@inbounds
不会在那里关闭边界检查吗?如果我使用 @propagate_inbounds
,它会进入嵌套的 for 循环吗?说
@inbounds
是否正确?总是胜出@boundscheck
?如果函数没有内联,唯一的异常(exception)就是前一个“一层”规则的情况,所以 @propagate_inbounds
即使在非内联函数调用中也会关闭边界检查?
最佳答案
当手册谈到@inbounds
通过“一层”传播,它特指函数调用边界。它只能影响被内联的函数这一事实是次要的要求,这使得这尤其令人困惑和难以测试,所以让我们稍后再担心内联。@inbounds
宏注释函数调用,以便它们能够省略边界检查。事实上,宏会对传递给它的表达式中的所有函数调用执行此操作,包括任意数量的嵌套 for
循环,begin
块,if
语句等。当然,索引和索引赋值只是低于函数调用的“糖”,因此它以相同的方式影响它们。这一切都是有道理的;作为被 @inbounds
包裹的代码的作者,您可以看到宏并确保这样做是安全的。
但是@inbounds
宏告诉 Julia 做一些有趣的事情。它改变了在完全不同的地方编写的代码的行为!例如,当您注释调用时:
julia> f() = @inbounds return getindex(4:5, 10);
f()
13
宏有效地进入 the standard library并禁用
@boundscheck
块,允许它计算范围有效区域之外的值。这是一个远距离的诡异 Action ……如果不小心约束,它最终可能会从库代码中删除边界检查,而这不是有意或完全安全的。这就是为什么存在“一层”限制的原因;我们只想在作者明确意识到它可能发生并选择加入删除时删除边界检查。
现在,作为图书馆作者,可能在某些情况下您希望选择允许
@inbounds
传播到您在方法中调用的所有函数。那就是Base.@propagate_inbounds
用来。不像 @inbounds
,注释函数调用,@propagate_inbounds
注释方法定义以允许调用方法的入站状态传播到您在方法实现中进行的所有函数调用。这有点难以抽象地描述,所以让我们看一个具体的例子。一个例子
让我们创建一个玩具自定义向量,它只是在它包装的向量中创建一个混洗的 View :
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 的索引,而是来自对置换向量的索引
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)
验证没有分支。 .但是,实际上,在这种情况下,我认为用
@boundscheck
编写这个 getindex 方法实现会好得多。自己的块:@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
通常,在确保其他一切正常、经过良好测试并遵循通常的性能提示后,边界检查删除应该是您进行的最后一次优化。
关于julia - Julia 中的 @inbounds 传播规则,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38901275/