ProgressMeter generates lots of allocations and overhead when multi-threaded (even if disabled)

Maybe I should post an issue on the package site, but lets go. Given this simple threaded loop:

julia> using ProgressMeter, BenchmarkTools

julia> function loop(n)
         s = zeros(Threads.nthreads())
         Threads.@threads for i in 1:n
           s[Threads.threadid()] += sin(i)
         end
         sum(s)
       end
loop (generic function with 1 method)

julia> @btime loop(1000000)
  4.401 ms (22 allocations: 1.97 KiB)
-0.1171095240981539

Now if I add a progress meter, even if it is disabled, I get:

julia> function loop(n)
         p = Progress(n,dt=0.1,enabled=false)
         s = zeros(Threads.nthreads())
         Threads.@threads for i in 1:n
           s[Threads.threadid()] += sin(i)
           next!(p)
         end
         sum(s)
       end
loop (generic function with 1 method)

julia> @btime loop(1000000)
  435.372 ms (369003 allocations: 11.26 MiB)
-0.1171095240981539

Thus, I get lots of allocations, and the overhead is also very large, considering that with it disabled I imagine that it should only return from next! without doing nothing.

I can even add this simple mynext! function to check if the progress meter is enabled, to get a much better result:

julia> function mynext!(p::Progress)
         if p.enabled
           next!(p)
         end
         nothing
       end
mynext! (generic function with 1 method)

julia> function loop(n)
         p = Progress(n,dt=0.1,enabled=false)
         s = zeros(Threads.nthreads())
         Threads.@threads for i in 1:n
           s[Threads.threadid()] += sin(i)
           mynext!(p)
         end
         sum(s)
       end
loop (generic function with 1 method)

julia> @btime loop(1000000)
  5.521 ms (31 allocations: 2.50 KiB)
-0.1171095240981539

I am doing something wrong, or should I just post an issue?

Thanks.

1 Like

I added a simple pull request that fixes that behavior: return immediately from next! if not enabled by lmiq · Pull Request #213 · timholy/ProgressMeter.jl · GitHub

Perhaps it is not what the developers would propose, but there it is.

One guess would be that a single progress meter should not be shared across threads.

Yes, you can, it is thread-safe. Basic example from the docs:

using ProgressMeter
p = Progress(10)
Threads.@threads for i in 1:10
    sleep(2*rand())
    next!(p)
end

I know nothing about internals of ProgressMeter, but I can guess, that in order for next to be thread safe operation, it should lock progressmeter. And each lock generates a pause in all calculations. In loop with 1_000_000 iterations you get 1_000_000 locks and it’ll generate a lot of overhead (and probably allocations).

All your suppositions are correct :-). Yet I think that when it is disabled it should avoid all that and just return.

(it does that one step down from the next! function, but apparently should be done earlier).

you’re just disabling the output, but it still keeping track of your progress in case you want to see it later

2 Likes

I see…

Then what I would want is a feature, which would be disabling it completely. (I can live with just creating my function that wraps next!, then).

This loop is buried inside a package and should be controlled by a user option.

use your flag then:

p = Progress(n,dt=0.1,enabled=do_progress)
...
...
do_progress && next!(p)
3 Likes

Well thought :slight_smile: