Skip to content

Commit 94ad115

Browse files
committed
compiler: use Tarjan's SCC algorithm to detect loops for defer
The compiler needs to know whether a defer is in a loop to determine whether to allocate stack or heap memory. Previously, this performed a DFS of the CFG every time a defer was found. This resulted in time complexity jointly proportional to the number of defers and the number of blocks in the function. Now, the compiler will instead use Tarjan's strongly connected components algorithm to find cycles in linear time. The search is performed lazily, so this has minimal performance impact on functions without defers. In order to implement Tarjan's SCC algorithm, additional state needed to be attached to the blocks. I chose to merge all of the per-block state into a single slice to simplify memory management.
1 parent bf61317 commit 94ad115

File tree

6 files changed

+368
-45
lines changed

6 files changed

+368
-45
lines changed

compiler/asserts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc
245245
// current insert position.
246246
faultBlock := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw")
247247
nextBlock := b.insertBasicBlock(blockPrefix + ".next")
248-
b.blockExits[b.currentBlock] = nextBlock // adjust outgoing block for phi nodes
248+
b.currentBlockInfo.exit = nextBlock // adjust outgoing block for phi nodes
249249

250250
// Now branch to the out-of-bounds or the regular block.
251251
b.CreateCondBr(assert, faultBlock, nextBlock)

compiler/compiler.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@ type builder struct {
152152
llvmFnType llvm.Type
153153
llvmFn llvm.Value
154154
info functionInfo
155-
locals map[ssa.Value]llvm.Value // local variables
156-
blockEntries map[*ssa.BasicBlock]llvm.BasicBlock // a *ssa.BasicBlock may be split up
157-
blockExits map[*ssa.BasicBlock]llvm.BasicBlock // these are the exit blocks
155+
locals map[ssa.Value]llvm.Value // local variables
156+
blockInfo []blockInfo
158157
currentBlock *ssa.BasicBlock
158+
currentBlockInfo *blockInfo
159+
tarjanStack []uint
160+
tarjanIndex uint
159161
phis []phiNode
160162
deferPtr llvm.Value
161163
deferFrame llvm.Value
@@ -187,11 +189,22 @@ func newBuilder(c *compilerContext, irbuilder llvm.Builder, f *ssa.Function) *bu
187189
info: c.getFunctionInfo(f),
188190
locals: make(map[ssa.Value]llvm.Value),
189191
dilocals: make(map[*types.Var]llvm.Metadata),
190-
blockEntries: make(map[*ssa.BasicBlock]llvm.BasicBlock),
191-
blockExits: make(map[*ssa.BasicBlock]llvm.BasicBlock),
192192
}
193193
}
194194

195+
type blockInfo struct {
196+
// entry is the LLVM basic block corresponding to the start of this *ssa.Block.
197+
entry llvm.BasicBlock
198+
199+
// exit is the LLVM basic block corresponding to the end of this *ssa.Block.
200+
// It will be different than entry if any of the block's instructions contain internal branches.
201+
exit llvm.BasicBlock
202+
203+
// tarjan holds state for applying Tarjan's strongly connected components algorithm to the CFG.
204+
// This is used by defer.go to determine whether to stack- or heap-allocate defer data.
205+
tarjan tarjanNode
206+
}
207+
195208
type deferBuiltin struct {
196209
callName string
197210
pos token.Pos
@@ -1221,13 +1234,17 @@ func (b *builder) createFunctionStart(intrinsic bool) {
12211234
// manually.
12221235
entryBlock = b.ctx.AddBasicBlock(b.llvmFn, "entry")
12231236
} else {
1237+
blocks := b.fn.Blocks
1238+
blockInfo := make([]blockInfo, len(blocks))
12241239
for _, block := range b.fn.DomPreorder() {
1240+
info := &blockInfo[block.Index]
12251241
llvmBlock := b.ctx.AddBasicBlock(b.llvmFn, block.Comment)
1226-
b.blockEntries[block] = llvmBlock
1227-
b.blockExits[block] = llvmBlock
1242+
info.entry = llvmBlock
1243+
info.exit = llvmBlock
12281244
}
1245+
b.blockInfo = blockInfo
12291246
// Normal functions have an entry block.
1230-
entryBlock = b.blockEntries[b.fn.Blocks[0]]
1247+
entryBlock = blockInfo[0].entry
12311248
}
12321249
b.SetInsertPointAtEnd(entryBlock)
12331250

@@ -1323,8 +1340,9 @@ func (b *builder) createFunction() {
13231340
if b.DumpSSA {
13241341
fmt.Printf("%d: %s:\n", block.Index, block.Comment)
13251342
}
1326-
b.SetInsertPointAtEnd(b.blockEntries[block])
13271343
b.currentBlock = block
1344+
b.currentBlockInfo = &b.blockInfo[block.Index]
1345+
b.SetInsertPointAtEnd(b.currentBlockInfo.entry)
13281346
for _, instr := range block.Instrs {
13291347
if instr, ok := instr.(*ssa.DebugRef); ok {
13301348
if !b.Debug {
@@ -1384,7 +1402,7 @@ func (b *builder) createFunction() {
13841402
block := phi.ssa.Block()
13851403
for i, edge := range phi.ssa.Edges {
13861404
llvmVal := b.getValue(edge, getPos(phi.ssa))
1387-
llvmBlock := b.blockExits[block.Preds[i]]
1405+
llvmBlock := b.blockInfo[block.Preds[i].Index].exit
13881406
phi.llvm.AddIncoming([]llvm.Value{llvmVal}, []llvm.BasicBlock{llvmBlock})
13891407
}
13901408
}
@@ -1498,11 +1516,11 @@ func (b *builder) createInstruction(instr ssa.Instruction) {
14981516
case *ssa.If:
14991517
cond := b.getValue(instr.Cond, getPos(instr))
15001518
block := instr.Block()
1501-
blockThen := b.blockEntries[block.Succs[0]]
1502-
blockElse := b.blockEntries[block.Succs[1]]
1519+
blockThen := b.blockInfo[block.Succs[0].Index].entry
1520+
blockElse := b.blockInfo[block.Succs[1].Index].entry
15031521
b.CreateCondBr(cond, blockThen, blockElse)
15041522
case *ssa.Jump:
1505-
blockJump := b.blockEntries[instr.Block().Succs[0]]
1523+
blockJump := b.blockInfo[instr.Block().Succs[0].Index].entry
15061524
b.CreateBr(blockJump)
15071525
case *ssa.MapUpdate:
15081526
m := b.getValue(instr.Map, getPos(instr))

compiler/defer.go

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (b *builder) createLandingPad() {
100100

101101
// Continue at the 'recover' block, which returns to the parent in an
102102
// appropriate way.
103-
b.CreateBr(b.blockEntries[b.fn.Recover])
103+
b.CreateBr(b.blockInfo[b.fn.Recover.Index].entry)
104104
}
105105

106106
// Create a checkpoint (similar to setjmp). This emits inline assembly that
@@ -234,41 +234,88 @@ func (b *builder) createInvokeCheckpoint() {
234234
continueBB := b.insertBasicBlock("")
235235
b.CreateCondBr(isZero, continueBB, b.landingpad)
236236
b.SetInsertPointAtEnd(continueBB)
237-
b.blockExits[b.currentBlock] = continueBB
237+
b.currentBlockInfo.exit = continueBB
238238
}
239239

240-
// isInLoop checks if there is a path from a basic block to itself.
241-
func isInLoop(start *ssa.BasicBlock) bool {
242-
// Use a breadth-first search to scan backwards through the block graph.
243-
queue := []*ssa.BasicBlock{start}
244-
checked := map[*ssa.BasicBlock]struct{}{}
245-
246-
for len(queue) > 0 {
247-
// pop a block off of the queue
248-
block := queue[len(queue)-1]
249-
queue = queue[:len(queue)-1]
250-
251-
// Search through predecessors.
252-
// Searching backwards means that this is pretty fast when the block is close to the start of the function.
253-
// Defers are often placed near the start of the function.
254-
for _, pred := range block.Preds {
255-
if pred == start {
256-
// cycle found
257-
return true
258-
}
240+
// isInLoop checks if there is a path from the current block to itself.
241+
// Use Tarjan's strongly connected components algorithm to search for cycles.
242+
// A one-node SCC is a cycle iff there is an edge from the node to itself.
243+
// A multi-node SCC is always a cycle.
244+
func (b *builder) isInLoop() bool {
245+
if b.currentBlockInfo.tarjan.index == 0 {
246+
b.strongConnect(b.currentBlock)
247+
}
248+
return b.currentBlockInfo.tarjan.cyclic
249+
}
259250

260-
if _, ok := checked[pred]; ok {
261-
// block already checked
262-
continue
263-
}
251+
func (b *builder) strongConnect(block *ssa.BasicBlock) {
252+
// Assign a new index.
253+
assignedIndex := b.tarjanIndex + 1
254+
b.tarjanIndex = assignedIndex
255+
256+
// Apply the new index.
257+
blockIndex := block.Index
258+
node := &b.blockInfo[blockIndex].tarjan
259+
node.index = assignedIndex
260+
node.lowLink = assignedIndex
261+
262+
// Push the node onto the stack.
263+
node.onStack = true
264+
b.tarjanStack = append(b.tarjanStack, uint(blockIndex))
265+
266+
// Process the successors.
267+
for _, successor := range block.Succs {
268+
// Look up the successor's state.
269+
successorIndex := successor.Index
270+
if successorIndex == blockIndex {
271+
// Handle a self-cycle specially.
272+
node.cyclic = true
273+
continue
274+
}
275+
successorNode := &b.blockInfo[successorIndex].tarjan
276+
277+
if successorNode.index == 0 {
278+
// This node has not yet been visisted.
279+
b.strongConnect(successor)
280+
} else if !successorNode.onStack {
281+
// This node is has been visited, but is in a different SCC.
282+
// Ignore it.
283+
continue
284+
}
285+
if successorNode.lowLink < node.lowLink {
286+
node.lowLink = successorNode.lowLink
287+
}
288+
}
264289

265-
// add to queue and checked map
266-
queue = append(queue, pred)
267-
checked[pred] = struct{}{}
290+
if node.lowLink == node.index {
291+
// This is a root node.
292+
// Pop the SCC off the stack.
293+
stack := b.tarjanStack
294+
top := stack[len(stack)-1]
295+
stack = stack[:len(stack)-1]
296+
blocks := b.blockInfo
297+
topNode := &blocks[top].tarjan
298+
topNode.onStack = false
299+
if top != uint(blockIndex) {
300+
// Mark all nodes in this SCC as cyclic.
301+
topNode.cyclic = true
302+
for top != uint(blockIndex) {
303+
top = stack[len(stack)-1]
304+
stack = stack[:len(stack)-1]
305+
topNode := &blocks[top].tarjan
306+
topNode.onStack = false
307+
topNode.cyclic = true
308+
}
268309
}
310+
b.tarjanStack = stack
269311
}
312+
}
270313

271-
return false
314+
type tarjanNode struct {
315+
index uint
316+
lowLink uint
317+
onStack bool
318+
cyclic bool
272319
}
273320

274321
// createDefer emits a single defer instruction, to be run when this function
@@ -410,7 +457,10 @@ func (b *builder) createDefer(instr *ssa.Defer) {
410457

411458
// Put this struct in an allocation.
412459
var alloca llvm.Value
413-
if !isInLoop(instr.Block()) {
460+
if instr.Block() != b.currentBlock {
461+
panic("block mismatch")
462+
}
463+
if !b.isInLoop() {
414464
// This can safely use a stack allocation.
415465
alloca = llvmutil.CreateEntryBlockAlloca(b.Builder, deferredCallType, "defer.alloca")
416466
} else {

compiler/interface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value {
737737
prevBlock := b.GetInsertBlock()
738738
okBlock := b.insertBasicBlock("typeassert.ok")
739739
nextBlock := b.insertBasicBlock("typeassert.next")
740-
b.blockExits[b.currentBlock] = nextBlock // adjust outgoing block for phi nodes
740+
b.currentBlockInfo.exit = nextBlock // adjust outgoing block for phi nodes
741741
b.CreateCondBr(commaOk, okBlock, nextBlock)
742742

743743
// Retrieve the value from the interface if the type assert was

0 commit comments

Comments
 (0)