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: <user expression 13>: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<TranslationUnitDecl> / 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 -- <a local> 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.
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 succeedare 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 singleexpression -- someUndeclaredNamewheresomeUndeclaredNamedoes not exist in the current frame:810 ms between lines 72 and 73, i.e. inside
ClangExpressionDeclMap::FindExternalVisibleDecls(context, module_sp, namespace_decl), reachedthrough the
isa<TranslationUnitDecl>/ root-namespace branch. The same sequence for$__lldb_argand$__lldb_expra few lines earlier takes tens of microseconds.None of the four enabled log categories emits anything during those 810 ms, and
log timersaccounts 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 onthe 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:
FindGlobalVariable()LookupFunction()LookupInModulesDeclVendor()FindBestGlobalDataSymbol()FindBestGlobalDataSymbol(), which doesFindSymbolsWithNameAndType(name, eSymbolTypeAny)over the frame's module and then overevery module in the target;
LookupFunction()'s failure path and/orLookupInModulesDeclVendor().Incidental observation:
FindBestGlobalDataSymbol()runs even whenLookupFunction()alreadysucceeded, because the guard only tests
!context.m_found_variableand a function match does notset that flag.
Ruled out
Each of these was checked rather than assumed:
expression -- 1+1is instantexpression -- <a local>is instanttarget variable someUndeclaredNameis instantimage lookup -s someUndeclaredNameis instanttype lookup someUndeclaredNameis instantimage lookup -n someUndeclaredNameis instantlang_opts.SpellChecking = falsein ClangExpressionParser.cppsomeMember) resolves instantly; the same unresolvable name is equally slow in a frame belonging to a free function, where there is no class scopesymbols.load-on-demandisfalseEnvironment
Caveats
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.
bundled
liblldb.dllships without symbols, so I could not sample the stack. Someone with aWindows/PDB target and a symbolised build should be able to profile this in minutes.