nx_membound.nx source
↩ module page · 116 lines · 6357 B
1// nx_membound.nx -- IS THE SOVEREIGN RASTERIZER MEMORY-BOUND AT HIGH RESOLUTION?
2//
3// WHY THIS PROBE EXISTS. Two measurements pointed away from arithmetic and nobody had tested the
4// alternative:
5// (1) gs_resolve costs ~12.3 ns/pixel at 4K while doing ONLY shifts, multiplies and one store --
6// nx_divprobe proved division by a constant power of two is already strength-reduced (7.1x
7// faster than a non-power-of-two divisor), so resolve is NOT divide-bound as was claimed.
8// At 40 bytes touched per pixel, 12.3 ns/px is ~3.3 GB/s: a MEMORY number, not an ALU number.
9// (2) A forward-differencing rewrite of the blend loop REMOVED six multiplies per pixel and made
10// the frame SLOWER. An arithmetic saving that does not show up is the signature of a pipeline
11// that is waiting on something other than the ALU.
12//
13// The estate is about to be told to TILE the blend for cache locality on the strength of that
14// reading. A recommendation is not a measurement, so this probe tests the premise first: if the
15// renderer is memory-bound, then confining the working set to cache must be markedly faster for the
16// SAME arithmetic and the SAME element count -- and if it is not, tiling will not pay and the
17// recommendation must be withdrawn before anyone builds it.
18//
19// THE EXPERIMENT DISCRIMINATES. Both arms do IDENTICAL total work: MB_TOTAL element-updates, the
20// same expression, the same accumulate-and-store shape as gs_resolve. They differ in ONE property:
21// BIG -- strides once through a buffer far larger than any cache, touching each element once.
22// SMALL -- loops many times over a buffer sized to sit in cache, touching each element many times.
23// Same instructions, same count; only the working set differs. A large BIG/SMALL ratio is the
24// definition of memory-bound.
25//
26// SIZES ARE DERIVED, NOT PICKED. MB_BIG_PX is the 4K pixel count the beauty tier actually renders
27// (3840*2160), so the BIG arm reproduces the real working set rather than a synthetic one. MB_SMALL_PX
28// is chosen small enough that its four arrays (4 * 8 B * MB_SMALL_PX = 512 KiB) fit comfortably inside
29// a typical L2, which is the whole point of the comparison. MB_TOTAL is the BIG arm's element count so
30// the two arms are directly comparable without scaling.
31
32import "nx_syscalls.nx"
33import "nx_gate_verdict.nx"
34
35const MB_W64: i64 = 8
36const MB_BIG_PX: i64 = 8294400 // 3840*2160: the real 4K working set
37// MB_REPS is chosen so that MB_BIG_PX divides EXACTLY by it: 8294400 = 512 * 16200. That exactness
38// is not cosmetic -- the first run of this probe used 16384, left a 4,096-element shortfall, and its
39// own "both arms did identical element counts" tooth FAILED and said so. The fixture was wrong, not
40// the tooth, so the fixture is what changed.
41const MB_REPS: i64 = 512
42const MB_SMALL_PX: i64 = 16200 // = MB_BIG_PX / MB_REPS; 5 arrays * 8 B * 16200 = 633 KiB, cache-resident
43const MB_TOTAL: i64 = 8294400 // identical element-updates in both arms, now EXACTLY so
44const MB_FXA: i64 = 256 // the renderer's own fixed-point shift, so the arithmetic matches
45
46// One element-update shaped exactly like gs_resolve's inner statement: three accumulator reads, one
47// transmittance read, some fixed-point arithmetic, one framebuffer store.
48func mb_pass(acc: *i64, trans: *i64, fb: *i64, n: i64, reps: i64) -> i64 {
49 var touched: i64 = 0
50 var r: i64 = 0
51 while r < reps {
52 var i: i64 = 0
53 while i < n {
54 let tr: i64 = trans[i]
55 var rr: i64 = acc[i*3]/MB_FXA + tr/MB_FXA
56 var gg: i64 = acc[i*3+1]/MB_FXA + tr/MB_FXA
57 var bb: i64 = acc[i*3+2]/MB_FXA + tr/MB_FXA
58 if rr > 255 { rr = 255 }
59 if gg > 255 { gg = 255 }
60 if bb > 255 { bb = 255 }
61 fb[i] = rr + gg*MB_FXA + bb*MB_FXA*MB_FXA
62 touched = touched + 1
63 i = i + 1
64 }
65 r = r + 1
66 }
67 return touched
68}
69
70func main() -> i64 {
71 gv_head("nx_membound -- same arithmetic, same element count, different working set" as *u8)
72
73 // BIG: one pass over the real 4K working set.
74 let accB: *i64 = sys_mmap(MB_BIG_PX * 3 * MB_W64) as *i64
75 let trB: *i64 = sys_mmap(MB_BIG_PX * MB_W64) as *i64
76 let fbB: *i64 = sys_mmap(MB_BIG_PX * MB_W64) as *i64
77 // SMALL: many passes over a cache-resident window.
78 let accS: *i64 = sys_mmap(MB_SMALL_PX * 3 * MB_W64) as *i64
79 let trS: *i64 = sys_mmap(MB_SMALL_PX * MB_W64) as *i64
80 let fbS: *i64 = sys_mmap(MB_SMALL_PX * MB_W64) as *i64
81
82 // Touch both buffers first so neither arm pays first-fault page-in inside its timed region --
83 // otherwise BIG would be timing the kernel's page allocator, not the memory system.
84 mb_pass(accB, trB, fbB, MB_BIG_PX, 1)
85 mb_pass(accS, trS, fbS, MB_SMALL_PX, 1)
86
87 let reps: i64 = MB_REPS
88
89 let t0: i64 = sys_clock_now_us()
90 let nb: i64 = mb_pass(accB, trB, fbB, MB_BIG_PX, 1)
91 let t1: i64 = sys_clock_now_us()
92 let ns: i64 = mb_pass(accS, trS, fbS, MB_SMALL_PX, reps)
93 let t2: i64 = sys_clock_now_us()
94
95 let ub: i64 = t1 - t0
96 let us: i64 = t2 - t1
97
98 gv_puts(" BIG working set = " as *u8); gv_num(MB_BIG_PX * 5 * MB_W64 / 1048576); gv_puts(" MiB, elements=" as *u8); gv_num(nb)
99 gv_puts(" us=" as *u8); gv_num(ub); gv_puts("\n" as *u8)
100 gv_puts(" SMALL working set = " as *u8); gv_num(MB_SMALL_PX * 5 * MB_W64 / 1024); gv_puts(" KiB, elements=" as *u8); gv_num(ns)
101 gv_puts(" us=" as *u8); gv_num(us); gv_puts("\n" as *u8)
102
103 gv_check("both-arms-did-identical-element-counts (else the comparison is void)" as *u8, nb == ns, gv_ctr())
104
105 if us > 0 {
106 gv_puts("\n BIG/SMALL ratio (x1000) = " as *u8); gv_num(ub * 1000 / us); gv_puts("\n" as *u8)
107 }
108 if nb > 0 {
109 gv_puts(" BIG ns/element (x1000) = " as *u8); gv_num(ub * 1000000 / nb); gv_puts("\n" as *u8)
110 gv_puts(" SMALL ns/element (x1000) = " as *u8); gv_num(us * 1000000 / ns); gv_puts("\n" as *u8)
111 }
112 gv_puts("\n READING: BIG much slower than SMALL => MEMORY-BOUND, and tiling the blend to a\n" as *u8)
113 gv_puts(" cache-resident window is predicted to pay. Ratio near 1 => NOT memory-bound,\n" as *u8)
114 gv_puts(" and the tiling recommendation must be WITHDRAWN before anyone builds it.\n" as *u8)
115 return 0
116}