Skip to content

Replace offsetof cache-line check with a well-defined runtime check (#1877) - #2264

Open
devtejasx wants to merge 1 commit into
google:mainfrom
devtejasx:fix-invalid-offsetof-state
Open

Replace offsetof cache-line check with a well-defined runtime check (#1877)#2264
devtejasx wants to merge 1 commit into
google:mainfrom
devtejasx:fix-invalid-offsetof-state

Conversation

@devtejasx

Copy link
Copy Markdown
Contributor

Fixes #1877.

The cache-line static_assert used offsetof(State, skipped_). State is not
a standard-layout type (it has non-static data members with differing access
control), so offsetof() on it is undefined behavior and is diagnosed by
-Winvalid-offsetof on clang (and equivalents on ICC/NVCC/NVHPC). Today this is
only kept quiet by a large stack of compiler-specific #pragmas, and it still
breaks -Werror builds on newer clang (clang-18/19 in the report; also seen
with Apple clang 21).

This replaces the offsetof compile-time check with a well-defined runtime
check that computes the offset from the live object (&skipped_ - this), guarded
by BM_CHECK. The invariant is therefore still enforced in debug/CI builds and
compiles to nothing under NDEBUG, and all of the offsetof-suppression pragmas
are removed.

This follows up on @dmah42's request in the issue for a way to "capture the
spirit of the check in another way".

Verification

  • Confirmed clang -Winvalid-offsetof -Werror flags the original pattern and
    compiles cleanly after the change (no pragmas needed).
  • Full release and debug builds pass (GCC and clang).

Per AGENTS.md: AI-assisted — the patch was drafted with AI assistance
(Claude) and then reviewed, tested, and understood by me. I take full
responsibility for it.

@LebedevRI

Copy link
Copy Markdown
Collaborator

I just checked, and i don't believe that diagnostic fires any more on clang-22 on linux.
Can you show a screenshot of the terminal with the compilation failure?

@devtejasx
devtejasx force-pushed the fix-invalid-offsetof-state branch from fa38102 to b3be22c Compare July 30, 2026 23:17
@devtejasx

Copy link
Copy Markdown
Contributor Author

@LebedevRI It does still fire — it just can't be seen on a plain build of main, because the #pragma GCC diagnostic ignored "-Winvalid-offsetof" stack that this PR removes suppresses it. Removing only the pragmas from main and compiling:

$ clang++ --version | head -1
clang version 20.1.8

$ clang++ -std=c++17 -Werror -Winvalid-offsetof -Iinclude -Isrc -DBENCHMARK_STATIC_DEFINE -fsyntax-only src/benchmark.cc
src/benchmark.cc:241:7: error: offset of on non-standard-layout type 'State' [-Werror,-Winvalid-offsetof]
  241 |       offsetof(State, skipped_) <= (cache_line_size - sizeof(skipped_)), "");
      |       ^               ~~~~~~~~
/usr/lib/clang/20/include/__stddef_offsetof.h:16:24: note: expanded from macro 'offsetof'
   16 | #define offsetof(t, d) __builtin_offsetof(t, d)
      |                        ^                     ~
1 error generated.

Since you asked specifically about clang-22, here it is reduced to State's relevant shape:

#include <cstddef>

class State {
 public:
  const long max_iterations;
 private:
  long total_iterations_;
  int skipped_;
 public:
  State() : max_iterations(0), total_iterations_(0), skipped_(0) {
    const int cache_line_size = 64;
    static_assert(offsetof(State, skipped_) <= (cache_line_size - sizeof(skipped_)), "");
  }
};

State s;

With -std=c++17 -Winvalid-offsetof -Werror, x86-64 linux, this is an error on clang 21.1.0, clang 22.1.0 and clang trunk (checked today):

<source>:16:19: error: 'offsetof' on non-standard-layout type 'State' [-Werror,-Winvalid-offsetof]

That matches clang's current BuildBuiltinOffsetOf, which is unchanged on main. State is not standard-layout in any language mode or on any platform, so this isn't compiler- or version-dependent.


@dmah42 On your objection to #2251 — that moving the check to runtime is a regression because it becomes debug-only — that was fair, and I've fixed it rather than repeating it. Two things:

1. There is no conforming compile-time option. You asked whether there was a better one; I went looking for it, and the answer is no. offsetof requires a standard-layout type, and State cannot be made standard-layout — that requires every non-static data member to be of standard-layout type, and counters is a std::map:

benchmark::State                    standard_layout=0
benchmark::UserCounters (std::map)  standard_layout=0

Fixing the access-control mix isn't enough; the member types themselves rule it out, and the standard guarantees nothing about the layout of any container. A constexpr alternative doesn't exist either, since reinterpret_cast is ill-formed in a constant expression. So the choice is between an unportable offsetof plus four vendors' pragmas, or a runtime check.

2. The check is no longer debug-only. This adds BM_CHECK_ALWAYS (BM_CHECK is now defined in terms of it), so the invariant is enforced in every build configuration, not just !NDEBUG. A State is constructed once per benchmark instance per thread — never inside the iteration loop — so an unconditional compare costs nothing measurable.

Verified in a Release/NDEBUG build by temporarily tightening the bound to 8 bytes:

$ ./test/basic_test                       # -DCMAKE_BUILD_TYPE=Release
src/benchmark.cc:238: State: Check `reinterpret_cast<const char*>(&skipped_) -
  reinterpret_cast<const char*>(this) <= 8 - static_cast<std::ptrdiff_t>(sizeof(skipped_))'
  failed. the commonly accessed members of State must fit in the first cache line

Restored to 64, Release and Debug both build clean and the test suite is unchanged from main. Rebased onto current main.

@devtejasx
devtejasx force-pushed the fix-invalid-offsetof-state branch from b3be22c to d33268c Compare August 4, 2026 19:38
Comment thread src/benchmark.cc Outdated
#if defined(__NVCOMPILER)
#pragma diagnostic pop
#endif
// Ensure commonly accessed data is on the first cache line.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we don't need a comment describing what we used to do, only what we do now.

@dmah42

dmah42 commented Aug 5, 2026

Copy link
Copy Markdown
Member

so what was wrong with the original version then? it worked, right?

@devtejasx
devtejasx force-pushed the fix-invalid-offsetof-state branch from d33268c to 40101c7 Compare August 5, 2026 15:05
@devtejasx

Copy link
Copy Markdown
Contributor Author

Functionally, yes — the pragma stack on main still compiles clean today. I just rebuilt it in isolation to confirm: clang++ -std=c++17 -Winvalid-offsetof -Werror -fsyntax-only src/benchmark.cc against current main, exit 0, no diagnostic.

But "compiles clean today" isn't the same as "not broken." offsetof(State, skipped_) is UB per the standard because State isn't standard-layout (mixed access control on its data members) — the pragma stack doesn't change that fact, it just tells four specific compiler families to stop reporting it. That's literally why #1877 exists: the pragma stack that was already on main when the issue was filed didn't cover the reporter's clang-18/19, because covering "every compiler that might warn about this" is a moving target, not a fixed set.

It's still moving: in my last comment I posted -Winvalid-offsetof -Werror output from clang 21.1.0, clang 22.1.0, and clang trunk, reduced to State's actual shape, showing the exact same pragma-suppressed pattern is a hard error again on those. So "it worked" is true only for the compiler versions in CI right now. The runtime check has nothing to suppress in the first place, so there's no diagnostic-naming treadmill to keep up with.

Rebased onto current main (45fca05) — no conflicts, clean release/debug rebuild.

State is not a standard-layout type, so offsetof(State, skipped_) is only
conditionally-supported. Compilers diagnose it, and the static_assert was
kept quiet by a stack of vendor pragmas for GCC, clang, ICC, NVCC and NVHPC.
That stack does not cover every compiler: google#1877 is clang-18/19 on Windows
reporting it anyway.

Measure the offset on the object being constructed instead. That is well
defined, needs no pragmas, and expresses the same invariant.

The check uses a new BM_CHECK_ALWAYS, which is BM_CHECK without the NDEBUG
opt-out, so it still holds in a release build. A State is constructed once
per benchmark instance and thread, never inside the iteration loop.

Verified: with the bound lowered to 8 bytes, a release build fails at run
time with

  src/benchmark.cc:231: State: Check `... <= 8 - ...' failed.
  the commonly accessed members of State must fit in the first cache line

and src/benchmark.cc compiles clean under -Winvalid-offsetof -Wall -Wextra
-Werror with both gcc 15 and clang 20.
@devtejasx
devtejasx force-pushed the fix-invalid-offsetof-state branch from 40101c7 to 87db41a Compare August 6, 2026 06:00
@devtejasx

Copy link
Copy Markdown
Contributor Author

Comment trimmed, it now only says what the code does.

Also worth stating for the record, since #2251 was rejected for making the check debug-only: this version is not. BM_CHECK_ALWAYS is BM_CHECK without the NDEBUG opt-out. With the bound lowered to 8 bytes, a release build fails at run time:

src/benchmark.cc:231: State: Check `reinterpret_cast<const char*>(&skipped_) -
reinterpret_cast<const char*>(this) <= 8 - static_cast<std::ptrdiff_t>(sizeof(skipped_))'
failed. the commonly accessed members of State must fit in the first cache line

A State is constructed once per benchmark instance and thread, never inside the iteration loop, so the cost is not measurable.

On "what was wrong with the original version": nothing that shows up today. It compiles clean on current main. The problem is that it only compiles clean because five vendor-specific pragmas tell those compilers to stop reporting offsetof on a non-standard-layout type. Any compiler not in that list reports it, which is #1877. This version has no pragmas, and src/benchmark.cc compiles clean under -Winvalid-offsetof -Wall -Wextra -Werror with gcc 15 and clang 20.

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.

[BUG] fails to compile on windows with clang-18 / clang-19

3 participants