[lldb] Resolving an identifier that does not exist costs ~1s per lookup in ClangExpressionDeclMap::FindExternalVisibleDecls on a large Windows/PDB target
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
On a Windows target built with MSVC (PDB debug info, ~110 loaded modules), evaluating an
expression that is a single identifier which does not resolve takes **0.8 - 2.7 s**. The time is
spent entirely inside `ClangExpressionDeclMap::FindExternalVisibleDecls()`. Lookups that succeed
are fast, and every equivalent lookup issued as a CLI command is fast.
This is not an edge case in practice: an IDE re-evaluates every watch expression each time the
process stops, and a watch expression referring to a variable that is out of scope in the current
frame is the normal state of affairs while stepping. In CodeLLDB this made stepping take seconds
per step (vadimcn/codelldb#1411).
## Measurement
*(Identifiers from the application have been renamed throughout this report. Timings, log
lines and line numbers are otherwise verbatim.)*
`log enable -T -f expr.log lldb expr types symbol module on-demand`, then a single
`expression -- someUndeclaredName` where `someUndeclaredName` does not exist in the current frame:
```
71 | 1787981780.795001268 ClangExpressionDeclMap::FindExternalVisibleDecls for 'someUndeclaredName' in a 'TranslationUnit'
>> 72 | 1787981780.795014620 CEDM::FEVD Searching the root namespace
>> 73 | 1787981781.605120897 ClangASTSource::FindExternalVisibleDecls on (ASTContext*)... for 'someUndeclaredName' in a 'TranslationUnit'
74 | 1787981781.605169296 CAS::FEVD Searching the root namespace
77 | 1787981781.605983257 ** [SBFrame::EvaluateExpression] Expression evaluation failed:
78 | error: :1:1: use of undeclared identifier 'someUndeclaredName'
```
**810 ms between lines 72 and 73**, i.e. inside
`ClangExpressionDeclMap::FindExternalVisibleDecls(context, module_sp, namespace_decl)`, reached
through the `isa` / root-namespace branch. The same sequence for
`$__lldb_arg` and `$__lldb_expr` a few lines earlier takes tens of microseconds.
None of the four enabled log categories emits anything during those 810 ms, and `log timers`
accounts for only ~2 ms over several such evaluations -- the code in question carries neither
log statements nor `LLDB_SCOPED_TIMER`.
## Two contributors
Evaluating a name that *is* found as a function (`someFreeFunction`, a real function on
the stack) is measurably slower than a local-variable lookup but faster than the fully
unresolvable name. Reading the code, that splits the cost in two:
| | name found as function | name not found at all |
| -------------------------------- | ---------------------- | --------------------- |
| `FindGlobalVariable()` | fails | fails |
| `LookupFunction()` | succeeds, returns | fails (exhaustive) |
| `LookupInModulesDeclVendor()` | skipped | runs |
| `FindBestGlobalDataSymbol()` | **runs** | **runs** |
- the cost shared by both points at `FindBestGlobalDataSymbol()`, which does
`FindSymbolsWithNameAndType(name, eSymbolTypeAny)` over the frame's module and then over
**every** module in the target;
- the extra cost of the unresolvable name points at `LookupFunction()`'s failure path and/or
`LookupInModulesDeclVendor()`.
Incidental observation: `FindBestGlobalDataSymbol()` runs even when `LookupFunction()` already
succeeded, because the guard only tests `!context.m_found_variable` and a function match does not
set that flag.
## Ruled out
Each of these was checked rather than assumed:
| Candidate | Evidence |
| --- | --- |
| Expression parser startup | `expression -- 1+1` is instant |
| Expression machinery, AST import, decl map setup | `expression -- ` is instant |
| All-module global variable lookup | `target variable someUndeclaredName` is instant |
| All-module symbol table lookup | `image lookup -s someUndeclaredName` is instant |
| All-module type lookup | `type lookup someUndeclaredName` is instant |
| All-module function lookup | `image lookup -n someUndeclaredName` is instant |
| Clang typo correction | `lang_opts.SpellChecking = false` in ClangExpressionParser.cpp |
| C++ class completion / class scope | a real member (`someMember`) resolves instantly; the same unresolvable name is equally slow in a frame belonging to a free function, where there is no class scope |
| On-demand symbol loading | `symbols.load-on-demand` is `false` |
## Environment
- Windows 10/11 x86_64, target built with MSVC, PDB debug info
- ~110 modules loaded; a large C++ desktop application (MFC UI, heavy template libraries)
- LLDB as bundled with CodeLLDB 1.12.3 (LLVM 22.x)
## Caveats
- **No minimal reproducer.** The same expression on a small C++ program is instant, so the cost
scales with something about the target -- most likely module count or the size of the debug
info. I could not narrow that further from outside.
- The localisation above is from the log plus reading the source, not from a profiler: the
bundled `liblldb.dll` ships without symbols, so I could not sample the stack. Someone with a
Windows/PDB target and a symbolised build should be able to profile this in minutes.
- I am happy to run further diagnostics on this target.
Contributor guide
Assessment
This issue has not been assessed yet.