tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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()