float_layouts.py (5694B)
1 #!/usr/bin/env python3 2 """Binary64 bit-layout benchmarks with complete field and transport checks.""" 3 import argparse 4 import hashlib 5 import json 6 import platform 7 import random 8 import statistics 9 from pathlib import Path 10 11 from collections_batch import ProbeRunner 12 from base_workloads import encode as natural 13 from http_headers import LEAF, TRUE, pair, tree_list, expect 14 15 ROOT = Path(__file__).resolve().parent.parent 16 17 18 def sha(path): 19 return hashlib.sha256(path.read_bytes()).hexdigest() 20 21 22 def corpus(size): 23 # Signed zeros, subnormal edges, normal edges, infinities, and NaN payloads. 24 special = [0, 1, (1 << 52)-1, 1 << 52, 0x3ff0000000000000, 25 0x3ff0000000000001, 0x7fefffffffffffff, 0x7ff0000000000000, 26 0x7ff0000000000001, 0x7ff8000000000000, 0x7fffffffffffffff] 27 words = special + [w | (1 << 63) for w in special] 28 rng = random.Random(640053) 29 words += [rng.getrandbits(64) for _ in range(max(0, size-len(words)))] 30 return words 31 32 33 def fields(w): 34 return pair(TRUE if w >> 63 else LEAF, 35 pair(natural((w >> 52) & 2047), natural(w & ((1 << 52)-1)))) 36 37 38 def representations(w): 39 return {'field': fields(w), 'packed': natural(w), 40 'word': pair(natural(w & 0xffffffff), natural(w >> 32)), 41 'byte': tree_list([natural(b) for b in w.to_bytes(8, 'little')])} 42 43 44 def main(): 45 p = argparse.ArgumentParser(description=__doc__) 46 p.add_argument('tricu', type=Path) 47 p.add_argument('--directory', type=Path, required=True) 48 p.add_argument('--repeats', type=int, default=5) 49 p.add_argument('--size', type=int, default=128) 50 p.add_argument('--seconds', type=int, default=600) 51 args = p.parse_args() 52 if not 3 <= args.repeats <= 15 or not 22 <= args.size <= 4096: 53 p.error('repeats must be 3..15 and size 22..4096') 54 root = args.directory.resolve() 55 root.mkdir(parents=True, exist_ok=False) 56 workspace = root/'workspace' 57 workspace.mkdir() 58 sources = {'driver.tri': ROOT/'bench/float-layouts.tri', 59 'native_math.tri': ROOT/'lib/math/native.tri'} 60 for name, path in sources.items(): 61 (workspace/name).write_bytes(path.read_bytes()) 62 runner = ProbeRunner(args.tricu, root, args.seconds, memory_mib=2048) 63 report = dict(complete=False, scope='fresh-process-invoke-through-full-force', 64 runtime_sha256=sha(args.tricu), harness_sha256=sha(Path(__file__)), 65 compiler_sha256=sha(ROOT/'bootstrap/build-stable.arboricx'), 66 platform=platform.platform(), sources={k: sha(v) for k,v in sources.items()}, 67 samples=[], summaries=[]) 68 try: 69 tool = runner.imported(ROOT/'bootstrap/build-stable.arboricx', 'compiler') 70 bundle = root/'layouts.arboricx' 71 runner.run('compile', ['compile', '--file', workspace/'driver.tri', 72 '--toolchain', tool, '--tool-store', runner.store, '--store', runner.store, 73 '--output', bundle, '--compile-timeout', '240'], 270) 74 pin = runner.imported(bundle, 'layouts') 75 report['bundle_sha256'] = sha(bundle) 76 words = corpus(args.size) 77 report['size'] = len(words) 78 inputs = {} 79 for layout in ['field', 'packed', 'word', 'byte']: 80 inputs[layout] = root/(layout+'.tree') 81 inputs[layout].write_bytes(tree_list([representations(w)[layout] for w in words])) 82 expected = {'Signs': tree_list([TRUE if w >> 63 else LEAF for w in words]), 83 'Exponents': tree_list([natural((w >> 52) & 2047) for w in words]), 84 'Unpacks': tree_list([fields(w) for w in words])} 85 jobs = [(layout+op, layout, expected[op]) 86 for layout in inputs for op in expected] 87 jobs += [(layout+'Fast'+op, layout, expected[op]) 88 for layout in ['packed', 'word', 'byte'] for op in ['Exponents', 'Unpacks']] 89 jobs.append(('constantSigns', 'field', tree_list([TRUE]*len(words)))) 90 rng = random.Random(1053) 91 for repeat in range(args.repeats): 92 rng.shuffle(jobs) 93 for name, layout, wanted in jobs: 94 label = f'{name}-{repeat}' 95 actual = runner.invoke(label, pin, name, 96 ['--arg-tree-file', inputs[layout], '--profile-full-result']) 97 expect(actual, wanted, label) 98 log = (root/(label+'.log')).read_text() 99 samples = {v['profile']:v for line in log.splitlines() if line.startswith('{') 100 for v in [json.loads(line)]} 101 before, after = samples['invoke-begin-1'], samples['force-end-1'] 102 report['samples'].append(dict(name=name, repeat=repeat, warmup=repeat==0, 103 wall_ns=after['wall_ns']-before['wall_ns'], 104 allocated_bytes=after['allocated_bytes']-before['allocated_bytes'])) 105 for name in ['fieldDemand', 'wordDemand']: 106 expect(runner.invoke(name, pin, name, ['--arg-tree-file', inputs['field']]), TRUE, name) 107 for name, _, _ in sorted(jobs): 108 rows = [r for r in report['samples'] if r['name']==name and not r['warmup']] 109 row = dict(name=name) 110 for key in ['wall_ns', 'allocated_bytes']: 111 row[key] = statistics.median(r[key] for r in rows) 112 row[key+'_min'] = min(r[key] for r in rows) 113 row[key+'_max'] = max(r[key] for r in rows) 114 report['summaries'].append(row) 115 print(json.dumps(row), flush=True) 116 report['complete'] = True 117 finally: 118 (root/'report.json').write_text(json.dumps(report, indent=2)+'\n') 119 120 121 if __name__ == '__main__': 122 main()