aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/038-past-c/bfsfloor.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-05 09:19:32 +0100
committereunhhu <[email protected]>2026-10-05 09:19:32 +0100
commit2654f58ae6a7d0fcaf356b0f4cd69038b3eaf00c (patch)
treee91946d647d8bacfc0aa6d2b569fd7019a41e4a2 /research/experiments/038-past-c/bfsfloor.c
parent1ef9dd24773058f766ad80ffc7a59295fc44c92d (diff)
downloadpure-2654f58ae6a7d0fcaf356b0f4cd69038b3eaf00c.tar.gz
pure-2654f58ae6a7d0fcaf356b0f4cd69038b3eaf00c.zip
Measure 021: BFS 190 to 90 ms over HTTP against C's 9.1, short of C; Pure's forms by hand beat C only packed, narrow and read ahead (123)
Diffstat (limited to 'research/experiments/038-past-c/bfsfloor.c')
-rw-r--r--research/experiments/038-past-c/bfsfloor.c334
1 files changed, 334 insertions, 0 deletions
diff --git a/research/experiments/038-past-c/bfsfloor.c b/research/experiments/038-past-c/bfsfloor.c
new file mode 100644
index 0000000..a6258a8
--- /dev/null
+++ b/research/experiments/038-past-c/bfsfloor.c
@@ -0,0 +1,334 @@
+/* The floor before building more (research 123): the whole breadth first
+ * search of 033's graph.aup as Pure could run it at best, written by hand in
+ * C, against 033's bfs.c on the same graph (bfs-run's) in the same run.
+ *
+ * c bfs.c's loop: offsets (u32), to (u32), depth (int32), queue (u32)
+ * c_bits bfs.c with what it has seen as bits, depth written only when seen
+ * (021's context: where Pure's share would come from)
+ * packed Pure's representations, every check kept: edges a stored keyed
+ * list packed as columns (key, to: doubles) with a bucket index
+ * (the first position at or past each bucket's start; buckets a
+ * power of two of keys wide, starts aligned); seen a dense event
+ * list (key bits, node and depth columns by key, count); queue a
+ * flat event list (node column, count); the work charged as the
+ * executor does (EACH: the block's length times its items; LOOP:
+ * each round)
+ * ahead packed, and while taking node i, the bucket of node i + FAR and
+ * the first edge of node i + NEAR are read ahead (prefetch)
+ * narrow packed with columns as narrow as their values: to and the queue's
+ * node u32, the bucket index u32, seen's depth u8 and no node
+ * column (a dense list's key column is its keys)
+ * narrow_ahead narrow, read ahead as ahead
+ * c_ahead bfs.c read ahead as ahead (context: what C would gain)
+ *
+ * bfsfloor NODES EDGES SEARCHES [FAR NEAR]: a JSON line per search and form
+ * (the forms in turns; the answers must agree), with this thread's counters
+ * (user space), then the medians. */
+#define _GNU_SOURCE
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <linux/perf_event.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+/* Counters of this thread, user space only: instructions, cycles, branch misses, L1D read misses, LLC read misses. */
+enum { COUNTERS = 5 };
+static int counter[COUNTERS];
+static void counters_open(void) {
+ static const struct { uint32_t type; uint64_t config; } what[COUNTERS] = {
+ {PERF_TYPE_HARDWARE, PERF_COUNT_HW_INSTRUCTIONS}, {PERF_TYPE_HARDWARE, PERF_COUNT_HW_CPU_CYCLES},
+ {PERF_TYPE_HARDWARE, PERF_COUNT_HW_BRANCH_MISSES},
+ {PERF_TYPE_HW_CACHE, PERF_COUNT_HW_CACHE_L1D | PERF_COUNT_HW_CACHE_OP_READ << 8 | PERF_COUNT_HW_CACHE_RESULT_MISS << 16},
+ {PERF_TYPE_HW_CACHE, PERF_COUNT_HW_CACHE_LL | PERF_COUNT_HW_CACHE_OP_READ << 8 | PERF_COUNT_HW_CACHE_RESULT_MISS << 16}};
+ for (int i = 0; i < COUNTERS; ++i) {
+ struct perf_event_attr a = {.size = sizeof(a), .type = what[i].type, .config = what[i].config, .exclude_kernel = 1,
+ .exclude_hv = 1};
+ counter[i] = (int)syscall(SYS_perf_event_open, &a, 0, -1, -1, 0);
+ }
+}
+static void counters_read(uint64_t *v) {
+ for (int i = 0; i < COUNTERS; ++i) if (counter[i] < 0 || read(counter[i], &v[i], 8) != 8) v[i] = 0;
+}
+
+static uint64_t state = 121;
+static uint64_t next(void) { state ^= state << 13; state ^= state >> 7; state ^= state << 17; return state; }
+static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec * 1e3 + t.tv_nsec / 1e6; }
+static int by_key(const void *a, const void *b) { uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b; return (x > y) - (x < y); }
+static int by_num(const void *a, const void *b) { double x = *(const double *)a, y = *(const double *)b; return (x > y) - (x < y); }
+
+typedef struct { int hops; uint32_t reached; uint64_t work; } Answer;
+
+/* ---- bfs.c */
+static uint32_t n, m, *start, *to32, *queue32;
+static int32_t *depth32;
+
+static Answer search_c(uint32_t s, uint32_t t) {
+ for (uint32_t v = 0; v < n; ++v) depth32[v] = -1;
+ uint32_t head = 0, tail = 0;
+ queue32[tail++] = s;
+ depth32[s] = 0;
+ while (head < tail) {
+ uint32_t v = queue32[head++];
+ for (uint32_t k = start[v]; k < start[v + 1]; ++k)
+ if (depth32[to32[k]] < 0) { depth32[to32[k]] = depth32[v] + 1; queue32[tail++] = to32[k]; }
+ }
+ return (Answer){depth32[t], tail, 0};
+}
+
+static uint64_t *seen_bits;
+
+static Answer search_c_bits(uint32_t s, uint32_t t) {
+ memset(seen_bits, 0, (n + 63) / 64 * 8);
+ uint32_t head = 0, tail = 0;
+ queue32[tail++] = s;
+ seen_bits[s >> 6] |= 1ull << (s & 63), depth32[s] = 0;
+ while (head < tail) {
+ uint32_t v = queue32[head++];
+ int32_t d = depth32[v] + 1;
+ for (uint32_t k = start[v]; k < start[v + 1]; ++k) {
+ uint32_t w = to32[k];
+ if (!(seen_bits[w >> 6] >> (w & 63) & 1)) { seen_bits[w >> 6] |= 1ull << (w & 63), depth32[w] = d; queue32[tail++] = w; }
+ }
+ }
+ return (Answer){seen_bits[t >> 6] >> (t & 63) & 1 ? depth32[t] : -1, tail, 0};
+}
+
+/* ---- Pure's representations */
+typedef struct { size_t count; double *key, *to; double low, scale; size_t buckets; uint32_t *first; } Packed;
+typedef struct { size_t count, keys; uint64_t *bits; double *node, *depth; } Dense;
+typedef struct { size_t count, room; double *node; } Flat;
+static Packed edges;
+static Dense seen;
+static Flat queue;
+enum { EACH_LENGTH = 23, LOOP_LENGTH = 30 }; /* the blocks' lengths, as graph.aup compiles (the charge is what matters, not the value) */
+
+/* The first position whose key is not below key: the bucket's first, then on while below. */
+static inline __attribute__((always_inline)) size_t seek(const Packed *p, double key) {
+ double at = (key - p->low) * p->scale;
+ if (!(at >= 0)) at = 0;
+ if (at >= (double)p->buckets) return p->count;
+ size_t b = (size_t)at, pos = p->first[b];
+ if ((double)b == at) return pos; /* a bucket's start: its first is the answer */
+ while (pos < p->count && p->key[pos] < key) ++pos;
+ return pos;
+}
+
+static inline __attribute__((always_inline)) bool small(double key, size_t keys, size_t *k) {
+ if (!(key >= 0 && key < (double)keys)) return false;
+ *k = (size_t)key;
+ return (double)*k == key;
+}
+
+static void fault(const char *what) { fprintf(stderr, "fault: %s\n", what); exit(1); }
+
+static inline __attribute__((always_inline)) Answer search_packed(uint32_t s, uint32_t t, int far, int near) {
+ uint64_t work = 0;
+ memset(seen.bits, 0, (seen.keys + 63) / 64 * 8), seen.count = 0, queue.count = 0; /* the event's lists, cleared */
+ queue.node[queue.count++] = s;
+ seen.bits[s >> 6] |= 1ull << (s & 63), seen.node[s] = s, seen.depth[s] = 0, ++seen.count;
+ double nodes = n;
+ for (double i = 0; i < nodes; ++i) {
+ work += LOOP_LENGTH;
+ if (i >= (double)queue.count) break;
+ size_t at = (size_t)i;
+ if (far) {
+ size_t ahead = at + (size_t)far, closer = at + (size_t)near;
+ if (ahead < queue.count) {
+ double a = (queue.node[ahead] * 1048576 - edges.low) * edges.scale;
+ if (a >= 0 && a < (double)edges.buckets) __builtin_prefetch(&edges.first[(size_t)a]);
+ }
+ if (closer < queue.count) {
+ double a = (queue.node[closer] * 1048576 - edges.low) * edges.scale;
+ if (a >= 0 && a < (double)edges.buckets) __builtin_prefetch(&edges.to[edges.first[(size_t)a]]);
+ }
+ }
+ double node = queue.node[at];
+ double lo = node * 1048576, hi = node * 1048576 + 1048576;
+ size_t from = seek(&edges, lo), until = seek(&edges, hi);
+ if (until < from) until = from;
+ work += (uint64_t)EACH_LENGTH * (until - from);
+ size_t q;
+ if (!small(node, seen.keys, &q) || !(seen.bits[q >> 6] >> (q & 63) & 1)) fault("absent");
+ double d = seen.depth[q] + 1; /* hoisted: nothing in the loop changes seen[q.node] before it is read */
+ for (size_t e = from; e < until; ++e) {
+ double key = edges.to[e];
+ size_t k;
+ if (!small(key, seen.keys, &k)) fault("key"); /* the runtime: another form, not a fault */
+ if (seen.bits[k >> 6] >> (k & 63) & 1) continue;
+ seen.bits[k >> 6] |= 1ull << (k & 63), seen.node[k] = key, seen.depth[k] = d, ++seen.count;
+ if (queue.count == queue.room) fault("room");
+ queue.node[queue.count++] = key;
+ }
+ }
+ int hops = (seen.bits[t >> 6] >> (t & 63) & 1) ? (int)seen.depth[t] : -1;
+ return (Answer){hops, (uint32_t)seen.count, work};
+}
+
+/* ---- narrow columns */
+typedef struct { size_t count; double *key; uint32_t *to; double low, scale; size_t buckets; uint32_t *first; } Narrow;
+typedef struct { size_t count, keys; uint64_t *bits; uint8_t *depth; } NarrowDense;
+typedef struct { size_t count, room; uint32_t *node; } NarrowFlat;
+static Narrow nedges;
+static NarrowDense nseen;
+static NarrowFlat nqueue;
+
+static inline __attribute__((always_inline)) size_t nseek(const Narrow *p, double key) {
+ double at = (key - p->low) * p->scale;
+ if (!(at >= 0)) at = 0;
+ if (at >= (double)p->buckets) return p->count;
+ size_t b = (size_t)at, pos = p->first[b];
+ if ((double)b == at) return pos;
+ while (pos < p->count && p->key[pos] < key) ++pos;
+ return pos;
+}
+
+static inline __attribute__((always_inline)) Answer search_narrow(uint32_t s, uint32_t t, int far, int near) {
+ uint64_t work = 0;
+ memset(nseen.bits, 0, (nseen.keys + 63) / 64 * 8), nseen.count = 0, nqueue.count = 0;
+ nqueue.node[nqueue.count++] = s;
+ nseen.bits[s >> 6] |= 1ull << (s & 63), nseen.depth[s] = 0, ++nseen.count;
+ double nodes = n;
+ for (double i = 0; i < nodes; ++i) {
+ work += LOOP_LENGTH;
+ if (i >= (double)nqueue.count) break;
+ size_t at = (size_t)i;
+ if (far) {
+ size_t ahead = at + (size_t)far, closer = at + (size_t)near;
+ if (ahead < nqueue.count) {
+ double a = ((double)nqueue.node[ahead] * 1048576 - nedges.low) * nedges.scale;
+ if (a >= 0 && a < (double)nedges.buckets) __builtin_prefetch(&nedges.first[(size_t)a]);
+ }
+ if (closer < nqueue.count) {
+ double a = ((double)nqueue.node[closer] * 1048576 - nedges.low) * nedges.scale;
+ if (a >= 0 && a < (double)nedges.buckets) __builtin_prefetch(&nedges.to[nedges.first[(size_t)a]]);
+ }
+ }
+ uint32_t q = nqueue.node[at];
+ double node = q;
+ double lo = node * 1048576, hi = node * 1048576 + 1048576;
+ size_t from = nseek(&nedges, lo), until = nseek(&nedges, hi);
+ if (until < from) until = from;
+ work += (uint64_t)EACH_LENGTH * (until - from);
+ if (q >= nseen.keys || !(nseen.bits[q >> 6] >> (q & 63) & 1)) fault("absent");
+ unsigned d = nseen.depth[q] + 1u;
+ if (d > 255) fault("wider"); /* the runtime: the column made wider */
+ for (size_t e = from; e < until; ++e) {
+ uint32_t k = nedges.to[e];
+ if (k >= nseen.keys) fault("key");
+ if (nseen.bits[k >> 6] >> (k & 63) & 1) continue;
+ nseen.bits[k >> 6] |= 1ull << (k & 63), nseen.depth[k] = (uint8_t)d, ++nseen.count;
+ if (nqueue.count == nqueue.room) fault("room");
+ nqueue.node[nqueue.count++] = k;
+ }
+ }
+ int hops = (nseen.bits[t >> 6] >> (t & 63) & 1) ? (int)nseen.depth[t] : -1;
+ return (Answer){hops, (uint32_t)nseen.count, work};
+}
+static Answer narrow_plain(uint32_t s, uint32_t t) { return search_narrow(s, t, 0, 0); }
+static Answer narrow_ahead(uint32_t s, uint32_t t);
+
+static Answer search_c_ahead(uint32_t s, uint32_t t);
+
+static Answer packed_plain(uint32_t s, uint32_t t) { return search_packed(s, t, 0, 0); }
+static int far_, near_;
+static Answer packed_ahead(uint32_t s, uint32_t t) { return search_packed(s, t, far_, near_); }
+static Answer narrow_ahead(uint32_t s, uint32_t t) { return search_narrow(s, t, far_, near_); }
+
+static Answer search_c_ahead(uint32_t s, uint32_t t) {
+ for (uint32_t v = 0; v < n; ++v) depth32[v] = -1;
+ uint32_t head = 0, tail = 0;
+ queue32[tail++] = s;
+ depth32[s] = 0;
+ while (head < tail) {
+ if (head + (uint32_t)far_ < tail) __builtin_prefetch(&start[queue32[head + (uint32_t)far_]]);
+ if (head + (uint32_t)near_ < tail) __builtin_prefetch(&to32[start[queue32[head + (uint32_t)near_]]]);
+ uint32_t v = queue32[head++];
+ for (uint32_t k = start[v]; k < start[v + 1]; ++k)
+ if (depth32[to32[k]] < 0) { depth32[to32[k]] = depth32[v] + 1; queue32[tail++] = to32[k]; }
+ }
+ return (Answer){depth32[t], tail, 0};
+}
+
+int main(int argc, char **argv) {
+ if (argc < 4) { fputs("bfsfloor NODES EDGES SEARCHES [FAR NEAR]\n", stderr); return 2; }
+ n = (uint32_t)atol(argv[1]);
+ size_t want = (size_t)atol(argv[2]), searches = (size_t)atol(argv[3]);
+ far_ = argc > 4 ? atoi(argv[4]) : 16, near_ = argc > 5 ? atoi(argv[5]) : 8;
+ /* bfs-run's graph: from and to uniform, no loops, each pair once, sorted */
+ uint64_t *keys = malloc(want * sizeof(*keys));
+ size_t count = 0;
+ while (count < want) {
+ uint64_t a = next() % n, b = next() % n;
+ if (a != b) keys[count++] = a << 20 | b;
+ }
+ qsort(keys, count, sizeof(*keys), by_key);
+ m = 0;
+ for (size_t i = 0; i < count; ++i)
+ if (!m || keys[i] != keys[m - 1]) keys[m++] = keys[i];
+ seen_bits = calloc((n + 63) / 64, 8), start = calloc(n + 1, 4), to32 = malloc(4ull * m), depth32 = malloc(4ull * n), queue32 = malloc(4ull * n);
+ for (uint32_t k = 0; k < m; ++k) ++start[(keys[k] >> 20) + 1], to32[k] = (uint32_t)(keys[k] & 0xFFFFF);
+ for (uint32_t v = 0; v < n; ++v) start[v + 1] += start[v];
+ /* packed: about eight items a bucket, a power of two wide, starts at multiples of the width */
+ edges.count = m, edges.key = malloc(8ull * m), edges.to = malloc(8ull * m);
+ for (size_t k = 0; k < m; ++k) edges.key[k] = (double)keys[k], edges.to[k] = (double)(keys[k] & 0xFFFFF);
+ double span = edges.key[m - 1] - edges.key[0];
+ int exponent;
+ frexp(span / (double)(m / 8 + 1), &exponent);
+ edges.scale = ldexp(1.0, -exponent);
+ edges.low = floor(edges.key[0] * edges.scale) / edges.scale;
+ edges.buckets = (size_t)floor((edges.key[m - 1] - edges.low) * edges.scale) + 2;
+ edges.first = malloc(4 * edges.buckets);
+ size_t filled = 0;
+ for (size_t k = 0; k < m; ++k) {
+ size_t b = (size_t)floor((edges.key[k] - edges.low) * edges.scale);
+ while (filled <= b) edges.first[filled++] = (uint32_t)k;
+ }
+ while (filled < edges.buckets) edges.first[filled++] = (uint32_t)m;
+ seen.keys = n, seen.bits = calloc((n + 63) / 64, 8), seen.node = malloc(8ull * n), seen.depth = malloc(8ull * n);
+ queue.room = n, queue.node = malloc(8ull * n);
+ nedges = (Narrow){edges.count, edges.key, malloc(4ull * m), edges.low, edges.scale, edges.buckets, edges.first};
+ for (size_t k = 0; k < m; ++k) nedges.to[k] = (uint32_t)edges.to[k];
+ nseen.keys = n, nseen.bits = calloc((n + 63) / 64, 8), nseen.depth = malloc(n);
+ nqueue.room = n, nqueue.node = malloc(4ull * n);
+ fprintf(stderr, "edges %u, buckets %zu (%zu KB, %g keys wide)\n", m, edges.buckets, edges.buckets * 4 / 1024,
+ 1 / edges.scale);
+ enum { FORMS = 7 };
+ counters_open();
+ const char *names[] = {"c", "c_bits", "packed", "ahead", "narrow", "narrow_ahead", "c_ahead"};
+ Answer (*forms[])(uint32_t, uint32_t) = {search_c, search_c_bits, packed_plain, packed_ahead, narrow_plain, narrow_ahead,
+ search_c_ahead};
+ double *took[FORMS];
+ for (int f = 0; f < FORMS; ++f) took[f] = malloc(searches * sizeof(double));
+ for (size_t r = 0; r < searches; ++r) {
+ uint32_t s = (uint32_t)(next() % n), t = (uint32_t)(next() % n);
+ Answer first = {0};
+ for (int j = 0; j < FORMS; ++j) {
+ int f = (int)((j + r) % FORMS); /* each form first in turn */
+ uint64_t c0[COUNTERS], c1[COUNTERS];
+ counters_read(c0);
+ double a = now();
+ Answer got = forms[f](s, t);
+ took[f][r] = now() - a;
+ counters_read(c1);
+ if (!j) first = got;
+ else if (got.hops != first.hops || got.reached != first.reached) { fprintf(stderr, "answers differ\n"); return 1; }
+ printf("{\"form\":\"%s\",\"from\":%u,\"to\":%u,\"hops\":%d,\"reached\":%u,\"ms\":%.3f,\"instructions\":%llu,"
+ "\"cycles\":%llu,\"branch_misses\":%llu,\"l1d_misses\":%llu,\"llc_misses\":%llu,\"work\":%llu}\n", names[f], s, t,
+ got.hops, got.reached, took[f][r], (unsigned long long)(c1[0] - c0[0]), (unsigned long long)(c1[1] - c0[1]),
+ (unsigned long long)(c1[2] - c0[2]), (unsigned long long)(c1[3] - c0[3]), (unsigned long long)(c1[4] - c0[4]),
+ (unsigned long long)got.work);
+ }
+ }
+ printf("{\"nodes\":%u,\"edges\":%u,\"far\":%d,\"near\":%d", n, m, far_, near_);
+ for (int f = 0; f < FORMS; ++f) {
+ qsort(took[f], searches, sizeof(double), by_num);
+ printf(",\"%s_ms\":%.3f", names[f], took[f][searches / 2]);
+ }
+ puts("}");
+ return 0;
+}