From 9034aff7a94c4ca888aeba547fea4cdec4fa86a8 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 16:56:21 -0500 Subject: [PATCH 01/10] at-views macro to convert a whole block of code to slices=views --- base/exports.jl | 1 + base/subarray.jl | 3 +- base/util.jl | 63 ++++++++++++++++++++++++++++++++++++++++ doc/src/stdlib/arrays.md | 1 + 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/base/exports.jl b/base/exports.jl index 1ad8ebf58f83a..e1f72b5e614a5 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -1384,6 +1384,7 @@ export @label, @goto, @view, + @views, # SparseArrays module re-exports SparseArrays, diff --git a/base/subarray.jl b/base/subarray.jl index 8079cafc1b1dd..ccae7e2207399 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -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 diff --git a/base/util.jl b/base/util.jl index 6cf42650cb7e1..b1926cbabf96a 100644 --- a/base/util.jl +++ b/base/util.jl @@ -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: +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)...) + 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 diff --git a/doc/src/stdlib/arrays.md b/doc/src/stdlib/arrays.md index 9171c68d652fc..a5cf36666d85b 100644 --- a/doc/src/stdlib/arrays.md +++ b/doc/src/stdlib/arrays.md @@ -56,6 +56,7 @@ Base.Broadcast.broadcast! Base.getindex(::AbstractArray, ::Any...) Base.view Base.@view +Base.@views Base.to_indices Base.Colon Base.parent From 60ba3ddd0604a2752977eb9386dfd49fefe4132c Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:02:37 -0500 Subject: [PATCH 02/10] inlining seems to eliminate the splatting penalty in maybeview(A, args...) = getindex(A, args...), and similarly for dotview --- base/broadcast.jl | 14 +++-------- base/subarray.jl | 49 ++++++++++++++++++++++++++++++++++++ base/util.jl | 63 ----------------------------------------------- 3 files changed, 52 insertions(+), 74 deletions(-) diff --git a/base/broadcast.jl b/base/broadcast.jl index db7e014c102df..716392f80de5d 100644 --- a/base/broadcast.jl +++ b/base/broadcast.jl @@ -503,16 +503,8 @@ end # explicit calls to view. (All of this can go away if slices # are changed to generate views by default.) -dotview(args...) = getindex(args...) -dotview(A::AbstractArray, args...) = view(A, args...) -dotview{T<:AbstractArray}(A::AbstractArray{T}, args...) = getindex(A, args...) -# avoid splatting penalty in common cases: -for nargs = 0:5 - args = Symbol[Symbol("x",i) for i = 1:nargs] - eval(Expr(:(=), Expr(:call, :dotview, args...), - Expr(:call, :getindex, args...))) - eval(Expr(:(=), Expr(:call, :dotview, :(A::AbstractArray), args...), - Expr(:call, :view, :A, args...))) -end +Base.@propagate_inbounds dotview(args...) = getindex(args...) +Base.@propagate_inbounds dotview(A::AbstractArray, args...) = view(A, args...) +Base.@propagate_inbounds dotview{T<:AbstractArray}(A::AbstractArray{T}, args...) = getindex(A, args...) end # module diff --git a/base/subarray.jl b/base/subarray.jl index ccae7e2207399..bfdd486fd4a3a 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -392,3 +392,52 @@ macro view(ex) throw(ArgumentError("Invalid use of @view macro: argument must be a reference expression A[...].")) end end + +############################################################################ +# @views macro code: + +# 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) + +_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)...) + 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 diff --git a/base/util.jl b/base/util.jl index b1926cbabf96a..6cf42650cb7e1 100644 --- a/base/util.jl +++ b/base/util.jl @@ -750,66 +750,3 @@ 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: -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)...) - 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 From f5f109881143d1287cf8564c4402268041281a98 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:03:03 -0500 Subject: [PATCH 03/10] handle :end in at-views --- base/subarray.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/base/subarray.jl b/base/subarray.jl index bfdd486fd4a3a..d242bf853e8a0 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -411,6 +411,7 @@ function _views(ex::Expr) # 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 + ex = replace_ref_end!(ex) Expr(:call, :maybeview, map(_views, ex.args)...) else h = string(ex.head) From c9e03864ee969207412049cc233145ab84255644 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:20:08 -0500 Subject: [PATCH 04/10] added at-views tests --- test/subarray.jl | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/subarray.jl b/test/subarray.jl index 8cb911b03e4e6..0d5d3cb81e032 100644 --- a/test/subarray.jl +++ b/test/subarray.jl @@ -472,7 +472,6 @@ Y = 4:-1:1 @test isa(@view(X[1:3]), SubArray) - @test X[1:end] == @view X[1:end] @test X[1:end-3] == @view X[1:end-3] @test X[1:end,2,2] == @view X[1:end,2,2] @@ -490,6 +489,37 @@ let size=(x,y)-> error("should not happen") @test X[1:end,2,2] == @view X[1:end,2,2] end +# test @views macro +@views let f!(x) = x[1:end-1] .+= x[2:end].^2 + x = [1,2,3,4] + f!(x) + @test x == [5,11,19,4] + @test x[1:3] isa SubArray + @test x[2] === 11 + @test Dict((1:3) => 4)[1:3] === 4 + x[1:2] = 0 + @test x == [0,0,19,4] + x[1:2] .= 5:6 + @test x == [5,6,19,4] + f!(x[3:end]) + @test x == [5,6,35,4] +end +@views @test isa(X[1:3], SubArray) +@test X[1:end] == @views X[1:end] +@test X[1:end-3] == @views X[1:end-3] +@test X[1:end,2,2] == @views X[1:end,2,2] +@test X[1,1:end-2] == @views X[1,1:end-2] +@test X[1,2,1:end-2] == @views X[1,2,1:end-2] +@test X[1,2,Y[2:end]] == @views X[1,2,Y[2:end]] +@test X[1:end,2,Y[2:end]] == @views X[1:end,2,Y[2:end]] +@test X[u...,2:end] == @views X[u...,2:end] +@test X[(1,)...,(2,)...,2:end] == @views X[(1,)...,(2,)...,2:end] + +# test macro hygiene +let size=(x,y)-> error("should not happen") + @test X[1:end,2,2] == @views X[1:end,2,2] +end + # issue #18034 # ensure that it is possible to create an isbits, LinearFast view of an immutable Array let From 0fa91818d2609cb8414812557d3b0959970bc076 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:22:21 -0500 Subject: [PATCH 05/10] NEWS for at-views --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 5d37f11030bc7..b5c52334da55f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -129,6 +129,9 @@ This section lists changes that do not have deprecation warnings. Library improvements -------------------- + * `@views` macro to convert a whole expression or block of code to + use views for all slices ([#20164]). + * `max`, `min`, and related functions (`minmax`, `maximum`, `minimum`, `extrema`) now return `NaN` for `NaN` arguments ([#12563]). From 03bd1c681ae056cb262c84fb82e1010ba3e16ac8 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:27:38 -0500 Subject: [PATCH 06/10] code simplification --- base/subarray.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/base/subarray.jl b/base/subarray.jl index d242bf853e8a0..119a6c67c84d9 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -412,16 +412,15 @@ function _views(ex::Expr) Expr(ex.head, esc(ex.args[1]), _views(ex.args[2])) elseif ex.head == :ref ex = replace_ref_end!(ex) - Expr(:call, :maybeview, map(_views, ex.args)...) + Expr(:call, :maybeview, _views.(ex.args)...) 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])...)) + Expr(:call, esc(Symbol(h[1:end-1])), _views.(ex.args)...) else - Expr(ex.head, map(_views, ex.args)...) + Expr(ex.head, _views.(ex.args)...) end end end From 416b338597b2b9e1201ea8653188f11392587a3c Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:29:50 -0500 Subject: [PATCH 07/10] doc tweak --- base/subarray.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/subarray.jl b/base/subarray.jl index 119a6c67c84d9..38bdeb481f31f 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -426,16 +426,16 @@ function _views(ex::Expr) end """ - @views code + @views expression -Convert every array-slicing operation in the given `code` +Convert every array-slicing operation in the given expression (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 +that appear explicitly in the given `expression`, not array slicing that occurs in functions called by the code. """ macro views(x) From 80522590655d7aaf08fd8f6326086095f9c85225 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:34:46 -0500 Subject: [PATCH 08/10] doc tweak --- base/subarray.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/subarray.jl b/base/subarray.jl index 38bdeb481f31f..9cb752eb859f2 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -436,7 +436,7 @@ unaffected. Note that the `@views` macro only affects `array[...]` expressions that appear explicitly in the given `expression`, not array slicing that -occurs in functions called by the code. +occurs in functions called by that code. """ macro views(x) _views(x) From dccb16b00bf1ff83f4928f53f95556b260ed0cd9 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 20 Jan 2017 21:53:34 -0500 Subject: [PATCH 09/10] added manual ref and performance tip on views --- doc/src/manual/arrays.md | 4 +++- doc/src/manual/performance-tips.md | 37 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/doc/src/manual/arrays.md b/doc/src/manual/arrays.md index c335e8ad42ee6..aca67d4dfbf8e 100644 --- a/doc/src/manual/arrays.md +++ b/doc/src/manual/arrays.md @@ -536,7 +536,9 @@ by copying. A `SubArray` is created with the [`view()`](@ref) function, which is way as [`getindex()`](@ref) (with an array and a series of index arguments). The result of [`view()`](@ref) looks the same as the result of [`getindex()`](@ref), except the data is left in place. [`view()`](@ref) stores the input index vectors in a `SubArray` object, which can later be used to index the original -array indirectly. +array indirectly. By putting the [`@views`](@ref) macro in front of an expression or +block of code, any `array[...]` slice in that expression will be converted to +create a `SubArray` view instead. `StridedVector` and `StridedMatrix` are convenient aliases defined to make it possible for Julia to call a wider range of BLAS and LAPACK functions by passing them either [`Array`](@ref) or diff --git a/doc/src/manual/performance-tips.md b/doc/src/manual/performance-tips.md index e61c4292c32d5..8707fbe7f9702 100644 --- a/doc/src/manual/performance-tips.md +++ b/doc/src/manual/performance-tips.md @@ -911,6 +911,43 @@ example, but in many contexts it is more convenient to just sprinkle some dots in your expressions rather than defining a separate function for each vectorized operation.) +## Consider using views for slices + +In Julia, an array "slice" expression like `array[1:5, :]` creates +a copy of that data (except on the left-hand side of an assignment, +where `array[1:5, :] = ...` assigns in-place to that portion of `array`). +If you are doing many operations on the slice, this can be good for +performance because it is more efficient to work with a smaller +contiguous copy than it would be to index into the original array. +On the other hand, if you are just doing a few simple operations on +the slice, the cost of the allocation and copy operations can be +substantial. + +An alternative is to create a "view" of the array, which is +an array object (a `SubArray`) that actually references the data +of the original array in-place, without making a copy. (If you +write to a view, it modifies the original array's data as well.) +This can be done for individual slices by calling [`view()`](@ref), +or more simply for a whole expression or block of code by putting +[`@views`](@ref) in front of that expression. For example: + +```julia +julia> fcopy(x) = sum(x[2:end-1]) + +julia> @views fview(x) = sum(x[2:end-1]) + +julia> x = rand(10^6); + +julia> @time fcopy(x); + 0.003051 seconds (7 allocations: 7.630 MB) + +julia> @time fview(x); + 0.001020 seconds (6 allocations: 224 bytes) +``` + +Notice both the 3× speedup and the decreased memory allocation +of the `fview` version of the function. + ## Avoid string interpolation for I/O When writing data to a file (or other I/O device), forming extra intermediate strings is a source From b7f8da6b96f02fbffd387d9b531a7108799b1e2b Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Sat, 21 Jan 2017 10:53:33 -0500 Subject: [PATCH 10/10] typo --- base/subarray.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/subarray.jl b/base/subarray.jl index 9cb752eb859f2..538bf8fd65059 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -418,7 +418,7 @@ function _views(ex::Expr) 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)...) + Expr(:call, esc(Symbol(h[1:end-1])), _views.(ex.args)...)) else Expr(ex.head, _views.(ex.args)...) end