From bd23ab2608a2bd71dce668637b2f874925018e98 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Mon, 27 Jun 2022 09:34:54 +0900 Subject: [PATCH] improve `@nospecialize`-d `[push!|pushfirst!]` implementations (#45790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently the `@nospecialize`-d `push!(::Vector{Any}, ...)` can only take a single item and we will end up with runtime dispatch when we try to call it with multiple items: ```julia julia> code_typed(push!, (Vector{Any}, Any)) 1-element Vector{Any}: CodeInfo( 1 ─ $(Expr(:foreigncall, :(:jl_array_grow_end), Nothing, svec(Any, UInt64), 0, :(:ccall), Core.Argument(2), 0x0000000000000001, 0x0000000000000001))::Nothing │ %2 = Base.arraylen(a)::Int64 │ Base.arrayset(true, a, item, %2)::Vector{Any} └── return a ) => Vector{Any} julia> code_typed(push!, (Vector{Any}, Any, Any)) 1-element Vector{Any}: CodeInfo( 1 ─ %1 = Base.append!(a, iter)::Vector{Any} └── return %1 ) => Vector{Any} ``` This commit adds a new specialization that it can take arbitrary-length items. Our compiler should still be able to optimize the single-input case as before via the dispatch mechanism. ```julia julia> code_typed(push!, (Vector{Any}, Any)) 1-element Vector{Any}: CodeInfo( 1 ─ $(Expr(:foreigncall, :(:jl_array_grow_end), Nothing, svec(Any, UInt64), 0, :(:ccall), Core.Argument(2), 0x0000000000000001, 0x0000000000000001))::Nothing │ %2 = Base.arraylen(a)::Int64 │ Base.arrayset(true, a, item, %2)::Vector{Any} └── return a ) => Vector{Any} julia> code_typed(push!, (Vector{Any}, Any, Any)) 1-element Vector{Any}: CodeInfo( 1 ─ %1 = Base.arraylen(a)::Int64 │ $(Expr(:foreigncall, :(:jl_array_grow_end), Nothing, svec(Any, UInt64), 0, :(:ccall), Core.Argument(2), 0x0000000000000002, 0x0000000000000002))::Nothing └── goto #7 if not true 2 ┄ %4 = φ (#1 => 1, #6 => %14)::Int64 │ %5 = φ (#1 => 1, #6 => %15)::Int64 │ %6 = Base.getfield(x, %4, true)::Any │ %7 = Base.add_int(%1, %4)::Int64 │ Base.arrayset(true, a, %6, %7)::Vector{Any} │ %9 = (%5 === 2)::Bool └── goto #4 if not %9 3 ─ goto #5 4 ─ %12 = Base.add_int(%5, 1)::Int64 └── goto #5 5 ┄ %14 = φ (#4 => %12)::Int64 │ %15 = φ (#4 => %12)::Int64 │ %16 = φ (#3 => true, #4 => false)::Bool │ %17 = Base.not_int(%16)::Bool └── goto #7 if not %17 6 ─ goto #2 7 ┄ return a ) => Vector{Any} ``` This commit also adds the equivalent implementations for `pushfirst!`. --- base/array.jl | 30 ++++++++++++++++++++++++++++-- test/compiler/inline.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/base/array.jl b/base/array.jl index cde8e292d881d..a572ee5c305e7 100644 --- a/base/array.jl +++ b/base/array.jl @@ -1053,9 +1053,19 @@ function push!(a::Array{T,1}, item) where T return a end -function push!(a::Array{Any,1}, @nospecialize item) +# specialize and optimize the single argument case +function push!(a::Vector{Any}, @nospecialize x) _growend!(a, 1) - arrayset(true, a, item, length(a)) + arrayset(true, a, x, length(a)) + return a +end +function push!(a::Vector{Any}, @nospecialize x...) + na = length(a) + nx = length(x) + _growend!(a, nx) + for i = 1:nx + arrayset(true, a, x[i], na+i) + end return a end @@ -1385,6 +1395,22 @@ function pushfirst!(a::Array{T,1}, item) where T return a end +# specialize and optimize the single argument case +function pushfirst!(a::Vector{Any}, @nospecialize x) + _growbeg!(a, 1) + a[1] = x + return a +end +function pushfirst!(a::Vector{Any}, @nospecialize x...) + na = length(a) + nx = length(x) + _growbeg!(a, nx) + for i = 1:nx + a[i] = x[i] + end + return a +end + """ popfirst!(collection) -> item diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 3dac08370c123..5a9cb1733be77 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -1375,3 +1375,30 @@ let src = code_typed1() do @test count(isnew, src.code) == 1 @test count(isinvoke(:noinline_finalizer), src.code) == 1 end + +# optimize `[push!|pushfirst!](::Vector{Any}, x...)` +@testset "optimize `$f(::Vector{Any}, x...)`" for f = Any[push!, pushfirst!] + @eval begin + let src = code_typed1((Vector{Any}, Any)) do xs, x + $f(xs, x) + end + @test count(iscall((src, $f)), src.code) == 0 + @test count(src.code) do @nospecialize x + isa(x, Core.GotoNode) || + isa(x, Core.GotoIfNot) || + iscall((src, getfield))(x) + end == 0 # no loop should be involved for the common single arg case + end + let src = code_typed1((Vector{Any}, Any, Any)) do xs, x, y + $f(xs, x, y) + end + @test count(iscall((src, $f)), src.code) == 0 + end + let xs = Any[] + $f(xs, :x, "y", 'z') + @test xs[1] === :x + @test xs[2] == "y" + @test xs[3] === 'z' + end + end +end