Skip to content

Fix #15143: Improve performance in the case of many template instantiations - #8960

Open
Oipo wants to merge 2 commits into
cppcheck-opensource:mainfrom
Oipo:main
Open

Oipo wants to merge 2 commits into
cppcheck-opensource:mainfrom
Oipo:main

Conversation

@Oipo

@Oipo Oipo commented Oct 25, 2026

Copy link
Copy Markdown

Two improvements:

  1. CheckClassImpl::virtualDestructor() scanned the whole variable list and the whole token list for every (derived class, public base class) pair. With many template instantiations (for example etl) this took up to 26% of the analysis time.
  2. Tokenizer::setVarIdPass2() tried every out-of-class member definition for every class. matchMemberName() can only succeed when the innermost scope name of the class matches a name in the member's scope, in one of its "using namespace" or in its qualification. The members are now indexed by those names, in their original order, so each class only tries its candidates. The index and matchMemberName() walk the qualification with the same skipQualification().

On my own private project, the total CPU time dropped from 88.7 minutes down to 52.9 minutes. Oh, and this also fixes a false negative: new Derived < int > would never match, and it does now. See some tests I added.

@dmcppcheck

Copy link
Copy Markdown

There should probably be a trac ticket for this PR. Please create it, or ask us to create it. Then the PR title should be updated.

@Oipo

Oipo commented Oct 25, 2026 via email

Copy link
Copy Markdown
Author

@danmar

danmar commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

I have created this ticket:
https://trac.cppcheck.net/ticket/15143

@Oipo Oipo changed the title Improve performance in the case of many template instantiations Fix #15143: Improve performance in the case of many template instantiations Oct 26, 2026
@Oipo

Oipo commented Oct 26, 2026

Copy link
Copy Markdown
Author

Thanks. Updated the PR title and the two commits.

Comment thread lib/checkclass.cpp Outdated
deletionsCollected = true;

for (const Variable* var : mSymbolDatabase->variableList()) {
if (var && var->isPointer() && var->type())

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.

I would suggest a && var->nameToken() also. unnamed parameters will not be added then.

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.

Yeah, that is more readable. Added.

Comment thread lib/checkclass.cpp Outdated
{
// pointer variables of type 'Base *'
std::set<int> baseClassPointers;
collectDeletions();

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.

it does not look right to me to call this in the loop. It will go through all variables in symboldatabase and add declaration ids to pointersByType over and over again? can't we just do that once?

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 initially thought making it lazy would save time on TU that don't have any public base classes, but I measure <0.05 ms difference so noise basically. I removed the lazy part.

Oipo added 2 commits October 26, 2026 21:05
https://trac.cppcheck.net/ticket/15143

CheckClassImpl::virtualDestructor() scanned the whole variable list and
the whole token list for every (derived class, public base class) pair.
With many template instantiations (for example etl) this took up to 26%
of the analysis time.

The named pointer variables of each type are now collected once, and a
single pass over the tokens records, for each pointer variable, the
classes 'Derived' of a "ptr = new Derived" that is followed by a
"delete ptr;". Checking a pair is then a lookup.

This also fixes a false negative: the name of a template instantiation
such as 'Derived < int >' is a single token that contains spaces, so the
old pattern "new Derived < int >" never matched it and the pair was never
reported.

On a project using etl, glaze and Catch2 (211 translation units in two
configurations) total CPU time drops from 88.7 to 61.6 minutes, with
identical output for every translation unit.
https://trac.cppcheck.net/ticket/15143

Tokenizer::setVarIdPass2() tried every out-of-class member definition
for every class. matchMemberName() can only succeed when the innermost
scope name of the class matches a name in the member's scope, in one of
its "using namespace" or in its qualification. The members are now
indexed by those names, in their original order, so each class only
tries its candidates. The index and matchMemberName() walk the
qualification with the same skipQualification().

On the same project total CPU time drops further from 61.6 to 52.9
minutes, with identical output for every translation unit.
@Oipo

Oipo commented Oct 28, 2026

Copy link
Copy Markdown
Author

I got an e-mail with a comment you made, but I cannot find it here anymore. If there's any questions you have, let me know and I'll do my best to answer them.

@dmcppcheck

Copy link
Copy Markdown

Test results for commit ab38a74 (tools/test-my-pr.py, main compared to this PR):

Test: http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8960/
packages: 1000
Differing warnings: 0 (+0 -0)
Timing: -0.8% (http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8960/2026-10-26_20-05-11-ab38a743b573_timing.html)

Posted automatically by the cppcheck PR test runner. +N: warnings only with this PR, -N: warnings only with main. The AI review is written by Claude and can be wrong.

@dmcppcheck dmcppcheck left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI review

I found no new problems.

I traced through matchMemberName(): every step that moves scopeIt forward first requires the name to be equal. So a match is only possible when the last name in scopeInfo occurs in member.scope, in a using namespace chain, or in the member's qualification. These are exactly the names that indexMembersByScopeName() indexes, and it walks them with the same skipQualification(). Two more things hold up:

  • Candidate order is kept.
  • The Member* pointers stay valid, because the members are in a std::list that the loop does not change.

The new virtualDestructor() gives the same results as the old per-pair scan, except for the intended fix for template-instantiation names (Derived < unsigned int >), which I confirmed works.

Testing

  • Build: make -j8 cppcheck testrunner succeeded in pr/, and make -j8 cppcheck succeeded in base/.
  • Test suites: ./testrunner passed with 0 failures for TestClass, TestVarID, TestTokenizer, TestSimplifyTemplate, TestSymbolDatabase, TestConstructors and TestUnusedVar.
  • Base vs PR on existing code: I ran cppcheck --debug --enable=all --inconclusive from base and from the PR on all test/cfg/*.cpp files (each with its matching --library) and on lib/token.cpp, lib/mathlib.cpp and lib/library.cpp. Debug output and warnings were byte-identical.
  • Varid edge cases (base vs PR): nested classes, using namespace N, using namespace X::Y, using namespace inside a namespace body, members of template classes (S<T>::h), and constructor initializer lists for out-of-class members. The debug output was identical and the varids were correct.
  • virtualDestructor edge cases (base vs PR): new NS::D3, new Derived(1) inside a nested block, a local Base *r, and several derived classes. Results were identical.
  • Template false negative: for p = new Derived<unsigned int>; delete p;, base reports nothing and the PR reports Class 'Base' which is inherited by class 'Derived < unsigned int >', as intended.
  • Stress test: a generated file with 1500 namespaced derived classes, their out-of-class member definitions and new/delete through Base* took 8.7 s with base and 2.7 s with the PR. Both produced the same output (4500 lines).

Automated review of ab38a74 by Claude. The pull request was built and tested locally.

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.

3 participants