17

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

@inbounds for ... end

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

@inbounds总是赢是正确的@boundscheck吗?如果函数没有内联,则唯一的例外,但这只是前面“一层”规则的一种情况,所以@propagate_inbounds即使在非内联函数调用中也会关闭边界检查?

4

1 回答 1

17

当手册谈到@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

一般来说,边界检查删除应该是您在确保其他一切正常工作、经过良好测试并遵循通常的性能提示之后所做的最后一次优化。

于 2016-08-13T04:24:07.347 回答