Standard streams much slower in Windows than in Linux

In case anybody is following along… I streamlined the test a little bit and profiled it. You need a file server.jl with these contents:

function server()
    while true
        s = String(readavailable(stdin))
        s[1] == 'q' && break
        println(stdout,"[Server via stdout]: received $(s[1])")
        println(stderr,"[Server via stderr]: received $(s[1])")
        flush(stdout)
        flush(stderr)
    end
end
server()
exit()

Then, run this code:

using Profile

function async_reader(io::IO)::Channel
    ch = Channel(1)
    task = @async begin
        buf = ""
        while isempty(buf) || buf[end] != '\n'
            data = String(readavailable(io))
            buf *= data
        end
        put!(ch,buf)
    end
    bind(ch, task)
    return ch
end

function init()
    pin = Pipe()
    pout = Pipe()
    perr = Pipe()
    proc = run(pipeline(`julia -L server.jl`,
                        stdin = pin, stdout = pout, stderr = perr),
               wait = false)
    close.([pout.in, perr.in, pin.out])
    return pin, pout, perr
end

function runtest(times, pin, pout, perr)
    for k=1:times
        write(pin,"a\n")
        flush(pin)
        ch_out = async_reader(pout)
        ch_err = async_reader(perr)
        out = take!(ch_out)
        err = take!(ch_err)
    end
    write(pin,"q\n")
    nothing
end

Profile.init(delay=0.001)
Profile.clear();
p = init();
runtest(1,p...);
p = init();
@profile runtest(1000,p...)
Profile.print()

In Linux, the resulting profile is

20  ./task.jl:259; (::getfield(REPL, Symbol("##26#27")){RE...
 20 ...lib/v1.1/REPL/src/REPL.jl:117; macro expansion
  20 ...lib/v1.1/REPL/src/REPL.jl:85; eval_user_input(::Any, ::REPL.REPLBackend)
   20 ./boot.jl:328; eval(::Module, ::Any)
    5 ./REPL[4]:3; runtest(::Int64, ::Pipe, ::Pipe, ::Pipe)
     5 ./strings/io.jl:165; write
      5 ./gcutils.jl:87; macro expansion
       5 ./io.jl:225; unsafe_write
        5 ./stream.jl:901; unsafe_write(::Base.PipeEndpoint, ::...
         5 ./stream.jl:845; uv_write(::Base.PipeEndpoint, ::Ptr...
          5 ./stream.jl:877; uv_write_async(::Base.PipeEndpoint...
    1 ./REPL[4]:5; runtest(::Int64, ::Pipe, ::Pipe, ::Pipe)
     1 ./REPL[2]:3; async_reader(::Pipe)
      1 ./task.jl:262; macro expansion
       1 ./boot.jl:384; Type
        1 ./boot.jl:384; Type
    1 ./REPL[4]:6; runtest(::Int64, ::Pipe, ::Pipe, ::Pipe)
     1 ./REPL[2]:3; async_reader(::Pipe)
      1 ./task.jl:262; macro expansion
       1 ./boot.jl:384; Type
        1 ./boot.jl:384; Type
    8 ./REPL[4]:7; runtest(::Int64, ::Pipe, ::Pipe, ::Pipe)
     8 ./channels.jl:315; take!
      8 ./channels.jl:362; take_buffered(::Channel{Any})
       8 ./event.jl:46; wait(::Condition)
        8 ./event.jl:256; wait()
         8 ./libuv.jl:100; process_events
    2 ./REPL[4]:8; runtest(::Int64, ::Pipe, ::Pipe, ::Pipe)
     2 ./channels.jl:315; take!
      2 ./channels.jl:362; take_buffered(::Channel{Any})
       2 ./event.jl:46; wait(::Condition)
        2 ./event.jl:246; wait()
         2 ./libuv.jl:98; process_events
561 ./task.jl:259; (::getfield(Main, Symbol("##5#6")){Pipe...
 561 ./REPL[2]:6; macro expansion
  561 ./io.jl:243; readavailable
   561 ./stream.jl:829; readavailable
    561 ./stream.jl:368; wait_readnb(::Base.PipeEndpoint, ::In...
     561 ./event.jl:46; wait(::Condition)
      538 ./event.jl:246; wait()
       538 ./libuv.jl:98; process_events
        1 ./stream.jl:523; uv_alloc_buf(::Ptr{Nothing}, ::UInt6...
         1 ./stream.jl:507; alloc_buf_hook(::Base.PipeEndpoint,...
          1 ./stream.jl:489; alloc_request(::Base.GenericIOBuff...
           1 ./iobuffer.jl:312; ensureroom
            1 ./iobuffer.jl:320; ensureroom
             1 ./array.jl:812; _growend!
      23  ./event.jl:256; wait()
       23 ./libuv.jl:100; process_events

In Windows, the profile is much longer. I’ll copy only the lines that correspond to ./task.jl:259:

2261 .\task.jl:259; (::getfield(Main, Symbol("##3#4")){Pipe,Channel{Any}})()
 2259 .\REPL[2]:6; macro expansion
  2259 .\io.jl:243; readavailable
   2259 .\stream.jl:829; readavailable
    1    .\stream.jl:367; wait_readnb(::Base.PipeEndpoint, ::Int64)
     1 .\stream.jl:715; start_reading
    401  .\stream.jl:368; wait_readnb(::Base.PipeEndpoint, ::Int64)
     400 .\event.jl:46; wait(::Condition)
      1   .\event.jl:245; wait()
       1 .\abstractarray.jl:863; isempty
        1 .\event.jl:266; ==
      389 .\event.jl:246; wait()
       389 .\libuv.jl:98; process_events
        3 .\stream.jl:516; uv_alloc_buf(::Ptr{Nothing}, ::UInt64, ::Ptr{Nothing})
        2 .\stream.jl:519; uv_alloc_buf(::Ptr{Nothing}, ::UInt64, ::Ptr{Nothing})
         1 .\sysimg.jl:18; getproperty
        3 .\stream.jl:523; uv_alloc_buf(::Ptr{Nothing}, ::UInt64, ::Ptr{Nothing})
        3 .\stream.jl:581; uv_readcb(::Ptr{Nothing}, ::Int64, ::Ptr{Nothing})
         3 .\stream.jl:564; (::getfield(Base, Symbol("#readcb_specialized#489")))(::Base.PipeEndpoint, :...
          3 .\event.jl:62; notify
           3 .\event.jl:62; notify
            3 .\event.jl:62; #notify#442
             3 .\event.jl:68; notify(::Condition, ::Nothing, ::Bool, ::Bool)
              3 .\event.jl:129; schedule
               3 .\event.jl:132; #schedule#443(::Bool, ::Function, ::Task, ::Nothing)
                3 .\sysimg.jl:19; setproperty!(::Task, ::Symbol, ::Nothing)
      2   .\event.jl:255; wait()
       1 .\event.jl:187; try_yieldto(::typeof(Base.ensure_rescheduled), ::Base.RefValue{Task})
       1 .\event.jl:199; try_yieldto(::typeof(Base.ensure_rescheduled), ::Base.RefValue{Task})
        1 .\sysimg.jl:19; setproperty!(::Task, ::Symbol, ::Nothing)
      8   .\event.jl:256; wait()
       8 .\libuv.jl:100; process_events
        2 .\stream.jl:523; uv_alloc_buf(::Ptr{Nothing}, ::UInt64, ::Ptr{Nothing})
         1 .\stream.jl:507; alloc_buf_hook(::Base.PipeEndpoint, ::UInt64)
          1 .\stream.jl:489; alloc_request(::Base.GenericIOBuffer{Array{UInt8,1}}, ::UInt64)
           1 .\iobuffer.jl:312; ensureroom
            1 .\iobuffer.jl:314; ensureroom
    1855 .\stream.jl:372; wait_readnb(::Base.PipeEndpoint, ::Int64)
     1855 .\stream.jl:737; stop_reading
    1    .\stream.jl:374; wait_readnb(::Base.PipeEndpoint, ::Int64)
     1 .\sysimg.jl:18; getproperty
    1    .\stream.jl:377; wait_readnb(::Base.PipeEndpoint, ::Int64)
     1 .\libuv.jl:57; unpreserve_handle(::Base.PipeEndpoint)
      1 .\abstractdict.jl:598; getindex
       1 .\abstractdict.jl:594; get
        1 .\sysimg.jl:18; getproperty
 1    .\REPL[2]:7; macro expansion
  1 .\strings\basic.jl:229; *
   1 .\strings\substring.jl:180; string(::String, ::String)
    1 .\strings\string.jl:60; _string_n
 1    .\int.jl:49; macro expansion
5    .\task.jl:294; task_done_hook(::Task)
 2 .\abstractarray.jl:1866; foreach(::getfield(Base, Symbol("##453#454")){Task}, ::Array{Any,1})
  2 .\task.jl:294; (::getfield(Base, Symbol("##453#454")){Task})(::getfield(Base, Symbol("##599#600"...
   2 .\channels.jl:197; #599
    2 .\channels.jl:230; close_chnl_on_taskdone(::Task, ::WeakRef)
 1 .\abstractdict.jl:598; getindex(::IdDict{Any,Any}, ::Any)
  1 .\abstractdict.jl:594; get
2    .\task.jl:295; task_done_hook(::Task)
 1 .\abstractdict.jl:605; delete!(::IdDict{Any,Any}, ::Any)

So, there is a lot more going on when running under Windows. I don’t understand the internals well enought to know if this points at a solution, though.

2 Likes