Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f34ab2d
[llvm-profgen] Loading binary functions from .symtab when DWARF info …
HighW4y2H3ll Oct 9, 2025
a1f32b5
Merge branch 'main' into fix-profgen-symbol
HighW4y2H3ll Oct 15, 2025
0fd352d
formatting
HighW4y2H3ll Oct 15, 2025
c097d37
Fix branch target check when an instruction branches to itself. (i.e.…
HighW4y2H3ll Oct 16, 2025
a19064d
Making the API compatible with non-ELF binaries
HighW4y2H3ll Oct 17, 2025
e12e694
Fix
HighW4y2H3ll Oct 20, 2025
5eead6b
Clean up getSymbolSize API and warnings
HighW4y2H3ll Oct 22, 2025
8f59bfa
Add cmdline option --load-function-from-symbol
HighW4y2H3ll Oct 23, 2025
a967994
Get symbol size only for ELFObjectFile
HighW4y2H3ll Oct 23, 2025
0dc2c66
Add unit test
HighW4y2H3ll Oct 23, 2025
75dc996
Nit
HighW4y2H3ll Oct 24, 2025
5600e83
Cleanup loggings and comments
HighW4y2H3ll Oct 30, 2025
0df504e
Mark function FromSymtab too if new range is found
HighW4y2H3ll Oct 31, 2025
644fce9
Formatting
HighW4y2H3ll Oct 31, 2025
9188eff
Fix suffix strip && refactor the function checks
HighW4y2H3ll Nov 1, 2025
b6ae0ba
Fixup corrupted DWARF function names using symbol table info
HighW4y2H3ll Nov 12, 2025
b1cbdd0
Fixup overwritten DWARF symbol name when decoding pseudo probe
HighW4y2H3ll Nov 14, 2025
8927a27
Fixup GuidFilter && Pseudo probe callee mismatch
HighW4y2H3ll Nov 16, 2025
108bc08
format
HighW4y2H3ll Nov 17, 2025
6e3a5ac
Clean up fixup logic in pseudo probe
HighW4y2H3ll Nov 17, 2025
3d8ad53
Further cleanup and add more GuidFilters
HighW4y2H3ll Nov 18, 2025
1344cdd
Infer callee name with pseudo probe names
HighW4y2H3ll Nov 21, 2025
2978e21
promote eligible entry point functions
HighW4y2H3ll Nov 22, 2025
eaab9df
Update pseudo probe search range && range checks
HighW4y2H3ll Nov 22, 2025
c53e0ae
Iterate through ProfiledFunctions to reduce scope
HighW4y2H3ll Nov 24, 2025
b707e80
Add usePseudoProbes() guard and comments
HighW4y2H3ll Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions llvm/include/llvm/MC/MCPseudoProbe.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ class MCDecodedPseudoProbeInlineTree

// Return false if it's a dummy inline site
bool hasInlineSite() const { return !isRoot() && !Parent->isRoot(); }
bool isTopLevelFunc() const { return !isRoot() && Parent->isRoot(); }
InlineSite getInlineSite() const { return InlineSite(Guid, ProbeId); }
void setProbes(MutableArrayRef<MCDecodedPseudoProbe> ProbesRef) {
Probes = ProbesRef.data();
Expand Down
14 changes: 10 additions & 4 deletions llvm/include/llvm/ProfileData/SampleProf.h
Original file line number Diff line number Diff line change
Expand Up @@ -1214,13 +1214,19 @@ class FunctionSamples {
// Note the sequence of the suffixes in the knownSuffixes array matters.
// If suffix "A" is appended after the suffix "B", "A" should be in front
// of "B" in knownSuffixes.
const char *KnownSuffixes[] = {LLVMSuffix, PartSuffix, UniqSuffix};
const SmallVector<StringRef> KnownSuffixes{LLVMSuffix, PartSuffix,
UniqSuffix};
return getCanonicalFnName(FnName, KnownSuffixes, Attr);
}

static StringRef getCanonicalFnName(StringRef FnName,
ArrayRef<StringRef> Suffixes,
StringRef Attr = "selected") {
if (Attr == "" || Attr == "all")
return FnName.split('.').first;
if (Attr == "selected") {
StringRef Cand(FnName);
for (const auto &Suf : KnownSuffixes) {
StringRef Suffix(Suf);
for (const auto Suffix : Suffixes) {
// If the profile contains ".__uniq." suffix, don't strip the
// suffix for names in the IR.
if (Suffix == UniqSuffix && FunctionSamples::HasUniqSuffix)
Expand All @@ -1229,7 +1235,7 @@ class FunctionSamples {
if (It == StringRef::npos)
continue;
auto Dit = Cand.rfind('.');
if (Dit == It + Suffix.size() - 1)
if (Dit == It || Dit == It + Suffix.size() - 1)
Cand = Cand.substr(0, It);
}
return Cand;
Expand Down
Binary file not shown.
37 changes: 37 additions & 0 deletions llvm/test/tools/llvm-profgen/missing-dwarf.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
; RUN: rm -rf %t
; RUN: mkdir -p %t
; RUN: cd %t

; RUN: echo -e "1\n401120-40113b:1\n1\n40112f->401110:1" > %t.prof

; Test --load-function-from-symbol=0
; RUN: llvm-profgen --format=text --unsymbolized-profile=%t.prof --binary=%S/Inputs/missing-dwarf.exe --output=%t1 --fill-zero-for-all-funcs --show-detailed-warning --use-offset=0 --load-function-from-symbol=0 2>&1 | FileCheck %s --check-prefix=CHECK-NO-LOAD-SYMTAB

; CHECK-NO-LOAD-SYMTAB: warning: Loading of DWARF info completed, but no binary functions have been retrieved.

; Test --load-function-from-symbol=1
; RUN: llvm-profgen --format=text --unsymbolized-profile=%t.prof --binary=%S/Inputs/missing-dwarf.exe --output=%t2 --fill-zero-for-all-funcs --show-detailed-warning --use-offset=0 --load-function-from-symbol=1
; RUN: FileCheck %s --input-file %t2 --check-prefix=CHECK-LOAD-SYMTAB

; CHECK-LOAD-SYMTAB: main:2:1
; CHECK-LOAD-SYMTAB-NEXT: 1: 1
; CHECK-LOAD-SYMTAB-NEXT: 2: 1 foo:1
; CHECK-LOAD-SYMTAB-NEXT: !CFGChecksum: 281479271677951
; CHECK-LOAD-SYMTAB-NEXT: foo:0:0
; CHECK-LOAD-SYMTAB-NEXT: 1: 0
; CHECK-LOAD-SYMTAB-NEXT: !CFGChecksum: 4294967295

; Build instructions:
; missing-dwarf.o: clang -gsplit-dwarf=split -fdebug-compilation-dir=. test.c -fdebug-info-for-profiling -fpseudo-probe-for-profiling -O0 -g -o missing-dwarf.o -c
; missing-dwarf.exe: clang -fdebug-compilation-dir=. missing-dwarf.o -o missing-dwarf.exe -fdebug-info-for-profiling -fpseudo-probe-for-profiling -O0 -g

; Source code:

int foo() {
return 1;
}

int main() {
foo();
return 0;
}
1 change: 1 addition & 0 deletions llvm/tools/llvm-profgen/Options.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extern cl::opt<bool> ShowDetailedWarning;
extern cl::opt<bool> InferMissingFrames;
extern cl::opt<bool> EnableCSPreInliner;
extern cl::opt<bool> UseContextCostForPreInliner;
extern cl::opt<bool> LoadFunctionFromSymbol;

} // end namespace llvm

Expand Down
7 changes: 7 additions & 0 deletions llvm/tools/llvm-profgen/PerfReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,7 @@ void PerfScriptReader::warnInvalidRange() {
uint64_t TotalRangeNum = 0;
uint64_t InstNotBoundary = 0;
uint64_t UnmatchedRange = 0;
uint64_t RecoveredRange = 0;
uint64_t RangeCrossFunc = 0;
uint64_t BogusRange = 0;

Expand All @@ -1309,6 +1310,9 @@ void PerfScriptReader::warnInvalidRange() {
continue;
}

if (FRange->Func->NameStatus != DwarfNameStatus::Matched)
RecoveredRange += I.second;

if (EndAddress >= FRange->EndAddress) {
RangeCrossFunc += I.second;
WarnInvalidRange(StartAddress, EndAddress, RangeCrossFuncMsg);
Expand All @@ -1328,6 +1332,9 @@ void PerfScriptReader::warnInvalidRange() {
emitWarningSummary(
UnmatchedRange, TotalRangeNum,
"of samples are from ranges that do not belong to any functions.");
emitWarningSummary(RecoveredRange, TotalRangeNum,
"of samples are from ranges that belong to functions "
"recovered from symbol table.");
emitWarningSummary(
RangeCrossFunc, TotalRangeNum,
"of samples are from ranges that do cross function boundaries.");
Expand Down
11 changes: 10 additions & 1 deletion llvm/tools/llvm-profgen/ProfileGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,11 @@ ProfileGenerator::getTopLevelFunctionProfile(FunctionId FuncName) {
void ProfileGenerator::generateProfile() {
collectProfiledFunctions();

if (Binary->usePseudoProbes())
if (Binary->usePseudoProbes()) {
Binary->decodePseudoProbe();
if (LoadFunctionFromSymbol)
Binary->loadSymbolsFromPseudoProbe();
}

if (SampleCounters) {
if (Binary->usePseudoProbes()) {
Expand Down Expand Up @@ -732,6 +735,10 @@ ProfileGeneratorBase::getCalleeNameForAddress(uint64_t TargetAddress) {
if (!FRange || !FRange->IsFuncEntry)
return StringRef();

auto FuncName = Binary->findPseudoProbeName(FRange->Func);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to be under Binary->usePseudoProbes()

also please add a comment for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated! :)

if (FuncName.size())
return FunctionSamples::getCanonicalFnName(FuncName);

return FunctionSamples::getCanonicalFnName(FRange->getFuncName());
}

Expand Down Expand Up @@ -919,6 +926,8 @@ void CSProfileGenerator::generateProfile() {
Binary->decodePseudoProbe();
if (InferMissingFrames)
initializeMissingFrameInferrer();
if (LoadFunctionFromSymbol)
Binary->loadSymbolsFromPseudoProbe();
}

if (SampleCounters) {
Expand Down
152 changes: 150 additions & 2 deletions llvm/tools/llvm-profgen/ProfiledBinary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ cl::opt<bool> ShowSourceLocations("show-source-locations",
cl::desc("Print source locations."),
cl::cat(ProfGenCategory));

cl::opt<bool>
LoadFunctionFromSymbol("load-function-from-symbol", cl::init(true),
cl::desc("Gather additional binary function info "
"from symbols (e.g. .symtab) in case "
"dwarf info is incomplete."),
cl::cat(ProfGenCategory));

static cl::opt<bool>
ShowCanonicalFnName("show-canonical-fname",
cl::desc("Print canonical function name."),
Expand Down Expand Up @@ -257,6 +264,9 @@ void ProfiledBinary::load() {
if (ShowDisassemblyOnly)
decodePseudoProbe(Obj);

if (LoadFunctionFromSymbol && UsePseudoProbes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is pseudo-probe required here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, this is to be compatible with the code here:

if (Binary->usePseudoProbes()) {
generateProbeBasedProfile();
} else {
generateLineNumBasedProfile();
}

UsePseudoProbes is needed so we won't use line numbers to generate profile, which won't work with symtab because symtab only have addresses

loadSymbolsFromSymtab(Obj);

// Disassemble the text sections.
disassemble(Obj);

Expand Down Expand Up @@ -461,6 +471,13 @@ void ProfiledBinary::decodePseudoProbe(const ObjectFile *Obj) {
} else {
for (auto *F : ProfiledFunctions) {
GuidFilter.insert(Function::getGUIDAssumingExternalLinkage(F->FuncName));
// DWARF name might be broken when a DWARF32 .debug_str.dwo section
// execeeds 4GB. We expect symbol table to contain the correct function
// names which matches the pseudo probe. Adding back all the GUIDs if
// possible.
auto AltGUIDs = AlternativeFunctionGUIDs.equal_range(F);
for (const auto &[_, Func] : make_range(AltGUIDs))
GuidFilter.insert(Func);
for (auto &Range : F->Ranges) {
auto GUIDs = StartAddrToSymMap.equal_range(Range.first);
for (const auto &[StartAddr, Func] : make_range(GUIDs))
Expand Down Expand Up @@ -604,13 +621,13 @@ bool ProfiledBinary::dissassembleSymbol(std::size_t SI, ArrayRef<uint8_t> Bytes,
// Record potential call targets for tail frame inference later-on.
if (InferMissingFrames && FRange) {
uint64_t Target = 0;
MIA->evaluateBranch(Inst, Address, Size, Target);
bool Err = MIA->evaluateBranch(Inst, Address, Size, Target);
if (MCDesc.isCall()) {
// Indirect call targets are unknown at this point. Recording the
// unknown target (zero) for further LBR-based refinement.
MissingContextInferrer->CallEdges[Address].insert(Target);
} else if (MCDesc.isUnconditionalBranch()) {
assert(Target &&
assert(Err &&
"target should be known for unconditional direct branch");
// Any inter-function unconditional jump is considered tail call at
// this point. This is not 100% accurate and could further be
Expand Down Expand Up @@ -820,6 +837,95 @@ void ProfiledBinary::populateSymbolAddressList(const ObjectFile *Obj) {
}
}

void ProfiledBinary::loadSymbolsFromSymtab(const ObjectFile *Obj) {
// Load binary functions from symbol table when Debug info is incomplete.
// Strip the internal suffixes which are not reflected in the DWARF info.
const SmallVector<StringRef, 10> Suffixes(
{// Internal suffixes from CoroSplit pass
".cleanup", ".destroy", ".resume",
// Internal suffixes from Bolt
".cold", ".warm",
// Compiler/LTO internal
".llvm.", ".part.", ".isra.", ".constprop.", ".lto_priv."});
StringRef FileName = Obj->getFileName();
for (const SymbolRef &Symbol : Obj->symbols()) {
const SymbolRef::Type Type = unwrapOrError(Symbol.getType(), FileName);
const uint64_t StartAddr = unwrapOrError(Symbol.getAddress(), FileName);
const StringRef Name = unwrapOrError(Symbol.getName(), FileName);
uint64_t Size = 0;
if (isa<ELFObjectFileBase>(Symbol.getObject())) {
ELFSymbolRef ElfSymbol(Symbol);
Size = ElfSymbol.getSize();
}

if (Size == 0 || Type != SymbolRef::ST_Function)
continue;

const uint64_t EndAddr = StartAddr + Size;
const StringRef SymName =
FunctionSamples::getCanonicalFnName(Name, Suffixes);
assert(StartAddr < EndAddr && StartAddr >= getPreferredBaseAddress() &&
"Function range is invalid.");

auto Range = findFuncRange(StartAddr);
if (!Range) {
assert(findFuncRange(EndAddr - 1) == nullptr &&
"Function range overlaps with existing functions.");
// Function from symbol table not found previously in DWARF, store ranges.
auto Ret = BinaryFunctions.emplace(SymName, BinaryFunction());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a range is found, we should expect function to be found. probably want to assert(!Range || !Ret.second).

auto &Func = Ret.first->second;
if (Ret.second) {
Func.FuncName = Ret.first->first;
HashBinaryFunctions[MD5Hash(StringRef(SymName))] = &Func;
}

Func.NameStatus = DwarfNameStatus::Missing;
Func.Ranges.emplace_back(StartAddr, EndAddr);

auto R = StartAddrToFuncRangeMap.emplace(StartAddr, FuncRange());
FuncRange &FRange = R.first->second;

FRange.Func = &Func;
FRange.StartAddress = StartAddr;
FRange.EndAddress = EndAddr;

} else if (SymName != Range->getFuncName()) {
// Function range already found from DWARF, but the symbol name from
// symbol table is inconsistent with debug info. Log this discrepancy and
// the alternative function GUID.
if (ShowDetailedWarning)
WithColor::warning()
<< "Conflicting name for symbol " << Name << " with range ("
<< format("%8" PRIx64, StartAddr) << ", "
<< format("%8" PRIx64, EndAddr) << ")"
<< ", but the DWARF symbol " << Range->getFuncName()
<< " indicates an overlapping range ("
<< format("%8" PRIx64, Range->StartAddress) << ", "
<< format("%8" PRIx64, Range->EndAddress) << ")\n";

assert(StartAddr == Range->StartAddress && EndAddr == Range->EndAddress &&
"Mismatched function range");

Range->Func->NameStatus = DwarfNameStatus::Mismatch;
AlternativeFunctionGUIDs.emplace(Range->Func,
MD5Hash(StringRef(SymName)));

} else if (StartAddr != Range->StartAddress &&
EndAddr != Range->EndAddress) {
// Function already found in DWARF, but the address range from symbol
// table conflicts/overlaps with the debug info.
WithColor::warning() << "Conflicting range for symbol " << Name
<< " with range (" << format("%8" PRIx64, StartAddr)
<< ", " << format("%8" PRIx64, EndAddr) << ")"
<< ", but the DWARF symbol " << Range->getFuncName()
<< " indicates another range ("
<< format("%8" PRIx64, Range->StartAddress) << ", "
<< format("%8" PRIx64, Range->EndAddress) << ")\n";
llvm_unreachable("invalid function range");
}
}
}

void ProfiledBinary::loadSymbolsFromDWARFUnit(DWARFUnit &CompilationUnit) {
for (const auto &DieInfo : CompilationUnit.dies()) {
llvm::DWARFDie Die(&CompilationUnit, &DieInfo);
Expand Down Expand Up @@ -1034,6 +1140,48 @@ void ProfiledBinary::computeInlinedContextSizeForFunc(
}
}

void ProfiledBinary::loadSymbolsFromPseudoProbe() {
if (!UsePseudoProbes)
return;

const AddressProbesMap &Address2ProbesMap = getAddress2ProbesMap();
for (auto &[Addr, Range] : StartAddrToFuncRangeMap) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe we can iterate over the ProfiledFunctions list, which should be smaller scope.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually leaning towards iterating over all the Ranges... because I can check Range.IsFuncEntry and skip a lot of non-entry function ranges... The computation complexity is likely still the same...? but BinaryFunction doesn't contain a full FuncRange struct, so checking function entry points won't be that convenient with ProfiledFunctions...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh.. wait.. my mistake, you mean the ProfiledFunctions.. let me update this!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh.. wait.. my mistake, you mean the ProfiledFunctions.. let me update this!

Yeah, we might just profile a small set of the functions in the binary, that would help. I think you're also right, it needs to find a full FuncRange struct in StartAddrToFuncRangeMap, before I thought it's in the BinaryFunction.Ranges.

auto Func = Range.Func;
if (!Range.IsFuncEntry || Func->NameStatus != DwarfNameStatus::Mismatch)
continue;
#ifndef NDEBUG
if (PseudoProbeNames.count(Func))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not NFC changes, why put it in to assertion mode? If we want to avoid the repeating, we also need it in release mode.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right.. Let me remove this at all! The llvm-profgen run time doesn't seem noticeably longer nor shorter... but now with the benefit of using the following assertations for safe...

continue;
#endif
const auto &Probe = Address2ProbesMap.find(Addr).begin();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we search in the function range(using the end address as the safeguard ) instead of the whole map

I found this from BOLT matching: https://github.com/llvm/llvm-project/pull/100446/files#diff-9c8ed910a52bb3d6726fd342b03b8fd3c77c651a9459531a439bbea725b44279R644-R651

I think we can use the same look-up

auto Probes = Decoder->getAddress2ProbesMap().find(Addr, Addr + Size);
... 

Otherwise if it returns something that's not in the current function range, we get a wrong function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! let me update this!

if (Probe != Address2ProbesMap.end()) {
const MCDecodedPseudoProbeInlineTree *InlineTreeNode =
Probe->get().getInlineTreeNode();
while (!InlineTreeNode->isTopLevelFunc())
InlineTreeNode = static_cast<MCDecodedPseudoProbeInlineTree *>(
InlineTreeNode->Parent);

auto TopLevelProbes = InlineTreeNode->getProbes();
auto TopProbe = TopLevelProbes.begin();
assert(TopProbe != TopLevelProbes.end() &&
TopProbe->getAddress() >= Addr &&
"Top level pseudo probe does not match function range");

const auto *ProbeDesc = getFuncDescForGUID(InlineTreeNode->Guid);
auto Ret = PseudoProbeNames.emplace(Func, ProbeDesc->FuncName);
assert((Ret.second || Ret.first->second == ProbeDesc->FuncName) &&
"Mismatched pseudo probe names");
}
}
}

StringRef ProfiledBinary::findPseudoProbeName(const BinaryFunction *Func) {
auto ProbeName = PseudoProbeNames.find(Func);
if (ProbeName == PseudoProbeNames.end())
return StringRef();
return ProbeName->second;
}
Comment on lines +1190 to +1195
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we really need an extra map. The symbol from pseudo probe should be the ground truth(otherwise it's not loaded by compiler), how about we just overwriting the function name field in BinaryFunction (or is it because the BinaryFunction is immutable in profile generator?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, the problem is BinaryFunctions is a map using its original DWARF name as the key.. If I'm going to replace the function name field of BinaryFunction, it probably be good to also update the key? but there're tons of pointer references pointing to the BinaryFunction in this map... It just feels safer (and.. maybe cleaner) to use a separate lookaside map and avoid patching the BinaryFunction at a later stage..?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense, thanks for the clarification!


void ProfiledBinary::inferMissingFrames(
const SmallVectorImpl<uint64_t> &Context,
SmallVectorImpl<uint64_t> &NewContext) {
Expand Down
Loading