# Threaded loop far slower than sequential loop (+ high compilation time)

**URL:** https://discourse.julialang.org/t/threaded-loop-far-slower-than-sequential-loop-high-compilation-time/68338
**Category:** Performance
**Tags:** multithreading
**Created:** [September 17, 2021, 2:24pm UTC](https://discourse.julialang.org/t/threaded-loop-far-slower-than-sequential-loop-high-compilation-time/68338 "2021-09-17T14:24:43Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![StefanMathis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stefanmathis/32/27106_2.png) [@StefanMathis](https://discourse.julialang.org/u/StefanMathis)
#### Post date: [September 17, 2021, 2:24pm UTC](https://discourse.julialang.org/t/threaded-loop-far-slower-than-sequential-loop-high-compilation-time/68338/1 "2021-09-17T14:24:43Z")

</div>

Hello,

when trying to parallelize a loop, the performance got absolutely thrashed - roundabout by a factor of 1000. I tried to replicate the problem with a MWE and found something interesting:

```julia
main() = loop(Vector{Float64}(undef, 100_000))
function loop(arr)
    for k in 1:length(arr)
        k_float = float(k)
        arr[k] = sin(k_float)*cos(k_float)*tan(k_float)/sqrt(k_float) # Just some calculation to sink time into
    end
end

mainthreaded() = loopthreaded(Vector{Float64}(undef, 100_000))
function loopthreaded(arr)
    Threads.@threads for k in 1:length(arr)
        k_float = float(k)
        arr[k] = sin(k_float)*cos(k_float)*tan(k_float)/sqrt(k_float) # Just some calculation to sink time into
    end
end

println("===============")
@time main()
@time mainthreaded()

```

When running this script four times in a row, I get the following REPL output:

```julia-auto
  0.004578 seconds (2 allocations: 781.328 KiB)
  0.041883 seconds (47.42 k allocations: 3.599 MiB, 95.31% compilation time)
===============
  0.004192 seconds (2 allocations: 781.328 KiB)
  0.022073 seconds (18.21 k allocations: 1.812 MiB, 91.24% compilation time)
===============
  0.004347 seconds (2 allocations: 781.328 KiB)
  0.023520 seconds (18.21 k allocations: 1.812 MiB, 91.38% compilation time)
===============
  0.005623 seconds (2 allocations: 781.328 KiB)
  0.032980 seconds (18.21 k allocations: 1.812 MiB, 29.23% gc time, 95.21% compilation time)

```

So the threaded loop is roughly 5 times slower than the sequential loop (not counting for GC time) after the first execution. I am aware that for such a trivial example, the computational cost of managing threads is much higher than simply calculating the loop, therefore explaining the loss in speed. In my RL-application, the calculation is sufficiently expensive for multithreading to make sense. However, two things really stood out for me:

1. The high number of allocations (from my understanding, due to the thread spawning?)
2. Every time the `mainthreaded()` function is invoked, most of the time is spent as compilation time?

In my real-world application, the Gtk package is used (not in the loop, just in general). Profiling reports that almost all time is spent in `gtk_main()`, which seems to be the same issue as reported here:

> <https://github.com/JuliaLang/julia/issues/35552>
>
> xref https://github.com/JuliaGraphics/Gtk.jl/issues/503, which seems likely to b…e a Julia issue. Note that I observe it on both Julia 1.4 and 1.2, but Julia 1.0.5 does not share this problem.

However, even when completely removing Gtk from the project, the issue simply shifts to threading-setup functions.

One suggestion I found was using the `ThreadPools` package, see here:

> <https://github.com/JuliaLang/julia/issues/40535>
>
> This is a rather obscure but difficult to find interaction between the Threads l…ibrary and the inspectdr() backend for the Plots library. I have some code that attempts to use a simple @threads for loop to parallelize multiple QR decompositions of a block of raw data.  
> 
> Calling the inspectdr() initializer will cause the threaded code to run anywhere from 200 to 7000 times slower in this example. The difference for the genprojmat() function timings can be seen by commenting and uncommenting the inspectdr() line. I don't do any plotting in this script.
> 
> I'm on linux mint 20.1, 8 core processor.
> The code follows:
> \`\`\`julia
> \# test simple thread idea
> using Plots
> \# uncommenting inspectdr() causes the threaded version of genprojmat to run up to 7000 times slower
> inspectdr()
> using LinearAlgebra
> using BenchmarkTools
> 
> \#using QThread
> \##
> """ A matlab version of Julias QR decomposition.
> Q,R = flatqr(X) """
> function flatqr(X)
> F = qr(X)
> return(Matrix(F.Q), F.R)
> end
> 
> """ Circularly symmetric Complex noise """
> function cgauss(varargin...)
> 
> Z = (1 ./ sqrt(2.)).\* (randn(varargin)+im.\*randn(varargin))
> end
> 
> 
> """ complex zeros functions since I forget how to write the type signatures """
> function czeros(x...)
> y = zeros(Complex{Float64}, x) ;
> return(y)
> end # function czeros
> 
> """ Generate all the projection matrices. Use threaded loop to exploit embarassing parallel problem """
> function genprojmat(xdata)
> Mants, Nf, Ns = size(xdata)
> Qall = czeros(Ns, Mants, Nf)
> Threads.@threads for k = 1:Nf
> Qx, Rx = flatqr(xdata\[:,k,:\]')
> Qall\[:, :, k\] = Qx
> end
> return(Qall)
> end
> 
> 
> """ Generate all the projection matrices. Non threaded case is much faster. Why? """
> function genprojmatnt(xdata)
> Mants, Nf, Ns = size(xdata)
> Qall = czeros(Ns, Mants, Nf)
> for k = 1:Nf
> Qx, Rx = flatqr(xdata\[:,k,:\]')
> Qall\[:, :, k\] = Qx
> end
> return(Qall)
> end
> \##
> 
> \# Set up the input data
> Mants =4
> Nf = 24
> nsgn = 0.1
> apr = cgauss(4)
> chan = cgauss(Nf)
> ac = apr \* transpose(chan)
> 
> \# Mants, Nf, Ns = size(xdata)
> Ns = 1024
> K = 3
> xdata = nsgn .\* cgauss(Mants, Nf, Ns)
> st = randn(Ns)
> for q=1:Ns
> xdata\[:,:,q\] = xdata\[:,:,q\] + ac .\* st\[q\]
> end
> 
> \# threaded version
> @btime genprojmat(xdata) ;
> 
> \# not threaded version
> @btime genprojmatnt(xdata) ;
> \`\`\`
> 
> The output of running the script, first with inspectdr() uncommented:
> \`\`\`
> matt@Hope /mnt/WorkSpace/projects/Maestro/QThread $ julia -t 8
> \_
> \_ \_ \_(\_)\_ | Documentation: https://docs.julialang.org
> (\_) | (\_) (\_) |
> \_ \_ \_| |\_ \_\_ \_ | Type "?" for help, "\]?" for Pkg help.
> | | | | | | |/ \_\` | |
> | | |\_| | | | (\_| | | Version 1.6.0 (2021-03-24)
> \_/ |\\\_\_'\_|\_|\_|\\\_\_'\_| | Official https://julialang.org/ release
> |\_\_/ |
> 
> julia\> include("testthreaded.jl")
> Gtk-Message: 16:20:03.457: Failed to load module "xapp-gtk3-module"
> 5.003 s (332 allocations: 7.54 MiB)
> 2.380 ms (290 allocations: 7.54 MiB)
> 1024×4×24 Array{ComplexF64, 3}: .....
> julia\> Threads.nthreads()
> 8
> 
> \`\`\`
> Now with inspectdr() commented out and after restarting julia.
> \`\`\`
> matt@Hope /mnt/WorkSpace/projects/Maestro/QThread $ julia -t 8
> \_
> \_ \_ \_(\_)\_ | Documentation: https://docs.julialang.org
> (\_) | (\_) (\_) |
> \_ \_ \_| |\_ \_\_ \_ | Type "?" for help, "\]?" for Pkg help.
> | | | | | | |/ \_\` | |
> | | |\_| | | | (\_| | | Version 1.6.0 (2021-03-24)
> \_/ |\\\_\_'\_|\_|\_|\\\_\_'\_| | Official https://julialang.org/ release
> |\_\_/ |
> 
> julia\> Threads.nthreads()
> 8
> 
> julia\> include("testthreaded.jl")
> 705.049 μs (331 allocations: 7.54 MiB)
> 2.547 ms (290 allocations: 7.54 MiB)
> 1024×4×24 Array{ComplexF64, 3}:
> \`\`\`

However, this didn’t help either.

Therefore, I am wondering whether an individual thread is spawned for each value `k` and the loop body is recompiled for each single iteration. Could this be true? This could also explain the multitude of threading-related issues here on Discourse, e.g.:

> [@Fully parallelized for-loop becomes slower with more threads](https://discourse.julialang.org/t/fully-parallelized-for-loop-becomes-slower-with-more-threads/62614):
>
> Basically the title says it all: I have a for-loop that is fully parallelizable, so all the operations in the loop can be calculated independently. They only need access to the same multidimensional array but I make sure that they never use the same index at the same time. Now for the first few threads, this loop becomes faster the more threads I add. However, after a certain amount, it becomes slower… I understand that I cannot expect a linear speed increase with increasing threads but a slow …

> [@Multithreaded synchronization using events allocates a lot](https://discourse.julialang.org/t/multithreaded-synchronization-using-events-allocates-a-lot/68158):
>
> Context I’d like to use threads to parallelise a physics simulation code. The amount of work each thread has to do in a single loop might be quite small, so I want to keep all the threads running all the time as a lightweight Single-Program-Multiple-Data, but with shared memory (whether this is a good idea is another question I have, for a separate thread). As part of this, I wanted a barrier() (analogous to MPI.Barrier()). I tried to use Event to implement one, but ran into problems because I …

> [@Why with @threads, the execution time is worse?](https://discourse.julialang.org/t/why-with-threads-the-execution-time-is-worse/68160/20):
>
> Thank you @stillyslalom for your explanation. I have compared between the @batch and the method here:[https://discourse.julialang.org/t/optional-macro-invocation/18588/21](https://discourse.julialang.org/t/optional-macro-invocation/18588/21) function ss() dt = 0.001; tmin = 0; tmax = 5; timeSim = tmin:dt:tmax; A\_Mat = spzeros(9,9); for i in 1:9 A\_Mat[i,i] = 1; end I\_Mat = ones(9,3); iter = 0; for t in timeSim iter += 1; n=101 #@threadsif n\>100 for i in 1:n #by using it the time is 182 ms @batch minbatch=15 for i in 1:n #by u…

Any insights would be appreciated very much!

---

<div class="post-metadata">

### Author: ![s-broda](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/s-broda/32/3946_2.png) [@s-broda](https://discourse.julialang.org/u/s-broda)
#### Post date: [September 17, 2021, 2:43pm UTC](https://discourse.julialang.org/t/threaded-loop-far-slower-than-sequential-loop-high-compilation-time/68338/2 "2021-09-17T14:43:20Z")

</div>

When you say “running the script 4 times in a row”, do you mean you are restarting julia inbetween runs? That would explain why you see these compilation times, as the compiled code isn’t cached.

When I run the code twice in the same session, I get the following:

```julia
julia> @time main()
  0.003548 seconds (2 allocations: 781.328 KiB)

julia> @time mainthreaded()
  0.037268 seconds (47.78 k allocations: 3.629 MiB, 47.28% compilation time)

julia> @time main()
  0.003816 seconds (2 allocations: 781.328 KiB)

julia> @time mainthreaded()
  0.001187 seconds (127 allocations: 793.719 KiB)

```

So the threaded code is, in fact, faster (this is a 12 core machine).  
Cheers  
Simon

---

<div class="post-metadata">

### Author: ![StefanMathis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stefanmathis/32/27106_2.png) [@StefanMathis](https://discourse.julialang.org/u/StefanMathis)
#### Post date: [September 17, 2021, 2:57pm UTC](https://discourse.julialang.org/t/threaded-loop-far-slower-than-sequential-loop-high-compilation-time/68338/3 "2021-09-17T14:57:03Z")

</div>

Thank you for your fast answer! Well, this is embarrassing… I run the entire script four times, but of course this redefined the functions, therefore a recompilation was necessary each time. Doing it properly solves the issue. It doesn’t solve my RL-application issue, but I think I have to come up with a new MWE for that 😃

---

<div class="post-metadata">

### Author: ![raminammour](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/raminammour/32/13572_2.png) [@raminammour](https://discourse.julialang.org/u/raminammour)
#### Post date: [September 17, 2021, 3:16pm UTC](https://discourse.julialang.org/t/threaded-loop-far-slower-than-sequential-loop-high-compilation-time/68338/4 "2021-09-17T15:16:32Z")

</div>

The real issue is that the function without multithreading is precompiled when you run the script whereas the threaded function is not. That is why you don’t see the compilation time of `main()` every time you re-run the script (even though, as you said, you are redefining the function). I tried adding precompile statements to no avail.

Someone more knowledgeable than me should pitch in as to why this is the case 🙂
