From bf5301b8c8912606d312d161bbaa318a6e283ac4 Mon Sep 17 00:00:00 2001 From: Mura Li Date: Wed, 23 Oct 2019 15:14:24 +0800 Subject: [PATCH 1/2] match: merge decision tree nodes when possible Sample count: 506 Dtree Refcnt avg: 5.38 95th percentile: 3.00 maximum: 100 Dtree Size avg: 5.23 95th percentile: 3.00 maximum: 84 Dtree Height avg: 1.39 95th percentile: 1.00 maximum: 12 References: Mikael Pettersson. A term pattern-match compiler inspired by finite automata theory. (p.6 "Step 3: Optimizing the DFA") --- mi/match.c | 169 ++++++++++++++++++++++++++++++++++------- mi/match_test.c | 4 +- support/matchstats.myr | 9 ++- 3 files changed, 153 insertions(+), 29 deletions(-) diff --git a/mi/match.c b/mi/match.c index cc3439095..f3096fed3 100644 --- a/mi/match.c +++ b/mi/match.c @@ -14,11 +14,12 @@ #include "parse.h" #include "mi.h" -Dtree *gendtree(Node *m, Node *val, Node **lbl, size_t nlbl, int startid); +Dtree *gendtree(Node *m, Node *val, Node **lbl, size_t nlbl); void dtreedump(FILE *fd, Dtree *dt); -static int ndtree; +static size_t ndtree; +static Dtree **dtree; /* Path is a integer sequence that labels a subtree of a subject tree. * For example, @@ -178,10 +179,82 @@ mkdtree(Srcloc loc, Node *lbl) t = zalloc(sizeof(Dtree)); t->lbl = lbl; t->loc = loc; - t->id = ndtree++; + t->id = ndtree; + lappend(&dtree, &ndtree, t); return t; } +static int +loadeq(Node *a, Node *b) +{ + if (a == b) + return 1; + + if (exprop(a) != exprop(b)) + return 0; + + switch (exprop(a)) { + case Outag: + case Oudata: + case Oderef: + return loadeq(a->expr.args[0], b->expr.args[0]); + case Omemb: + return loadeq(a->expr.args[0], b->expr.args[0]) && strcmp(a->expr.args[1]->name.name, b->expr.args[1]->name.name); + case Otupget: + case Oidx: + return loadeq(a->expr.args[0], b->expr.args[0]) && a->expr.args[1]->expr.args[0]->lit.intval == b->expr.args[1]->expr.args[0]->lit.intval; + case Ovar: + return a == b; + default: + fatal(a, "unsupported opcode %s of type %s", opstr[exprop(a)], tystr(exprtype(a))); + } +} + +static int +pateq(Node *a, Node *b) +{ + if (exprop(a) != exprop(b)) + return 0; + + switch (exprop(a)) { + case Olit: + return liteq(a->expr.args[0], b->expr.args[0]); + case Ogap: + case Ovar: + return 1; + default: + die("unreachable"); + } + return 0; +} + +static int +dtreusable(Dtree *dt, Node *load, Node **pat, size_t npat, Dtree **next, size_t nnext, Dtree *any) +{ + size_t i; + + if (!loadeq(dt->load, load)) + return 0; + + if (dt->npat != npat) + return 0; + + for (i = 0; i < npat; i++) { + if (dt->next[i]->id != next[i]->id) + return 0; + if (!pateq(dt->pat[i], pat[i])) + return 0; + } + + if (dt->any == NULL && any == NULL) + return 1; + + if (dt->any->id != any->id) + return 0; + + return 1; +} + void dtreedumplit(FILE *fd, Dtree *dt, Node *n, size_t depth) { @@ -260,24 +333,6 @@ patheq(Path *a, Path *b) return 1; } -static int -pateq(Node *a, Node *b) -{ - if (exprop(a) != exprop(b)) - return 0; - - switch (exprop(a)) { - case Olit: - return liteq(a->expr.args[0], b->expr.args[0]); - case Ogap: - case Ovar: - return 1; - default: - die("unreachable"); - } - return 0; -} - char * pathfmt(Path *p) { @@ -683,7 +738,24 @@ compile(Frontier **frontier, size_t nfrontier) any = NULL; /* construct the result dtree */ - out = mkdtree(slot->pat->loc, genlbl(slot->pat->loc)); + out = NULL; + + /* TODO + * use a hash table to avoid the quadratic complexity + * when we have a large N and the bottleneck becomes obvious. + */ + for (i = 0; i < ndtree; i++) { + if (!dtree[i]->accept && dtreusable(dtree[i], slot->load, pat, npat, edge, nedge, any)) { + out = dtree[i]; + out->refcnt++; + break; + } + } + if (out == NULL) { + out = mkdtree(slot->pat->loc, genlbl(slot->pat->loc)); + out->refcnt++; + } + out->load = slot->load; out->npat = npat, out->pat = pat, @@ -749,8 +821,49 @@ dtheight(Dtree *dt) return m+1; } +static size_t +refcntsum(Dtree *dt) +{ + size_t i; + size_t sum; + + if (dt == NULL) + return 0; + + dt->emitted = 1; + + /* NOTE + * MATCH nodes are always pre-allocated and shared, + * so counted as 1 for the size measurement. + */ + if (dt->accept) + return 1; + + sum = 0; + for (i = 0; i < dt->nnext; i++) + if (!dt->next[i]->emitted) + sum += refcntsum(dt->next[i]); + if (dt->any && !dt->any->emitted) + sum += refcntsum(dt->any); + + return dt->refcnt + sum; +} + +static void +dtresetemitted(Dtree *dt) +{ + size_t i; + + if (dt == NULL) + return; + for (i = 0; i < dt->nnext; i++) + dtresetemitted(dt->next[i]); + dtresetemitted(dt->any); + dt->emitted = 0; +} + Dtree * -gendtree(Node *m, Node *val, Node **lbl, size_t nlbl, int startid) +gendtree(Node *m, Node *val, Node **lbl, size_t nlbl) { Dtree *root; Node **pat; @@ -762,7 +875,8 @@ gendtree(Node *m, Node *val, Node **lbl, size_t nlbl, int startid) char *dbgloc, *dbgfn, *dbgln; - ndtree = startid; + ndtree = 0; + dtree = NULL; pat = m->matchstmt.matches; npat = m->matchstmt.nmatches; @@ -799,8 +913,11 @@ gendtree(Node *m, Node *val, Node **lbl, size_t nlbl, int startid) if (getenv("MATCH_STATS")) { csv = fopen("match.csv", "a"); assert(csv != NULL); - fprintf(csv, "%s@L%d, %d, %ld\n", fname(m->loc), lnum(m->loc), ndtree, dtheight(root)); + fprintf(csv, "%s@L%d, %ld, %ld, %ld\n", fname(m->loc), lnum(m->loc), refcntsum(root), ndtree, dtheight(root)); fclose(csv); + + /* clear 'emitted' so it can be reused by genmatchcode. */ + dtresetemitted(root); } return root; @@ -887,7 +1004,7 @@ genmatch(Node *m, Node *val, Node ***out, size_t *nout) endlbl = genlbl(m->loc); - dt = gendtree(m, val, lbl, nlbl, 0); + dt = gendtree(m, val, lbl, nlbl); genmatchcode(dt, out, nout); for (i = 0; i < npat; i++) { diff --git a/mi/match_test.c b/mi/match_test.c index dda8fdbc5..d190aba4c 100644 --- a/mi/match_test.c +++ b/mi/match_test.c @@ -26,7 +26,7 @@ File file; char debugopt[128]; typedef struct Dtree Dtree; -extern Dtree *gendtree(Node *m, Node *val, Node **lbl, size_t nlbl, int startid); +extern Dtree *gendtree(Node *m, Node *val, Node **lbl, size_t nlbl); extern void dtreedump(FILE *fd, Dtree *dt); @@ -250,7 +250,7 @@ test_match(int idx, Node *val, Node **pat, Dtree *want) lappend(&lbl, &nlbl, genlbl(pat[i]->match.block->loc)); } - dt = gendtree(m, v, lbl, nlbl, 0); + dt = gendtree(m, v, lbl, nlbl); if (getenv("d")) { fprintf(stderr, "dtree >>\n"); dtreedump(stderr, dt); diff --git a/support/matchstats.myr b/support/matchstats.myr index ae20ae985..b0375d52a 100755 --- a/support/matchstats.myr +++ b/support/matchstats.myr @@ -60,7 +60,7 @@ const maximum = {xs } const main = {args : byte[:][:] - var f, locs, sizes, heights, count + var f, locs, refcnts, sizes, heights, count if args.len < 2 std.put("need input file\n") @@ -73,6 +73,7 @@ const main = {args : byte[:][:] ;; locs = [][:] + refcnts = [][:] sizes = [][:] heights = [][:] count = 0 @@ -84,6 +85,11 @@ const main = {args : byte[:][:] | `std.Err e: std.fatal("error read loc: {}\n", e) ;; + match bio.readto(f, ",") + | `std.Ok refcnt: std.slpush(&refcnts, atoi(std.strstrip(refcnt))) + | `std.Err e: std.fatal("error read refcnt: {}\n", e) + ;; + match bio.readto(f, ",") | `std.Ok size: std.slpush(&sizes, atoi(std.strstrip(size))) | `std.Err e: std.fatal("error read size: {}\n", e) @@ -97,6 +103,7 @@ const main = {args : byte[:][:] ;; std.put("Sample count: {}\n", count) + std.put("Dtree Refcnt\tavg: {s=3}\t95th percentile: {s=3}\t maximum: {}\n", avg(refcnts), percentile(95, refcnts), maximum(refcnts)) std.put("Dtree Size\tavg: {s=3}\t95th percentile: {s=3}\t maximum: {}\n", avg(sizes), percentile(95, sizes), maximum(sizes)) std.put("Dtree Height\tavg: {s=3}\t95th percentile: {s=3}\t maximum: {}\n", avg(heights), percentile(95, heights), maximum(heights)) From 0d01af21f7c9c93ab48875972e312e70d1db4e0c Mon Sep 17 00:00:00 2001 From: Ori Bernstein Date: Wed, 23 Oct 2019 15:41:01 -0700 Subject: [PATCH 2/2] Add heuristics for the match compiler --- mi/match.c | 133 +++++++++++++++++++++++++++++++++++++++++++-- test/matchctup.myr | 32 +++++++++++ 2 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 test/matchctup.myr diff --git a/mi/match.c b/mi/match.c index f3096fed3..471c7ab4b 100644 --- a/mi/match.c +++ b/mi/match.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "util.h" #include "parse.h" @@ -57,6 +58,7 @@ typedef struct Slot { Path *path; Node *pat; Node *load; + int score; } Slot; static Slot * @@ -67,6 +69,7 @@ mkslot(Path *path, Node *pat, Node *val) s->path = path; s->pat = pat; s->load = val; + s->score = 0; assert(path != (void *)1); return s; } @@ -408,6 +411,18 @@ nconstructors(Type *t) return 0; } +static inline int +iswildcard(Node *pat) +{ + switch (exprop(pat)) { + case Ovar: + case Ogap: + return 1; + default: + return 0; + } +} + /* addrec generates a list of slots for a Frontier by walking a pattern tree. * It collects only the terminal patterns like union tags and literals. * Non-terminal patterns like tuple/struct/array help encode the path only. @@ -509,6 +524,100 @@ addrec(Frontier *fs, Node *pat, Node *val, Path *path) } } +static size_t +ncons(Frontier **frontier, size_t nfrontier, Path *path) +{ + Frontier *fs; + Node **cs; + Slot *s; + size_t ncs, i, j, k; + + + cs = NULL; + ncs = 0; + for (i = 0; i < nfrontier; i++) { + fs = frontier[i]; + for (j = 0; j < fs->nslot; j++) { + s = fs->slot[j]; + switch (exprop(s->pat)) { + case Ovar: + case Ogap: + break; + case Olit: + if (patheq(path, s->path)) { + /* NOTE: + * we could use a hash table, but given that the n is usually small, + * an exhaustive search would suffice. + */ + /* look for a duplicate entry to skip it; we want a unique set of constructors. */ + for (k = 0; k < ncs; k++) { + if (pateq(cs[k], s->pat)) + break; + } + if (ncs == 0 || k == ncs) + lappend(&cs, &ncs, s->pat); + } + break; + default: + assert(0); + } + } + } + return ncs; +} + +static void +score_qb(Frontier **frontier, size_t nfrontier) +{ + Frontier *fj, *fk; + Slot *s; + size_t i, j, k, l; + int needed, q, b; + + + for (j = 0; j < nfrontier; j++) { + fj = frontier[j]; + for (i = 0; i < fj->nslot; i++) { + s = fj->slot[i]; + q = 0; + b = 0; + + for (k = 0; k < nfrontier; k++) { + fk = frontier[k]; + + /* scan for the pattern at the same column */ + for (l = 0; l < fk->nslot; l++) + if (patheq(fk->slot[l]->path, fj->slot[i]->path)) + break; + + needed = (l != fk->nslot && !iswildcard(fk->slot[l]->pat)); + if (!needed) + break; + q++; + } + + + b = ncons(frontier, nfrontier, fj->slot[i]->path); + /* we assign the score when the top k rows at the path are all needed */ + s->score = -10000*s->path->len+1000*q+b; + } + } +} + +static void +scoredump(Frontier **frontier, size_t nfrontier) +{ + Frontier *fj; + size_t i, j; + + for (j = 0; j < nfrontier; j++) { + fj = frontier[j]; + for (i = 0; i < fj->nslot; i++) + fprintf(stderr, "%d, ", fj->slot[i]->score); + fprintf(stderr, "\n"); + } +} + static void genfrontier(int i, Node *val, Node *pat, Node *lbl, Frontier ***frontier, size_t *nfrontier) { @@ -623,6 +732,7 @@ compile(Frontier **frontier, size_t nfrontier) Node **cs, **pat; Slot *slot, *s; size_t ncs, ncons, nrow, nedge, ndefaults, npat; + int max; assert(nfrontier > 0); @@ -651,19 +761,29 @@ compile(Frontier **frontier, size_t nfrontier) * we always select the first found constructor, i.e. the top-left one. */ + max = INT_MIN; slot = NULL; for (i = 0; i < fs->nslot; i++) { - switch (exprop(fs->slot[i]->pat)) { + /* NOTE: + * scan backward so that slots with equal scores will have left-to-right order + * rationale: left-to-right order is more or less natural/expected + */ + k = fs->nslot-i-1; + + switch (exprop(fs->slot[k]->pat)) { case Ovar: case Ogap: continue; default: - slot = fs->slot[i]; - goto pi_found; + if (fs->slot[k]->score >= max) { + slot = fs->slot[k]; + max = slot->score; + } } } -pi_found: + assert(slot != NULL); + /* scan constructors vertically at pi to create the set 'CS' */ cs = NULL; ncs = 0; @@ -889,6 +1009,7 @@ gendtree(Node *m, Node *val, Node **lbl, size_t nlbl) for (i = 0; i < nfrontier; i++) { addcapture(pat[i]->match.block, frontier[i]->cap, frontier[i]->ncap); } + score_qb(frontier, nfrontier); root = compile(frontier, nfrontier); for (i = 0; i < nfrontier; i++) @@ -900,8 +1021,10 @@ gendtree(Node *m, Node *val, Node **lbl, size_t nlbl) if (strchr(dbgloc, '@')) { dbgfn = strtok(dbgloc, "@"); dbgln = strtok(NULL, "@")+1; /* offset by 1 to skip the charactor 'L' */ - if (!strcmp(fname(m->loc), dbgfn) && lnum(m->loc) == strtol(dbgln, 0, 0)) + if (!strcmp(fname(m->loc), dbgfn) && lnum(m->loc) == strtol(dbgln, 0, 0)) { + scoredump(frontier, nfrontier); dtreedump(stdout, root); + } } else dtreedump(stdout, root); diff --git a/test/matchctup.myr b/test/matchctup.myr new file mode 100644 index 000000000..07d51e6a3 --- /dev/null +++ b/test/matchctup.myr @@ -0,0 +1,32 @@ +use std + +const main = { + match (gen_a(), gen_b()) + | (`std.Err _, _): std.put("error case 1") + | (_, `std.Err _): std.put("error case 2") + | (`std.Ok L, `std.Ok N): + std.put("l = {} n = {}\n", L, N) + ;; +} + +var gen_a : (-> std.result(byte[:][:], void)) = { + var ret = [][:] + + for var j = 0; j < 4; ++j + std.slpush(&ret, std.fmt("{}", j)) + ;; + + if 1 + 1 > 3 + -> `std.Err void + ;; + + -> `std.Ok ret +} + +var gen_b : (-> std.result(int, void)) = { + if 1 + 1 > 3 + -> `std.Err void + ;; + + -> `std.Ok 5 +}