Skip to content

Commit d87239c

Browse files
authored
at-views macro to convert a whole block of code to slices=views (#20164)
at-views macro to convert a whole block of code to slices=views
1 parent c12a74b commit d87239c

File tree

8 files changed

+130
-14
lines changed

8 files changed

+130
-14
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ This section lists changes that do not have deprecation warnings.
133133
Library improvements
134134
--------------------
135135

136+
* `@views` macro to convert a whole expression or block of code to
137+
use views for all slices ([#20164]).
138+
136139
* `max`, `min`, and related functions (`minmax`, `maximum`, `minimum`,
137140
`extrema`) now return `NaN` for `NaN` arguments ([#12563]).
138141

base/broadcast.jl

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -503,16 +503,8 @@ end
503503
# explicit calls to view. (All of this can go away if slices
504504
# are changed to generate views by default.)
505505

506-
dotview(args...) = getindex(args...)
507-
dotview(A::AbstractArray, args...) = view(A, args...)
508-
dotview{T<:AbstractArray}(A::AbstractArray{T}, args...) = getindex(A, args...)
509-
# avoid splatting penalty in common cases:
510-
for nargs = 0:5
511-
args = Symbol[Symbol("x",i) for i = 1:nargs]
512-
eval(Expr(:(=), Expr(:call, :dotview, args...),
513-
Expr(:call, :getindex, args...)))
514-
eval(Expr(:(=), Expr(:call, :dotview, :(A::AbstractArray), args...),
515-
Expr(:call, :view, :A, args...)))
516-
end
506+
Base.@propagate_inbounds dotview(args...) = getindex(args...)
507+
Base.@propagate_inbounds dotview(A::AbstractArray, args...) = view(A, args...)
508+
Base.@propagate_inbounds dotview{T<:AbstractArray}(A::AbstractArray{T}, args...) = getindex(A, args...)
517509

518510
end # module

base/exports.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,7 @@ export
13861386
@label,
13871387
@goto,
13881388
@view,
1389+
@views,
13891390

13901391
# SparseArrays module re-exports
13911392
SparseArrays,

base/subarray.jl

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ end
381381
382382
Creates a `SubArray` from an indexing expression. This can only be applied directly to a
383383
reference expression (e.g. `@view A[1,2:end]`), and should *not* be used as the target of
384-
an assignment (e.g. `@view(A[1,2:end]) = ...`).
384+
an assignment (e.g. `@view(A[1,2:end]) = ...`). See also [`@views`](@ref)
385+
to switch an entire block of code to use views for slicing.
385386
"""
386387
macro view(ex)
387388
if isa(ex, Expr) && ex.head == :ref
@@ -391,3 +392,52 @@ macro view(ex)
391392
throw(ArgumentError("Invalid use of @view macro: argument must be a reference expression A[...]."))
392393
end
393394
end
395+
396+
############################################################################
397+
# @views macro code:
398+
399+
# maybeview is like getindex, but returns a view for slicing operations
400+
# (while remaining equivalent to getindex for scalar indices and non-array types)
401+
@propagate_inbounds maybeview(A, args...) = getindex(A, args...)
402+
@propagate_inbounds maybeview(A::AbstractArray, args...) = view(A, args...)
403+
@propagate_inbounds maybeview(A::AbstractArray, args::Number...) = getindex(A, args...)
404+
@propagate_inbounds maybeview(A) = getindex(A)
405+
@propagate_inbounds maybeview(A::AbstractArray) = getindex(A)
406+
407+
_views(x) = x
408+
_views(x::Symbol) = esc(x)
409+
function _views(ex::Expr)
410+
if ex.head in (:(=), :(.=))
411+
# don't use view on the lhs of an assignment
412+
Expr(ex.head, esc(ex.args[1]), _views(ex.args[2]))
413+
elseif ex.head == :ref
414+
ex = replace_ref_end!(ex)
415+
Expr(:call, :maybeview, _views.(ex.args)...)
416+
else
417+
h = string(ex.head)
418+
if last(h) == '='
419+
# don't use view on the lhs of an op-assignment
420+
Expr(first(h) == '.' ? :(.=) : :(=), esc(ex.args[1]),
421+
Expr(:call, esc(Symbol(h[1:end-1])), _views.(ex.args)...))
422+
else
423+
Expr(ex.head, _views.(ex.args)...)
424+
end
425+
end
426+
end
427+
428+
"""
429+
@views expression
430+
431+
Convert every array-slicing operation in the given expression
432+
(which may be a `begin`/`end` block, loop, function, etc.)
433+
to return a view. Scalar indices, non-array types, and
434+
explicit `getindex` calls (as opposed to `array[...]`) are
435+
unaffected.
436+
437+
Note that the `@views` macro only affects `array[...]` expressions
438+
that appear explicitly in the given `expression`, not array slicing that
439+
occurs in functions called by that code.
440+
"""
441+
macro views(x)
442+
_views(x)
443+
end

doc/src/manual/arrays.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ by copying. A `SubArray` is created with the [`view()`](@ref) function, which is
540540
way as [`getindex()`](@ref) (with an array and a series of index arguments). The result of [`view()`](@ref)
541541
looks the same as the result of [`getindex()`](@ref), except the data is left in place. [`view()`](@ref)
542542
stores the input index vectors in a `SubArray` object, which can later be used to index the original
543-
array indirectly.
543+
array indirectly. By putting the [`@views`](@ref) macro in front of an expression or
544+
block of code, any `array[...]` slice in that expression will be converted to
545+
create a `SubArray` view instead.
544546

545547
`StridedVector` and `StridedMatrix` are convenient aliases defined to make it possible for Julia
546548
to call a wider range of BLAS and LAPACK functions by passing them either [`Array`](@ref) or

doc/src/manual/performance-tips.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,43 @@ example, but in many contexts it is more convenient to just sprinkle
911911
some dots in your expressions rather than defining a separate function
912912
for each vectorized operation.)
913913

914+
## Consider using views for slices
915+
916+
In Julia, an array "slice" expression like `array[1:5, :]` creates
917+
a copy of that data (except on the left-hand side of an assignment,
918+
where `array[1:5, :] = ...` assigns in-place to that portion of `array`).
919+
If you are doing many operations on the slice, this can be good for
920+
performance because it is more efficient to work with a smaller
921+
contiguous copy than it would be to index into the original array.
922+
On the other hand, if you are just doing a few simple operations on
923+
the slice, the cost of the allocation and copy operations can be
924+
substantial.
925+
926+
An alternative is to create a "view" of the array, which is
927+
an array object (a `SubArray`) that actually references the data
928+
of the original array in-place, without making a copy. (If you
929+
write to a view, it modifies the original array's data as well.)
930+
This can be done for individual slices by calling [`view()`](@ref),
931+
or more simply for a whole expression or block of code by putting
932+
[`@views`](@ref) in front of that expression. For example:
933+
934+
```julia
935+
julia> fcopy(x) = sum(x[2:end-1])
936+
937+
julia> @views fview(x) = sum(x[2:end-1])
938+
939+
julia> x = rand(10^6);
940+
941+
julia> @time fcopy(x);
942+
0.003051 seconds (7 allocations: 7.630 MB)
943+
944+
julia> @time fview(x);
945+
0.001020 seconds (6 allocations: 224 bytes)
946+
```
947+
948+
Notice both the 3× speedup and the decreased memory allocation
949+
of the `fview` version of the function.
950+
914951
## Avoid string interpolation for I/O
915952

916953
When writing data to a file (or other I/O device), forming extra intermediate strings is a source

doc/src/stdlib/arrays.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Base.Broadcast.broadcast!
5656
Base.getindex(::AbstractArray, ::Any...)
5757
Base.view
5858
Base.@view
59+
Base.@views
5960
Base.to_indices
6061
Base.Colon
6162
Base.parent

test/subarray.jl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,6 @@ Y = 4:-1:1
472472

473473
@test isa(@view(X[1:3]), SubArray)
474474

475-
476475
@test X[1:end] == @view X[1:end]
477476
@test X[1:end-3] == @view X[1:end-3]
478477
@test X[1:end,2,2] == @view X[1:end,2,2]
@@ -490,6 +489,37 @@ let size=(x,y)-> error("should not happen")
490489
@test X[1:end,2,2] == @view X[1:end,2,2]
491490
end
492491

492+
# test @views macro
493+
@views let f!(x) = x[1:end-1] .+= x[2:end].^2
494+
x = [1,2,3,4]
495+
f!(x)
496+
@test x == [5,11,19,4]
497+
@test x[1:3] isa SubArray
498+
@test x[2] === 11
499+
@test Dict((1:3) => 4)[1:3] === 4
500+
x[1:2] = 0
501+
@test x == [0,0,19,4]
502+
x[1:2] .= 5:6
503+
@test x == [5,6,19,4]
504+
f!(x[3:end])
505+
@test x == [5,6,35,4]
506+
end
507+
@views @test isa(X[1:3], SubArray)
508+
@test X[1:end] == @views X[1:end]
509+
@test X[1:end-3] == @views X[1:end-3]
510+
@test X[1:end,2,2] == @views X[1:end,2,2]
511+
@test X[1,1:end-2] == @views X[1,1:end-2]
512+
@test X[1,2,1:end-2] == @views X[1,2,1:end-2]
513+
@test X[1,2,Y[2:end]] == @views X[1,2,Y[2:end]]
514+
@test X[1:end,2,Y[2:end]] == @views X[1:end,2,Y[2:end]]
515+
@test X[u...,2:end] == @views X[u...,2:end]
516+
@test X[(1,)...,(2,)...,2:end] == @views X[(1,)...,(2,)...,2:end]
517+
518+
# test macro hygiene
519+
let size=(x,y)-> error("should not happen")
520+
@test X[1:end,2,2] == @views X[1:end,2,2]
521+
end
522+
493523
# issue #18034
494524
# ensure that it is possible to create an isbits, LinearFast view of an immutable Array
495525
let

0 commit comments

Comments
 (0)