Skip to content

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

Open
amoralpha wants to merge 7 commits into
google:mainfrom
amoralpha:fix-issue-410-clean
Open

amoralpha wants to merge 7 commits into
google:mainfrom
amoralpha:fix-issue-410-clean

Conversation

@amoralpha

Copy link
Copy Markdown

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
default:
return [](IterationCount) { return 1.0; };
}
}

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.

These apparently take ComplexityN, not IterationCount, even though both are int64_t.
Please adjust these (and in the docs) while here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done! Updated 'FittingCurve', 'Result::complexity_n', 'test/complexity_test.cc', and 'docs/user_guide.md' to use 'ComplexityN' instead of 'IterationCount'.

Comment thread src/thread_manager.h Outdated
double cpu_time_used = 0;
double manual_time_used = 0;
int64_t complexity_n = 0;
int64_t complexity_n = -1;

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.

Ah, so complexity_n is a signed integer, that's weird, but allows the fix to be not too intrusive...

Comment thread test/benchmark_gtest.cc
Comment thread src/benchmark_runner.cc Outdated
- 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 Outdated
Comment thread src/benchmark_runner.cc Outdated
report.use_real_time_for_initial_big_o = b.use_manual_time();
report.cpu_accumulated_time = results.cpu_time_used;
report.complexity_n = results.complexity_n;
report.complexity_n = results.complexity_n.value_or(-1);

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.

report.complexity_n should also be `Optional<>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I tried making BenchmarkReporter::Run::complexity_n an std::optional<ComplexityN>, but because BenchmarkReporter is in include/benchmark/reporter.h (a public header), doing so breaks cxx11_test (test/CMakeLists.txt:77-86), which compiles public headers in C++11/C++14 mode where <optional> is unavailable:

warning STL4038: The contents of <optional> are available only with C++17 or later.

Therefore, Run::complexity_n uses -1 internally as a sentinel for the public reporter struct while internal runner/thread manager logic uses std::optional.

@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
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
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
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 Outdated
Comment on lines +38 to +40
std::fprintf(stderr, "Complexity N must be non-negative: %lld\n",
static_cast<long long>(n));
std::abort();

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.

These should be BM_CHECK (maaaybe BM_CHECK_always or however it's called, but i wouldn't go there)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done! Since BM_CHECK lives in internal src/check.h, I defined OptionalComplexityN(ComplexityN) and value() out-of-line in src/statistics.cc (exporting OptionalComplexityN with BENCHMARK_EXPORT), and guarded the two new death tests with TEST_BENCHMARK_LIBRARY_HAS_NO_ASSERTIONS.

Comment thread include/benchmark/statistics.h Outdated
Comment on lines +47 to +50
if (!has_value()) {
std::fprintf(stderr, "Did you forget to call SetComplexityN?\n");
std::abort();
}

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.

same

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

DONE

Comment thread src/complexity.cc Outdated
Comment on lines +186 to +188
BM_CHECK(run.complexity_n.has_value())
<< "Did you forget to call SetComplexityN?";
n.push_back(run.complexity_n);
n.push_back(run.complexity_n.value());

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 getter already handles the error case

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — dropped the redundant BM_CHECK(run.complexity_n.has_value()) here.

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

This branch has not been deployed

No deployments
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

2 participants