Skip to content

Fix complexity calculation when range includes 0 (#410) - #2346

Merged
dmah42 merged 14 commits into
google:mainfrom
amoralpha:fix-issue-410-clean
Oct 30, 2026
Merged

dmah42 merged 14 commits into
google:mainfrom
amoralpha:fix-issue-410-clean

Conversation

@amoralpha

Copy link
Copy Markdown
Contributor

Fixes #410.

complexity_n now defaults to -1 (“not set”) instead of 0, so SetComplexityN(0) is distinguishable from never calling it, as suggested in the issue. The check in ComputeBigO is relaxed to >= 0. Since n == 0 is now reachable, I added guards for log2(0) in oLogN/oNLogN and for a zero denominator in MinimalLeastSq.

Tests: zero-starting ranges for oN, auto, lambda, and oLogN with Range(0, 1), plus a death test for the missing SetComplexityN case. Without the fix, complexity_benchmark fails with the exact error from the issue (“Did you forget to call SetComplexityN?”).

Design note: n == 0 carries no asymptotic information, so the motivation is only that Range(0, ...) is a natural parameterization and currently aborts with a misleading message. If you’d prefer, I can instead keep n == 0 runs in the report but exclude them from the BigO fit.

Differentiate between uncalled SetComplexityN and SetComplexityN(0) by initializing State::complexity_n_ and ThreadManager::Result::complexity_n to -1, and relaxing the check in ComputeBigO to BM_CHECK_GE(run.complexity_n, 0).
Add guards against n <= 1 in logarithmic curves and sigma_gn_squared == 0 in MinimalLeastSq.
Add unit tests for zero-starting ranges and a death test for missing SetComplexityN calls.
Comment thread src/complexity.cc Resolved
Comment thread src/thread_manager.h Resolved
Comment thread test/benchmark_gtest.cc Resolved
Comment thread src/benchmark_runner.cc Resolved
- Use ComplexityN instead of IterationCount for complexity curves, Result::complexity_n, tests, and documentation.
- Update complexity_n accumulation logic in RunInThread.

@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.

The more i stare at this -1 the more i don't like it.
Please just do std::Optional<ComplexityN> complexity_n = None;

Comment thread src/benchmark.cc Resolved
Comment thread src/benchmark_runner.cc Resolved

@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.

Please update all complexity_n/complexity_n_ fields to all be Optional<>.
There should not be any == -1/<0/>=0 checks.

@amoralpha

amoralpha commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Please update all complexity_n/complexity_n_ fields to all be Optional<>. There should not be any == -1/<0/>=0 checks.

Thanks for the review. I ran into an issue making everything std::optional, so I’d like to ask for your input before going further.

ThreadManager::Result is internal, so std::optional works fine there. The problem is State::complexity_n_ and BenchmarkReporter::Run::complexity_n, which are in public headers. cxx11_test builds those headers in C++11/14 mode, where doesn’t exist, so it breaks (MSVC: 'optional': is not a member of 'std'). Everything else builds fine.

Also, changing the type of the public Run::complexity_n would break any custom reporter that reads it as an integer.

I see three options:

1.Bump cxx11_test to require C++17, and use std::optional everywhere, including the public headers.
2.Keep the public headers C++11-compatible: use std::optional in the internal implementation, and in the public structs use a plain value plus a bool flag.
3.Add a benchmark::Optional that is the same for all standards (no #if) and use it for every complexity_n.

Since this is more of a policy decision, I’d rather not pick one myself. Which would you prefer?

@LebedevRI

Copy link
Copy Markdown
Collaborator

Please update all complexity_n/complexity_n_ fields to all be Optional<>. There should not be any == -1/<0/>=0 checks.

Thanks for the review. I ran into an issue making everything std::optional, so I’d like to ask for your input before going further.

ThreadManager::Result is internal, so std::optional works fine there. The problem is State::complexity_n_ and BenchmarkReporter::Run::complexity_n, which are in public headers. cxx11_test builds those headers in C++11/14 mode, where doesn’t exist, so it breaks (MSVC: 'optional': is not a member of 'std'). Everything else builds fine.

Oh well, i forgot about that.

Also, changing the type of the public Run::complexity_n would break any custom reporter that reads it as an integer.

So here's the thing, this diff already breaks that. They most probably expect 0 as 'no-value', and suddenly it's -1.

I see three options:

1.Bump cxx11_test to require C++17, and use std::optional everywhere, including the public headers. 2.Keep the public headers C++11-compatible: use std::optional in the internal implementation, and in the public structs use a plain value plus a bool flag. 3.Add a benchmark::Optional that is the same for all standards (no #if) and use it for every complexity_n.

Since this is more of a policy decision, I’d rather not pick one myself. Which would you prefer?

I think it shouldn't be 1.
I guess 3., but just make a strict ComplexityN type that has a method to check if it has a value (internally an <0 check), and a getter that panics if the value isn't there.

@amoralpha

Copy link
Copy Markdown
Contributor Author

Please update all complexity_n/complexity_n_ fields to all be Optional<>. There should not be any == -1/<0/>=0 checks.

Thanks for the review. I ran into an issue making everything std::optional, so I’d like to ask for your input before going further.
ThreadManager::Result is internal, so std::optional works fine there. The problem is State::complexity_n_ and BenchmarkReporter::Run::complexity_n, which are in public headers. cxx11_test builds those headers in C++11/14 mode, where doesn’t exist, so it breaks (MSVC: 'optional': is not a member of 'std'). Everything else builds fine.

Oh well, i forgot about that.

Also, changing the type of the public Run::complexity_n would break any custom reporter that reads it as an integer.

So here's the thing, this diff already breaks that. They most probably expect 0 as 'no-value', and suddenly it's -1.

I see three options:
1.Bump cxx11_test to require C++17, and use std::optional everywhere, including the public headers. 2.Keep the public headers C++11-compatible: use std::optional in the internal implementation, and in the public structs use a plain value plus a bool flag. 3.Add a benchmark::Optional that is the same for all standards (no #if) and use it for every complexity_n.
Since this is more of a policy decision, I’d rather not pick one myself. Which would you prefer?

I think it shouldn't be 1. I guess 3., but just make a strict ComplexityN type that has a method to check if it has a value (internally an <0 check), and a getter that panics if the value isn't there.

Got it, I've implemented ComplexityN locally as a strict class in include/benchmark/statistics.h (default-constructed as unset with n_ = -1, has_value() doing the >= 0 check, and value() aborting if unset, plus aborting on negative construction), and updated State::complexity_n_, ThreadManager::Result::complexity_n, and BenchmarkReporter::Run::complexity_n to use it with no external -1 / < 0 / >= 0 checks.

One quick question before I push: since ComplexityN is now a class rather than an integer typedef, what would you prefer for BigOFunc (typedef double(BigOFunc)(...)) and MinimalLeastSq?

  1. Keep BigOFunc taking int64_t so existing user lambdas (e.g. [](int64_t n) -> double) keep compiling without changes.
  2. Have BigOFunc take ComplexityN (callers/lambdas access n.value()).

@LebedevRI

Copy link
Copy Markdown
Collaborator

Code around

    BM_CHECK_GE(run.complexity_n, 0)
        << "Did you forget to call SetComplexityN?";

would desugar the new type into a valid int64_t.
I think the existing ComplexityN type alias should stay the way it is with the existing name.

@amoralpha

Copy link
Copy Markdown
Contributor Author

Code around

    BM_CHECK_GE(run.complexity_n, 0)
        << "Did you forget to call SetComplexityN?";

would desugar the new type into a valid int64_t. I think the existing ComplexityN type alias should stay the way it is with the existing name.

Thanks! I've pushed this. ComplexityN stays the int64_t alias, and BigOFunc, MinimalLeastSq, the fitting curves, the tests and the user guide are unchanged.

The strict type is a separate OptionalComplexityN (default-constructed means "no value", has_value(), and value() aborts if unset; constructing it from a negative number also aborts). It is used only for State::complexity_n_, ThreadManager::Result::complexity_n and BenchmarkReporter::Run::complexity_n. ComputeBigO checks has_value() and unwraps it into a plain ComplexityN, as you described.

Please let me know if you'd like a different name. The Python bindings (State.complexity_n) are updated too, but they aren't compiled in my local CMake setup, so CI will be the first real build of that part.

Comment thread include/benchmark/statistics.h Resolved
Comment thread include/benchmark/statistics.h Resolved
Comment thread src/complexity.cc Resolved

@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.

Nice, this looks somewhat reasonable.
I'll need to sleep on this, but i do believe this will be fine overall.
Thank you.

Comment thread include/benchmark/statistics.h Resolved
Comment thread include/benchmark/statistics.h Resolved
Comment thread src/statistics.cc Outdated
ComplexityN OptionalComplexityN::value() const {
BM_CHECK(has_value()) << "Did you forget to call SetComplexityN?";
return n_;
}

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.

Can these be in the header?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

include/benchmark/statistics.h is a public installed header (and compiled under C++11 in cxx11_test), whereas BM_CHECK lives in internal src/check.h (which isn't installed and uses C++17 std::string_view), so BM_CHECK can only be used from .cc files under src/.

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.

Right. No idea why i said BM_CHECK. These should just be assert(), like in #2274.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've updated OptionalComplexityN to be header-only using assert() just like PR #2274, removed the definitions from src/statistics.cc, and switched to ASSERT_DEBUG_DEATH for the tests. Tested it locally and all 87 tests pass smoothly in both debug and release.
Thanks for pointing that out and bearing with me! Let me know if it looks good now.

@LebedevRI

Copy link
Copy Markdown
Collaborator

Never seen that bazel failure before:

ERROR: /home/runner/work/benchmark/benchmark/BUILD.bazel:41:11: Compiling src/reporter.cc failed: absolute path inclusion(s) found in rule '//:benchmark':
the source file 'src/reporter.cc' includes the following non-builtin files with absolute paths (if these are builtin files, make sure these paths are in your toolchain):
  '/usr/lib/gcc/x86_64-linux-gnu/15/include/stddef.h'
  '/usr/lib/gcc/x86_64-linux-gnu/15/include/stdarg.h'
  '/usr/lib/gcc/x86_64-linux-gnu/15/include/stdint.h'

The actual source file seems to vary between runs.
@dmah42 Does bzl version needs to be bumped, because underlying compiler version is too new for it to be aware of it?

@amoralpha

Copy link
Copy Markdown
Contributor Author

Hi @LebedevRI just a follow up - any other change you want me to do on my side? thx!

@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.

I do not understand why the CI check is failing here, but not in other PR's/branches.

@amoralpha

Copy link
Copy Markdown
Contributor Author

Could you re-run the earlier green bazel.ubuntu-latest job from this PR (commit f079722)? If it fails now, it’s the runner i think?

@LebedevRI

Copy link
Copy Markdown
Collaborator

@dmah42 halp. That workflow is marked as required and github does not allow me to merge this without it passing.

@LebedevRI
LebedevRI requested a review from dmah42 October 30, 2026 01:26
@dmah42

dmah42 commented Oct 30, 2026

Copy link
Copy Markdown
Member

i've been working on this, sorry. it's because there's a rollout changing "-latest" between two versions and our bazel cache is version-agnostic.

@dmah42
dmah42 merged commit 53aaafd into google:main Oct 30, 2026
106 of 107 checks passed
@amoralpha

Copy link
Copy Markdown
Contributor Author

Thank you @LebedevRI and @dmah42, I really appreciate the detailed reviews and your patience with the CI issues. I learned a lot from this one.
If there are other open issues you think would suit a newer contributor, I’d be glad to take a look. Is there a preferred place for contributor questions (issues, Discussions, or somewhere else)?

@LebedevRI

Copy link
Copy Markdown
Collaborator

@amoralpha thank you!

@dmah42 thanks!

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.

Fail when calculating complexity

3 participants