Skip to content

Commit ebadd92

Browse files
authored
Fix #15129: Improve matching performance for hash based suppressions (#8970)
Makes it so that hash based suppressions are indexed by hash. Matching hash based suppressions is now O(1) (ignoring collisions) instead of O(n). For large code bases using baseline suppressions (which may be in the number of tens of thousands) this leads to a substantial speed up. We could probably speed up matching of other suppressions in the future as well, but I assume these are in general manually added and won't even get close to the same number as baseline suppressions. Also fixed a bug in ProcessExecutor that would cause new suppressions to be added unintentionally.
1 parent ffe7e9b commit ebadd92

5 files changed

Lines changed: 138 additions & 26 deletions

File tree

‎cli/processexecutor.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ namespace {
9696
{
9797
if (suppr.isInline)
9898
writeToPipe(REPORT_SUPPR_INLINE, suppressionToString(suppr));
99-
else if (suppr.checked)
99+
else if (suppr.checked && suppr.hash == 0)
100100
writeToPipe(REPORT_SUPPR, suppressionToString(suppr));
101101
}
102102
}

‎lib/suppressions.cpp‎

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
#include <cctype> // std::isdigit, std::isalnum, etc
3333
#include <cstring>
3434
#include <exception>
35-
#include <functional> // std::bind, std::placeholders
3635
#include <sstream>
3736
#include <stdexcept>
3837
#include <utility>
@@ -325,14 +324,27 @@ std::string SuppressionList::addSuppressionLine(const std::string &line)
325324
}
326325
}
327326

327+
SuppressionList::Suppression *SuppressionList::findSameParameters(const Suppression &suppression)
328+
{
329+
const std::vector<Suppression*> *candidates = &mNonHashSuppressions;
330+
if (suppression.hash > 0) {
331+
const auto it = mHashSuppressions.find(suppression.hash);
332+
if (it == mHashSuppressions.end())
333+
return nullptr;
334+
candidates = &it->second;
335+
}
336+
const auto found = std::find_if(candidates->cbegin(), candidates->cend(), [&](const Suppression *s) {
337+
return suppression.isSameParameters(*s);
338+
});
339+
return found != candidates->cend() ? *found : nullptr;
340+
}
341+
328342
std::string SuppressionList::addSuppression(SuppressionList::Suppression suppression)
329343
{
330344
std::lock_guard<std::mutex> lg(mSuppressionsSync);
331345

332346
// Check if suppression is already in list
333-
auto foundSuppression = std::find_if(mSuppressions.begin(), mSuppressions.end(),
334-
std::bind(&Suppression::isSameParameters, &suppression, std::placeholders::_1));
335-
if (foundSuppression != mSuppressions.end()) {
347+
if (findSameParameters(suppression)) {
336348
return "suppression '" + suppression.toString() + "' already exists";
337349
}
338350

@@ -356,6 +368,12 @@ std::string SuppressionList::addSuppression(SuppressionList::Suppression suppres
356368

357369
mSuppressions.push_back(std::move(suppression));
358370

371+
Suppression *const added = &mSuppressions.back();
372+
if (added->hash > 0)
373+
mHashSuppressions[added->hash].push_back(added);
374+
else
375+
mNonHashSuppressions.push_back(added);
376+
359377
return "";
360378
}
361379

@@ -374,9 +392,8 @@ bool SuppressionList::updateSuppressionState(const SuppressionList::Suppression&
374392
std::lock_guard<std::mutex> lg(mSuppressionsSync);
375393

376394
// Check if suppression is already in list
377-
auto foundSuppression = std::find_if(mSuppressions.begin(), mSuppressions.end(),
378-
std::bind(&Suppression::isSameParameters, &suppression, std::placeholders::_1));
379-
if (foundSuppression != mSuppressions.end()) {
395+
Suppression *const foundSuppression = findSameParameters(suppression);
396+
if (foundSuppression) {
380397
if (suppression.checked)
381398
foundSuppression->checked = true;
382399
if (suppression.matched)
@@ -525,13 +542,21 @@ bool SuppressionList::isSuppressed(const SuppressionList::ErrorMessage &errmsg,
525542
// TODO: handle unmatchedPolyspaceSuppression?
526543
const bool unmatchedSuppression(errmsg.errorId == "unmatchedSuppression");
527544
bool returnValue = false;
528-
for (Suppression &s : mSuppressions) {
529-
if (!global && !s.isLocal())
530-
continue;
531-
if (unmatchedSuppression && s.errorId != errmsg.errorId)
532-
continue;
533-
if (s.isMatch(errmsg))
534-
returnValue = true;
545+
const auto match = [&](const std::vector<Suppression*> &suppressions) {
546+
for (Suppression *s : suppressions) {
547+
if (!global && !s->isLocal())
548+
continue;
549+
if (unmatchedSuppression && s->errorId != errmsg.errorId)
550+
continue;
551+
if (s->isMatch(errmsg))
552+
returnValue = true;
553+
}
554+
};
555+
match(mNonHashSuppressions);
556+
if (errmsg.hash != 0) {
557+
const auto it = mHashSuppressions.find(errmsg.hash);
558+
if (it != mHashSuppressions.end())
559+
match(it->second);
535560
}
536561
return returnValue;
537562
}
@@ -540,13 +565,21 @@ bool SuppressionList::isSuppressedExplicitly(const SuppressionList::ErrorMessage
540565
{
541566
std::lock_guard<std::mutex> lg(mSuppressionsSync);
542567

543-
for (Suppression &s : mSuppressions) {
544-
if (!global && !s.isLocal())
545-
continue;
546-
if (s.errorId != errmsg.errorId) // Error id must match exactly
547-
continue;
548-
if (s.isMatch(errmsg))
549-
return true;
568+
const auto match = [&](const std::vector<Suppression*> &suppressions) {
569+
return std::any_of(suppressions.cbegin(), suppressions.cend(), [&](Suppression *s) {
570+
if (!global && !s->isLocal())
571+
return false;
572+
if (s->errorId != errmsg.errorId) // Error id must match exactly
573+
return false;
574+
return s->isMatch(errmsg);
575+
});
576+
};
577+
if (match(mNonHashSuppressions))
578+
return true;
579+
if (errmsg.hash != 0) {
580+
const auto it = mHashSuppressions.find(errmsg.hash);
581+
if (it != mHashSuppressions.end())
582+
return match(it->second);
550583
}
551584
return false;
552585
}
@@ -687,7 +720,9 @@ void SuppressionList::markUnmatchedInlineSuppressionsAsChecked(const TokenList &
687720
if (currFileIdx != tok->fileIndex() || currLineNr != tok->linenr()) {
688721
currLineNr = tok->linenr();
689722
currFileIdx = tok->fileIndex();
690-
for (auto &suppression : mSuppressions) {
723+
// checked is not used for hash suppressions
724+
for (Suppression *s : mNonHashSuppressions) {
725+
Suppression &suppression = *s;
691726
if (suppression.type == SuppressionList::Type::unique) {
692727
if (!suppression.checked && (suppression.lineNumber == currLineNr) && (suppression.fileName == tokenlist.file(tok))) {
693728
suppression.checked = true;

‎lib/suppressions.h‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <mutex>
3030
#include <set>
3131
#include <string>
32+
#include <unordered_map>
3233
#include <utility>
3334
#include <vector>
3435
#include <map>
@@ -51,14 +52,14 @@ class CPPCHECKLIB SuppressionList {
5152
};
5253

5354
struct CPPCHECKLIB ErrorMessage {
54-
std::size_t hash;
55+
std::size_t hash{};
5556
std::string errorId;
5657
void setFileName(std::string s);
5758
const std::string &getFileName() const {
5859
return mFileName;
5960
}
60-
int lineNumber; // TODO: need to be unsigned
61-
Certainty certainty;
61+
int lineNumber{}; // TODO: need to be unsigned
62+
Certainty certainty{};
6263
std::string symbolNames;
6364
std::set<std::string> macroNames;
6465

@@ -294,9 +295,20 @@ class CPPCHECKLIB SuppressionList {
294295
void markUnmatchedInlineSuppressionsAsChecked(const TokenList &tokenlist);
295296

296297
private:
298+
/**
299+
* @brief Find a suppression with the same parameters. mSuppressionsSync must be locked.
300+
* @param suppression the suppression to look for
301+
* @return the found suppression, or nullptr
302+
*/
303+
Suppression *findSameParameters(const Suppression &suppression);
304+
297305
mutable std::mutex mSuppressionsSync;
298306
/** @brief List of error which the user doesn't want to see. */
299307
std::list<Suppression> mSuppressions;
308+
/** @brief Suppressions in mSuppressions with hash > 0, indexed by hash */
309+
std::unordered_map<std::size_t, std::vector<Suppression*>> mHashSuppressions;
310+
/** @brief Suppressions in mSuppressions with hash == 0 */
311+
std::vector<Suppression*> mNonHashSuppressions;
300312
};
301313

302314
struct Suppressions

‎test/cli/other_test.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4925,6 +4925,40 @@ def test_ipc_inline_suppressions(tmp_path):
49254925
assert stdout_lines == stdout_exp
49264926
assert stderr.splitlines() == []
49274927

4928+
4929+
@pytest.mark.skipif(sys.platform == 'win32', reason="requires ProcessExecutor")
4930+
def test_ipc_hash_suppressions(tmp_path):
4931+
test_file = tmp_path / 'test.c'
4932+
with open(test_file, "w") as f:
4933+
f.write('void f() { int x = 1; }')
4934+
4935+
_, _, stderr = cppcheck(['--enable=style', '--xml-version=3', str(test_file)])
4936+
error_hash = ElementTree.fromstring(stderr).find('.//error').get('hash')
4937+
4938+
suppr_file = tmp_path / 'suppr.xml'
4939+
with open(suppr_file, "w") as f:
4940+
f.write(f'<suppressions><suppress><id>unreadVariable</id><fileName>{test_file}</fileName><hash>{error_hash}</hash></suppress></suppressions>')
4941+
4942+
args = [
4943+
'-q',
4944+
'--debug-ipc',
4945+
'-j2',
4946+
'--executor=process',
4947+
'--no-cppcheck-build-dir',
4948+
'--enable=style',
4949+
f'--suppress-xml={suppr_file}',
4950+
str(test_file)
4951+
]
4952+
4953+
exitcode, stdout, stderr = cppcheck(args)
4954+
assert exitcode == 0
4955+
# the hash suppression is not sent to the parent
4956+
assert stdout.splitlines() == [
4957+
'writeToPipe - 5 - 0',
4958+
'handleRead - 5 - 0'
4959+
]
4960+
assert stderr.splitlines() == []
4961+
49284962
test_redundant_file_reads_params = [
49294963
([], 3),
49304964
(['--suppress=zerodiv'], 1),

‎test/testsuppressions.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class TestSuppressions : public TestFixture {
117117
TEST_CASE(addSuppressionDuplicate);
118118
TEST_CASE(updateSuppressionState);
119119
TEST_CASE(addSuppressionLineMultiple);
120+
TEST_CASE(hashSuppression);
120121

121122
TEST_CASE(suppressionsParseXmlFile);
122123
TEST_CASE(xmlMacroSuppressions);
@@ -1959,6 +1960,36 @@ class TestSuppressions : public TestFixture {
19591960
}
19601961
}
19611962

1963+
void hashSuppression() const {
1964+
SuppressionList supprs;
1965+
SuppressionList::Suppression s;
1966+
s.errorId = "id";
1967+
s.hash = 1;
1968+
ASSERT_EQUALS("", supprs.addSuppression(s));
1969+
ASSERT_EQUALS("suppression 'id' already exists", supprs.addSuppression(s));
1970+
ASSERT_EQUALS(true, supprs.updateSuppressionState(s));
1971+
s.hash = 2;
1972+
ASSERT_EQUALS(false, supprs.updateSuppressionState(s));
1973+
// same hash, different id
1974+
s.errorId = "id2";
1975+
s.hash = 1;
1976+
ASSERT_EQUALS("", supprs.addSuppression(s));
1977+
ASSERT_EQUALS(true, supprs.updateSuppressionState(s));
1978+
1979+
SuppressionList::ErrorMessage error = errorMessage("id");
1980+
error.hash = 1;
1981+
ASSERT_EQUALS(true, supprs.isSuppressed(error));
1982+
error.hash = 2;
1983+
ASSERT_EQUALS(false, supprs.isSuppressed(error));
1984+
error.hash = 0;
1985+
ASSERT_EQUALS(false, supprs.isSuppressed(error));
1986+
error.hash = 1;
1987+
error.errorId = "id2";
1988+
ASSERT_EQUALS(true, supprs.isSuppressed(error));
1989+
error.errorId = "id3";
1990+
ASSERT_EQUALS(false, supprs.isSuppressed(error));
1991+
}
1992+
19621993
void addSuppressionLineMultiple() const {
19631994
SuppressionList supprlist;
19641995

0 commit comments

Comments
 (0)