Skip to content

Commit 4f9bfd8

Browse files
committed
bpf/analyze: replace BlockIterator.Previous() with Backtracker
This commit removes the concept of 'global backtracking' (backtracking through chains of blocks with single predecessors) and refactors BlockIterator.Previous() into its own object. This removes the hazard of accidentally backtracking using the main iterator. Since no loop/depth tracking or block rollovers need to be done, this makes the implementation significantly more straightforward. This makes starting a backtracking session for reachability analysis on a program with many branches a lot faster. Compared to main before this PR: goos: linux goarch: amd64 pkg: github.com/cilium/cilium/pkg/bpf/analyze cpu: AMD Ryzen 7 3700X 8-Core Processor │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ ComputeBlocks-16 654.6µ ± 1% 637.1µ ± 3% -2.67% (p=0.009 n=6) Reachability-16 29.16µ ± 2% 16.30µ ± 2% -44.10% (p=0.002 n=6) geomean 138.1µ 101.9µ -26.24% │ old.txt │ new.txt │ │ B/op │ B/op vs base │ ComputeBlocks-16 372.5Ki ± 0% 372.5Ki ± 0% ~ (p=0.935 n=6) Reachability-16 352.0 ± 0% 352.0 ± 0% ~ (p=1.000 n=6) ¹ geomean 11.32Ki 11.32Ki +0.00% ¹ all samples are equal │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ ComputeBlocks-16 8.053k ± 0% 8.053k ± 0% ~ (p=1.000 n=6) ¹ Reachability-16 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ geomean 155.4 155.4 +0.00% ¹ all samples are equal Signed-off-by: Timo Beckers <timo@isovalent.com>
1 parent 34e6ae5 commit 4f9bfd8

5 files changed

Lines changed: 83 additions & 283 deletions

File tree

pkg/bpf/analyze/blocks.go

Lines changed: 54 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,16 @@ func (b *Block) iterateGlobal(blocks Blocks, insns asm.Instructions) *BlockItera
219219
}
220220
}
221221

222-
// predecessor returns the previous Block in the control flow if there is
223-
// exactly one predecessor, otherwise returns nil.
222+
// backtrack returns a Backtracker starting at the end of the block.
224223
//
225-
// This is useful for walking the control flow backwards when there is no
226-
// branching, e.g. for finding the last instruction that wrote to a register
227-
// before it is read.
228-
func (b *Block) predecessor() *Block {
229-
if len(b.predecessors) == 1 {
230-
return b.predecessors[0]
224+
// After the next call to [Backtracker.Previous], the backtracker will point to
225+
// the last instruction in the block.
226+
func (b *Block) backtrack(insns asm.Instructions) *Backtracker {
227+
return &Backtracker{
228+
insns: insns,
229+
stop: b.start,
230+
index: b.end,
231231
}
232-
return nil
233232
}
234233

235234
func (b *Block) String() string {
@@ -285,12 +284,6 @@ func (b *Block) Dump(insns asm.Instructions) string {
285284
return sb.String()
286285
}
287286

288-
// maxDepth is the maximum depth of block traversal when backtracking to
289-
// predecessors. Used in favor of a visited set since it's much cheaper than
290-
// frequent map lookups. Typical depth while looking for map pointer loads is
291-
// 1-3 with some double-digit outliers.
292-
const maxDepth = 128
293-
294287
// BlockIterator is an iterator over the instructions in a block or a list of
295288
// blocks.
296289
//
@@ -308,7 +301,6 @@ type BlockIterator struct {
308301
block *Block
309302

310303
insns asm.Instructions
311-
depth uint8
312304

313305
ins *asm.Instruction
314306
index int
@@ -332,19 +324,6 @@ func (i *BlockIterator) Offset() asm.RawInstructionOffset {
332324
return i.offset
333325
}
334326

335-
func (i *BlockIterator) Clone() *BlockIterator {
336-
return &BlockIterator{
337-
i.blocks,
338-
i.block,
339-
i.insns,
340-
i.depth,
341-
i.ins,
342-
i.index,
343-
i.offset,
344-
i.local,
345-
}
346-
}
347-
348327
// nextBlock pulls the next block by identifier, if it exists. Otherwise,
349328
// returns false.
350329
//
@@ -358,62 +337,15 @@ func (i *BlockIterator) nextBlock() bool {
358337
if i.block.id+1 >= i.blocks.count() {
359338
return false
360339
}
361-
next := i.blocks[i.block.id+1]
362-
363-
// Reset loop detection when moving forward since we're primarily concerned
364-
// with infinite loops while backtracking continuously. Fallthroughs always
365-
// point to the end of the program eventually.
366-
i.depth = 0
367340

368-
i.block = next
341+
i.block = i.blocks[i.block.id+1]
369342
i.index = i.block.start
370343
i.offset = i.block.raw
371344
i.ins = &i.insns[i.index]
372345

373346
return true
374347
}
375348

376-
// prevBlock pulls the previous block in the control flow if there is exactly
377-
// one predecessor, otherwise returns false.
378-
//
379-
// Sometimes, map pointers are loaded into a register in a previous block.
380-
// Backtracking into multiple predecessors is not useful since the contents of
381-
// the pointer register would be ambiguous if assigned from multiple
382-
// predecessors.
383-
//
384-
// Positions the iterator at the end of the previous block. Offset is set to 0
385-
// since backtracking requires summing up instruction sizes from the start of
386-
// the block.
387-
func (i *BlockIterator) prevBlock() bool {
388-
if i.block == nil {
389-
return false
390-
}
391-
392-
prev := i.block.predecessor()
393-
if prev == nil || prev == i.block {
394-
return false
395-
}
396-
397-
// Simple loop detection to avoid infinite loops when backtracking
398-
// continuously. Depth gets reset when moving forward.
399-
if i.depth >= maxDepth {
400-
return false
401-
}
402-
i.depth++
403-
404-
i.block = prev
405-
i.index = i.block.end
406-
407-
// Raw offset tracking disabled for backtracking since it requires summing
408-
// up instruction sizes from the start of the block. Raw offsets are only
409-
// used for dumping instructions in forward order.
410-
i.offset = 0
411-
412-
i.ins = &i.insns[i.index]
413-
414-
return true
415-
}
416-
417349
// Next advances the iterator to the next instruction in the block. If the end
418350
// of the block is reached, it will either stop (if iterating locally) or roll
419351
// over to the next block (if iterating globally).
@@ -445,43 +377,64 @@ func (i *BlockIterator) Next() bool {
445377
return true
446378
}
447379

448-
func (i *BlockIterator) Previous() bool {
449-
if i.block == nil || i.index < i.block.start || i.index > i.block.end {
450-
return false
380+
// Backtrack returns a Backtracker starting at the current instruction of the
381+
// BlockIterator.
382+
//
383+
// [Backtracker.Instruction] will return the same instruction as the current
384+
// instruction of the BlockIterator.
385+
//
386+
// [Backtracker.Previous] will return the instruction preceding the current one,
387+
// if any.
388+
func (i *BlockIterator) Backtrack() *Backtracker {
389+
return &Backtracker{
390+
insns: i.insns,
391+
stop: i.block.start,
392+
index: i.index,
393+
ins: &i.insns[i.index],
451394
}
395+
}
452396

453-
if i.ins == nil {
454-
i.index = i.block.end
455-
i.ins = &i.insns[i.index]
456-
457-
// Raw offset tracking disabled for backtracking since it requires summing
458-
// up instruction sizes from the start of the block. Raw offsets are only
459-
// used for dumping instructions in forward order.
460-
i.offset = 0
397+
// Backtracker is an iterator that walks backwards through a Block's
398+
// instructions.
399+
//
400+
// This is useful for finding the last instruction that wrote to a register
401+
// before it is read, by following the control flow backwards.
402+
type Backtracker struct {
403+
insns asm.Instructions
404+
stop int
461405

462-
return true
463-
}
406+
index int
407+
ins *asm.Instruction
408+
}
464409

465-
if i.index-1 < i.block.start {
466-
if !i.local {
467-
// Iterating globally, roll over to the previous block if it exists.
468-
return i.prevBlock()
469-
}
410+
// Instruction returns the current instruction.
411+
func (bt *Backtracker) Instruction() *asm.Instruction {
412+
return bt.ins
413+
}
470414

471-
// Iterating locally, stop here.
415+
// Previous moves to the previous instruction within the block.
416+
// Returns false when reaching the start of the block.
417+
func (bt *Backtracker) Previous() bool {
418+
if bt.index <= bt.stop {
472419
return false
473420
}
474421

475-
i.index--
476-
i.ins = &i.insns[i.index]
422+
// Only decrement if Previous was called before.
423+
if bt.ins != nil {
424+
bt.index--
425+
}
477426

478-
// Prevent offset underflow.
479-
raw := asm.RawInstructionOffset(i.ins.Size() / asm.InstructionSize)
480-
i.offset = min(i.offset, i.offset-raw)
427+
bt.ins = &bt.insns[bt.index]
481428

482429
return true
483430
}
484431

432+
// Clone creates a copy of the Backtracker at its current position.
433+
func (bt *Backtracker) Clone() *Backtracker {
434+
cpy := *bt
435+
return &cpy
436+
}
437+
485438
// getBlock retrieves the block associated with an instruction. It checks both
486439
// the leader and edge metadata to find the block. If neither is found, it
487440
// returns nil, indicating that the instruction forms neither the start nor end
@@ -523,13 +476,6 @@ func (bl Blocks) first() *Block {
523476
return bl[0]
524477
}
525478

526-
func (bl Blocks) last() *Block {
527-
if len(bl) == 0 {
528-
return nil
529-
}
530-
return bl[len(bl)-1]
531-
}
532-
533479
func (bl Blocks) iterate(insns asm.Instructions) *BlockIterator {
534480
if len(bl) == 0 {
535481
return nil

pkg/bpf/analyze/blocks_test.go

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -54,65 +54,6 @@ func TestMakeBlocksSimple(t *testing.T) {
5454
assert.Equal(t, b, b2)
5555
}
5656

57-
func TestBlocksMultiplePredecessors(t *testing.T) {
58-
// A program with multiple predecessors to the last block.
59-
insns := asm.Instructions{
60-
asm.Mov.Imm32(asm.R0, 1),
61-
asm.JEq.Imm(asm.R0, 0, "target"),
62-
asm.Mov.Imm32(asm.R1, 1),
63-
asm.JEq.Imm(asm.R1, 0, "target"),
64-
asm.Mov.Imm32(asm.R0, 0).WithSymbol("target"),
65-
asm.Return(),
66-
}
67-
68-
// Marshal instructions to fix up references.
69-
require.NoError(t, insns.Marshal(io.Discard, binary.LittleEndian))
70-
71-
blocks, err := MakeBlocks(insns)
72-
require.NoError(t, err)
73-
74-
assert.EqualValues(t, 3, blocks.count())
75-
76-
first := blocks.first()
77-
assert.EqualValues(t, 0, first.id)
78-
assert.Empty(t, first.predecessors)
79-
assert.Equal(t, 0, first.start)
80-
assert.Equal(t, 1, first.end)
81-
assert.Equal(t, blocks[2], first.branch)
82-
assert.Equal(t, blocks[1], first.fthrough)
83-
84-
second := blocks[1]
85-
assert.EqualValues(t, 1, second.id)
86-
assert.Len(t, second.predecessors, 1)
87-
assert.Equal(t, first, second.predecessors[0])
88-
assert.Equal(t, 2, second.start)
89-
assert.Equal(t, 3, second.end)
90-
assert.Equal(t, blocks[2], second.branch)
91-
assert.Equal(t, blocks[2], second.fthrough)
92-
93-
last := blocks.last()
94-
assert.EqualValues(t, 2, last.id)
95-
assert.Len(t, last.predecessors, 2)
96-
assert.Equal(t, first, last.predecessors[0])
97-
assert.Equal(t, second, last.predecessors[1])
98-
assert.Equal(t, 4, last.start)
99-
assert.Equal(t, 5, last.end)
100-
assert.Nil(t, last.branch)
101-
assert.Nil(t, last.fthrough)
102-
103-
// Pull instructions from the last block and make sure it doesn't continue
104-
// past the start of the block since it has multiple predecessors.
105-
iter := last.iterateGlobal(blocks, insns)
106-
107-
require.True(t, iter.Previous())
108-
assert.Equal(t, iter.Instruction(), &insns[5])
109-
110-
require.True(t, iter.Previous())
111-
assert.Equal(t, iter.Instruction(), &insns[4])
112-
113-
require.False(t, iter.Previous())
114-
}
115-
11657
func TestMakeBlocksManyBranches(t *testing.T) {
11758
insns := branchingProg(t, 1000)
11859

@@ -141,9 +82,6 @@ func TestBlocksIterateLocal(t *testing.T) {
14182
assert.True(t, iter.Next())
14283
assert.False(t, iter.Next())
14384
assert.Equal(t, 0, iter.index)
144-
145-
// Iterator has nowhere to move. First instruction is already pulled.
146-
assert.False(t, iter.Previous())
14785
}
14886

14987
func TestBlocksIterateGlobal(t *testing.T) {
@@ -169,41 +107,6 @@ func TestBlocksIterateGlobal(t *testing.T) {
169107
// We should have seen all instructions.
170108
assert.Equal(t, 100, i)
171109
assert.Equal(t, 99, iter.index)
172-
173-
i = 0
174-
for ; iter.Previous(); i++ {
175-
assert.EqualValues(t, iter.index, iter.ins.Constant)
176-
}
177-
178-
// Iterator should be back at the start.
179-
assert.Equal(t, 99, i)
180-
assert.Equal(t, 0, iter.index)
181-
}
182-
183-
func TestBlocksIterateGlobalLoop(t *testing.T) {
184-
insns := asm.Instructions{
185-
asm.Mov.Imm32(asm.R0, 1).WithSymbol("loop"),
186-
asm.JEq.Imm(asm.R0, 0, "exit"),
187-
asm.Mov.Imm32(asm.R0, 0),
188-
asm.JEq.Imm(asm.R0, 1, "loop"),
189-
asm.Return().WithSymbol("exit"),
190-
}
191-
192-
// Marshal instructions to fix up references.
193-
require.NoError(t, insns.Marshal(io.Discard, binary.LittleEndian))
194-
195-
bl, err := MakeBlocks(insns)
196-
require.NoError(t, err)
197-
198-
assert.EqualValues(t, 3, bl.count())
199-
200-
iter := bl.first().iterateGlobal(bl, insns)
201-
for iter.Previous() {
202-
}
203-
204-
// Iterator should be back at the start and depth limit should be hit.
205-
assert.Equal(t, 0, iter.index)
206-
assert.Equal(t, uint8(maxDepth), iter.depth)
207110
}
208111

209112
func TestBlocksIterateOffset(t *testing.T) {

0 commit comments

Comments
 (0)