Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 35 additions & 7 deletions src/schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// at each use.
// The result is only kept if it is smaller and symbolically equivalent.

const { OP, numOp, pushOp, pushCost, opsSize } = require('./script')
const { OP, numOp, pushOp, pushCost, opSize, opsSize } = require('./script')
const { Interner, run, equivalent } = require('./symbolic')
const { search } = require('./superopt')
const { peephole } = require('./peephole')
Expand Down Expand Up @@ -80,10 +80,15 @@ class Machine {
for (const x of S) this.cnt.set(x, (this.cnt.get(x) || 0) + 1)
this.out = []
this.maxLen = S.length
this.bytes = 0
this.budget = Infinity
}

clone () {
return new Machine(this.I, this.S.slice(), this.live.clone())
const m = new Machine(this.I, this.S.slice(), this.live.clone())
m.bytes = this.bytes
m.budget = this.budget
return m
}

cheap (id) { return this.I.isConst(id) && pushCost(this.I.constBuf(id)) <= 2 }
Expand All @@ -94,7 +99,14 @@ class Machine {
if (this.S.length > this.maxLen) this.maxLen = this.S.length
}
removeAt (q) { const id = this.S.splice(q, 1)[0]; this.cnt.set(id, this.count(id) - 1); return id }
emit (ops) { for (const o of ops) this.out.push(o) }
// A schedule is only used when it is smaller than the fragment it replaces.
// Giving up once it is far past that keeps deep stacks (a constant ROLL
// index in the tens of thousands) from turning finish() quadratic.
emit (ops) {
for (const o of ops) { this.out.push(o); this.bytes += opSize(o) }
if (this.bytes > this.budget) throw new Error('scheduler: over budget')
}

excess (id) { return this.count(id) > this.live.need(id) }

stage (order) {
Expand Down Expand Up @@ -250,11 +262,14 @@ class Machine {

const tailS = S.slice(prefix)
const tailF = F.slice(prefix)
const small = tailS.length <= 6 && tailF.length <= 7
const generic = this.clone()
generic.out = []
// A short tail is searched for below, which may beat the generic arrangement.
if (small) generic.budget = Infinity
generic.arrange(F, prefix)
let ops = generic.out
if (tailS.length <= 6 && tailF.length <= 7) {
if (small) {
const sym = new Map()
const symOf = id => { if (!sym.has(id)) sym.set(id, sym.size); return sym.get(id) }
const start = tailS.map(symOf)
Expand Down Expand Up @@ -447,7 +462,14 @@ function beamSchedule (I, S0, live, apps, needed, F, opts) {
for (const m of beam) {
const plain = new Machine(I, m.S.slice(), m.live.clone())
plain.out = m.ops()
plain.finish(F, opts)
plain.bytes = m.bytes
plain.budget = budgetFor(opts)
try {
plain.finish(F, opts)
} catch (e) {
if (opts.debug && !/over budget/.test(e.message)) throw e
continue
}
if (!best || opsSize(plain.out) < opsSize(best)) best = plain.out
}
return best
Expand Down Expand Up @@ -483,8 +505,9 @@ function rescheduleFragment (ops, g, ga, opts = {}) {
for (const list of appLists.slice()) appLists.push(list.filter(a => !isAlt(a)))
}
let best = null
const scheduleOpts = Object.assign({}, opts, { budget: 2 * opsSize(ops) + 16 })
for (const apps of appLists) {
const r = scheduleApps(I, st.D, apps, F, opts)
const r = scheduleApps(I, st.D, apps, F, scheduleOpts)
if (r && (!best || opsSize(r) < opsSize(best))) best = r
}
if (!best) return null
Expand Down Expand Up @@ -516,6 +539,7 @@ function scheduleApps (I, D, apps, F, opts) {
let maxLen = 0
for (const hoist of opts.hoistVariants || HOIST_VARIANTS) {
const mach = new Machine(I, S0.slice(), live.clone())
mach.budget = budgetFor(opts)
mach.hoist = hoist
try {
mach.cleanup()
Expand All @@ -524,7 +548,7 @@ function scheduleApps (I, D, apps, F, opts) {
} catch (e) {
// An inconsistency here means a missed case in the scheduler, never a
// wrong script: the fragment is simply left as it was.
if (opts.debug) throw e
if (opts.debug && !/over budget/.test(e.message)) throw e
continue
}
if (!best || opsSize(mach.out) < opsSize(best)) best = mach.out
Expand All @@ -546,6 +570,10 @@ function scheduleApps (I, D, apps, F, opts) {
return best
}

// Bytes a schedule may emit before it is abandoned: peephole runs afterwards,
// so allow room above the size it has to beat.
const budgetFor = opts => opts.budget ?? Infinity

// Scheduling is greedy, so it runs once per hoisting policy and keeps the smallest.
const HOIST_VARIANTS = [null, { minUses: 3, minDepth: 17 }, { minUses: 6, minDepth: 17 }, { minUses: 10, minDepth: 17 }]

Expand Down
9 changes: 9 additions & 0 deletions test/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ test('CLI optimizes, explains and profiles', () => {
assert.strictEqual(OP.OP_DROP, 0x75)
})

// A constant ROLL index in the tens of thousands makes the fragment need that
// many inputs. Arranging them one by one was quadratic (75 s here); a schedule
// is abandoned once it is far larger than the fragment it would replace.
test('gives up on schedules far larger than the fragment, even on very deep stacks', { timeout: 40000 }, () => {
const r = opt('OP_DUP OP_DROP e35901 OP_ROLL OP_SWAP OP_DROP', { differential: 0 })
assert.ok(r.report.verification.symbolic.ok)
assert.ok(r.script.length <= 8)
})

test('schedules through the alt stack without keeping spare copies', () => {
const r = opt('OP_TOALTSTACK OP_2 OP_PICK OP_2 OP_PICK OP_MUL OP_FROMALTSTACK OP_3 OP_ROLL OP_DROP OP_ADD OP_NIP OP_NIP')
assert.strictEqual(r.script.length, 5)
Expand Down
Loading