# Scaling of @threads for "embarrassingly parallel" problem

**URL:** https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949
**Category:** Performance
**Tags:** threads
**Created:** [January 14, 2023, 4:08am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949 "2023-01-14T04:08:00Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 14, 2023, 4:08am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/1 "2023-01-14T04:08:00Z")

</div>

I’m having ~~a “trivially parallel”~~ an “embarrassingly parallel” problem, where I’m running the following function:

```julia
function map_fidelity(potential_depth_values, separation_time_values; kwargs...)
    N = length(potential_depth_values)
    M = length(separation_time_values)
    F = zeros(N, M)
    @inbounds Threads.@threads for j = 1:M
        @inbounds for i = 1:N
            t_r = separation_time_values[j]
            V0 = potential_depth_values[i]
            F[i, j] = propagate_splitting(t_r, V0; kwargs...)
        end
    end
    return F
end

```

The number of values, N and M are 22 and 13, each call to `propagate_splitting` takes on the order of a second (between 0.5 and 3 seconds, depending on the values). I was expecting that the runtime should scale almost perfectly linear with the number of threads, since each outer loop body has a relatively long runtime. However, I’m measuring the following runtimes, with `JULIA_EXCLUSIVE=1 julia --project=. -t n benchmark.jl`:

- 1 Thread: 267 sec
- 2 Threads: 200 sec
- 4 Threads: 170 sec
- 8 Threads: 152 sec

Each runtime is ± a couple of seconds. I can check with `tmux` that the process runs at `n * 100%` for `n` threads. There is no I/O in `propagate_splitting` or anything like that. The runtime with 1 thread is pretty close to the runtime for a version where I completely remove `Threads.@threads`.

Is there anything obvious I’m missing here that would explain why the scaling is so terrible? Any way I can get this to run in 35 sec or so with 8 threads?

---

<div class="post-metadata">

### Author: ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)
#### Post date: [January 14, 2023, 5:11am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/2 "2023-01-14T05:11:38Z")

</div>

Needs to know what propagate splitting is actually doing

---

<div class="post-metadata">

### Author: ![PetrKryslUCSD](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/petrkryslucsd/32/215825_2.png) [@PetrKryslUCSD](https://discourse.julialang.org/u/PetrKryslUCSD)
#### Post date: [January 14, 2023, 5:02pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/3 "2023-01-14T17:02:14Z")

</div>

What about if you replace `propagates_splitting` with a sleep to simulate work? Any difference?

---

<div class="post-metadata">

### Author: ![giordano](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/giordano/32/2166_2.png) [@giordano](https://discourse.julialang.org/u/giordano)
#### Post date: [January 14, 2023, 6:12pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/4 "2023-01-14T18:12:35Z")

</div>

Perhaps it’s not a problem here, but in general don’t put `@inbounds` in front of `@threads`: [Warn `@inbounds @threads for` pattern etc. during macro expansion by tkf · Pull Request #41893 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/pull/41893)

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [January 14, 2023, 6:13pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/5 "2023-01-14T18:13:35Z")

</div>

My first instinct here is to modify your code as follows. The most important part is assigning `t_r` outside of the inner loop.

```julia
function map_fidelity2(potential_depth_values, separation_time_values; kwargs...)
    N = length(potential_depth_values)
    M = length(separation_time_values)
    F = zeros(N, M)
    Threads.@threads for j in eachindex(separation_time_values)
        @inbounds t_r = separation_time_values[j]
        @inbounds for i in eachindex(potential_depth_values)
            V0 = potential_depth_values[i]
            F[i, j] = propagate_splitting(t_r, V0; kwargs...)
        end
    end
    return F
end

```

There could be some [false sharing](https://en.wikipedia.org/wiki/False_sharing#:~:text=In%20computer%20science%2C%20false%20sharing,managed%20by%20the%20caching%20mechanism.) issues here.

To address false sharing, we might try allocating an independent vector per task, and then assemble the matrix at the end.

```julia
function map_fidelity3(potential_depth_values, separation_time_values; kwargs...)
    N = length(potential_depth_values)
    M = length(separation_time_values)
    F = Vector{Vector{Float64}}(undef, M)
    Threads.@threads for j in eachindex(separation_time_values)
        @inbounds t_r = separation_time_values[j]
        F_j = zeros(N)
        @inbounds for i in eachindex(potential_depth_values)
            V0 = potential_depth_values[i]
            F_j[i] = propagate_splitting(t_r, V0; kwargs...)
        end
        F[j] = F_j
    end
    F = hcat(F_j...)
    return F
end

```

I’m not an expert at addressing false sharing issues, so perhaps there is a better way.

Let’s try to setup a minimum working example (MWE) so others can actually try to execute something. As others have said, we probably need to know more about `propagate_splitting`. For now, I’ll just substitute `sleep` in:

```julia
julia> begin
           const V0 = 1.5
           const N, M = 22, 13
           propagate_splitting(t_r, V0) = (sleep(1); t_r + V0)
           
           potential_depth_values = rand(N)
           separation_time_values = rand(M)
       end;

```

---

<div class="post-metadata">

### Author: ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)
#### Post date: [January 14, 2023, 6:16pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/6 "2023-01-14T18:16:01Z")

</div>

> [@mkitti](#):
>
> There could be some [false sharing](https://en.wikipedia.org/wiki/False_sharing#:~:text=In%20computer%20science%2C%20false%20sharing,managed%20by%20the%20caching%20mechanism.) issues here.

given how slow the inner most loop is I doubt it’s an false sharing issue. Also, OP was already using the correct loop ordering – different threads are accessing different column of the matrix, so unless the matrix is very small (col length \< 8?) it further reduces the likelihood of false sharing.

---

<div class="post-metadata">

### Author: ![jmair](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jmair/32/35117_2.png) [@jmair](https://discourse.julialang.org/u/jmair)
#### Post date: [January 14, 2023, 6:22pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/7 "2023-01-14T18:22:03Z")

</div>

Just to get some more information, what processor are you using?

---

<div class="post-metadata">

### Author: ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)
#### Post date: [January 14, 2023, 6:51pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/8 "2023-01-14T18:51:12Z")

</div>

> [@goerz](#):
>
> The number of values, N and M are 22 and 13

`Threads.@threads` is applied only to the `M` loop, which is 13 iterations.  
One thread doing 6 and another doing 7 should still get much better speedup than you’re seeing.

But, once you’re using 8 threads (or more!), it’d make sense IMO to `@threads :dynamic` both the inner and outer loop.

Of course, you do note that you saw 100% utilization.

Without more information, or better yet a minimal example, it’s hard for us to do more than speculate in the dark.

---

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 14, 2023, 10:14pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/9 "2023-01-14T22:14:29Z")

</div>

Thanks everyone for their suggestions!

@jling:

> Needs to know what propagate splitting is actually doing

Well, not exactly something trivial, but also not something crazy: initialize a model for a quantum system and a complex vector of size 1024, and then do a whole lot of FFT/iFFT, vector-vector-multiplication (or rather Diagonal matrix - vector multiplication) and elementwise exponentiation.

I put the full code for the benchmark here: [GitHub - goerz-testing/2023-01\_rotating\_tai\_benchmark: Benchmarking of parallelization for rotating TAI](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark)

It’s “minimal”, but still somewhat complicated. _Maybe_ I can do a self-contained single file that does something bogus but structurally similar to the original code. You _should_ be able to run it by just instantiating the project, as described in the README.

@Elrod:

> `Threads.@threads` is applied only to the `M` loop, which is 13 iterations.  
> One thread doing 6 and another doing 7 should still get much better speedup than you’re seeing.

Yeah, but I’m generally OK with that. Actually, a potentially larger issue is that the different calls to `propagate_splitting` have a significantly different runtime, and I think potentially all “short” runtimes end up in one thread and all “long” runtimes in another. To avoid both of these issues, I tried settting `N=M=16` and I’m also using a constant value for the input arrays, that that every call actually does the exact same thing. This is [“benchmark 2”](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark#equal-workload). This brings out an even worse scaling:

```julia
:> JULIA_EXCLUSIVE=1 julia --project=. -t 1 benchmark2.jl
153.666410 seconds (290.36 M allocations: 410.962 GiB, 5.05% gc time, 0.04% compilation time)
:> JULIA_EXCLUSIVE=1 julia --project=. -t 2 benc
 98.435518 seconds (309.35 M allocations: 411.444 GiB, 6.31% gc time, 0.08% compilation time)
:> JULIA_EXCLUSIVE=1 julia --project=. -t 4 benchmark2.jl
 99.298586 seconds (333.82 M allocations: 412.088 GiB, 5.35% gc time, 0.08% compilation time)
:> JULIA_EXCLUSIVE=1 julia --project=. -t 8 benchmark2.jl
121.498563 seconds (374.74 M allocations: 413.286 GiB, 3.96% gc time, 0.07% compilation time)

```

It gets slower again with more than 2 threads!

@PetrKryslUCSD:

> What about if you replace `propagates_splitting` with a sleep to simulate work? Any difference?

Yeah, I tried that as [“benchmark 5”](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark#sleeping) in the test repo, and the scaling is perfect in that case.

So yes, the “problem” is what’s happening inside `propagate_splitting`, but my question is: why should it matter? It’s a self-contained black box function, accessesing non-overlapping data on each thread. It all should be completely independent.

The only thing I could imagine is something like saturating memory bandwidth… but that seems very bad Julia behavior for this kind of code. What I mean: I’ve never had a problem like that doing similar kinds of numerics by “trivially” parallelizing with OpenMP in Fortran.

I mean, maybe I need to go in and optimize allocations. They seem a bit high in the benchmark, but not outrageous. Although the point of the question here isn’t necessarily to optimize to propagation code, at this point. This is kinda throw-away “daily research” code, and I’m just trying to reduce the time I have to wait for some calculations by a few hours by utilizing the cores in my workstation. I do wish Julia had manual memory management like Fortran, though!

@giordano:

> Perhaps it’s not a problem here, but in general don’t put `@inbounds` in front of `@threads`

The `@inbounds` actually don’t make any difference at all. I didn’t have any `@inbounds` initially and then added them later just to make sure. And then I left them in under the assumption that otherwise someone would say “Try adding `@inbounds`” 😉

@mkitti:

> The most important part is assigning `t_r` outside of the inner loop

> There could be some false sharing

Worth a try, but didn’t make a difference (results are in the test repo).

@jmair:

> Just to get some more information, what processor are you using?

An Intel Core-i9 with 8 cores (16 hyperthreads) running Linux. See the [“System Information”](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark#system-information) in the example repo for more details.

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [January 14, 2023, 10:33pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/10 "2023-01-14T22:33:50Z")

</div>

> [@goerz](#):
>
> Well, not exactly something trivial, but also not something crazy: initialize a model for a quantum system and a complex vector of size 1024, and then do a whole lot of FFT/iFFT, vector-vector-multiplication (or rather Diagonal matrix - vector multiplication) and elementwise exponentiation.

This is not what we call a “trivially parallel problem”.

Part of the issue here is that some of these downstream calls may themselves already be using parallel algorithms. For example, for matrix-matrix multiplication you are already using multiple threads to do that. You can check how many threads OpenBLAS is set to use.

```julia
julia> LinearAlgebra.BLAS.get_num_threads()
4

```

Now if you set the environment variable `OPENBLAS_NUM_THREADS=1` or use `LinearAlgebra.BLAS.set_num_threads(1)` you might see more linear scaling.

@carstenbauer has an explanation of this here:

> **[Julia Threads + BLAS Threads · ThreadPinning.jl](https://carstenbauer.github.io/ThreadPinning.jl/dev/explanations/blas/)**
>
> Documentation for ThreadPinning.jl.

---

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 14, 2023, 10:43pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/11 "2023-01-14T22:43:00Z")

</div>

Sorry, [“Embarrassingly parallel”](https://en.wikipedia.org/wiki/Embarrassingly_parallel), is the technical term I was looking for. What I mean is: I could do this by running multiple programs in parallel, writing their output to disk (I might test that out, even though it adds overhead on me having to organize everything). There is no communication between the different threads.

And yeah, Julia’s (and Python’s, for that matter) default of allowing multiple threads in BLAS etc. is one of the worst decisions, ever, IMO. I can’t express how much it drives me up the wall, and I really hope it changes in Julia 2.0. Code should never run multithreaded unless I ask for it! Alas, I have

```julia
export NUMEXPR_NUM_THREADS=1
export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
export OMP_NUM_THREADS=1
export MKL_NUM_THREADS=1

```

in my `.bashrc`, and I do keep an eye on `htop` during these benchmarks to check that I’m using as many cores as I’m requesting at 100% utilization, and that there are no weird unexpected sub-threads.

---

<div class="post-metadata">

### Author: ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)
#### Post date: [January 14, 2023, 10:56pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/12 "2023-01-14T22:56:31Z")

</div>

> [@goerz](#):
>
> And yeah, Julia’s (and Python’s, for that matter) default of allowing multiple threads in BLAS etc. is one of the worst decisions, ever, IMO.

without it how are you gonna multiply matrices together in an efficient way? (syntax wise)

can you set `LinearAlgebra.BLAS.set_num_threads(1)` explicitly?

and also, it sounds like your problem might be memory bounded not (entirely) CPU, which could explain why adding threads don’t speed up things linearly

---

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 15, 2023, 5:54am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/13 "2023-01-15T05:54:22Z")

</div>

> > default of allowing multiple threads in BLAS etc. is one of the worst decisions, ever,
> 
> without it how are you gonna multiply matrices together in an efficient way? (syntax wise)

I’m not saying multi-threaded BLAS shouldn’t exist. If there’s nothing better you can do, and you’re really only running one computation at a time, you might as well use the extra parallelization. Personally, I never want it: I can always parallelize at a much higher level, and (at least in Fortran), I’ve never had much trouble getting nearly linear speedups (if I use 8 cores, it’s 8 times faster).

Automatic threading should just not ever be a _default_: More than once, I’ve run into accidentally oversubscribing a cluster node because I didn’t set some (undocumented!) environment variable, and BLAS or something else decided to run multiple threads on top of my program which was already carefully crafted to exactly utilize the full cluster node. I need to have control over exactly how many processes/threads are running at all times!

> can you set `LinearAlgebra.BLAS.set_num_threads(1)` explicitly?

Sure! I tried it, it has no effect.

> and also, it sounds like your problem might be memory bounded not (entirely) CPU, which could explain why adding threads don’t speed up things linearly

I guess…? That’s the only thing that really comes to mind…

I did add [one more benchmark](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark#multi-process-parallelization) where I’m parallelizing across multiple processes (with `@distributed`). Those results are much more promising:

- 1 Process: 160 s
- 2 Processes: 92 s
- 4 Processes: 57 s
- 8 Processes: 48 s

However, that result is extremely alarming: shared-memory parallelization of the same problem should always have _less_ overhead than multi-process parallelization. So if there’s a memory bandwidth problem in the shared-memory `@threads` variant, it seems to me that Julia is _seriously_ mismanaging memory access here.

In any case: how would one go about analyzing memory bounded issues? Is it just about the allocations? Is there some other way to see how memory is being accessed?

---

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 15, 2023, 6:21am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/14 "2023-01-15T06:21:25Z")

</div>

Just to boil it down to the basics: I’m essentially just trying to do a parallel map here, where I have 256 values `x` (tuples of two numbers, but it doesn’t really matter), and I’m just trying to do `y = map(f, x)`. The `f` in this case is a relatively complicated function that takes about a second to run and does a fair amount of manipulating vectors, but it really shouldn’t matter: This kind of problem should always be perfectly parallelizable, almost no matter what `f` is.

One caveat would be if a single call to `f` saturates memory or I/O bandwidth, but I don’t _really_ see how that would be happening here. Besides, I think that would be reflected in a reduced CPU utilization that would be visible in `htop`, no? Plus, it seems fine in the multi-process setup, which should be worse, if anything. So, very strange… 🤷

---

<div class="post-metadata">

### Author: ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)
#### Post date: [January 15, 2023, 8:00am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/15 "2023-01-15T08:00:57Z")

</div>

> [@goerz](#):
>
> Besides, I think that would be reflected in a reduced CPU utilization that would be visible in `htop`, no?

No, it wouldn’t be visible in `htop`. Stalled cores waiting on memory would still be at 100%.

I would recommend [LIKWID](https://github.com/JuliaPerf/LIKWID.jl), however.

---

<div class="post-metadata">

### Author: ![jmair](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jmair/32/35117_2.png) [@jmair](https://discourse.julialang.org/u/jmair)
#### Post date: [January 15, 2023, 10:03am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/16 "2023-01-15T10:03:02Z")

</div>

I think the GC can get in the way of parallel threads and if your code is allocating a lot then all the threads have to stop to wait for the GC (someone who knows more can correct me if I’m mistaken).

Since you’ve switched to distributed and seen a much better improvement, this makes me think the GC is the issue bottlenecking your performance.

---

<div class="post-metadata">

### Author: ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)
#### Post date: [January 15, 2023, 3:38pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/17 "2023-01-15T15:38:50Z")

</div>

> [@goerz](#):
>
> shared-memory parallelization of the same problem should always have _less_ overhead than multi-process parallelization.

GC is not multi-threaded but is parallelized when you do multi-process. This makes sense and just means you need to reduce allocation in your workload

---

<div class="post-metadata">

### Author: ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)
#### Post date: [January 15, 2023, 4:40pm UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/18 "2023-01-15T16:40:43Z")

</div>

I didn’t bring up GC because it didn’t look like a major problem in this comment:

> [@Scaling of @threads for "embarrassingly parallel" problem](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/9):
>
> Thanks everyone for their suggestions! @jling: Needs to know what propagate splitting is actually doing Well, not exactly something trivial, but also not something crazy: initialize a model for a quantum system and a complex vector of size 1024, and then do a whole lot of FFT/iFFT, vector-vector-multiplication (or rather Diagonal matrix - vector multiplication) and elementwise exponentiation. I put the full code for the benchmark here: [GitHub - goerz-testing/2023-01\_rotating\_tai\_benchmark:…](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark)

Specifically:

```julia
:> JULIA_EXCLUSIVE=1 julia --project=. -t 1 benchmark2.jl
153.666410 seconds (290.36 M allocations: 410.962 GiB, 5.05% gc time, 0.04% compilation time)
:> JULIA_EXCLUSIVE=1 julia --project=. -t 2 benc
 98.435518 seconds (309.35 M allocations: 411.444 GiB, 6.31% gc time, 0.08% compilation time)
:> JULIA_EXCLUSIVE=1 julia --project=. -t 4 benchmark2.jl
 99.298586 seconds (333.82 M allocations: 412.088 GiB, 5.35% gc time, 0.08% compilation time)
:> JULIA_EXCLUSIVE=1 julia --project=. -t 8 benchmark2.jl
121.498563 seconds (374.74 M allocations: 413.286 GiB, 3.96% gc time, 0.07% compilation time)

```

Only 4% GC time with 8 threads.  
I’ve often seen \>80% gc time in multithreaded workloads when using 18 or more threads.  
This is far away from those really problematic cases.

---

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 16, 2023, 3:22am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/19 "2023-01-16T03:22:10Z")

</div>

> you need to reduce allocation in your workload

Indeed, that’s what turned out to solve the problem.

There was just one routine that was sloppily written, [which I was able to rewrite with an in-place version](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark/commit/b155236e399147c88d29d9865ec3986be5e87aa0#diff-a9ead8c60453b6d7ff21448e0842318f1e7210160716c3eb26c5c10fe800160b).

A big shoutout to [`TimerOutputs.jl`](https://github.com/KristofferC/TimerOutputs.jl), which was extremely helpful for the profiling!

With the reduced allocations, [I’m now seeing perfectly linear scaling](https://github.com/goerz-testing/2023-01_rotating_tai_benchmark#benchmark-of-julia-threads-for-trivially-parallel-problem):

```julia
:> JULIA_EXCLUSIVE=1 julia --project=. -t 1 benchmark2.jl
  0.210143 seconds (33.80 k allocations: 1.917 MiB)
  0.221251 seconds (33.88 k allocations: 1.922 MiB, 1.29% compilation time)
 54.201312 seconds (8.73 M allocations: 494.694 MiB, 0.10% gc time, 0.07% compilation time)

:> JULIA_EXCLUSIVE=1 julia --project=. -t 2 benchmark2.jl
  0.212420 seconds (33.80 k allocations: 1.917 MiB)
  0.225637 seconds (33.88 k allocations: 1.922 MiB, 1.24% compilation time)
 27.806800 seconds (8.73 M allocations: 494.716 MiB, 0.24% gc time, 0.17% compilation time)

:> JULIA_EXCLUSIVE=1 julia --project=. -t 4 benchmark2.jl
  0.213204 seconds (33.80 k allocations: 1.917 MiB)
  0.215185 seconds (33.88 k allocations: 1.922 MiB, 1.33% compilation time)
 14.431739 seconds (8.73 M allocations: 494.777 MiB, 0.41% gc time, 0.34% compilation time)

:> JULIA_EXCLUSIVE=1 julia --project=. -t 8 benchmark2.jl
  0.207609 seconds (33.80 k allocations: 1.917 MiB)
  0.212293 seconds (33.88 k allocations: 1.922 MiB, 1.33% compilation time)
  8.424910 seconds (8.73 M allocations: 494.711 MiB, 0.48% compilation time)

```

So, problem solved!

The whole issue does leave a bad aftertaste, though: I really feel that Julia’s garbage collector should handle this situation much better. At this point, garbage collection is the number one worry I have about Julia’s efficacy. Maybe in future versions, (in addition to just fixing the GC in parallel threads), Julia could gain a built-in `@deallocate` for manual memory management, which would allow turning off garbage collection for large section of a program — although I understand that this inevitably opens up the door to potential segfaults.

Anyway, thanks again, everyone. This has been very instructive!

---

<div class="post-metadata">

### Author: ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)
#### Post date: [January 16, 2023, 3:26am UTC](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949/20 "2023-01-16T03:26:22Z")

</div>

so as you can see the allocation turns out to be the problem?

I wonder if GC % is deceiving because it shows how much % of time GC used but it doesn’t account for the delay GC caused blocking other stuff from running.

I suspect that because GC is blocking further workload from running, the total time is increased, and the GC % doesn’t look as bad.

[Next page](https://discourse.julialang.org/t/scaling-of-threads-for-embarrassingly-parallel-problem/92949.md?page=2)
