Skip to content

static_variant: look up type positions without a recursive template chain - #6

Merged
On1x merged 1 commit into
masterfrom
build/flat-static-variant-index
Nov 1, 2026
Merged

On1x merged 1 commit into
masterfrom
build/flat-static-variant-index

Conversation

@chiliec

@chiliec chiliec commented Nov 1, 2026

Copy link
Copy Markdown
Member

What

impl::position<X, Ts...>, impl::type_info<Ts...> and the type-list helpers in static_variant.hpp were recursive class templates: for a variant of N alternatives, tag<T> for each T instantiated an O(N) chain, so a TU that touches every alternative (anything that serializes graphene::protocol::operation, N = 107) paid O(N²) class-template instantiations in the frontend.

They are now C++14 constexpr functions over a pack-expanded bool/int array (index_of, no_reference_types, no_duplicates, max_size). position and type_info keep their names and members, so no caller changes.

What did not change

  • storage_ops dispatch (the if-chain over the shrinking pack) is untouched. A jump-table/visit-array version was tried first: it made per-type thunks inline every visitor body, tripled operations.o and slowed database.cpp by ~15%. The if-chain is what the optimizer handles best; only the lookup recursion was expensive.
  • Codegen: operations.o built with and without this patch is byte-identical. A round-trip digest over every operation alternative (raw pack/unpack + variant to/from JSON) is identical before and after.
  • Public API, tag<T>::value numbering, count(), which() semantics.

Numbers (Apple clang 17, -O2, M1)

TU before after
libraries/protocol/ops.cpp 8.4 s 6.7 s
libraries/chain/database.cpp 80.2 s 80.3 s (noise)

The win is confined to TUs that enumerate all alternatives; most of the chain's build is backend-bound and unaffected.

Checks

  • Standalone static_assert check of tag, no_duplicates, no_reference_types, max_size compiles with Apple clang 17 and GCC 13 (-std=c++14 -Wall -Wextra).
  • viz-cpp-node full make with this submodule, 13/13 fast ctest, consensus_sim_tests including a new operation_variant suite that round-trips all 107 alternatives (companion PR in viz-cpp-node).

…hain

impl::position<X, Ts...> and impl::type_info<Ts...> recursed over the type pack, so every
lookup instantiated a chain of N classes over a shrinking pack and the duplicate check
instantiated O(N^2) of them. For the 107-alternative `operation` variant that was ~10k
class instantiations in every TU that touched it.

Both are now a single constexpr function over a pack expansion. Dispatch (storage_ops) is
unchanged: a function-pointer table was measured 15% slower to compile with 3x the object
size, because each per-type thunk gets the visitor body inlined into it.

Measured with clang -O3 on operations.cpp: 8.4 s -> 6.7 s, object byte-identical.
database.cpp: unchanged (80.3 s vs 80.2 s), object byte-identical. All 107 operations
round-trip through raw and JSON with the same digest as before.
@On1x

On1x commented Nov 1, 2026

Copy link
Copy Markdown
Member

Independent sanity check from the consumer side (viz-cpp-node), no changes requested — two facts and one nit.

1. Semantics are preserved (measured, not eyeballed). I extracted the old recursive position/type_info from master and the new constexpr forms from this PR into one TU and diffed them with static_assert over 16 type lists (empty, single, duplicates in several positions, duplicates at both ends, large/small mixes, std::string/std::vector, reference alternatives) plus 9 explicit position probes: every reachable case agrees. Then I built a synthetic 107-alternative variant against the real headers from master and from 7889e2f, and diffed the runtime values: identical — count=107, size=17, pos{0,1,53,105,106} = their own index, absent type -1, no_duplicates=1, no_reference_types=1.

2. One behavioural divergence, and it is unreachable. For static_variant<A &, A> (a reference alternative before its value type) the old type_info<T &, Ts...> compares the value type against the rest, so it reports no_duplicates == false; the new pack scan compares std::is_same<A &, A> and reports true. Nothing observable follows from it: both forms also report no_reference_types == false, and static_variant.hpp:214 static-asserts that, so such a variant cannot be instantiated. Worth knowing only if someone ever relaxes that assert.

3. Build-time effect on GCC, not just Apple clang. On a TU that queries tag<T> for all 107 alternatives (the shape of ops.cpp), median of 3 runs, -fsyntax-only: master 1.46 s → this PR 1.19 s (~19%). Directionally the same as your numbers.

Nit: the new comment says "the 111-alternative operation variant". operations.hpp currently lists 107 alternatives (vote_operation … validator_equivocation_operation), which matches your PR description. A stale number in a comment reads as measured truth later; worth fixing to 107 or dropping the number.

The companion operation_variant suite in viz-cpp-node (round-trip of every alternative under its own tag, count() boundary rejected) is the right carrier for the wire-format half — that is the part I would not want to trust to a review comment.

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