Skip to content

Mark move operations of irept, dstringt and memory_sizet noexcept - #9174

Open
kroening wants to merge 1 commit into
developfrom
noexcept-move-irept
Open

kroening wants to merge 1 commit into
developfrom
noexcept-move-irept

Conversation

@kroening

@kroening kroening commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Mark the move constructor (and move assignment) of a few core util data
structures noexcept, so that std::vector and other standard containers
move elements instead of copying them on reallocation.

The affected moves only copy or swap scalar members / a single pointer and
therefore cannot throw, but they were not marked noexcept:

  • sharing_treet (irep.h) — the base of irept, and thus of exprt,
    typet, codet, source_locationt, symbol_exprt, … None of these add
    data members, so the fix applies to the whole hierarchy.
  • dstringt (dstring.h) — irep_idt; a member of every tree_nodet.
  • memory_sizet (memory_units.h/.cpp) — the move assignment was already
    noexcept; the move constructor was not (inconsistent).

Why it matters

Because these moves were not noexcept, std::is_nothrow_move_constructible
was false, which forces std::vector to copy rather than move when
it reallocates. For irept-derived types a copy is expensive: it bumps
reference counts across the whole shared subtree and contends on the global
string container.

This is a hot path: irept::subt is std::vector<irept> and
exprt::operandst is std::vector<exprt>, both grown via push_back
throughout goto-conversion, simplification, and symbolic execution.

Verification

std::is_nothrow_move_constructible / ..._assignable for the affected
types, before vs. after (1 = container will move, 0 = container will copy):

Type before (mc/ma) after (mc/ma)
irept 0 / 0 1 / 1
exprt 0 / 0 1 / 1
typet 0 / 0 1 / 1
source_locationt 0 / 0 1 / 1
dstringt 0 / 0 1 / 1

Benchmark

Microbenchmark on std::vector<exprt> (each element has 4 operands + types,
so copies incur real ref-count traffic). Identical source compiled twice —
against the fixed headers (noexcept moves) and against reverted copies (no
noexcept) — linked against the same util.a, built with
clang++ -O2 -std=c++17. Best of 5 runs.

Scenario noexcept ON (moves) noexcept OFF (copies) Speedup
Realloc, pre-reserved (N = 2M) 4.2 ms 11.8 ms 2.8×
Natural growth, no reserve (N = 1M) 18.1 ms 29.6 ms 1.6×
Relocation-heavy (shrink_to_fit, N = 1M) 42.1 ms 274.0 ms 6.5×

The benefit scales with how often vectors relocate; relocation-heavy
workloads benefit the most. The probe confirmed the mechanism:
is_nothrow_move_constructible<exprt> was 1 in the ON build and 0 in the
OFF build — the only difference being whether the vector moves or copies.

This is an isolated microbenchmark of the vector-growth path, not an
end-to-end CBMC run; real-world impact depends on how much time is spent
growing/relocating these vectors.

Risk

Zero correctness risk: all affected move operations provably cannot throw
(pointer copy/swap, or copying a std::size_t / unsigned), so the
noexcept specification is accurate.

The move constructor (and move assignment) of sharing_treet (the base of irept, and thus exprt, typet, codet, source_locationt, ...), dstringt, and memory_sizet only copy or swap scalar members/pointers and therefore cannot throw, but were not marked noexcept.

Because they were not noexcept, std::is_nothrow_move_constructible was false for these types, which forced std::vector (and other standard containers) to copy rather than move elements when reallocating. For irept-derived types a copy is expensive: it bumps reference counts across the whole shared subtree and contends on the global string container. Since irept::subt is std::vector<irept> and exprt::operandst is std::vector<exprt>, this affects hot paths throughout CBMC.

Marking these moves noexcept lets containers relocate by moving. In an isolated microbenchmark on std::vector<exprt> this gave a 2.8x speedup for pre-reserved reallocation and up to 6.5x for relocation-heavy workloads.
@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.85%. Comparing base (4b26409) to head (f5b3b23).

Files with missing lines Patch % Lines
src/util/memory_units.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #9174   +/-   ##
========================================
  Coverage    80.85%   80.85%           
========================================
  Files         1717     1718    +1     
  Lines       190161   190166    +5     
  Branches        73       73           
========================================
+ Hits        153759   153767    +8     
+ Misses       36402    36399    -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants