Allocation due to `@noinline` for unsafe_read and unsafe_write in io.jl

I’m currently working with a lot of File IO, and I noticed that whenever i call the function write(s::IO, x::Union{Int16,UInt16,Int32,UInt32,Int64,UInt64,Int128,UInt128,Float16,Float32,Float64}), there is an Allocation.

julia> using BenchmarkTools

julia> const io = open(tempname(; cleanup=true), "w")
IOStream(<file /tmp/jl_vp1BTP>)

julia> @btime write(io, 100)
  59.570 ns (1 allocation: 16 bytes)
8

After testing around with it and where this allocation happens, I’ve concluded that this is due to the function

@noinline unsafe_write(s::IO, p::Ref{T}, n::Integer) where {T} =
    unsafe_write(s, unsafe_convert(Ref{T}, p)::Ptr, n) # mark noinline to ensure ref is gc-rooted somewhere (by the caller)

in io.jl, line 673-674. I’m wondering why the @noinline here is needed, and why GC.@preserve p would not be used instead. From my testing, overwriting that function, using GC.@preserve p leads to no allocations.

julia> function Base.unsafe_write(s::IO, p::Ref{T}, n::Integer) where T
           GC.@preserve p unsafe_write(s, Base.unsafe_convert(Ref{T}, p)::Ptr, n)
       end

julia> @btime write(io, 100)
  51.872 ns (0 allocations: 0 bytes)
8

Am I missing something here? Am I misunderstanding the role of GC-Rooting it via the caller instead of using GC.@preserve?

The same @noinline is present in

@noinline unsafe_read(s::IO, p::Ref{T}, n::Integer) where {T} = unsafe_read(s, unsafe_convert(Ref{T}, p)::Ptr, n) # mark noinline to ensure ref is gc-rooted somewhere (by the caller)

and generic IO types that go into this function when calling e.g. read(io, Int) (IOStream goes into a specialized function that doesn’t cause an allocation) have the same issue of having an allocation.

julia> struct tstio <: IO
           b::UInt8
       end

julia> Base.read(t::tstio, ::Type{UInt8}) = t.b

julia> Base.write(::tstio, x::UInt8) = 1

julia> const t = tstio(0xFF)
tstio(0xff)

julia> @btime read(t, UInt64)
  11.796 ns (1 allocation: 16 bytes)
0xffffffffffffffff

julia> function Base.unsafe_read(s::IO, p::Ref{T}, n::Integer) where {T}
           GC.@preserve p unsafe_read(s, Base.unsafe_convert(Ref{T}, p)::Ptr, n)
       end

julia> @btime read(t, UInt64)
  1.494 ns (0 allocations: 0 bytes)
0xffffffffffffffff

Is the @noinline in these functions, which seems to have last been edited 4/5 years ago according to the git blame still needed, or does switching it out with GC.@preserve make more sense?

I have found that this has previously been discussed over on the github, and since I barely know anything about Julias Task switching yet my understanding of that ends here. Preserve GC reachability with `GC.@preserve` instead of `@inline` by melonedo · Pull Request #39759 · JuliaLang/julia · GitHub