aboutsummaryrefslogtreecommitdiff
path: root/src/regalloc.c
diff options
context:
space:
mode:
authorKimplul <kimi.h.kuparinen@gmail.com>2024-04-03 23:32:57 +0300
committerKimplul <kimi.h.kuparinen@gmail.com>2024-04-03 23:32:57 +0300
commit68059b52c389143e766c5499c76c6c90c6b82846 (patch)
tree6e10707dbc417946dee5c4dbb333a4952914a9e8 /src/regalloc.c
parentf4be2429f38faba0c54862055f808cb242a7eb49 (diff)
downloadqbt-68059b52c389143e766c5499c76c6c90c6b82846.tar.gz
qbt-68059b52c389143e766c5499c76c6c90c6b82846.zip
add callsite register saving
Diffstat (limited to 'src/regalloc.c')
-rw-r--r--src/regalloc.c233
1 files changed, 215 insertions, 18 deletions
diff --git a/src/regalloc.c b/src/regalloc.c
index 68e68a2..ffefeb5 100644
--- a/src/regalloc.c
+++ b/src/regalloc.c
@@ -9,7 +9,11 @@
* Idea is that callee-save cost is paid once at the start of the function call,
* after that they're free. Argument registers are generally important to keep
* free to make sure function calls in loops etc. don't have to shuffle
- * registers around as much */
+ * registers around as much. The preferred order can probably be bikeshedded to death,
+ * eventually would be kind of cool to add some heuristic for which category
+ * might be best suited for a specific location.
+ * One possible one would be that callee-save should be preferred if the
+ * lifetime overlaps a function call? */
static const int64_t tr_map[] = {
RT0, RT1, RT2, RT3, RT4, RT5, RT6, RT7, RT8, RT9,
RT10, RT11, RT12, RT13, RT14, RT15, RT16, RT17, RT18, RT19,
@@ -69,7 +73,7 @@ static struct val get_hint(struct vec *hints, struct val from)
if ((int64_t)vec_len(hints) <= from.r)
return noclass();
- return rewrite_tmp(hints, from);
+ return reg_at(*hints, from.r);
}
struct lifetime {
@@ -94,7 +98,7 @@ static void add_def(struct vec *lifetimes, struct val v, size_t i)
/* I guess a def isn't techincally a 'use' but good enough, we assume
* that unused variables have already been removed by some earlier stage
* (not currently true but that's the intention) */
- lifetime_at(*lifetimes, v.r) = (struct lifetime){v, i, 0, 1};
+ lifetime_at(*lifetimes, v.r) = (struct lifetime){v, i, i, 1};
}
static void add_use(struct vec *lifetimes, struct val v, size_t i)
@@ -108,7 +112,7 @@ static void add_use(struct vec *lifetimes, struct val v, size_t i)
}
static void collect_lifetimes(struct blk *b, struct vec *hints,
- struct vec *lifetimes)
+ struct vec *lifetimes, struct vec *rmap, struct vec *calls)
{
foreach_blk_param(pi, b->params) {
struct val v = blk_param_at(b->params, pi);
@@ -130,13 +134,21 @@ static void collect_lifetimes(struct blk *b, struct vec *hints,
/* collect some early hints */
if (i.type == MOVE) {
/* input arguments, retvals */
- if (i.out.class == TMP && i.in[0].class == REG)
+ if (i.out.class == TMP && i.in[0].class == REG) {
add_hint(hints, i.out, i.in[0]);
+ /* retvals must be treated as reserving that
+ * virtual register */
+ add_rewrite_rule(rmap, i.out, i.in[0]);
+ }
if (i.out.class == REG && i.in[0].class == TMP)
add_hint(hints, i.in[0], i.out);
}
+ /* collect calls, to be used later */
+ if (i.type == CALL)
+ vec_append(calls, &pos);
+
pos++;
}
@@ -157,24 +169,29 @@ static void collect_lifetimes(struct blk *b, struct vec *hints,
}
}
-static void build_active(struct vec *active, struct vec *lifetimes, size_t i)
+static void build_active_between(struct vec *active, struct vec *lifetimes, size_t start, size_t end)
{
- struct lifetime ref = lifetime_at(*lifetimes, i);
foreach_lifetime(li, *lifetimes) {
struct lifetime l = lifetime_at(*lifetimes, li);
if (l.used == 0)
continue;
- if (l.end < ref.start)
+ if (l.end < start)
continue;
- if (l.start > ref.end)
+ if (l.start > end)
continue;
vec_append(active, &l);
}
}
+static void build_active(struct vec *active, struct vec *lifetimes, size_t i)
+{
+ struct lifetime ref = lifetime_at(*lifetimes, i);
+ build_active_between(active, lifetimes, ref.start, ref.end);
+}
+
static void build_reserved(struct vec *reserved, struct vec *active,
struct vec *rmap)
{
@@ -212,14 +229,51 @@ static struct val find_free_reg(struct vec *reserved)
}
/* handle spill case later */
+ /* right now I'm thinking that spills should behave like SAVE/RESTORE,
+ * i.e. they get an index and the spilled number of registers is counted
+ * somewhere and added to the frame size like call registers. */
assert(0 &&
"ran out of registers, time to implement proper spill handling");
abort();
}
-static void build_rmap(struct vec *hints, struct vec *lifetimes,
+static size_t highest_sreg(struct val f)
+{
+ assert(f.class == REG);
+ switch (f.r) {
+ case RS0: return 1;
+ case RS1: return 2;
+ case RS2: return 3;
+ case RS3: return 4;
+ case RS4: return 5;
+ case RS5: return 6;
+ case RS6: return 7;
+ case RS7: return 8;
+ case RS8: return 9;
+ case RS9: return 10;
+ case RS10: return 11;
+ case RS11: return 12;
+ case RS12: return 13;
+ case RS13: return 14;
+ case RS14: return 15;
+ case RS15: return 16;
+ case RS16: return 17;
+ case RS17: return 18;
+ case RS18: return 19;
+ case RS19: return 20;
+ case RS20: return 21;
+ case RS21: return 22;
+ case RS22: return 23;
+ case RS23: return 24;
+ }
+
+ return 0;
+}
+
+static size_t build_rmap(struct vec *hints, struct vec *lifetimes,
struct vec *rmap)
{
+ size_t max_callee_save = 0;
struct vec active = vec_create(sizeof(struct lifetime));
struct vec reserved = vec_create(sizeof(struct val));
@@ -228,7 +282,11 @@ static void build_rmap(struct vec *hints, struct vec *lifetimes,
if (l.used == 0)
continue;
+ if (has_rewrite_rule(rmap, l.v))
+ continue;
+
vec_reset(&active);
+ vec_reset(&reserved);
build_active(&active, lifetimes, li);
build_reserved(&reserved, &active, rmap);
@@ -244,12 +302,20 @@ static void build_rmap(struct vec *hints, struct vec *lifetimes,
* least used register, but that's a bit more complicated than
* just this linear scan */
struct val f = find_free_reg(&reserved);
+ if (highest_sreg(f) > max_callee_save)
+ max_callee_save = highest_sreg(f);
+
add_rewrite_rule(rmap, l.v, f);
}
+
+ vec_destroy(&active);
+ vec_destroy(&reserved);
+
+ return max_callee_save;
}
/* has_calls is kind of in an iffy place but I guess it's fine */
-static void do_rewrites(struct blk *b, struct vec *rmap, struct fn *f)
+static void do_rewrites(struct blk *b, struct vec *rmap)
{
foreach_blk_param(pi, b->params) {
struct val p = blk_param_at(b->params, pi);
@@ -267,10 +333,6 @@ static void do_rewrites(struct blk *b, struct vec *rmap, struct fn *f)
if (i.out.class == TMP)
i.out = rewrite_tmp(rmap, i.out);
- /** @todo save caller-save at callsites here */
- if (i.type == CALL)
- f->has_calls = true;
-
/* write back changes, christ I forget this a lot */
insn_at(b->insns, ii) = i;
}
@@ -292,25 +354,160 @@ static void do_rewrites(struct blk *b, struct vec *rmap, struct fn *f)
}
}
+static bool callee_save(struct val r)
+{
+ assert(r.class == REG);
+ switch (r.r) {
+ case RS0: return true;
+ case RS1: return true;
+ case RS2: return true;
+ case RS3: return true;
+ case RS4: return true;
+ case RS5: return true;
+ case RS6: return true;
+ case RS7: return true;
+ case RS8: return true;
+ case RS9: return true;
+ case RS10: return true;
+ case RS11: return true;
+ case RS12: return true;
+ case RS13: return true;
+ case RS14: return true;
+ case RS15: return true;
+ case RS16: return true;
+ case RS17: return true;
+ case RS18: return true;
+ case RS19: return true;
+ case RS20: return true;
+ case RS21: return true;
+ case RS22: return true;
+ case RS23: return true;
+ }
+
+ return false;
+}
+
+static void insn_insert_before_call(struct blk *b, struct insn save, size_t pos)
+{
+ assert((insn_at(b->insns, pos)).type == CALL);
+ struct insn setup;
+ do {
+ /* the first instruction in a block can be part of a call setup */
+ if (pos == 0)
+ break;
+
+ pos--;
+ setup = insn_at(b->insns, pos);
+ } while (has_insn_flag(setup, CALL_SETUP));
+
+ /* the do-while loop technically went one too far, fix */
+ insn_insert(b, save, pos == 0 ? 0 : pos + 1);
+}
+
+static void insn_insert_after_call(struct blk *b, struct insn restore, size_t pos)
+{
+ assert((insn_at(b->insns, pos)).type == CALL);
+ size_t max = vec_len(&b->insns) - 1;
+
+ struct insn setup;
+ do {
+ if (pos == max)
+ break;
+
+ pos++;
+ setup = insn_at(b->insns, pos);
+ } while (has_insn_flag(setup, CALL_TEARDOWN));
+
+ insn_insert(b, restore, pos);
+}
+
+static size_t do_call_saves(struct blk *b, struct vec *lifetimes, struct vec *rmap, struct vec *calls)
+{
+ size_t offset = 0;
+ size_t max_counter = 0;
+ struct vec active = vec_create(sizeof(struct lifetime));
+ struct vec reserved = vec_create(sizeof(struct val));
+
+ foreach_vec(ci, *calls) {
+ size_t call_pos = vect_at(size_t, *calls, ci);
+
+ vec_reset(&active);
+ vec_reset(&reserved);
+ build_active_between(&active, lifetimes, call_pos, call_pos);
+ build_reserved(&reserved, &active, rmap);
+
+ /* what follows is a slight bit of index counting, a bit
+ * difficult to follow but not too bad */
+
+ /* add a save right before call, offset is how many
+ * save/restores we've already added, very important */
+ size_t counter = 0;
+ foreach_val(ri, reserved) {
+ struct val r = val_at(reserved, ri);
+ assert(r.class == REG);
+ if (callee_save(r))
+ continue;
+
+ struct insn save = insn_create(SAVE, NOTYPE,
+ noclass(), r, imm_val(counter, I27));
+ insn_insert_before_call(b, save, call_pos + offset - 1);
+ offset++;
+ counter++;
+ }
+
+ call_pos += offset;
+
+ counter = 0;
+ foreach_val(ri, reserved) {
+ struct val r = val_at(reserved, ri);
+ if (callee_save(r))
+ continue;
+
+ /* add a restore right after call ~area~ */
+ /* hmm, restore should maybe put r as its output to be
+ * more consistent... */
+ struct insn restore = insn_create(RESTORE, NOTYPE,
+ noclass(), r, imm_val(counter, I27));
+ insn_insert_after_call(b, restore, call_pos - 1);
+ counter++;
+ offset++;
+ }
+
+ if (counter > max_counter)
+ max_counter = counter;
+ }
+
+ vec_destroy(&active);
+ vec_destroy(&reserved);
+
+ return max_counter;
+}
+
/* assumes ssa form */
void regalloc(struct fn *f)
{
struct vec hints = vec_create(sizeof(struct val));
struct vec lifetimes = vec_create(sizeof(struct lifetime));
struct vec rmap = vec_create(sizeof(struct val));
+ struct vec calls = vec_create(sizeof(size_t));
/* here it would probably make sense to iterate through lifetimes in
* order of priority, like loop nesting/count etc */
foreach_blk(bi, f->blks) {
struct blk *b = blk_at(f->blks, bi);
vec_reset(&lifetimes);
+ vec_reset(&calls);
/** @todo collect hints from args/params */
- collect_lifetimes(b, &hints, &lifetimes);
- build_rmap(&hints, &lifetimes, &rmap);
- do_rewrites(b, &rmap, f);
+ collect_lifetimes(b, &hints, &lifetimes, &rmap, &calls);
+ f->max_callee_save = build_rmap(&hints, &lifetimes, &rmap);
+ do_rewrites(b, &rmap);
+ f->max_call_save = do_call_saves(b, &lifetimes, &rmap, &calls);
/** @todo forward_hints(b, &hints, &rmap) */
+ if (vec_len(&calls) != 0)
+ f->has_calls = true;
}
+ vec_destroy(&calls);
vec_destroy(&hints);
vec_destroy(&lifetimes);
vec_destroy(&rmap);