Skip to content

Fix order-dependent complexity_n accumulation across threads - #2382

Merged
dmah42 merged 1 commit into
mainfrom
complexity_threads
Nov 12, 2026
Merged

dmah42 merged 1 commit into
mainfrom
complexity_threads

Conversation

@dmah42

@dmah42 dmah42 commented Nov 12, 2026

Copy link
Copy Markdown
Member

Since #2346 made complexity_n an OptionalComplexityN, RunInThread used
"results.complexity_n is unset" to mean both "no thread has reported
yet" and "a thread reported without calling SetComplexityN()". In a
->Threads(N)->Complexity() benchmark where only some threads call
SetComplexityN() (e.g. only thread_index() == 0), the result depended
on which thread took the benchmark mutex first:

  • setting thread first: a later non-setting thread reset complexity_n
    to unset, so ComputeBigO aborted in debug builds ("Did you forget to
    call SetComplexityN?") or used -1 in release builds.
  • non-setting thread first: the setting thread then filled in a valid
    value.

Now threads that did not call SetComplexityN() leave the accumulated
value alone, and threads that did either start it or add to it. The
result is unset only if no thread set it, and it no longer depends on
thread completion order. This matches the behaviour from before #2346,
where complexity_n defaulted to 0 and was summed across threads.

Add a test with a two-thread Complexity() benchmark where only thread 0
calls SetComplexityN(). The other thread sleeps after its loop, so
thread 0 always reports first. Without the fix this test aborts.

AI disclosure: this change was developed with the help of an AI coding
assistant (Jetski/Gemini). I reviewed and verified the change.

…hmarks.

Failure path: In a `->Threads(N)->Complexity()` benchmark where one
thread calls `SetComplexityN()` and another does not (e.g., only
`thread_index() == 0` sets it), `results.complexity_n` starts unset in
src/thread_manager.h:31; if a setting thread acquires
`manager->GetBenchmarkMutex()` before a non-setting thread, we clear
`results.complexity_n` back to unset, whereas if the non-setting thread
acquires the mutex first, the setting thread overwrites it with a valid
value.

Checked: `RunThreads` runs threads concurrently without ordering mutex
acquisition, and `ThreadManager::Result` has no separate field
distinguishing "no thread reported yet" from "a thread reported without
setting complexity_n".

Fix: Track whether any thread has reported (or whether any thread missed
`SetComplexityN()`) with a dedicated flag in `ThreadManager::Result`
instead of overloading `!results.complexity_n.has_value()`, so all
thread completion orders produce a deterministic result.
@LebedevRI

Copy link
Copy Markdown
Collaborator

Thank you!
Hm, this whole ->Threads() thing is so convoluted.
I expected that we'd want all the threads to report, but i guess not?

@dmah42

dmah42 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

we could make it an error not to report i guess... i thought this defensive approach was safer (and it's a regression from the optional thing rather than a change in prior behaviour).

@LebedevRI LebedevRI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG as a fix.

@LebedevRI

Copy link
Copy Markdown
Collaborator

we could make it an error not to report i guess... i thought this defensive approach was safer (and it's a regression from the optional thing rather than a change in prior behaviour).

I kind-of expected such subtle invariants to be exposed by that change.
I'm not really sure what the behaviour should be, though.

@dmah42
dmah42 merged commit cf4d7fe into main Nov 12, 2026
125 checks passed
@dmah42
dmah42 deleted the complexity_threads branch November 12, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants