// analyze-simd computes speedup ratios from aggregated pqc-bench results. // // Usage: // // analyze-simd [--baseline ref] [--in results.json] [--out speedups.json] // // It reads the JSON produced by 'aggregate', computes per-operation speedups // relative to the baseline variant, and emits both a human-readable table // and a structured JSON file suitable for downstream plotting. package main import ( "cmp" "encoding/json" "flag" "fmt" "math" "os" "slices" "strings" "text/tabwriter" ) // Record mirrors the aggregate output schema (fields we need). type Record struct { Algorithm string `json:"algorithm"` Variant string `json:"variant"` Operation string `json:"operation"` Median float64 `json:"median"` CI95 [2]float64 `json:"ci95"` NRuns int `json:"n_runs"` } // Speedup is one variant-vs-baseline comparison for a single (algorithm, operation). type Speedup struct { Variant string `json:"variant"` Median float64 `json:"median"` Speedup float64 `json:"speedup"` SpeedupCI [2]float64 `json:"speedup_ci95"` } // Result is one output row: all comparisons for one (algorithm, operation) pair. type Result struct { Algorithm string `json:"algorithm"` Operation string `json:"operation"` BaselineVariant string `json:"baseline_variant"` BaselineMedian float64 `json:"baseline_median"` BaselineCI95 [2]float64 `json:"baseline_ci95"` Comparisons []Speedup `json:"comparisons"` } func main() { baseline := flag.String("baseline", "ref", "variant to use as the speedup denominator") inFile := flag.String("in", "results/kyber.json", "input JSON from aggregate") outFile := flag.String("out", "", "write speedup JSON to this file (default: stdout)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: analyze-simd [--baseline VARIANT] [--in FILE] [--out FILE]\n") flag.PrintDefaults() } flag.Parse() raw, err := os.ReadFile(*inFile) if err != nil { fmt.Fprintf(os.Stderr, "error reading %s: %v\n", *inFile, err) os.Exit(1) } var records []Record if err := json.Unmarshal(raw, &records); err != nil { fmt.Fprintf(os.Stderr, "error parsing JSON: %v\n", err) os.Exit(1) } // Index by (algorithm, variant, operation). type key struct{ algorithm, variant, operation string } idx := make(map[key]Record, len(records)) for _, r := range records { idx[key{r.Algorithm, r.Variant, r.Operation}] = r } // Collect sorted unique values for stable output. algorithms := unique(records, func(r Record) string { return r.Algorithm }) operations := unique(records, func(r Record) string { return r.Operation }) variants := unique(records, func(r Record) string { return r.Variant }) // Remove baseline from comparison variants. variants = slices.DeleteFunc(variants, func(v string) bool { return v == *baseline }) // Build results. var results []Result for _, alg := range algorithms { for _, op := range operations { baseRec, ok := idx[key{alg, *baseline, op}] if !ok || baseRec.Median == 0 { continue } res := Result{ Algorithm: alg, Operation: op, BaselineVariant: *baseline, BaselineMedian: baseRec.Median, BaselineCI95: baseRec.CI95, } for _, v := range variants { cmpRec, ok := idx[key{alg, v, op}] if !ok || cmpRec.Median == 0 { continue } sp := baseRec.Median / cmpRec.Median // Conservative CI: ratio of interval bounds. // speedup_lo = baseline_lo / cmp_hi // speedup_hi = baseline_hi / cmp_lo var spCI [2]float64 if cmpRec.CI95[1] > 0 { spCI[0] = safeDiv(baseRec.CI95[0], cmpRec.CI95[1]) } if cmpRec.CI95[0] > 0 { spCI[1] = safeDiv(baseRec.CI95[1], cmpRec.CI95[0]) } res.Comparisons = append(res.Comparisons, Speedup{ Variant: v, Median: cmpRec.Median, Speedup: sp, SpeedupCI: spCI, }) } if len(res.Comparisons) > 0 { results = append(results, res) } } } // Print human-readable table to stderr. printTable(os.Stderr, results, variants, *baseline) // Emit JSON. out, err := json.MarshalIndent(results, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "error marshalling JSON: %v\n", err) os.Exit(1) } if *outFile != "" { if err := os.WriteFile(*outFile, out, 0o644); err != nil { fmt.Fprintf(os.Stderr, "error writing %s: %v\n", *outFile, err) os.Exit(1) } fmt.Fprintf(os.Stderr, "wrote %d results to %s\n", len(results), *outFile) } else { fmt.Println(string(out)) } } func printTable(w *os.File, results []Result, variants []string, baseline string) { tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) // Group by algorithm. byAlg := make(map[string][]Result) for _, r := range results { byAlg[r.Algorithm] = append(byAlg[r.Algorithm], r) } algs := make([]string, 0, len(byAlg)) for a := range byAlg { algs = append(algs, a) } slices.Sort(algs) for _, alg := range algs { fmt.Fprintf(tw, "\n── %s (baseline: %s) ──\n", strings.ToUpper(alg), baseline) // Header. var hdr strings.Builder fmt.Fprintf(&hdr, "%-38s\t%12s", "operation", baseline+"(cycles)") for _, v := range variants { fmt.Fprintf(&hdr, "\t%10s", v) } fmt.Fprintln(tw, hdr.String()) fmt.Fprintln(tw, strings.Repeat("-", 38+13+11*len(variants))) rows := byAlg[alg] slices.SortFunc(rows, func(a, b Result) int { // Sort by descending avx2 speedup if available, else alphabetically. sa := speedupFor(a, "avx2") sb := speedupFor(b, "avx2") if sa != sb { return cmp.Compare(sb, sa) // descending } return strings.Compare(a.Operation, b.Operation) }) for _, r := range rows { var line strings.Builder fmt.Fprintf(&line, "%-38s\t%12s", r.Operation, formatCycles(r.BaselineMedian)) for _, v := range variants { sp := speedupFor(r, v) if math.IsNaN(sp) { fmt.Fprintf(&line, "\t%10s", "---") } else { fmt.Fprintf(&line, "\t%9.2fx", sp) } } fmt.Fprintln(tw, line.String()) } } tw.Flush() } func speedupFor(r Result, variant string) float64 { for _, c := range r.Comparisons { if c.Variant == variant { return c.Speedup } } return math.NaN() } func formatCycles(c float64) string { if c >= 1_000_000 { return fmt.Sprintf("%.2fM", c/1_000_000) } if c >= 1_000 { return fmt.Sprintf("%.1fK", c/1_000) } return fmt.Sprintf("%.0f", c) } func safeDiv(a, b float64) float64 { if b == 0 { return 0 } return a / b } func unique(records []Record, fn func(Record) string) []string { seen := make(map[string]struct{}) for _, r := range records { seen[fn(r)] = struct{}{} } out := make([]string, 0, len(seen)) for k := range seen { out = append(out, k) } slices.Sort(out) return out }