Repository navigation
Conversation
…le past end of function For a global variable the scope body end is null so the lifetime was forwarded through the rest of the file, into unrelated functions. colobot restext.cpp: ValueFlow 615s => 5s. In 10000 Debian packages (--check-level=exhaustive) this removes 11 false positives (danglingLifetime, objectIndex) and adds no warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For 'arr[i]' where arr is an array of pointers, isDecayedPointer() returned true because the type of 'arr[i]' is a pointer. A lifetime value was then created and forwarded for each subscript; with many 'arr[N] = ...' statements in one function this is quadratic. spice rop3.c: ValueFlow 352s => 28s. In 10000 Debian packages (--check-level=exhaustive) this adds 11 warnings: 9 correct nullPointerOutOfMemory/nullPointerOutOfResources and 2 false positives (uninitvar, arrayIndexOutOfBoundsCond). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The values of the reference token were filtered again for every node in the expression. Filter them once and skip the AST search when there is nothing that can match. No changed warnings in a 50 package debian corpus, ValueFlow ~10% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iterated ValueFlow passes are run again for all functions until no new values are found. A function where no value changed in the previous iteration will not get new values from the intra-function passes, so skip it. Running the passes again on an unchanged function can add a possible value to a comparison, which then keeps a known value from being set later. In 10000 Debian packages (--check-level=exhaustive) this adds 16 correct knownConditionTrueFalse warnings and loses none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getEnumType() scans the whole enum body each time a token with enum type gets a valuetype. With big enums that are used a lot this is quadratic. Cache the result per enum scope while the valuetypes are set; the result is only cached when every literal it looked at already had a valuetype. crispy-doom: total time 124s => 78s. No changed warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eDeclaration()
For a name after a comma the tokens before it were searched back to
';', '(', '[' or '{'. In a long comma separated list of names this search
was repeated from every element. Reuse the result of the search from the
previous comma.
crispy-doom: total time 78s => 64s. No changed warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions Skipping a function in the next iteration when its number of values did not change missed changes where a value is replaced or changes kind (possible -> known) without changing the count. For example a call whose return value got known in valueFlowFunctionReturn. Compare a hash of all values in the function instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only some passes skip the unchanged functions. Another pass can remove a value that a skipping pass then adds again, so the function's values look unchanged after the iteration. When such a function was skipped only the removal happened, and the values kept changing in the next iterations. In selfcheck lib/mathlib.cpp then needed 6 iterations instead of 4 and gave 'ValueFlow maximum iterations exceeded'. Now a function is skipped only if its values are unchanged and the skipping passes did not change it in the previous iteration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dmcppcheck
left a comment
There was a problem hiding this comment.
AI review
I found no problems that I could reproduce, and no warning changed compared with base on the inputs I tried. Testing found no false positives, false negatives or crashes from the lifetime changes, the alias filter, the enum-type cache or the comma-search cache. Two remarks from reading the code, both about the new "skip unchanged functions" logic:
hashValue()leaves out someValuefields, at leastindirectandcontainer. If a value changes only in one of those fields, the function's hash stays the same and the function can be skipped by mistake.- Whether a pass "uses skippedFunctions" is decided by looking for the text
skippedFunctionsin the pass name, which is the stringified macro argument. This is fragile.
No earlier review comments exist.
Testing
- Built
cppcheckandtestrunnerin pr/, andcppcheckin base/. ./testrunner(all tests): 5392 tests, 0 failures. The new tests in TestValueFlow pass.- Lifetime checks, compared with base. All gave the same results as base:
- A global pointer or member pointer set to a local inside a nested block and used after the block in the same function.
danglingLifetimeandinvalidLifetimeare still reported. - The same inside a lambda inside a member function.
- A global
std::vector<int*>givenarr[0] = &xthroughpush_back, and returningarr[0]that holds&x. Both are still detected. - 2-D arrays
int a[3][4]; int *p = a[1]; return p;. Base does not warn either, so this is not a regression.
- A global pointer or member pointer set to a local inside a nested block and used after the block in the same function.
- Corpus comparison, base vs PR, on test/cfg/.c and samples/ with
--check-level=exhaustive --enable=warning,style,performance,portability: the output is identical. - cppcheck's own sources, base vs PR, on lib/{mathlib,token,checkother,astutils,library,checkstl,programmemory,checkbufferoverrun}.cpp with
--debug-warnings --library=cppcheck-lib:- Non-bailout output is identical, apart from line shifts in astutils.cpp, which the PR edits.
- No "ValueFlow maximum iterations exceeded" message appeared.
- Wall time went from 404 s (base) to 202 s (PR).
- I did not build a test input that gets skipped wrongly because of the hash fields it leaves out. That remark comes from reading the code.
Automated review of 4cc4ad2 by Claude. The pull request was built and tested locally.
| combine(static_cast<std::size_t>(v.conditional)); | ||
| combine(static_cast<std::size_t>(v.moveKind)); | ||
| combine(static_cast<std::size_t>(v.lifetimeKind)); | ||
| combine(static_cast<std::size_t>(v.lifetimeScope)); |
There was a problem hiding this comment.
AI review
The hash leaves out indirect, container and a few other fields of ValueFlow::Value. If a pass replaces a value with one that differs only in indirect (for example indirect 0 vs 1 for uninit or null values) or only in container, the function's hash stays the same. The function is then skipped in the next iteration even though its values changed. Consider adding at least indirect and container to the hash.
| F mRun; | ||
| ValueFlowPassAdaptor(const char* pname, bool pcpp, F prun) : ValueFlowPass(), mName(pname), mCPP(pcpp), mRun(prun) {} | ||
| ValueFlowPassAdaptor(const char* pname, bool pcpp, F prun) | ||
| : ValueFlowPass(), mName(pname), mCPP(pcpp), mUsesSkippedFunctions(std::strstr(pname, "skippedFunctions") != nullptr), mRun(prun) {} |
There was a problem hiding this comment.
AI review
Whether a pass honours skippedFunctions is found by searching for the text "skippedFunctions" in its stringified name. A pass that is renamed, or that gets the set through a helper without that literal text in the VFA(...) argument, is quietly misclassified. Then changedBySkippingPass is not tracked for it, which brings back the iteration problem that commit 4cc4ad2 fixed. An explicit flag in the VFA macro, such as a VFA_SKIP variant, would be more robust.
|
@pfultz2 I would be interested in your opinion about these tweaks. This is made by AI, however I feel it's pretty well tested and has very promising results. Some tweaks were needed to make CI happy so I am extending the testing, the scripts are measuring performance and warnings on more debian packages with the latest changes.. If your on-going work in valueflow/symboldatabase/etc will be messed up then feel free to say that we should delay this until your changes are in.. |
|
I could split this up into several PRs also if wanted. |
Partial fix for #15174. These commits make ValueFlow faster without limiting the analysis: nothing is switched off or removed. This is a first step; making normal level give about the same warnings as exhaustive is left for later PRs.
arr[i]was treated as a decayed pointer. Both were quadratic.Commits
restext.cpp: ValueFlow 615 s → 5 s)rop3.c: ValueFlow 352 s → 28 s)getEnumType()duringsetValueTypeInTokenList()setVarIdParseDeclaration()Results on 10,019 Debian packages
This branch vs
main, both with--check-level=exhaustive. The packages are random Debian source packages with 3–800 C/C++ files and at most 40 MB. Each package was checked on its own (-j1) with a 20-minute timeout. Times are whole Cppcheck CPU time.Speed (9,853 packages that both versions finish)
main(packages ≥ 1 s).mainand finish with this PR.Warnings
Of 4,962,744 warnings, 11 are lost and 55 are new. Every difference was reproduced and traced to a commit:
danglingLifetime,objectIndexknownConditionTrueFalsenullPointerOutOfMemory/nullPointerOutOfResourcesuninitvar(simh),arrayIndexOutOfBoundsCond(funtools)mainWhy commit 4 gives more warnings: in
main, running the passes again on an unchanged function can add a possible value to a comparison, and that keeps a known value from being set later. All 16 new warnings have this shape:testrunner: 5,392 tests, 0 failures.
🤖 Generated with Claude Code