Summary
Support for NOLINT(BEGIN/END) blocks (implemented in D108560) is currently very costly. This patch aims to improve the performance with the following changes:
- The use of tokenized NOLINTs instead of a bunch of repetitive ad-hoc string operations (find(), split(), slice(), regex matching etc).
- The caching of NOLINT(BEGIN/END) block locations. Determining these locations each time a new diagnostic is raised is wasteful as it requires reading and parsing the entire source file.
Move NOLINT-specific code from ClangTidyDiagnosticConsumer to new purpose-built class NoLintDirectiveHandler and simplify public interface.
Use tokenized NOLINTs
Currently, the implementation repeats a lot of string operations, such as:
- searching for magic strings like "NOLINT", "NOLINTNEXTLINE", "NOLINTBEGIN", "NOLINTEND"
- searching for and splitting on \n and , characters
- searching for the check list enclosed in (brackets)
- constructing temporary GlobList objects
- regex operations
The implementation is rather "stringly typed".
This patch improves things by introducing a new NoLintToken class (implementation detail in NoLintDirectiveHandler.cpp). This class stores parsed information about a NOLINT marker, such as its type (NOLINT/NEXTLINE/BEGIN/END), its location in the source file, and whether it specifies any checks. The NOLINT handler has been adopted to use this class. Passing around NoLintTokens is preferable to passing raw string content, because NoLintTokens are already parsed and are smaller in size.
Cache the locations of NOLINT(BEGIN/END) blocks.
The NOLINT handler runs each time a diagnostic is raised. It has to read the entire source file to determine the locations of the NOLINT(BEGIN/END) blocks. If the diagnostic is due to a problem in an included header, the handler has to read the included header in its entirety as well. Currently, the implementation throws away this information and has to regenerate it the next time there is a diagnostic.
This patch adds a cache for this information. It is not enough to keep a cache for just the current file being analyzed. If it is to be performant, the cache should contain data for all files that have been analyzed, not just the current one. We do not want to invalidate the cache as we context switch between files as we trace diagnostics generated in macros and headers.
The performance should be better now (see test results below). In any case, to ensure we aren't paying for something that we don't need, we only generate the NOLINT(BEGIN/END) cache if we didn't find a valid NOLINT or NOLINTNEXTLINE first (checking for these is usually cheaper so they are checked first).
Performance testing
YMMV with the performance improvements you see. It depends on the code you are analyzing - factors include the file size, number of included files, number of diagnostics, number of NOLINTs...
To give some indication of the performance improvement on a real world project, I have run clang-tidy over the LLVM codebase and got the following results:
time clang-tools-extra\clang-tidy\tool\run-clang-tidy.py -p build -checks="-*,bugprone-*,cppcoreguidelines-*,llvm-*,modernize-*,misc-*,performance-*,readability-*" llvm
Baseline (NOLINT, NOLINTNEXTLINE support only):
3h 44m 05s
Current NOLINTBEGIN/END implementation (BEFORE performance improvements):
7h 36m 37s
New NOLINTBEGIN/END implementation (AFTER performance improvements):
3h 40m 05s
So the new implementation is about the same speed as the baseline (before NOLINTBEGIN/END) implementation. The performance hit added by the NOLINTBEGIN/END support seems to have been counter-acted by the general performance improvements to do with NoLintToken etc.
Other changes
The NOLINT handler code has come to a stage that it deserves its own CPP file instead of hitching a ride on ClangTidyDiagnosticConsumer.cpp, which is already pretty long. As suggested by review comments, I have used a PIMPL for the NOLINT handler class.
I have split up/renamed some unit test files so that they test one aspect per file, and added new tests to lock in some long-standing behavior.
I think "Pragma" is a very specific term, used for example in #pragma gcc diagnostic or // IWYU pragma: keep, but in clang-tidy we don't use the word pragma, so that might be confusing. What about renaming it to NoLintHandler.cpp or NoLintDirectiveHandler.cpp?