在Julia中@inbounds传播规则

Chr*_*kas 9 bounds-checker julia

我正在寻找Julia中边界检查规则的一些说明.这意味着如果我放在@inboundsfor循环的开头,

@inbounds for ... end
Run Code Online (Sandbox Code Playgroud)

然后只有"一层"的内部传播,所以如果内部有一个for循环,那么@inbounds不会关闭那里的边界检查吗?如果我使用@propagate_inbounds它,它将进入嵌套的for循环?

@inbounds永远胜利是否正确@boundscheck?如果函数不是内联,但唯一的例外是,刚刚过去的"一层"的规则的情况下,因此@propagate_inbounds将关闭边界的非内联函数调用,即使检查?

Mat*_* B. 9

当手册谈到@inbounds通过"一层"传播时,它具体指的是函数调用边界.事实上,它只能影响内联函数是次要的要求,这使得这一点特别容易混淆和难以测试,所以让我们不要担心直到后来内联.

@inbounds宏诠释的函数调用,使得他们能界的Elid检查.实际上,宏将对传递给它的表达式中的所有函数调用执行此操作,包括任意数量的嵌套for循环,begin块,if语句等.当然,索引和索引赋值只是"糖",它们更低函数调用,所以它以相同的方式影响它们.这一切都有道理; 作为被包裹的代码的作者@inbounds,您可以看到宏并确保这样做是安全的.

@inbounds宏观告诉朱莉娅做一些有趣的事情.它改变了在完全不同的地方编写的代码的行为!例如,当您注释通话时:

julia> f() = @inbounds return getindex(4:5, 10);
       f()
13
Run Code Online (Sandbox Code Playgroud)

宏有效地进入标准库并禁用该@boundscheck块,允许它计算范围有效区域之外的值.

这是一个远距离的怪异行为......如果它没有受到严格约束,它最终可能会从库代码中删除边界检查,这样做是不可能或完全安全的.这就是"一层"限制的原因; 我们只想在作者明确意识到它可能发生并选择加入删除时删除边界检查.

现在,作为库作者,可能存在您希望选择加入以允许@inbounds传播到您在方法中调用的所有函数的情况.这Base.@propagate_inbounds就是使用的地方.与@inbounds注释函数调用不同,@propagate_inbounds注释方法定义以允许调用方法的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
Run Code Online (Sandbox Code Playgroud)

这非常简单 - 我们包装任何矢量类型,创建随机排列,然后在索引时我们只使用排列索引到原始数组.我们知道所有对数组子部分的访问都应该基于外部构造函数...所以即使我们不自己检查边界,如果我们索引越界,我们可以依赖内部索引表达式抛出错误.

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
Run Code Online (Sandbox Code Playgroud)

注意边界错误不是来自索引到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
Run Code Online (Sandbox Code Playgroud)

但他们仍然遇到错误!这是因为@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
Run Code Online (Sandbox Code Playgroud)

您可以验证没有分支@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
Run Code Online (Sandbox Code Playgroud)

它有点冗长,但现在它实际上会抛出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
Run Code Online (Sandbox Code Playgroud)

在全球范围内没有编译或内联,因此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
Run Code Online (Sandbox Code Playgroud)