TranscodingStreams.jl
TranscodingStreams.jl copied to clipboard
Poor performance when reading primitive types that aren't UInt8?
I am trying to read some data in a looping way, reading a mix of primitive types. Here's a simple MWE where I am just reading Int32s:
import TranscodingStreams
function writer()
open(joinpath(homedir(), "test.dat"), write=true) do io
write(io, rand(Int32, 1_000_000))
end
end
function reader()
open(TranscodingStreams.NoopStream, joinpath(homedir(), "test.dat")) do io
for i in 1:1_000_000
read(io, Int32)
# read(io, UInt8)
end
end
end
writer()
reader()
julia> @time reader()
0.025751 seconds (1.00 M allocations: 15.276 MiB)
This produces an allocation per read
call, which becomes very slow in a tight loop! Reading 4 million UInt8s (see commented out line) has effectively zero allocations in the loop.
Is this expected behavior? Is my use-case uncommon? It does not seem to be specific to the NoopStream
, since I first encountered it with the CodecZstd.ZsdtDecompressorStream
.
Thanks.
The allocation happens here: https://github.com/JuliaLang/julia/blob/c094a89b9bf5ae9dccbf5d85c3925d619cfe1dcf/base/io.jl#L766, because in order to read from a stream julia allocates a Ref{Int32}
, and reads into it.
Also, while read(io,UInt8)
doesn't allocate, it is still much much slower compared to reading to a buffer of data all at once. For example, reading from stdin without any overhead of TranscodingStream has to at least obtain a lock on stdin, once per byte if you call it like that.
At least on my machine, reading from open("test.dat")
is even slower than with NoopStream (3.9s vs 3.1s), despite allocating only 712 bytes. Reading one word at a time is an anti-pattern that shouldn't really be expected to work well no matter what.
This is the relevant stacktrace I'm getting with Profile.Allocs.@profile
:
46-element Vector{Base.StackTraces.StackFrame}:
maybe_record_alloc_to_profile at gc-alloc-profiler.h:42 [inlined]
ijl_gc_pool_alloc at gc.c:1311
RefValue at refvalue.jl:8 [inlined]
Ref at refpointer.jl:136 [inlined]
read at io.jl:766 [inlined]
(::var"#7#8")(io::NoopStream{IOStream}) at REPL[14]:1
open(f::var"#7#8", #unused#::Type{NoopStream}, args::String) at stream.jl:161
reader(fn::String) at REPL[14]:1
reader(fn::String)
jl_apply at julia.h:1848 [inlined]
do_call at interpreter.c:126
eval_value at interpreter.c:215
eval_body at interpreter.c:467
eval_body at interpreter.c:522
jl_interpret_toplevel_thunk at interpreter.c:750
ip:0xffffffffffffffff
ip:0x2442
ip:0x7f4fe9967910
jl_system_image_data at sys.so:?
jl_toplevel_eval_flex at toplevel.c:912
jl_toplevel_eval_flex at toplevel.c:856
ijl_toplevel_eval_in at toplevel.c:971
eval at boot.jl:370 [inlined]
eval_user_input(ast::Any, backend::REPL.REPLBackend, mod::Module) at REPL.jl:152
repl_backend_loop(backend::REPL.REPLBackend, get_module::Function) at REPL.jl:248
start_repl_backend(backend::REPL.REPLBackend, consumer::Any; get_module::Function) at REPL.jl:233
start_repl_backend at REPL.jl:230 [inlined]
run_repl(repl::REPL.AbstractREPL, consumer::Any; backend_on_current_task::Bool) at REPL.jl:372
run_repl(repl::REPL.AbstractREPL, consumer::Any) at REPL.jl:357
run_repl(repl::REPL.AbstractREPL, consumer::Any) at sys.so:?
(::Base.var"#991#993"{Bool, Bool, Bool})(REPL::Module) at client.jl:413
(::Base.var"#991#993"{Bool, Bool, Bool})(REPL::Module) at sys.so:?
jl_apply at julia.h:1848 [inlined]
jl_f__call_latest at builtins.c:774
#invokelatest#2 at essentials.jl:808 [inlined]
invokelatest at essentials.jl:805 [inlined]
run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool) at client.jl:397
exec_options(opts::Base.JLOptions) at client.jl:314
_start() at client.jl:514
_start() at sys.so:?
jl_apply at julia.h:1848 [inlined]
true_main at jlapi.c:567
jl_repl_entrypoint at jlapi.c:711
main at loader_exe.c:59
__libc_start_main at libc.so.6:?
_start at julia:?