isocpp / isocpp/CppCoreGuidelines

Pessimization Concerns

Open
#1,846 3 comments 0 reactions 1 assignee View on GitHub

@GabrielDosReis is already working on this.

Since Nov 11, 2021.

Dominant language
CSS
Stars
45.3k
Forks
5.6k
PR merge metrics
No merged PRs in 30d

Description

(Preface: the Microsoft implementation of the GSL is by far the most visible / vigorously maintained version, so when I refer to "the GSL", I refer to this implementation)

I think we can agree that the general spirit of the guidelines is to ensure safety / correctness efficiently (hilosphy section claims as much; including avoiding "redundant checks"). I've seen two places where the last concern might not have been applied so well. Part of this might very well be an issue specifically with the GSL implementation, but the guidelines as of the last few days seem to more-or-less encourage these issues.

  1. Expects()/Ensures() being pessimized choices for contract statement. The rules encouraging Expects()/Ensures() are themselves fine: expose contracts directly to tooling, provide opportunity for runtime enforcement, etc. But the current specification of their behavior is that they terminate if false. Suddenly all contract checks (which these guidelines encourage to be used very frequently) "do something". Sure, the optimizer can probably see through the check in simple cases, but it just as often can't in the more complicated cases (MSVC in particular seems particularly conservative when reasoning about when a condition can/cannot be always true/false). It seems extremely pessimistic to ask for a generally difficult to optimize runtime check to be injected at every place an interface its used, considering how often the usage is statically correct. The GSL used to have support for essentially-free contracts (__assume(condition), etc.), removed them some time ago, and I have seen at least one stale PR in the GSL that aimed to reintroduce this configurability. This has ramifications for span and not_null as well, as both use these liberally.

  2. gsl::span and gsl::not_null. The latter's issue is simple: for plain pointers, assuming the correctness of not_null itself, the Expects(/* inner wrapped pointer */ != nullptr) found in not_null::get() is unnecessary. The justification that the Microsoft implementation gives is a) this is a documentation of the contract b) this check isn't unnecessary for some unique_ptr edge cases. Once again, asking for this check to be enabled for all cases where only one case actually needs it, when Expects() "does something", seems a pessimization. As for gsl::span, I refer to the mandatory bounds-checking of its iterators. Take the logical equivalent of the ranged for-loop:

auto iter = my_span.begin();
const auto end = my_span.end();
for (; iter != end; ++iter)
    do_something(*iter);

While clang and gcc are able to completely see through the entire loop, MSVC completely fails to eliminate at least one per-loop check at /O2. The GSL implementers have taken to using the undocumented _Unchecked_begin and _Unchecked_end hooks to prevent otherwise grossly suboptimal code in the for (auto& thing : span) {} case. Now, if gsl::span is designed to pessimistically check all iterator operations and terminate on violations, I don't think it should be recommended as the kosher general-use alternative. std::span made the right choice, in my opinion, by not requiring iterator validation, while leaving it open for such validation to be configured (similar to debug iterators in the MSVC STL). If it is instead, like the not_null case, meant to a documentation of its contract, we fall back to the issue with the pessimized behavior of Ensure()/Expect().

(In fact, the LLVM version that ships as a drop-in replacement for MSVC in visual studio also seems to have issues in the simple case above)

I apologize if this is very long-winded issue; I have thought about this issue for some time now and have tried, as best as I can, to find the causes in both the guidelines and implementation decisions for the current state of affairs.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.