code wiki / (root) / nx_membound.nx

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}