llvm / llvm/llvm-project

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

Open
#219,632 4 comments 0 reactions 1 assignee Claimed by @iamalok123 View on GitHub
lldb platform:windows
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.