From 8fac39b71ca1c2a423ae1c7fc768722aff9005ee Mon Sep 17 00:00:00 2001 From: Oscar Smith Date: Mon, 23 Dec 2024 17:11:01 -0500 Subject: [PATCH] simplify and slightly improve memorynew inference (#56857) while investigating some missed optimizations in https://github.com/JuliaLang/julia/pull/56847, @gbaraldi and I realized that `copy(::Array)` was using `jl_genericmemory_copy_slice` rather than the `memmove`/`jl_genericmemory_copyto` that `copyto!` lowers to. This version lets us use the faster LLVM based Memory initialization, and the memove can theoretically be further optimized by LLVM (e.g. not copying elements that get over-written without ever being read). ``` julia> @btime copy($[1,2,3]) 15.521 ns (2 allocations: 80 bytes) # before 12.116 ns (2 allocations: 80 bytes) #after julia> m = Memory{Int}(undef, 3); julia> m.=[1,2,3]; julia> @btime copy($m) 11.013 ns (1 allocation: 48 bytes) #before 9.042 ns (1 allocation: 48 bytes) #after ``` We also optimize the `memorynew` type inference to make it so that getting the length of a memory with known length will propagate that length information (which is important for cases like `similar`/`copy` etc). --- Compiler/src/tfuncs.jl | 20 +++++++------------- base/array.jl | 9 +++++---- base/genericmemory.jl | 9 ++++++++- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Compiler/src/tfuncs.jl b/Compiler/src/tfuncs.jl index 906bc9521adf9..e51d43e5b2fe1 100644 --- a/Compiler/src/tfuncs.jl +++ b/Compiler/src/tfuncs.jl @@ -2017,9 +2017,12 @@ function tuple_tfunc(𝕃::AbstractLattice, argtypes::Vector{Any}) return anyinfo ? PartialStruct(𝕃, typ, argtypes) : typ end -@nospecs function memorynew_tfunc(𝕃::AbstractLattice, memtype, m) - hasintersect(widenconst(m), Int) || return Bottom - return tmeet(𝕃, instanceof_tfunc(memtype, true)[1], GenericMemory) +@nospecs function memorynew_tfunc(𝕃::AbstractLattice, memtype, memlen) + hasintersect(widenconst(memlen), Int) || return Bottom + memt = tmeet(𝕃, instanceof_tfunc(memtype, true)[1], GenericMemory) + memt == Union{} && return memt + # PartialStruct so that loads of Const `length` get inferred + return PartialStruct(𝕃, memt, Any[memlen, Ptr{Nothing}]) end add_tfunc(Core.memorynew, 2, 2, memorynew_tfunc, 10) @@ -3125,16 +3128,7 @@ add_tfunc(Core.get_binding_type, 2, 2, @nospecs((𝕃::AbstractLattice, args...) const FOREIGNCALL_ARG_START = 6 -function foreigncall_effects(@specialize(abstract_eval), e::Expr) - args = e.args - name = args[1] - isa(name, QuoteNode) && (name = name.value) - if name === :jl_alloc_genericmemory - nothrow = new_genericmemory_nothrow(abstract_eval, args) - return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow) - elseif name === :jl_genericmemory_copy_slice - return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow=false) - end +function foreigncall_effects(@nospecialize(abstract_eval), e::Expr) # `:foreigncall` can potentially perform all sorts of operations, including calling # overlay methods, but the `:foreigncall` itself is not dispatched, and there is no # concern that the method calls that potentially occur within the `:foreigncall` will diff --git a/base/array.jl b/base/array.jl index 4c3dde73d52ba..aafcfc182124b 100644 --- a/base/array.jl +++ b/base/array.jl @@ -346,12 +346,13 @@ See also [`copy!`](@ref Base.copy!), [`copyto!`](@ref), [`deepcopy`](@ref). """ copy -@eval function copy(a::Array{T}) where {T} - # `jl_genericmemory_copy_slice` only throws when the size exceeds the max allocation - # size, but since we're copying an existing array, we're guaranteed that this will not happen. +@eval function copy(a::Array) + # `copy` only throws when the size exceeds the max allocation size, + # but since we're copying an existing array, we're guaranteed that this will not happen. @_nothrow_meta ref = a.ref - newmem = ccall(:jl_genericmemory_copy_slice, Ref{Memory{T}}, (Any, Ptr{Cvoid}, Int), ref.mem, ref.ptr_or_offset, length(a)) + newmem = typeof(ref.mem)(undef, length(a)) + @inbounds unsafe_copyto!(memoryref(newmem), ref, length(a)) return $(Expr(:new, :(typeof(a)), :(memoryref(newmem)), :(a.size))) end diff --git a/base/genericmemory.jl b/base/genericmemory.jl index 9d4614135050b..2a33336c0aad6 100644 --- a/base/genericmemory.jl +++ b/base/genericmemory.jl @@ -144,6 +144,7 @@ function unsafe_copyto!(dest::Memory{T}, doffs, src::Memory{T}, soffs, n) where{ return dest end +#fallback method when types don't match function unsafe_copyto!(dest::Memory, doffs, src::Memory, soffs, n) @_terminates_locally_meta n == 0 && return dest @@ -171,7 +172,13 @@ function unsafe_copyto!(dest::Memory, doffs, src::Memory, soffs, n) return dest end -copy(a::T) where {T<:Memory} = ccall(:jl_genericmemory_copy, Ref{T}, (Any,), a) +function copy(a::T) where {T<:Memory} + # `copy` only throws when the size exceeds the max allocation size, + # but since we're copying an existing array, we're guaranteed that this will not happen. + @_nothrow_meta + newmem = T(undef, length(a)) + @inbounds unsafe_copyto!(newmem, 1, a, 1, length(a)) +end copyto!(dest::Memory, src::Memory) = copyto!(dest, 1, src, 1, length(src)) function copyto!(dest::Memory, doffs::Integer, src::Memory, soffs::Integer, n::Integer)