Skip to content

Commit 02fe366

Browse files
committed
[KeyInstr][DwarfDebug] Add is_stmt emission support (llvm#133495)
Interpret Key Instructions metadata to determine is_stmt placement. The lowest rank (highest precedent) instructions in each {InlinedAt, atomGroup} set are candidates for is_stmt. Only the last instruction in each set in a given block gets is_stmt. Calls always get is_stmt. RFC: https://discourse.llvm.org/t/rfc-improving-is-stmt-placement-for-better-interactive-debugging/82668
1 parent 038b380 commit 02fe366

File tree

8 files changed

+628
-13
lines changed

8 files changed

+628
-13
lines changed

llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp

Lines changed: 168 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "DwarfExpression.h"
1818
#include "DwarfUnit.h"
1919
#include "llvm/ADT/APInt.h"
20+
#include "llvm/ADT/ScopeExit.h"
2021
#include "llvm/ADT/Statistic.h"
2122
#include "llvm/ADT/StringExtras.h"
2223
#include "llvm/ADT/Twine.h"
@@ -170,6 +171,9 @@ static cl::opt<DwarfDebug::MinimizeAddrInV5> MinimizeAddrInV5Option(
170171
"Stuff")),
171172
cl::init(DwarfDebug::MinimizeAddrInV5::Default));
172173

174+
static cl::opt<bool> KeyInstructionsAreStmts("dwarf-use-key-instructions",
175+
cl::Hidden, cl::init(false));
176+
173177
static constexpr unsigned ULEB128PadSize = 4;
174178

175179
void DebugLocDwarfExpression::emitOp(uint8_t Op, const char *Comment) {
@@ -2072,6 +2076,10 @@ void DwarfDebug::beginInstruction(const MachineInstr *MI) {
20722076
unsigned LastAsmLine =
20732077
Asm->OutStreamer->getContext().getCurrentDwarfLoc().getLine();
20742078

2079+
bool IsKey = false;
2080+
if (KeyInstructionsAreStmts && DL && DL.getLine())
2081+
IsKey = KeyInstructions.contains(MI);
2082+
20752083
if (!DL && MI == PrologEndLoc) {
20762084
// In rare situations, we might want to place the end of the prologue
20772085
// somewhere that doesn't have a source location already. It should be in
@@ -2086,17 +2094,22 @@ void DwarfDebug::beginInstruction(const MachineInstr *MI) {
20862094
(!PrevInstBB ||
20872095
PrevInstBB->getSectionID() == MI->getParent()->getSectionID());
20882096
bool ForceIsStmt = ForceIsStmtInstrs.contains(MI);
2089-
if (DL == PrevInstLoc && PrevInstInSameSection && !ForceIsStmt) {
2097+
if (PrevInstInSameSection && !ForceIsStmt && DL.isSameSourceLocation(PrevInstLoc)) {
20902098
// If we have an ongoing unspecified location, nothing to do here.
20912099
if (!DL)
20922100
return;
2093-
// We have an explicit location, same as the previous location.
2094-
// But we might be coming back to it after a line 0 record.
2095-
if ((LastAsmLine == 0 && DL.getLine() != 0) || Flags) {
2096-
// Reinstate the source location but not marked as a statement.
2097-
RecordSourceLine(DL, Flags);
2101+
2102+
// Skip this if the instruction is Key, else we might accidentally miss an
2103+
// is_stmt.
2104+
if (!IsKey) {
2105+
// We have an explicit location, same as the previous location.
2106+
// But we might be coming back to it after a line 0 record.
2107+
if ((LastAsmLine == 0 && DL.getLine() != 0) || Flags) {
2108+
// Reinstate the source location but not marked as a statement.
2109+
RecordSourceLine(DL, Flags);
2110+
}
2111+
return;
20982112
}
2099-
return;
21002113
}
21012114

21022115
if (!DL) {
@@ -2139,11 +2152,17 @@ void DwarfDebug::beginInstruction(const MachineInstr *MI) {
21392152
Flags |= DWARF2_FLAG_PROLOGUE_END | DWARF2_FLAG_IS_STMT;
21402153
PrologEndLoc = nullptr;
21412154
}
2142-
// If the line changed, we call that a new statement; unless we went to
2143-
// line 0 and came back, in which case it is not a new statement.
2144-
unsigned OldLine = PrevInstLoc ? PrevInstLoc.getLine() : LastAsmLine;
2145-
if (DL.getLine() && (DL.getLine() != OldLine || ForceIsStmt))
2146-
Flags |= DWARF2_FLAG_IS_STMT;
2155+
2156+
if (KeyInstructionsAreStmts) {
2157+
if (IsKey)
2158+
Flags |= DWARF2_FLAG_IS_STMT;
2159+
} else {
2160+
// If the line changed, we call that a new statement; unless we went to
2161+
// line 0 and came back, in which case it is not a new statement.
2162+
unsigned OldLine = PrevInstLoc ? PrevInstLoc.getLine() : LastAsmLine;
2163+
if (DL.getLine() && (DL.getLine() != OldLine || ForceIsStmt))
2164+
Flags |= DWARF2_FLAG_IS_STMT;
2165+
}
21472166

21482167
RecordSourceLine(DL, Flags);
21492168

@@ -2336,6 +2355,139 @@ DwarfDebug::emitInitialLocDirective(const MachineFunction &MF, unsigned CUID) {
23362355
return PrologEndLoc;
23372356
}
23382357

2358+
void DwarfDebug::computeKeyInstructions(const MachineFunction *MF) {
2359+
// New function - reset KeyInstructions.
2360+
KeyInstructions.clear();
2361+
2362+
// The current candidate is_stmt instructions for each source atom.
2363+
// Map {(InlinedAt, Group): (Rank, Instructions)}.
2364+
// NOTE: Anecdotally, for a large C++ blob, 99% of the instruction
2365+
// SmallVectors contain 2 or fewer elements; use 2 inline elements.
2366+
DenseMap<std::pair<DILocation *, uint32_t>,
2367+
std::pair<uint16_t, SmallVector<const MachineInstr *, 2>>>
2368+
GroupCandidates;
2369+
2370+
// For each instruction:
2371+
// * Skip insts without DebugLoc, AtomGroup or AtomRank, and line zeros.
2372+
// * Check if insts in this group have been seen already in GroupCandidates.
2373+
// * If this instr rank is equal, add this instruction to GroupCandidates.
2374+
// Remove existing instructions from GroupCandidates if they have the
2375+
// same parent.
2376+
// * If this instr rank is higher (lower precedence), ignore it.
2377+
// * If this instr rank is lower (higher precedence), erase existing
2378+
// instructions from GroupCandidates and add this one.
2379+
//
2380+
// Then insert each GroupCandidates instruction into KeyInstructions.
2381+
2382+
for (auto &MBB : *MF) {
2383+
// Rather than apply is_stmt directly to Key Instructions, we "float"
2384+
// is_stmt up to the 1st instruction with the same line number in a
2385+
// contiguous block. That instruction is called the "buoy". The
2386+
// buoy gets reset if we encouner an instruction with an atom
2387+
// group.
2388+
const MachineInstr *Buoy = nullptr;
2389+
// The atom group number associated with Buoy which may be 0 if we haven't
2390+
// encountered an atom group yet in this blob of instructions with the same
2391+
// line number.
2392+
uint64_t BuoyAtom = 0;
2393+
2394+
for (auto &MI : MBB) {
2395+
if (MI.isMetaInstruction())
2396+
continue;
2397+
2398+
if (!MI.getDebugLoc() || !MI.getDebugLoc().getLine())
2399+
continue;
2400+
2401+
// Reset the Buoy to this instruction if it has a different line number.
2402+
if (!Buoy ||
2403+
Buoy->getDebugLoc().getLine() != MI.getDebugLoc().getLine()) {
2404+
Buoy = &MI;
2405+
BuoyAtom = 0; // Set later when we know which atom the buoy is used by.
2406+
}
2407+
2408+
// Call instructions are handled specially - we always mark them as key
2409+
// regardless of atom info.
2410+
const auto &TII =
2411+
*MI.getParent()->getParent()->getSubtarget().getInstrInfo();
2412+
bool IsCallLike = MI.isCall() || TII.isTailCall(MI);
2413+
if (IsCallLike) {
2414+
assert(MI.getDebugLoc() && "Unexpectedly missing DL");
2415+
2416+
// Calls are always key. Put the buoy (may not be the call) into
2417+
// KeyInstructions directly rather than the candidate map to avoid it
2418+
// being erased (and we may not have a group number for the call).
2419+
KeyInstructions.insert(Buoy);
2420+
2421+
// Avoid floating any future is_stmts up to the call.
2422+
Buoy = nullptr;
2423+
BuoyAtom = 0;
2424+
2425+
if (!MI.getDebugLoc()->getAtomGroup() ||
2426+
!MI.getDebugLoc()->getAtomRank())
2427+
continue;
2428+
}
2429+
2430+
auto *InlinedAt = MI.getDebugLoc()->getInlinedAt();
2431+
uint64_t Group = MI.getDebugLoc()->getAtomGroup();
2432+
uint8_t Rank = MI.getDebugLoc()->getAtomRank();
2433+
if (!Group || !Rank)
2434+
continue;
2435+
2436+
// Don't let is_stmts float past instructions from different source atoms.
2437+
if (BuoyAtom && BuoyAtom != Group) {
2438+
Buoy = &MI;
2439+
BuoyAtom = Group;
2440+
}
2441+
2442+
auto &[CandidateRank, CandidateInsts] =
2443+
GroupCandidates[{InlinedAt, Group}];
2444+
2445+
// If CandidateRank is zero then CandidateInsts should be empty: there
2446+
// are no other candidates for this group yet. If CandidateRank is nonzero
2447+
// then CandidateInsts shouldn't be empty: we've got existing candidate
2448+
// instructions.
2449+
assert((CandidateRank == 0 && CandidateInsts.empty()) ||
2450+
(CandidateRank != 0 && !CandidateInsts.empty()));
2451+
2452+
assert(Rank && "expected nonzero rank");
2453+
// If we've seen other instructions in this group with higher precedence
2454+
// (lower nonzero rank), don't add this one as a candidate.
2455+
if (CandidateRank && CandidateRank < Rank)
2456+
continue;
2457+
2458+
// If we've seen other instructions in this group of the same rank,
2459+
// discard any from this block (keeping the others). Else if we've
2460+
// seen other instructions in this group of lower precedence (higher
2461+
// rank), discard them all.
2462+
if (CandidateRank == Rank)
2463+
llvm::remove_if(CandidateInsts, [&MI](const MachineInstr *Candidate) {
2464+
return MI.getParent() == Candidate->getParent();
2465+
});
2466+
else if (CandidateRank > Rank)
2467+
CandidateInsts.clear();
2468+
2469+
if (Buoy) {
2470+
// Add this candidate.
2471+
CandidateInsts.push_back(Buoy);
2472+
CandidateRank = Rank;
2473+
2474+
assert(!BuoyAtom || BuoyAtom == MI.getDebugLoc()->getAtomGroup());
2475+
BuoyAtom = MI.getDebugLoc()->getAtomGroup();
2476+
} else {
2477+
// Don't add calls, because they've been dealt with already. This means
2478+
// CandidateInsts might now be empty - handle that.
2479+
assert(IsCallLike);
2480+
if (CandidateInsts.empty())
2481+
CandidateRank = 0;
2482+
}
2483+
}
2484+
}
2485+
2486+
for (const auto &[_, Insts] : GroupCandidates.values())
2487+
for (auto *I : Insts)
2488+
KeyInstructions.insert(I);
2489+
}
2490+
23392491
/// For the function \p MF, finds the set of instructions which may represent a
23402492
/// change in line number from one or more of the preceding MBBs. Stores the
23412493
/// resulting set of instructions, which should have is_stmt set, in
@@ -2495,7 +2647,10 @@ void DwarfDebug::beginFunctionImpl(const MachineFunction *MF) {
24952647
PrologEndLoc = emitInitialLocDirective(
24962648
*MF, Asm->OutStreamer->getContext().getDwarfCompileUnitID());
24972649

2498-
findForceIsStmtInstrs(MF);
2650+
if (KeyInstructionsAreStmts)
2651+
computeKeyInstructions(MF);
2652+
else
2653+
findForceIsStmtInstrs(MF);
24992654
}
25002655

25012656
unsigned

llvm/lib/CodeGen/AsmPrinter/DwarfDebug.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,10 @@ class DwarfDebug : public DebugHandlerBase {
464464
};
465465

466466
private:
467+
/// Instructions which should get is_stmt applied because they implement key
468+
/// functionality for a source atom.
469+
SmallDenseSet<const MachineInstr *> KeyInstructions;
470+
467471
/// Force the use of DW_AT_ranges even for single-entry range lists.
468472
MinimizeAddrInV5 MinimizeAddr = MinimizeAddrInV5::Disabled;
469473

@@ -701,6 +705,11 @@ class DwarfDebug : public DebugHandlerBase {
701705

702706
void findForceIsStmtInstrs(const MachineFunction *MF);
703707

708+
/// Compute instructions which should get is_stmt applied because they
709+
/// implement key functionality for a source location atom, store results in
710+
/// DwarfDebug::KeyInstructions.
711+
void computeKeyInstructions(const MachineFunction *MF);
712+
704713
protected:
705714
/// Gather pre-function debug information.
706715
void beginFunctionImpl(const MachineFunction *MF) override;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
; RUN: llc %s --filetype=obj -o - --dwarf-use-key-instructions \
2+
; RUN: | llvm-objdump -d - --no-show-raw-insn \
3+
; RUN: | FileCheck %s --check-prefix=OBJ
4+
5+
; RUN: llc %s --filetype=obj -o - --dwarf-use-key-instructions \
6+
; RUN: | llvm-dwarfdump - --debug-line \
7+
; RUN: | FileCheck %s --check-prefix=DBG
8+
9+
;; 1. [[gnu::nodebug]] void prologue_end();
10+
;; 2.
11+
;; 3. int f(int *a, int b, int c) {
12+
;; 4. prologue_end();
13+
;; 5. *a =
14+
;; 6. b + c;
15+
;; 7. return *a;
16+
;; 8. }
17+
;;
18+
;; The add and store are in the same group (1). The add (line 6) has lower
19+
;; precedence (rank 2) so should not get is_stmt applied.
20+
21+
; OBJ: 0000000000000000 <_Z1fPiii>:
22+
; OBJ-NEXT: 0: pushq %rbp
23+
; OBJ-NEXT: 1: pushq %r14
24+
; OBJ-NEXT: 3: pushq %rbx
25+
; OBJ-NEXT: 4: movl %edx, %ebx
26+
; OBJ-NEXT: 6: movl %esi, %ebp
27+
; OBJ-NEXT: 8: movq %rdi, %r14
28+
; OBJ-NEXT: b: callq 0x10 <_Z1fPiii+0x10>
29+
; OBJ-NEXT: 10: addl %ebx, %ebp
30+
; OBJ-NEXT: 12: movl %ebp, (%r14)
31+
; OBJ-NEXT: 15: movl %ebp, %eax
32+
; OBJ-NEXT: 17: popq %rbx
33+
; OBJ-NEXT: 18: popq %r14
34+
; OBJ-NEXT: 1a: popq %rbp
35+
36+
; DBG: Address Line Column File ISA Discriminator OpIndex Flags
37+
; DBG-NEXT: ------------------ ------ ------ ------ --- ------------- ------- -------------
38+
; DBG-NEXT: 0x0000000000000000 3 0 0 0 0 0 is_stmt
39+
; DBG-NEXT: 0x000000000000000b 4 0 0 0 0 0 is_stmt prologue_end
40+
; DBG-NEXT: 0x0000000000000010 6 0 0 0 0 0
41+
; DBG-NEXT: 0x0000000000000012 5 0 0 0 0 0 is_stmt
42+
; DBG-NEXT: 0x0000000000000015 7 0 0 0 0 0 is_stmt
43+
; DBG-NEXT: 0x0000000000000017 7 0 0 0 0 0 epilogue_begin
44+
; DBG-NEXT: 0x000000000000001c 7 0 0 0 0 0 end_sequence
45+
46+
target triple = "x86_64-unknown-linux-gnu"
47+
48+
define hidden noundef i32 @_Z1fPiii(ptr %a, i32 %b, i32 %c) local_unnamed_addr !dbg !11 {
49+
entry:
50+
tail call void @_Z12prologue_endv(), !dbg !DILocation(line: 4, scope: !11)
51+
%add = add nsw i32 %c, %b, !dbg !DILocation(line: 6, scope: !11, atomGroup: 1, atomRank: 2)
52+
store i32 %add, ptr %a, align 4, !dbg !DILocation(line: 5, scope: !11, atomGroup: 1, atomRank: 1)
53+
ret i32 %add, !dbg !DILocation(line: 7, scope: !11, atomGroup: 2, atomRank: 1)
54+
}
55+
56+
declare void @_Z12prologue_endv() local_unnamed_addr #1
57+
58+
!llvm.dbg.cu = !{!0}
59+
!llvm.module.flags = !{!2, !3}
60+
!llvm.ident = !{!10}
61+
62+
!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_17, file: !1, producer: "clang version 19.0.0", isOptimized: true, runtimeVersion: 0, emissionKind: LineTablesOnly, splitDebugInlining: false, nameTableKind: None)
63+
!1 = !DIFile(filename: "test.cpp", directory: "/")
64+
!2 = !{i32 7, !"Dwarf Version", i32 5}
65+
!3 = !{i32 2, !"Debug Info Version", i32 3}
66+
!10 = !{!"clang version 19.0.0"}
67+
!11 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 3, type: !12, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
68+
!12 = !DISubroutineType(types: !13)
69+
!13 = !{}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
; RUN: llc %s --filetype=obj -o - --dwarf-use-key-instructions \
2+
; RUN: | llvm-objdump -d - --no-show-raw-insn \
3+
; RUN: | FileCheck %s --check-prefix=OBJ
4+
5+
; RUN: llc %s --filetype=obj -o - --dwarf-use-key-instructions \
6+
; RUN: | llvm-dwarfdump - --debug-line \
7+
; RUN: | FileCheck %s --check-prefix=DBG
8+
9+
;; 1. int f(int a) {
10+
;; 2. int x = a + 1;
11+
;; 3. return x;
12+
;; 4. }
13+
;; 5. int g(int b) {
14+
;; 6. return f(b);
15+
;; 7. }
16+
;;
17+
;; Both functions contain 2 instructions in unique atom groups. In f we see
18+
;; groups 1 and 3, and in g we see {!18, 1} and 1. All of these instructions
19+
;; should get is_stmt.
20+
21+
; OBJ: 0000000000000000 <_Z1fi>:
22+
; OBJ-NEXT: 0: leal 0x1(%rdi), %eax
23+
; OBJ-NEXT: 3: retq
24+
; OBJ: 0000000000000010 <_Z1gi>:
25+
; OBJ-NEXT: 10: leal 0x1(%rdi), %eax
26+
; OBJ-NEXT: 13: retq
27+
28+
; DBG: Address Line Column File ISA Discriminator OpIndex Flags
29+
; DBG-NEXT: ------------------ ------ ------ ------ --- ------------- ------- -------------
30+
; DBG-NEXT: 0x0000000000000000 2 0 0 0 0 0 is_stmt prologue_end
31+
; DBG-NEXT: 0x0000000000000003 3 0 0 0 0 0 is_stmt
32+
; DBG-NEXT: 0x0000000000000010 2 0 0 0 0 0 is_stmt prologue_end
33+
; DBG-NEXT: 0x0000000000000013 6 0 0 0 0 0 is_stmt
34+
35+
target triple = "x86_64-unknown-linux-gnu"
36+
37+
define hidden noundef i32 @_Z1fi(i32 noundef %a) local_unnamed_addr !dbg !11 {
38+
entry:
39+
%add = add nsw i32 %a, 1, !dbg !DILocation(line: 2, scope: !11, atomGroup: 1, atomRank: 2)
40+
ret i32 %add, !dbg !DILocation(line: 3, scope: !11, atomGroup: 3, atomRank: 1)
41+
}
42+
43+
define hidden noundef i32 @_Z1gi(i32 noundef %b) local_unnamed_addr !dbg !16 {
44+
entry:
45+
%add.i = add nsw i32 %b, 1, !dbg !DILocation(line: 2, scope: !11, inlinedAt: !18, atomGroup: 1, atomRank: 2)
46+
ret i32 %add.i, !dbg !DILocation(line: 6, scope: !16, atomGroup: 1, atomRank: 1)
47+
}
48+
49+
!llvm.dbg.cu = !{!0}
50+
!llvm.module.flags = !{!2, !3}
51+
!llvm.ident = !{!10}
52+
53+
!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_17, file: !1, producer: "clang version 19.0.0", isOptimized: true, runtimeVersion: 0, emissionKind: LineTablesOnly, splitDebugInlining: false, nameTableKind: None)
54+
!1 = !DIFile(filename: "test.cpp", directory: "/")
55+
!2 = !{i32 7, !"Dwarf Version", i32 5}
56+
!3 = !{i32 2, !"Debug Info Version", i32 3}
57+
!10 = !{!"clang version 19.0.0"}
58+
!11 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 1, type: !12, scopeLine: 1, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
59+
!12 = !DISubroutineType(types: !13)
60+
!13 = !{}
61+
!16 = distinct !DISubprogram(name: "g", scope: !1, file: !1, line: 5, type: !12, scopeLine: 5, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
62+
!18 = distinct !DILocation(line: 6, scope: !16)

0 commit comments

Comments
 (0)