Running AST directly (for scripts), bypassing parsing; deserialize ok in the sysimage?

In short it’s faster, should go down to 1.02 sec from 1.318, 23% faster, assuming precompile overhead 0.51+ sec goes away, to run the abstract syntax tree (AST) than parse a script. I was testing with:

However it’s not currently because (i.e. would be if deserialize were in the sysimage, assuming it doesn’t slow down startup much):

$ julia +1.13 --trace-compile=stderr -e "using Serialization; forms = deserialize(\"input.jast\")"
precompile(Tuple{typeof(Serialization.deserialize), String})
precompile(Tuple{typeof(Serialization.deserialize_fillarray!), Array{Any, 1}, Serialization.Serializer{Base.IOStream}})
precompile(Tuple{typeof(Serialization.deserialize), Serialization.Serializer{Base.IOStream}, typeof(DataType)})

$ hyperfine 'julia +1.13 -e "using Serialization; forms = deserialize(\"input.jast\")" >/dev/null'

708.8-191.2 = 517.6 ms after accounting for startup. 1.318

But deserialize only costs 0.3935 ms (2821-212.8)/10000 = 0.26 ms on Julia 1.14, and timed with more accurate 10,000 iterations, for my example code (35% faster than parsing the code, if I recall). Because of the precomilation overhead it’s much more now, shown by:

$ hyperfine 'julia/julia -e "using Serialization; for i in 1:10000 forms = deserialize(\"input.jast\") end" '
Benchmark 1: julia/julia -e "using Serialization; for i in 1:10000 forms = deserialize(\"input.jast\") end" 
  Time (mean ± σ):      2.927 s ±  0.082 s    [User: 2.726 s, System: 0.194 s]
  Range (min … max):    2.810 s …  3.052 s    10 runs

$ hyperfine 'julia +1.13 -e "using Serialization; for i in 1:1000 forms = deserialize(\"input.jast\") end" '
Benchmark 1: julia +1.13 -e "using Serialization; for i in 1:1000 forms = deserialize(\"input.jast\") end"
  Time (mean ± σ):      1.201 s ±  0.076 s    [User: 1.058 s, System: 0.141 s]
  Range (min … max):    1.104 s …  1.328 s    10 runs

So are we willing to have deserialize in the sysimage?

Currently 0.508 sec slower (only 1.530-1.318 = 0.21 sec slower on Julia 1.14, much better then the 0.51 sec precompilation overhead on 1.14) than running directly:

$ hyperfine "julia +1.13 -e 'using Serialization; forms = deserialize(\"input.jast\"); foreach(eval, forms);' 25000000 >/dev/null"

$ hyperfine "julia +1.13 fasta2.jl 25000000 >/dev/null"


using Serialization

filename = "fasta2.jl"
src = read(filename, String)

forms = Any[]
pos = 1

while pos <= lastindex(src)
    ex, nextpos = Meta.parse(src, pos; raise=false, filename=filename)
    ex === nothing && break

    push!(forms, ex)

    nextpos <= pos && break
    pos = nextpos
end

open("input.jast", "w") do io
    serialize(io, forms)
end

I got more precompiles on 1.14, running separately:

julia> using Serialization
julia> @time precompile(Tuple{typeof(Base.ScopedValues.hasdefault), Base.ScopedValues.ScopedValue{Union{Nothing, Module}}})
  0.001457 seconds (1.65 k allocations: 80.516 KiB, 97.14% compilation time)
julia> @time precompile(Tuple{typeof(Base.ScopedValues.getdefault), Base.ScopedValues.ScopedValue{Union{Nothing, Module}}})
  0.002533 seconds (2.58 k allocations: 126.953 KiB, 97.56% compilation time)
julia> @time precompile(Tuple{Core.TypeEgal{Base.Fix{1, F, T} where T where F}, typeof(Base.ScopedValues.getdefault), Base.ScopedValues.ScopedValue{Union{Nothing, Module}}})
  0.001903 seconds (1.38 k allocations: 75.141 KiB, 95.06% compilation time)
julia> @time precompile(Tuple{Base.Fix{1, typeof(Base.ScopedValues.getdefault), Base.ScopedValues.ScopedValue{Union{Nothing, Module}}}})
  0.004907 seconds (8.37 k allocations: 415.836 KiB, 98.68% compilation time)


julia> @time precompile(Tuple{typeof(Serialization.deserialize), String})
  **0.510236** seconds (436.34 k allocations: 23.950 MiB, 6.44% gc time, 99.99% compilation time)

julia> @time precompile(Tuple{typeof(Serialization.deserialize_fillarray!), Array{Any, 1}, Serialization.Serializer{Base.IOStream}})
  0.002802 seconds (4.28 k allocations: 230.438 KiB, 98.47% compilation time)

julia> @time precompile(Tuple{typeof(Serialization.deserialize), Serialization.Serializer{Base.IOStream}, Core.TypeEgal{LineNumberNode}})
  0.046021 seconds (13.89 k allocations: 800.102 KiB, 99.90% compilation time)

julia> @time precompile(Tuple{typeof(Base.foreach), Core.EvalInto, Array{Any, 1}})
  0.003172 seconds (2.59 k allocations: 149.766 KiB, 98.61% compilation time)
julia> @time precompile(Tuple{Core.TypeEgal{Ref{T} where T}, UInt32})
  0.001111 seconds (704 allocations: 34.969 KiB, 94.53% compilation time)
julia> @time precompile(Tuple{typeof(Base.rem), Char, Core.TypeEgal{UInt8}})
  0.001404 seconds (256 allocations: 11.781 KiB, 96.56% compilation time)
julia> @time precompile(Tuple{typeof(Base.convert), Core.TypeEgal{Tuple{Vararg{Int64}}}, Tuple{Int32}})
  0.001155 seconds (65 allocations: 3.375 KiB, 91.74% compilation time)
julia> @time precompile(Tuple{typeof(Base.:(-)), Int32, Int64})
  0.001469 seconds (51 allocations: 2.891 KiB, 93.78% compilation time)
julia> @time precompile(Tuple{typeof(Base.:(/)), Int64, Int32})
  0.001248 seconds (978 allocations: 48.484 KiB, 94.51% compilation time)