375 lines
10 KiB
V
375 lines
10 KiB
V
module optimize
|
|
|
|
import v3.ssa
|
|
|
|
// --- Mem2Reg (promote allocas to SSA values + phi nodes) ---
|
|
// Flat arrays indexed by value_id / block_id. Phi predecessor operands store
|
|
// raw block ids, matching the rest of the v3 SSA (and the arm64 backend's
|
|
// emit_phi_edge_copies, which reads operand[odd] as a block id).
|
|
struct Mem2RegCtx {
|
|
mut:
|
|
defs [][]int // alloc_id -> blocks storing to it
|
|
uses [][]int // alloc_id -> blocks loading from it
|
|
phi_placements [][]int // block_id -> alloc_ids needing a phi here
|
|
phi_vals [][]int // block_id -> parallel phi value ids
|
|
stacks [][]int // alloc_id -> reaching-definition stack
|
|
is_promotable []bool
|
|
phi_blocks []int
|
|
}
|
|
|
|
fn array2d_append(mut arr [][]int, idx int, val int) {
|
|
mut inner := arr[idx]
|
|
inner << val
|
|
arr[idx] = inner
|
|
}
|
|
|
|
// promote_memory_to_register promotes scalar allocas to SSA registers, inserting
|
|
// phi nodes at dominance frontiers and renaming load/store chains.
|
|
fn promote_memory_to_register(mut m ssa.Module, dom DomInfo, cfg &CfgData) {
|
|
n_values := m.values.len
|
|
n_blocks := m.blocks.len
|
|
|
|
mut ctx := Mem2RegCtx{
|
|
defs: [][]int{len: n_values}
|
|
uses: [][]int{len: n_values}
|
|
phi_placements: [][]int{len: n_blocks}
|
|
phi_vals: [][]int{len: n_blocks}
|
|
stacks: [][]int{len: n_values}
|
|
is_promotable: []bool{len: n_values}
|
|
phi_blocks: []int{}
|
|
}
|
|
|
|
mut df := [][]int{len: n_blocks}
|
|
mut df_blocks := []int{}
|
|
mut visited := []bool{len: n_blocks}
|
|
mut has_phi := []bool{len: n_blocks}
|
|
|
|
for fi in 0 .. m.funcs.len {
|
|
mut promotable := []int{}
|
|
func := m.funcs[fi]
|
|
for blk_id in func.blocks {
|
|
blk := m.blocks[blk_id]
|
|
for val_id in blk.instrs {
|
|
val := m.values[val_id]
|
|
if val.kind != .instruction {
|
|
continue
|
|
}
|
|
instr := m.instrs[val.index]
|
|
if instr.op == .alloca {
|
|
if is_promotable(m, val_id) {
|
|
promotable << val_id
|
|
ctx.is_promotable[val_id] = true
|
|
}
|
|
}
|
|
if instr.op == .store {
|
|
ptr := instr.operands[1]
|
|
if ptr < n_values && blk_id !in ctx.defs[ptr] {
|
|
array2d_append(mut ctx.defs, ptr, blk_id)
|
|
}
|
|
} else if instr.op == .load {
|
|
ptr := instr.operands[0]
|
|
if ptr < n_values && blk_id !in ctx.uses[ptr] {
|
|
array2d_append(mut ctx.uses, ptr, blk_id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
compute_dominance_frontier_flat(m, fi, &dom, cfg, mut df, mut df_blocks)
|
|
|
|
// Insert phis at iterated dominance frontiers of each promotable alloca.
|
|
for alloc_id in promotable {
|
|
mut worklist := ctx.defs[alloc_id].clone()
|
|
mut visited_list := []int{}
|
|
mut has_phi_list := []int{}
|
|
for worklist.len > 0 {
|
|
b := worklist.pop()
|
|
if b < 0 || b >= n_blocks {
|
|
continue
|
|
}
|
|
for d in df[b] {
|
|
if !has_phi[d] {
|
|
array2d_append(mut ctx.phi_placements, d, alloc_id)
|
|
if d !in ctx.phi_blocks {
|
|
ctx.phi_blocks << d
|
|
}
|
|
has_phi[d] = true
|
|
has_phi_list << d
|
|
if !visited[d] {
|
|
visited[d] = true
|
|
visited_list << d
|
|
worklist << d
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for d in visited_list {
|
|
visited[d] = false
|
|
}
|
|
for d in has_phi_list {
|
|
has_phi[d] = false
|
|
}
|
|
}
|
|
|
|
// Materialize the phi instructions (empty operands, filled during rename).
|
|
for blk_id in ctx.phi_blocks {
|
|
for alloc_id in ctx.phi_placements[blk_id] {
|
|
alloc_val := m.values[alloc_id]
|
|
alloc_typ := m.type_store.types[alloc_val.typ]
|
|
typ := alloc_typ.elem_type
|
|
phi_val := m.add_instr_front(.phi, blk_id, typ, [])
|
|
mut pv := m.values[phi_val]
|
|
pv.name = '${alloc_val.name}.phi_${blk_id}'
|
|
m.values[phi_val] = pv
|
|
array2d_append(mut ctx.phi_vals, blk_id, phi_val)
|
|
}
|
|
}
|
|
|
|
// Rename: walk the dominator tree, threading reaching definitions.
|
|
if func.blocks.len > 0 {
|
|
rename_blocks(mut m, func.blocks[0], mut ctx, dom, cfg)
|
|
}
|
|
|
|
// Reset per-function state.
|
|
for alloc_id in promotable {
|
|
ctx.defs[alloc_id] = []
|
|
ctx.uses[alloc_id] = []
|
|
ctx.stacks[alloc_id] = []
|
|
ctx.is_promotable[alloc_id] = false
|
|
}
|
|
for pb in ctx.phi_blocks {
|
|
ctx.phi_placements[pb] = []
|
|
ctx.phi_vals[pb] = []
|
|
}
|
|
ctx.phi_blocks = []
|
|
for d in df_blocks {
|
|
df[d] = []
|
|
}
|
|
df_blocks = []
|
|
}
|
|
}
|
|
|
|
// is_promotable returns true if every use of the alloca is a direct load/store,
|
|
// i.e. the pointer never escapes. Array- and pointer-backed slots stay in memory.
|
|
fn is_promotable(m &ssa.Module, alloc_id int) bool {
|
|
if alloc_id > 0 && alloc_id < m.values.len {
|
|
alloc_typ_id := m.values[alloc_id].typ
|
|
if alloc_typ_id > 0 && alloc_typ_id < m.type_store.types.len {
|
|
alloc_typ := m.type_store.types[alloc_typ_id]
|
|
if alloc_typ.kind == .ptr_t {
|
|
elem_typ_id := alloc_typ.elem_type
|
|
if elem_typ_id > 0 && elem_typ_id < m.type_store.types.len {
|
|
elem_typ := m.type_store.types[elem_typ_id]
|
|
// Keep pointer-, array- and aggregate-backed slots in memory.
|
|
// Promoting them to SSA values would require the backend to
|
|
// thread by-value aggregates through phi copies, which the v3
|
|
// arm64 lowering does not yet handle reliably.
|
|
if elem_typ.kind in [.ptr_t, .array_t, .struct_t] {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for u in m.values[alloc_id].uses {
|
|
if u >= m.values.len {
|
|
continue
|
|
}
|
|
user := m.values[u]
|
|
if user.kind != .instruction {
|
|
return false
|
|
}
|
|
instr := m.instrs[user.index]
|
|
match instr.op {
|
|
.load {
|
|
if instr.operands.len == 0 || instr.operands[0] != alloc_id {
|
|
return false
|
|
}
|
|
}
|
|
.store {
|
|
if instr.operands.len < 2 || instr.operands[1] != alloc_id {
|
|
return false
|
|
}
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
fn compute_dominance_frontier_flat(m &ssa.Module, func_idx int, dom &DomInfo, cfg &CfgData, mut df [][]int, mut df_blocks []int) {
|
|
func := m.funcs[func_idx]
|
|
for blk_id in func.blocks {
|
|
if blk_id < 0 || blk_id >= m.blocks.len {
|
|
continue
|
|
}
|
|
num_preds := cfg.preds[blk_id].len
|
|
if num_preds >= 2 {
|
|
for pi in 0 .. num_preds {
|
|
mut runner := cfg.preds[blk_id][pi]
|
|
idom := dom.idom[blk_id]
|
|
for runner != -1 && runner != idom {
|
|
if runner < 0 || runner >= m.blocks.len {
|
|
break
|
|
}
|
|
if blk_id !in df[runner] {
|
|
array2d_append(mut df, runner, blk_id)
|
|
if runner !in df_blocks {
|
|
df_blocks << runner
|
|
}
|
|
}
|
|
if runner == dom.idom[runner] {
|
|
break
|
|
}
|
|
runner = dom.idom[runner]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RenameFrame {
|
|
mut:
|
|
blk_id int
|
|
child_idx int
|
|
pushed_allocs []int
|
|
processed bool
|
|
}
|
|
|
|
fn rename_blocks(mut m ssa.Module, root_blk int, mut ctx Mem2RegCtx, dom DomInfo, cfg &CfgData) {
|
|
mut work := []RenameFrame{}
|
|
work << RenameFrame{
|
|
blk_id: root_blk
|
|
}
|
|
mut visited := []bool{len: m.blocks.len}
|
|
|
|
for work.len > 0 {
|
|
fi := work.len - 1
|
|
blk_id := work[fi].blk_id
|
|
|
|
if !work[fi].processed {
|
|
if blk_id >= 0 && blk_id < visited.len && visited[blk_id] {
|
|
work.pop()
|
|
continue
|
|
}
|
|
if blk_id >= 0 && blk_id < visited.len {
|
|
visited[blk_id] = true
|
|
}
|
|
mut frame2 := work[fi]
|
|
frame2.processed = true
|
|
work[fi] = frame2
|
|
|
|
// 1. Push phis defined in this block.
|
|
if blk_id < ctx.phi_placements.len && ctx.phi_placements[blk_id].len > 0 {
|
|
n_phi := ctx.phi_placements[blk_id].len
|
|
for pai in 0 .. n_phi {
|
|
alloc_id := ctx.phi_placements[blk_id][pai]
|
|
if pai < ctx.phi_vals[blk_id].len {
|
|
phi_val_id := ctx.phi_vals[blk_id][pai]
|
|
mut new_stack := ctx.stacks[alloc_id].clone()
|
|
new_stack << phi_val_id
|
|
ctx.stacks[alloc_id] = new_stack
|
|
mut wf := work[fi]
|
|
wf.pushed_allocs << alloc_id
|
|
work[fi] = wf
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Process instructions: rewrite loads, record store values, nop them.
|
|
mut instrs_to_nop := []int{}
|
|
blk2 := m.blocks[blk_id]
|
|
for val_id in blk2.instrs {
|
|
val := m.values[val_id]
|
|
if val.kind != .instruction {
|
|
continue
|
|
}
|
|
instr := m.instrs[val.index]
|
|
if instr.op == .store {
|
|
ptr := instr.operands[1]
|
|
if ptr < ctx.is_promotable.len && ctx.is_promotable[ptr] {
|
|
mut new_stack := ctx.stacks[ptr].clone()
|
|
new_stack << instr.operands[0]
|
|
ctx.stacks[ptr] = new_stack
|
|
mut wf := work[fi]
|
|
wf.pushed_allocs << ptr
|
|
work[fi] = wf
|
|
instrs_to_nop << val_id
|
|
}
|
|
} else if instr.op == .load {
|
|
ptr := instr.operands[0]
|
|
if ptr < ctx.is_promotable.len && ctx.is_promotable[ptr] {
|
|
stack := ctx.stacks[ptr]
|
|
mut repl := 0
|
|
if stack.len > 0 {
|
|
repl = stack.last()
|
|
} else {
|
|
repl = m.get_or_add_const(val.typ, 'undef')
|
|
}
|
|
m.replace_uses(val_id, repl)
|
|
instrs_to_nop << val_id
|
|
}
|
|
} else if instr.op == .alloca {
|
|
if val_id < ctx.is_promotable.len && ctx.is_promotable[val_id] {
|
|
instrs_to_nop << val_id
|
|
}
|
|
}
|
|
}
|
|
for vid in instrs_to_nop {
|
|
vid_val := m.values[vid]
|
|
mut nop_instr := m.instrs[vid_val.index]
|
|
nop_instr.op = .bitcast
|
|
nop_instr.operands = []
|
|
m.instrs[vid_val.index] = nop_instr
|
|
}
|
|
|
|
// 3. Fill phi operands in CFG successors (raw block id as predecessor).
|
|
for succ_id in cfg.succs[blk_id] {
|
|
if succ_id < ctx.phi_placements.len && ctx.phi_placements[succ_id].len > 0 {
|
|
n_succ_phi := ctx.phi_placements[succ_id].len
|
|
for spai in 0 .. n_succ_phi {
|
|
alloc_id := ctx.phi_placements[succ_id][spai]
|
|
if spai < ctx.phi_vals[succ_id].len {
|
|
vid := ctx.phi_vals[succ_id][spai]
|
|
phi_v := m.values[vid]
|
|
phi_val := if ctx.stacks[alloc_id].len > 0 {
|
|
ctx.stacks[alloc_id].last()
|
|
} else {
|
|
alloc_v := m.values[alloc_id]
|
|
alloc_typ := m.type_store.types[alloc_v.typ]
|
|
m.get_or_add_const(alloc_typ.elem_type, 'undef')
|
|
}
|
|
m.append_phi_operands(phi_v.index, phi_val, blk_id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Recurse into dominator-tree children.
|
|
n_children := dom.dom_tree[blk_id].len
|
|
child_idx := work[fi].child_idx
|
|
if child_idx < n_children {
|
|
child := dom.dom_tree[blk_id][child_idx]
|
|
mut frame := work[fi]
|
|
frame.child_idx++
|
|
work[fi] = frame
|
|
work << RenameFrame{
|
|
blk_id: child
|
|
}
|
|
} else {
|
|
// 5. Pop reaching definitions pushed in this block.
|
|
pushed := work[fi].pushed_allocs.clone()
|
|
work.pop()
|
|
for i := pushed.len - 1; i >= 0; i-- {
|
|
mut s := ctx.stacks[pushed[i]].clone()
|
|
s.pop()
|
|
ctx.stacks[pushed[i]] = s
|
|
}
|
|
}
|
|
}
|
|
}
|