Julia中的@inbounds传播规则 [英] @inbounds propagation rules in Julia
问题描述
我正在寻找有关范围检查规则的说明.朱莉娅这就是说,如果我将@inbounds
放在for循环的开头,
I'm looking for some clarification of the bounds checking rules in Julia. Is this meaning that if I put @inbounds
at the beginning of the for loop,
@inbounds for ... end
然后仅传播一层"入站,因此如果其中有一个for循环,@inbounds
会不会关闭在那里的边界检查?如果我使用@propagate_inbounds
,它将进入嵌套的for循环中吗?
then only for "one layer" inbounds propagates, so if there is a for loop inside of that, @inbounds
will not turn off the bounds checking in there? And if I use @propagate_inbounds
, it will go inside the nested for loop?
并且说@inbounds
总是胜过@boundscheck
是正确的吗?唯一的例外是该函数未内联,而仅仅是前一个一层"规则的一种情况,因此@propagate_inbounds
即使在非内联函数调用中也会关闭边界检查吗?
And is it correct to say @inbounds
always wins over @boundscheck
? The only exception if the function is not inlined, but is that just a case of the previous "one layer" rule, so @propagate_inbounds
would turn off the bounds checking even in the non-inlined function call?
推荐答案
当手册中提到@inbounds
通过一层"传播时,它特别是指函数调用边界.它只能影响要内联的功能这一事实是一个次要条件,这使得它特别令人困惑且难以测试,因此,让我们不必担心内联,直到稍后.
When the manual speaks about @inbounds
propagating through "one layer," it's specifically referring to function call boundaries. The fact that it's only able to affect functions that get inlined is a secondary requirement that makes this especially confusing and tough to test, so let's not worry about inlining until later.
@inbounds
宏可以对函数调用进行注释,以使它们能够取消边界检查.实际上,宏将对传递给它的表达式中的 all 函数调用执行此操作,包括任意数量的嵌套for
循环,begin
块,if
语句等.而且,当然,索引和索引赋值只是降低了函数调用的糖",因此它以相同的方式影响它们.所有这些都是有道理的;作为@inbounds
包装的代码的作者,您可以看到宏并确保这样做是安全的.
The @inbounds
macro annotates function calls such that they're able to elide bounds checks. In fact, the macro will do this for all function calls in the expression that is passed to it, including any number of nested for
loops, begin
blocks, if
statements, etc. And, of course, indexing and indexed assignment are simply "sugars" that lower to function calls, so it affects those the same way. All this makes sense; as the author of the code that's wrapped by @inbounds
, you're able to see the macro and ensure that it's safe to do so.
但是@inbounds
宏告诉Julia做一些有趣的事情.它改变了在完全不同的地方编写的代码的行为!例如,当您注释呼叫时:
But the @inbounds
macro tells Julia to do something funny. It changes the behavior of code that's written in a totally different place! For example when you annotate the call:
julia> f() = @inbounds return getindex(4:5, 10);
f()
13
该宏有效地进入标准库并禁用该@boundscheck
块,从而允许它计算范围有效区域之外的值.
The macro effectively reaches into the standard library and disables that @boundscheck
block, allowing it to compute values outside of the range's valid region.
这是一个遥不可及的鬼动作……如果不仔细限制它,最终可能会从不希望这样做或完全安全的库代码中删除边界检查.这就是为什么存在单层"限制的原因.我们只希望在作者明确意识到可能会发生并选择删除时删除边界检查.
This is a spooky action at a distance… and if it's not carefully constrained, it could end up removing bounds-checks from library code where it's not intended or fully safe to do so. That's why there's the "one-layer" restriction; we only want to remove bounds checks when authors are explicitly aware that it might occur and opt-in to the removal.
现在,作为库作者,在某些情况下,您可能希望选择允许@inbounds
传播到您在方法中调用的所有函数.那就是使用Base.@propagate_inbounds
的地方.与@inbounds
注释函数调用不同,@propagate_inbounds
注释方法定义以允许调用方法的入站状态传播到 all 函数调用在方法的实现中进行.很难用抽象来描述,所以让我们看一个具体的例子.
Now, as a library author, there may be cases where you want to opt-in to allow @inbounds
to propagate through to all functions you call within the method. That's where Base.@propagate_inbounds
is used. Unlike @inbounds
, which annotates function calls, @propagate_inbounds
annotates method definitions to allow for the inbounds state that the method gets called with to propagate through to all function calls you make in the method's implementation. This is a bit tough to describe in the abstract, so let's look at a concrete example.
让我们创建一个玩具自定义向量,该向量将简单地在其包装的向量中创建一个随机播放的视图:
Let's create a toy custom vector that simply creates a shuffled view into the vector it wraps:
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
这很简单-我们包装任何向量类型,创建一个随机排列,然后在建立索引后,我们就使用该排列索引到原始数组中.而且我们知道,根据外部构造函数,对数组子部分的所有访问都应该可以.因此,即使我们自己不检查边界,但如果我们超出边界索引,我们也可以依靠内部索引表达式抛出错误. /p>
This is pretty straight-forward — we wrap any vector type, create a random permutation, and then upon indexing we just index into the original array using the permutation. And we know that all accesses into the subparts of the array should be okay based upon the outer constructor… so even though we aren't checking bounds ourselves, we can rely upon the inner indexing expressions throwing errors if we index out of bounds.
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
:
Note how the bounds error is coming not from the indexing into the ShuffledVector, but rather from indexing into the permutation vector A.perm[5]
. Now perhaps a user of our ShuffledVector wants its accesses to be faster, so they try turning off bounds-checking with @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
批注,该批注将允许该方法继承"其调用者的入站状态:
But they're still getting bounds errors! This is because @inbounds
annotation only tried to remove the @boundscheck
blocks from the method we wrote above. It doesn't propagate through to the standard library to remove the bounds-checking from either the A.perm
array nor the A.data
range. That's quite a bit of overhead, even though they tried to remove bounds! So, we can instead write the above getindex
method with a Base.@propagate_inbounds
annotation which will allow for this method to "inherit" its caller's in-bounds state:
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)
确认没有分支.
You can verify that there are no branches with @code_llvm f(s, 5)
.
但是,实际上,在这种情况下,我认为用自己的@boundscheck
块编写此getindex方法实现会更好:
But, really, in this case I think it'd be much better to write this getindex method implementation with a @boundscheck
block of its own:
@inline function Base.getindex(A::ShuffledVector, i::Int)
@boundscheck checkbounds(A, i)
@inbounds r = A.data[A.shuffle[i]]
return r
end
这有点冗长,但是现在它实际上会将边界错误抛出给ShuffledVector
类型,而不是在错误消息中泄漏实现细节.
It's a little more verbose, but now it'll actually throw the bounds error on the ShuffledVector
type instead of leaking the implementation details in the error message.
您会注意到,我没有在上面的全局范围内测试@inbounds
,而是使用这些小辅助函数.这是因为边界检查删除仅在方法被内联和编译时才起作用.因此,简单地尝试在全局范围内删除边界是行不通的,因为它无法将函数调用内联到交互式REPL中:
You'll notice that I don't test @inbounds
in the global scope above, and instead use these little helper functions. That's because bounds check removal only works when the method gets inlined and compiled. So simply trying to remove bounds at the global scope isn't going to work since it can't inline the function call into the interactive 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也无法内联方法(例如,访问非常量全局变量时),因此,它也不能删除这些边界检查:
There's no compilation or inlining occurring here at global scope, so Julia is unable to remove these bounds. Similarly, Julia isn't able to inline methods when there's a type instability (like when accessing a non-constant global), so it can't remove these bounds checks, either:
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
通常,边界检查删除应该是您在确保其他所有功能正常运行,经过充分测试并遵循通常的性能提示后所做的最后一次优化.
In general, bounds-check removal should be the last optimization you make after ensuring everything else works, is well-tested, and follows the usual performance tips.
这篇关于Julia中的@inbounds传播规则的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!