reducer_comparison.py (4413B)
1 #!/usr/bin/env python3 2 """Paired Haskell/Zig runtime measurements with complete-result checks.""" 3 import hashlib 4 import json 5 import os 6 from pathlib import Path 7 import platform 8 import statistics 9 import sys 10 import time 11 from process_rss import run_guarded 12 13 haskell, zig, fixtures, output = sys.argv[1:] 14 fixtures = Path(fixtures) 15 affinity = sorted(os.sched_getaffinity(0)) 16 os.sched_setaffinity(0, {affinity[0]}) 17 cases = [(kind, n, 20) for kind in ('id', 'append') for n in (256, 4096, 16384)] 18 cases += [(kind, n, 5) for kind in ('deep', 'distribution') for n in (1000, 10000, 100000)] 19 cases += [('append', 4096, 256)] 20 report = dict( 21 complete=False, platform=platform.platform(), machine=platform.machine(), 22 cpu_model=next(line.split(':', 1)[1].strip() for line in Path('/proc/cpuinfo').read_text().splitlines() if line.startswith('model name')), 23 pinned_cpu=affinity[0], original_affinity=affinity, samples_per_host_case=5, 24 haskell_compiler=(Path(haskell).resolve().parents[1]/'compiler-version').read_text().strip(), 25 fixture_sha256={name: hashlib.sha256((fixtures/name).read_bytes()).hexdigest() for name in ('id.arboricx', 'append.arboricx')}, 26 runners={host: dict(path=path, sha256=hashlib.sha256(Path(path).read_bytes()).hexdigest()) for host,path in [('haskell',haskell),('zig',zig)]}, 27 limits=dict(rss_mib=512, wall_seconds=65, cpu_seconds=60, native_stack_mib=8, 28 haskell_rts=['-N1','-M512m','-K8m'], zig_address_space_mib=512), 29 notes=[ 30 'Identical normal-form fixture functions and canonical input values; fresh application graphs each iteration.', 31 'Primary metric includes WHNF reduction, complete output decoding/checking and automatic GC.', 32 'Input construction is separate; Haskell forces input normal forms and uses opaque suspension boxes to prepare unevaluated applications.', 33 'Native allocations, FFI, codec implementations and automatic GC differ. All automatic GC remains charged to its triggering phase.', 34 'Explicit GC follows identical round cadence; total sums build, reduction, observation and explicit GC phases.', 35 'Memory is complete-process VmHWM, including runtime initialization.', 36 'Five fresh-process samples per case, sequential execution, alternating host order, single CPU affinity.' 37 ], cases=[]) 38 def save(): 39 Path(output).write_text(json.dumps(report, indent=2)+'\n') 40 save() 41 for kind,n,rounds in cases: 42 samples = {host: [] for host in ('haskell','zig')} 43 fixture = fixtures / ('append.arboricx' if kind == 'append' else 'id.arboricx') 44 for repeat in range(5): 45 for host in (('haskell','zig') if repeat % 2 == 0 else ('zig','haskell')): 46 command = [haskell if host == 'haskell' else zig, kind, str(n), str(rounds), str(fixture)] 47 if host == 'haskell': command += ['+RTS','-N1','-M512m','-K8m','-RTS'] 48 start = time.monotonic() 49 text, sampled_peak = run_guarded(command, memory_mib=512, seconds=65) 50 sample = json.loads(text) 51 assert (sample['kind'],sample['n'],sample['rounds']) == (kind,n,rounds) 52 assert 0 < sample['peak_rss_kib'] <= 512*1024 53 sample['process_wall_s'] = time.monotonic()-start 54 sample['sampled_peak_rss_kib'] = sampled_peak 55 samples[host].append(sample) 56 row = dict(kind=kind,n=n,rounds=rounds,hosts={}) 57 for host,values in samples.items(): 58 metrics = {} 59 for label,fields in dict(evaluate_observe=['reduce_s','observe_s'], 60 total=['build_s','reduce_s','observe_s','gc_s'], 61 build=['build_s'], whnf=['reduce_s'], observe=['observe_s'], gc=['gc_s']).items(): 62 series = [sum(s[f] for f in fields)*1000/rounds for s in values] 63 metrics[label] = dict(median=statistics.median(series),min=min(series),max=max(series)) 64 row['hosts'][host] = dict(samples=values, ms_per_iteration=metrics, 65 max_peak_rss_mib=max(s['peak_rss_kib'] for s in values)/1024) 66 h,z = (row['hosts'][host]['ms_per_iteration']['evaluate_observe']['median'] for host in ('haskell','zig')) 67 row['zig_over_haskell_evaluate_observe'] = z/h 68 report['cases'].append(row) 69 save() 70 print(f'{kind}/{n} x{rounds}: Haskell {h:.4f} ms, Zig {z:.4f} ms; Zig/Haskell {z/h:.2f}x', flush=True) 71 report['complete'] = True 72 save()