Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

at-views macro to convert a whole block of code to slices=views #20164

Merged
merged 10 commits into from
Jan 24, 2017
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,7 @@ export
@label,
@goto,
@view,
@views,

# SparseArrays module re-exports
SparseArrays,
Expand Down
3 changes: 2 additions & 1 deletion base/subarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,8 @@ end

Creates a `SubArray` from an indexing expression. This can only be applied directly to a
reference expression (e.g. `@view A[1,2:end]`), and should *not* be used as the target of
an assignment (e.g. `@view(A[1,2:end]) = ...`).
an assignment (e.g. `@view(A[1,2:end]) = ...`). See also [`@views`](@ref)
to switch an entire block of code to use views for slicing.
"""
macro view(ex)
if isa(ex, Expr) && ex.head == :ref
Expand Down
63 changes: 63 additions & 0 deletions base/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -750,3 +750,66 @@ kwdef_val(::Type{Cwstring}) = Cwstring(C_NULL)
kwdef_val{T<:Integer}(::Type{T}) = zero(T)

kwdef_val{T}(::Type{T}) = T()

############################################################################
# @views macro (not defined in subarray.jl because of a bootstrapping
# issue with the code generation below).

# maybeview is like getindex, but returns a view for slicing operations
# (while remaining equivalent to getindex for scalar indices and non-array types)
@propagate_inbounds maybeview(A, args...) = getindex(A, args...)
@propagate_inbounds maybeview(A::AbstractArray, args...) = view(A, args...)
@propagate_inbounds maybeview(A::AbstractArray, args::Number...) = getindex(A, args...)
@propagate_inbounds maybeview(A) = getindex(A)
@propagate_inbounds maybeview(A::AbstractArray) = getindex(A)
# avoid splatting penalty in common cases:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this really necessary in your testing? I'd be surprised if the @propagate_inbounds definitions above have a splatting penalty since they get inlined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a simple test case

f(x) = 1
f(x,y) = 2
g(x...) = f(x...)

and it wasn't getting inlined, but I guess I should try again with @propagate_inbounds?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I was getting deceived: @code_llvm g(3) looks more complicated than @code_llvm f(3), but foo() = g(3) + g(4) - g(6,7) + g(8) is definitely inlining g (and simplifies to ret i64 1).

Great, that will simply things. The dotview function in broadcast.jl can be similarly simplified.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, @code_llvm can be deceiving with splatted @inline functions because it shows the LLVM function for them... but that's not at all what happens when they get inlined into another function. I almost always will define simple fixed-argument wrapper functions when I'm trying to test these guys.

let pi(expr) = :(@propagate_inbounds $expr)
for nargs = 1:5
args = Symbol[Symbol("x",i) for i = 1:nargs]
numargs = Expr[:($(Symbol("x",i))::Number) for i = 1:nargs]
eval(pi(Expr(:(=), Expr(:call, :maybeview, :A, args...),
Expr(:block, Expr(:call, :getindex, :A, args...)))))
eval(pi(Expr(:(=), Expr(:call, :maybeview, :(A::AbstractArray), args...),
Expr(:block, Expr(:call, :view, :A, args...)))))
eval(pi(Expr(:(=), Expr(:call, :maybeview, :(A::AbstractArray), numargs...),
Expr(:block, Expr(:call, :getindex, :A, args...)))))
end
end

_views(x) = x
_views(x::Symbol) = esc(x)
function _views(ex::Expr)
if ex.head in (:(=), :(.=))
# don't use view on the lhs of an assignment
Expr(ex.head, esc(ex.args[1]), _views(ex.args[2]))
elseif ex.head == :ref
Expr(:call, :maybeview, map(_views, ex.args)...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace_ref_end!?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks.

else
h = string(ex.head)
if last(h) == '='
# don't use view on the lhs of an op-assignment
Expr(first(h) == '.' ? :(.=) : :(=), esc(ex.args[1]),
Expr(:call, esc(Symbol(h[1:end-1])), _views(ex.args[1]),
map(_views, ex.args[2:end])...))
else
Expr(ex.head, map(_views, ex.args)...)
end
end
end

"""
@views code

Convert every array-slicing operation in the given `code`
(which may be a `begin`/`end` block, loop, function, etc.)
to return a view. Scalar indices, non-array types, and
explicit `getindex` calls (as opposed to `array[...]`) are
unaffected.

Note that the `@views` macro only affects `array[...]` expressions
that appear explicitly in the given code, not array slicing that
occurs in functions called by the code.
"""
macro views(x)
_views(x)
end
1 change: 1 addition & 0 deletions doc/src/stdlib/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Base.Broadcast.broadcast!
Base.getindex(::AbstractArray, ::Any...)
Base.view
Base.@view
Base.@views
Base.to_indices
Base.Colon
Base.parent
Expand Down