1

Hello everyone. I'm currently designing a brush engine for which the goal is to take arbitrarily defined node graphs and execute them with parallelism in mind. Think something like

NewLayerDelta = GraphOutput()
GraphOuput := Blend(
    CurrentLayerDelta, 
    ApplyMask(
        Fill(Black), 
        CircleMask(PenX, PenY, PenPressure)
    )
)

for a simple example.
While it works decently well (and outperforms some industry standard programs!) the main bottleneck seems to be coming from the overhead in the task scheduler. The canvas is organized in tiles and since many operations are "tilewise" (eg. blending two layers can be done in pairs of tiles from each) this makes the task a great fit for parallelism. Tiles usually contain 4kb of pixel data (64 x 64 x RGBA 8 bit channels) which makes every individual tilewise task tiny.

I've been trying so far to make my scheduler as lightweight as possible but I might be missing something really obvious (or maybe not so obvious) because when I try to scale past ~12 threads performance starts to decrease greatly, so much so that I lose any gains I got from multithreading by the time I get to 24 threads (how many logical cores I have and what Krita defaults to for parallelism on my machine.)

I'm assuming this has to do in some way with contention on cache lines or false sharing. I've tried to eliminate as much of it as possible, and it did work pretty well for the most part, but I'm reaching the limit of what I can figure out on my own.

Here's my code :

Any help / feedback appreciated!

2

Did you first try just using standard threads and channels?

I did try using standard library features at first, but I found that many of them would make my threads yield back to the OS scheduler and that was simply way too much overhead. I do use standard threads though (I don't think I know of any fitting alternative for that anyway.)

Are you referring to some specific place where I'd use standard channels?

I suggest using a profiler to narrow it down. cargo flamegraph is one easy first step. I've started using (on Linux) perf record -F 9999 --call-graph dwarf,32768 <exe_name> and then hotspot to analyze the output, as hotspot provides more views of the output, in addition to flamegraphs.

When the problem is fetching memory into CPU caches, this will show up as a simple memory access with an unusually high cost. I've found several such problems this way.