Skip to content

[lldb] Resolving an identifier that does not exist costs ~1s per lookup in ClangExpressionDeclMap::FindExternalVisibleDecls on a large Windows/PDB target #219632

Description

@zhuwanhong

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions