llvm / llvm/llvm-project

[ThinLTO] Distributed backends keep dllimport on declarations that resolve within the link unit; codegen diverges from in-process ThinLTO (COFF)

Open
#220,371 1 comment 0 reactions 0 assignees View on GitHub
LTO
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

The below AI analysis is the second of two issues that are preventing DTLTO from working with our DLL setup and crashing. I'm currently using `/OPT:NOREF` and `/ignore:4217` as a workaround but the generated code is not optimal. I'll link the other issue in the comments.

---

In-process ThinLTO on COFF clears the dllimport storage class of a declaration whose prevailing definition is inside the link unit; the backend emits a direct call. Distributed backends (clang -fthinlto-index=, including DTLTO) keep the dllimport storage and emit call [__imp_X]. The linker fulfills __imp_X as a local import, producing one "locally defined symbol imported" warning per reference. Consequences:

1. Codegen divergence from in-process ThinLTO: an IAT-indirect call at each such site instead of a direct call. In one large application, the distributed link contained 78,017 local-import slots where the in-process link emits direct calls and no warnings.
2. Under /OPT:REF, the referenced definitions are stripped and the slots contain the image base ().

Repro:

```c++
// def.cpp
extern "C" __declspec(noinline) int target(int x) { volatile int v = x; return v * 7; }
extern "C" __declspec(noinline) int keeper(int x) { volatile int v = x; return v + 1; }
```

```c++
// user.cpp
extern "C" __declspec(dllimport) int target(int);
extern "C" int keeper(int);
extern "C" int entry() { return target(1) + keeper(2); }
```

`clang-cl -c -O2 -flto=thin def.cpp`
`clang-cl -c -O2 -flto=thin user.cpp`

:: in-process: direct call to target, no warnings
`lld-link /entry:entry /subsystem:console /out:a.exe def.obj user.obj`

:: distributed: user's backend emits call [__imp_target]; local-import warning at final link
`lld-link /entry:entry /subsystem:console /out:b.exe /thinlto-distributor: /thinlto-remote-compiler:\clang-cl.exe def.obj user.obj`

The distributed backend object for user.cpp contains U __imp_target; the in-process equivalent has a direct relocation to target. noinline prevents the call from being removed by cross-module import and inlining; the divergence itself is independent of importing (in-process still emits a direct call with /mllvm:-import-instr-limit=0).

Mechanism (current main):

- The clearing happens in FunctionImportGlobalProcessing::processGlobalForThinLTO (FunctionImportUtils.cpp). It only fires when the backend's index has a summary for the symbol — VI below is null otherwise:

```c++
} else if (VI && VI.isDSOLocal(ImportIndex.withDSOLocalPropagation())) {
// If all summaries are dso_local, symbol gets resolved to a known local
// definition.
GV.setDSOLocal(true);
if (GV.hasDLLImportStorageClass())
GV.setDLLStorageClass(GlobalValue::DefaultStorageClass);
}
```

- The in-process backend uses the full combined index; the summary and its dso_local resolution (D35702) are present.
- Distributed backends read the per-module index from gatherImportedSummariesForModule (FunctionImport.cpp), which contains the module's own summaries plus summaries of symbols selected for import. A referenced-but-not-imported external has no summary there; VI is null; the dllimport storage reaches codegen.

Suggested direction: include summary entries, or at least the dso_local resolution, for referenced-but-not-imported globals when writing per-module indexes.

Verified with clang/lld 23.1.0; the cited code matches current main.

Contributor guide

Open the contributing guide

Research direction

Start with the two-file clang-cl/lld-link ThinLTO repro and compare the in-process and distributed backend objects. Read FunctionImportUtils.cpp at FunctionImportGlobalProcessing::processGlobalForThinLTO, then trace per-module index construction in FunctionImport.cpp at gatherImportedSummariesForModule. Done means distributed codegen resolves the internal target directly, without __imp_target or the local-import warning, matching in-process ThinLTO.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.