reframe liveness_analysis as eliminate_dead_code
Seems nicer to keep encapsulated in a program->program pass so nothing upstream of it has to think about liveness. I will be circling back around to profiling the cost of these tempoaries, copies, etc. I just want to start writing them as if cost were no object first. Change-Id: I1d1187b521fbe963e720e0d8de90316a549f7797 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/281182 Reviewed-by: Herb Derby <herb@google.com> Commit-Queue: Mike Klein <mtklein@google.com>
This commit is contained in:
parent
b5d39b54a3
commit
7542ab5e5b
@ -424,7 +424,7 @@ namespace skvm {
|
||||
}
|
||||
|
||||
void specialize_for_jit(std::vector<Instruction>* program) {
|
||||
Builder specialized;
|
||||
Builder b;
|
||||
for (Val i = 0; i < (Val)program->size(); i++) {
|
||||
Instruction inst = (*program)[i];
|
||||
|
||||
@ -472,13 +472,46 @@ namespace skvm {
|
||||
} break;
|
||||
}
|
||||
#endif
|
||||
SkDEBUGCODE(Val id =) specialized.push(inst);
|
||||
SkDEBUGCODE(Val id =) b.push(inst);
|
||||
// If we replace single instructions with multiple, this will start breaking,
|
||||
// and we'll need a table to remap them like we have in optimize().
|
||||
SkASSERT(id == i);
|
||||
}
|
||||
|
||||
*program = specialized.program();
|
||||
*program = b.program();
|
||||
}
|
||||
|
||||
void eliminate_dead_code(std::vector<Instruction>* program) {
|
||||
// Determine which Instructions are live by working back from side effects.
|
||||
std::vector<bool> live(program->size(), false);
|
||||
auto mark_live = [&](Val id, auto& recurse) -> void {
|
||||
if (live[id] == false) {
|
||||
live[id] = true;
|
||||
Instruction inst = (*program)[id];
|
||||
if (inst.x != NA) { recurse(inst.x, recurse); }
|
||||
if (inst.y != NA) { recurse(inst.y, recurse); }
|
||||
if (inst.z != NA) { recurse(inst.z, recurse); }
|
||||
}
|
||||
};
|
||||
for (Val id = 0; id < (Val)program->size(); id++) {
|
||||
if (has_side_effect((*program)[id].op)) {
|
||||
mark_live(id, mark_live);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a new program with only live Instructions.
|
||||
Builder b;
|
||||
std::vector<Val> new_id(program->size(), NA);
|
||||
for (Val id = 0; id < (Val)program->size(); id++) {
|
||||
if (live[id]) {
|
||||
Instruction inst = (*program)[id];
|
||||
if (inst.x != NA) { inst.x = new_id[inst.x]; SkASSERT(inst.x != NA); }
|
||||
if (inst.y != NA) { inst.y = new_id[inst.y]; SkASSERT(inst.y != NA); }
|
||||
if (inst.z != NA) { inst.z = new_id[inst.z]; SkASSERT(inst.z != NA); }
|
||||
new_id[id] = b.push(inst);
|
||||
}
|
||||
}
|
||||
*program = b.program();
|
||||
}
|
||||
|
||||
std::vector<OptimizedInstruction> Builder::optimize(bool for_jit) const {
|
||||
@ -486,17 +519,22 @@ namespace skvm {
|
||||
if (for_jit) {
|
||||
specialize_for_jit(&program);
|
||||
}
|
||||
eliminate_dead_code(&program);
|
||||
|
||||
std::vector<bool> live_instructions;
|
||||
std::vector<Val> frontier;
|
||||
int liveInstructionCount = liveness_analysis(program, &live_instructions, &frontier);
|
||||
skvm::Usage usage{program, live_instructions};
|
||||
skvm::Usage usage{program};
|
||||
|
||||
std::vector<int> remaining_uses;
|
||||
for (Val id = 0; id < (Val)program.size(); id++) {
|
||||
remaining_uses.push_back((int)usage.users(id).size());
|
||||
}
|
||||
|
||||
std::vector<Val> frontier;
|
||||
for (Val id = 0; id < (Val)program.size(); id++) {
|
||||
if (has_side_effect(program[id].op)) {
|
||||
frontier.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Map old Val index to rewritten index in optimized.
|
||||
std::vector<Val> new_index(program.size(), NA);
|
||||
|
||||
@ -505,7 +543,7 @@ namespace skvm {
|
||||
Instruction inst = program[id];
|
||||
|
||||
// If this is not a sink, then it takes up a register
|
||||
if (inst.op > Op::store32) { pressure += 1; }
|
||||
if (!has_side_effect(inst.op)) { pressure += 1; }
|
||||
|
||||
// If this is the last use of the value, then that register will be free.
|
||||
if (inst.x != NA && remaining_uses[inst.x] == 1) { pressure -= 1; }
|
||||
@ -548,9 +586,8 @@ namespace skvm {
|
||||
// Schedule the instructions last to first from the DAG. Produce a schedule that executes
|
||||
// instructions that reduce register pressure before ones that increase register
|
||||
// pressure.
|
||||
std::vector<OptimizedInstruction> optimized;
|
||||
optimized.resize(liveInstructionCount);
|
||||
for (int i = liveInstructionCount; i-- > 0;) {
|
||||
std::vector<OptimizedInstruction> optimized(program.size());
|
||||
for (int i = (int)program.size(); i-- > 0;) {
|
||||
SkASSERT(!frontier.empty());
|
||||
std::pop_heap(frontier.begin(), frontier.end(), compare);
|
||||
Val id = frontier.back();
|
||||
@ -596,7 +633,7 @@ namespace skvm {
|
||||
for (Val id = 0; id < (Val)optimized.size(); id++) {
|
||||
OptimizedInstruction& inst = optimized[id];
|
||||
// Stores don't really produce values. Just mark them as dying on issue.
|
||||
if (inst.op <= Op::store32) {
|
||||
if (has_side_effect(inst.op)) {
|
||||
inst.death = id;
|
||||
}
|
||||
// Extend the lifetime of this instruction's inputs to live until it issues.
|
||||
@ -1426,37 +1463,6 @@ namespace skvm {
|
||||
}
|
||||
}
|
||||
|
||||
// Fill live and sinks each if non-null:
|
||||
// - (*live)[id]: notes whether each input instruction is live
|
||||
// - *sinks: an unsorted set of live instructions with side effects (stores, assert_true)
|
||||
// Returns the number of live instructions.
|
||||
int liveness_analysis(const std::vector<Instruction>& instructions,
|
||||
std::vector<bool>* live,
|
||||
std::vector<Val>* sinks) {
|
||||
int instruction_count = instructions.size();
|
||||
live->resize(instruction_count, false);
|
||||
int liveInstructionCount = 0;
|
||||
auto trace = [&](Val id, auto& recurse) -> void {
|
||||
if (!(*live)[id]) {
|
||||
(*live)[id] = true;
|
||||
liveInstructionCount++;
|
||||
Instruction inst = instructions[id];
|
||||
if (inst.x != NA) { recurse(inst.x, recurse); }
|
||||
if (inst.y != NA) { recurse(inst.y, recurse); }
|
||||
if (inst.z != NA) { recurse(inst.z, recurse); }
|
||||
}
|
||||
};
|
||||
|
||||
// For all the sink instructions.
|
||||
for (Val id = 0; id < instruction_count; id++) {
|
||||
if (instructions[id].op <= skvm::Op::store32) {
|
||||
sinks->push_back(id);
|
||||
trace(id, trace);
|
||||
}
|
||||
}
|
||||
return liveInstructionCount;
|
||||
}
|
||||
|
||||
// For a given program we'll store each Instruction's users contiguously in a table,
|
||||
// and track where each Instruction's span of users starts and ends in another index.
|
||||
// Here's a simple program that loads x and stores kx+k:
|
||||
@ -1487,16 +1493,14 @@ namespace skvm {
|
||||
return SkMakeSpan(fTable.data() + begin, end - begin);
|
||||
}
|
||||
|
||||
Usage::Usage(const std::vector<Instruction>& program, const std::vector<bool>& live) {
|
||||
Usage::Usage(const std::vector<Instruction>& program) {
|
||||
// uses[id] counts the number of times each Instruction is used.
|
||||
std::vector<int> uses(program.size(), 0);
|
||||
for (Val id = 0; id < (Val)program.size(); id++) {
|
||||
if (live[id]) {
|
||||
Instruction inst = program[id];
|
||||
if (inst.x != NA) { ++uses[inst.x]; }
|
||||
if (inst.y != NA) { ++uses[inst.y]; }
|
||||
if (inst.z != NA) { ++uses[inst.z]; }
|
||||
}
|
||||
Instruction inst = program[id];
|
||||
if (inst.x != NA) { ++uses[inst.x]; }
|
||||
if (inst.y != NA) { ++uses[inst.y]; }
|
||||
if (inst.z != NA) { ++uses[inst.z]; }
|
||||
}
|
||||
|
||||
// Build our index into fTable, with an extra entry marking the final Instruction's end.
|
||||
@ -1511,12 +1515,10 @@ namespace skvm {
|
||||
// Tick down each Instruction's uses to fill in fTable.
|
||||
fTable.resize(total_uses, NA);
|
||||
for (Val id = (Val)program.size(); id --> 0; ) {
|
||||
if (live[id]) {
|
||||
Instruction inst = program[id];
|
||||
if (inst.x != NA) { fTable[fIndex[inst.x] + --uses[inst.x]] = id; }
|
||||
if (inst.y != NA) { fTable[fIndex[inst.y] + --uses[inst.y]] = id; }
|
||||
if (inst.z != NA) { fTable[fIndex[inst.z] + --uses[inst.z]] = id; }
|
||||
}
|
||||
Instruction inst = program[id];
|
||||
if (inst.x != NA) { fTable[fIndex[inst.x] + --uses[inst.x]] = id; }
|
||||
if (inst.y != NA) { fTable[fIndex[inst.y] + --uses[inst.y]] = id; }
|
||||
if (inst.z != NA) { fTable[fIndex[inst.z] + --uses[inst.z]] = id; }
|
||||
}
|
||||
for (int n : uses ) { (void)n; SkASSERT(n == 0 ); }
|
||||
for (Val id : fTable) { (void)id; SkASSERT(id != NA); }
|
||||
|
@ -339,6 +339,10 @@ namespace skvm {
|
||||
#undef M
|
||||
};
|
||||
|
||||
static inline bool has_side_effect(Op op) {
|
||||
return op <= Op::store32;
|
||||
}
|
||||
|
||||
using Val = int;
|
||||
// We reserve an impossibe Val ID as a sentinel
|
||||
// NA meaning none, n/a, null, nil, etc.
|
||||
@ -730,20 +734,12 @@ namespace skvm {
|
||||
|
||||
// Optimization passes and data structures normally used by Builder::optimize(),
|
||||
// extracted here so they can be unit tested.
|
||||
|
||||
void specialize_for_jit(std::vector<Instruction>* program);
|
||||
|
||||
// Fill live and sinks each if non-null:
|
||||
// - (*live)[id]: notes whether each input instruction is live
|
||||
// - *sinks: an unsorted set of live instructions with side effects (stores, assert_true)
|
||||
// Returns the number of live instructions.
|
||||
int liveness_analysis(const std::vector<Instruction>&,
|
||||
std::vector<bool>* live,
|
||||
std::vector<Val>* sinks);
|
||||
void specialize_for_jit (std::vector<Instruction>*);
|
||||
void eliminate_dead_code(std::vector<Instruction>*);
|
||||
|
||||
class Usage {
|
||||
public:
|
||||
Usage(const std::vector<Instruction>&, const std::vector<bool>&);
|
||||
Usage(const std::vector<Instruction>&);
|
||||
|
||||
// Return a sorted span of Vals which use result of Instruction id.
|
||||
SkSpan<const Val> users(Val id) const;
|
||||
|
@ -272,52 +272,42 @@ DEF_TEST(SkVM, r) {
|
||||
});
|
||||
}
|
||||
|
||||
DEF_TEST(SkVM_UsageAndLiveness, r) {
|
||||
DEF_TEST(SkVM_eliminate_dead_code, r) {
|
||||
skvm::Builder b;
|
||||
{
|
||||
skvm::Builder b;
|
||||
{
|
||||
skvm::Arg arg = b.varying<int>(),
|
||||
buf = b.varying<int>();
|
||||
skvm::I32 l = b.load32(arg);
|
||||
skvm::I32 a = b.add(l, l);
|
||||
skvm::I32 s = b.add(a, b.splat(7));
|
||||
b.store32(buf, s);
|
||||
}
|
||||
skvm::Arg arg = b.varying<int>();
|
||||
skvm::I32 l = b.load32(arg);
|
||||
skvm::I32 a = b.add(l, l);
|
||||
b.add(a, b.splat(7));
|
||||
}
|
||||
|
||||
std::vector<bool> live;
|
||||
std::vector<skvm::Val> sinks;
|
||||
skvm::liveness_analysis(b.program(), &live, &sinks);
|
||||
skvm::Usage u{b.program(), live};
|
||||
REPORTER_ASSERT(r, b.program()[0].op == skvm::Op::load32);
|
||||
REPORTER_ASSERT(r, u.users(0).size() == 2);
|
||||
REPORTER_ASSERT(r, b.program()[1].op == skvm::Op::add_i32);
|
||||
REPORTER_ASSERT(r, u.users(1).size() == 1);
|
||||
REPORTER_ASSERT(r, b.program()[2].op == skvm::Op::splat);
|
||||
REPORTER_ASSERT(r, u.users(2).size() == 1);
|
||||
REPORTER_ASSERT(r, b.program()[3].op == skvm::Op::add_i32);
|
||||
REPORTER_ASSERT(r, u.users(3).size() == 1);
|
||||
}
|
||||
std::vector<skvm::Instruction> program = b.program();
|
||||
REPORTER_ASSERT(r, program.size() == 4);
|
||||
|
||||
skvm::eliminate_dead_code(&program);
|
||||
REPORTER_ASSERT(r, program.size() == 0);
|
||||
}
|
||||
|
||||
DEF_TEST(SkVM_Usage, r) {
|
||||
skvm::Builder b;
|
||||
{
|
||||
skvm::Builder b;
|
||||
{
|
||||
skvm::Arg arg = b.varying<int>();
|
||||
skvm::I32 l = b.load32(arg);
|
||||
skvm::I32 a = b.add(l, l);
|
||||
b.add(a, b.splat(7));
|
||||
}
|
||||
std::vector<bool> live;
|
||||
std::vector<skvm::Val> sinks;
|
||||
skvm::liveness_analysis(b.program(), &live, &sinks);
|
||||
skvm::Usage u{b.program(), live};
|
||||
REPORTER_ASSERT(r, b.program()[0].op == skvm::Op::load32);
|
||||
REPORTER_ASSERT(r, u.users(0).size() == 0);
|
||||
REPORTER_ASSERT(r, b.program()[1].op == skvm::Op::add_i32);
|
||||
REPORTER_ASSERT(r, u.users(1).size() == 0);
|
||||
REPORTER_ASSERT(r, b.program()[2].op == skvm::Op::splat);
|
||||
REPORTER_ASSERT(r, u.users(2).size() == 0);
|
||||
REPORTER_ASSERT(r, b.program()[3].op == skvm::Op::add_i32);
|
||||
REPORTER_ASSERT(r, u.users(3).size() == 0);
|
||||
skvm::Arg arg = b.varying<int>(),
|
||||
buf = b.varying<int>();
|
||||
skvm::I32 l = b.load32(arg);
|
||||
skvm::I32 a = b.add(l, l);
|
||||
skvm::I32 s = b.add(a, b.splat(7));
|
||||
b.store32(buf, s);
|
||||
}
|
||||
|
||||
skvm::Usage u{b.program()};
|
||||
REPORTER_ASSERT(r, b.program()[0].op == skvm::Op::load32);
|
||||
REPORTER_ASSERT(r, u.users(0).size() == 2);
|
||||
REPORTER_ASSERT(r, b.program()[1].op == skvm::Op::add_i32);
|
||||
REPORTER_ASSERT(r, u.users(1).size() == 1);
|
||||
REPORTER_ASSERT(r, b.program()[2].op == skvm::Op::splat);
|
||||
REPORTER_ASSERT(r, u.users(2).size() == 1);
|
||||
REPORTER_ASSERT(r, b.program()[3].op == skvm::Op::add_i32);
|
||||
REPORTER_ASSERT(r, u.users(3).size() == 1);
|
||||
}
|
||||
|
||||
DEF_TEST(SkVM_Pointless, r) {
|
||||
|
Loading…
Reference in New Issue
Block a user