asmdiff 0.1.6__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. asmdiff-0.2.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
  2. asmdiff-0.2.0/2026-07-07-table-layout-handoff.md +90 -0
  3. asmdiff-0.1.6/README.md → asmdiff-0.2.0/PKG-INFO +112 -36
  4. asmdiff-0.1.6/PKG-INFO → asmdiff-0.2.0/README.md +94 -54
  5. asmdiff-0.2.0/Screenshot 2026-07-07 225145.jpg +0 -0
  6. asmdiff-0.2.0/amy-hotspot-analysis.md +214 -0
  7. {asmdiff-0.1.6 → asmdiff-0.2.0}/asmdiff.py +76 -31
  8. asmdiff-0.2.0/demo/esp32-matrix.gif +0 -0
  9. asmdiff-0.2.0/demo/quickstart.gif +0 -0
  10. asmdiff-0.2.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +277 -0
  11. asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +147 -0
  12. asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +16 -0
  13. asmdiff-0.2.0/gif-plan.md +248 -0
  14. asmdiff-0.2.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +3 -0
  15. {asmdiff-0.1.6 → asmdiff-0.2.0}/pyproject.toml +2 -2
  16. {asmdiff-0.1.6 → asmdiff-0.2.0}/test_asmdiff.py +110 -8
  17. {asmdiff-0.1.6 → asmdiff-0.2.0}/.claude/settings.local.json +0 -0
  18. {asmdiff-0.1.6 → asmdiff-0.2.0}/.gitignore +0 -0
  19. {asmdiff-0.1.6 → asmdiff-0.2.0}/2026-07-06-inspect-mode.md +0 -0
  20. {asmdiff-0.1.6 → asmdiff-0.2.0}/2026-07-07-elf-mode-handoff.md +0 -0
  21. {asmdiff-0.1.6 → asmdiff-0.2.0}/CLAUDE.md +0 -0
  22. {asmdiff-0.1.6 → asmdiff-0.2.0}/FIELD-TEST-REPORT.md +0 -0
  23. {asmdiff-0.1.6 → asmdiff-0.2.0}/LICENSE +0 -0
  24. {asmdiff-0.1.6 → asmdiff-0.2.0}/asmdiff.example.toml +0 -0
  25. {asmdiff-0.1.6 → asmdiff-0.2.0}/asmdiff_example.c +0 -0
  26. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/.gitignore +0 -0
  27. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
  28. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
  29. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
  30. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
  31. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
  32. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
  33. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
  34. {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
  35. {asmdiff-0.1.6 → asmdiff-0.2.0}/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -0
@@ -0,0 +1,90 @@
1
+ # asmdiff summary-table legibility - checkpoint 2026-07-07
2
+
3
+ Handoff for a fresh session in THIS repo (`~/esp-idf/tools/asmdiff`).
4
+ Feature is COMPLETE and verified, NOTHING COMMITTED. Origin: ELF-mode
5
+ output (`asmdiff build/S3-Amysynth.elf --filter 'core_'`) was unreadable -
6
+ every row hard-wrapped in the terminal (screenshot in repo root,
7
+ `Screenshot 2026-07-07 225145.jpg`).
8
+
9
+ ## Root cause
10
+
11
+ `render_table` pads every column to its widest cell. The `calls` column
12
+ sat in the middle; with real firmware call lists (~150 chars at the
13
+ 8-callee cap) plus 50-char LTO symbol names, `loop spans` started past
14
+ column ~210 and every row wrapped: dangling `-` lines, split headers,
15
+ call lists colliding with the next function name.
16
+
17
+ ## What changed (asmdiff.py)
18
+
19
+ 1. **Column order**: all three builders now emit
20
+ `function / [role] / insns / loop spans / calls` - calls (the one
21
+ unbounded column) moved last, ragged right. Builders:
22
+ `summary_table`, `file_summary_table` (TOTAL row spans cell stays
23
+ `-`), `inspect_table`. Each gained a `max_width=None` passthrough
24
+ param.
25
+ 2. **Width-aware trim**: `render_table(rows, max_width=None)` - when
26
+ set, fixed columns keep full width, the last column's over-budget
27
+ cells are trimmed by `_fit_calls`: whole callees dropped from the
28
+ end, closed with `Total Calls:N` (N = full callee count, preserved
29
+ from an existing summary). The FIRST callee is never dropped - a
30
+ single huge symbol overflows/wraps instead of vanishing
31
+ (`sequencer_core_set_melodic_envelope2` is the real-world case).
32
+ Cells that fit, `-`, and headers are untouched. `TOTAL_CALLS_RE`
33
+ guards summary parsing.
34
+ 3. **`format_calls`** cap suffix changed from `+N more` to
35
+ `Total Calls:N` (user-requested format; one consistent suffix for
36
+ both the 8-callee cap and the width trim).
37
+ 4. **`table_width()`**: `shutil.get_terminal_size().columns` when
38
+ `sys.stdout.isatty()`, else `None` - piped/redirected output skips
39
+ the trim so it stays deterministic and greppable. Passed at all six
40
+ print sites (both summary_table sites, both file_summary_table
41
+ sites, both inspect_table sites).
42
+
43
+ All new code sits together around `render_table` (~line 1053 region).
44
+
45
+ ## Tests (test_asmdiff.py)
46
+
47
+ - New `TestTableMaxWidth` (9 tests): trim-to-fit, fitting cell left
48
+ alone, merges existing `Total Calls:N` (does not recount), single
49
+ callee never dropped, dash untouched, `max_width=None` unchanged,
50
+ builder passthrough, `table_width()` tty/piped (via `mock.patch` on
51
+ `asmdiff.sys.stdout.isatty` / `asmdiff.shutil.get_terminal_size`).
52
+ - Updated for new column order / suffix: `TestFormatCalls`
53
+ `test_long_list_capped`, `test_summary_table`,
54
+ `test_summary_table_loop_spans_column`,
55
+ `test_file_summary_totals_and_call_union`, both
56
+ `TestInspectRendering` inspect_table tests.
57
+ - Suite: 191 tests, sole failure the KNOWN env-dependent
58
+ `TestShortAliases.test_p_is_pair` (global ~/.config/asmdiff.toml with
59
+ target esp32s3 short-circuits it; fails at HEAD too).
60
+
61
+ ## Docs
62
+
63
+ README: all 8 example tables reordered; the truncation paragraph (ELF
64
+ section, "Only labels the assembler types...") now documents
65
+ `Total Calls:N`, the tty-only width trim, and the piped-output
66
+ exemption; intro prose order updated. Module docstring summary line
67
+ reordered to match. No flags or config keys changed, so
68
+ `asmdiff.example.toml` / `EXAMPLE_CONFIG` are untouched (the
69
+ byte-match unit test still applies to any future config edit).
70
+
71
+ ## Verification done
72
+
73
+ - Full suite as above.
74
+ - Real ELF (`~/esp-idf/S3-Amysynth/build/S3-Amysynth.elf`, config-derived
75
+ xtensa objdump), `--filter 'core_'`:
76
+ - piped: full capped lists, new column order, aligned.
77
+ - fake tty via `script -qec "stty cols 190 ..."`: all rows one line
78
+ except the designed first-callee-floor overflow (4 chars over).
79
+ - 110-col tty: fixed columns alone can exceed width when a loopy
80
+ function widens `loop spans`; calls floor at one callee - accepted
81
+ graceful degradation, only `calls` ever trims (scope decision).
82
+
83
+ ## Next steps
84
+
85
+ 1. Commit (user has not asked yet; suggested scope: asmdiff.py,
86
+ test_asmdiff.py, README.md in one commit - pure output-format
87
+ change, no behavior change to analysis).
88
+ 2. Optional follow-up the user explicitly deferred: making the
89
+ `loop spans` cells themselves friendlier (e.g. "2 ZOL + 3 br"
90
+ summary) - scoped OUT of this change ("layout only").
@@ -1,7 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: asmdiff
3
+ Version: 0.2.0
4
+ Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
5
+ Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
+ Project-URL: Repository, https://github.com/rt-rtos/asmdiff
7
+ Author: Rasmus Tikkanen
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Compilers
15
+ Classifier: Topic :: Software Development :: Debuggers
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+
1
19
  # [asmdiff](https://pypi.org/project/asmdiff/)
2
- ## per-function assembly comparison for paired C implementations
20
+ ## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
21
+
22
+ > asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for
23
+ investigating compiler code generation rather than benchmarking runtime performance.
24
+
25
+ <details>
26
+
27
+ <summary>Quickstart Example</summary>
28
+
29
+ [![asmdiff comparing gcc and clang assembly for paired C functions, with a fold-vs-libcall summary](demo/quickstart.gif)](demo/quickstart.gif)
30
+
31
+ *Each function appears twice - `old_*` and `new_*`, the same routine
32
+ before and after a rewrite - compiled across a gcc/clang matrix and diffed
33
+ side by side. The summary's `insns` and `calls` columns are the payoff:
34
+ `old_rt`'s 9-instruction `exp2f` call collapses to `new_rt`'s 2-instruction
35
+ `ldexpf`. (Click to enlarge.)*
3
36
 
4
- > asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
37
+ ---
38
+
39
+ </details>
5
40
 
6
41
  ### Try it yourself:
7
42
 
@@ -12,7 +47,7 @@
12
47
  does the compiler actually emit - before and after?** It compiles a small
13
48
  harness file across a matrix of compilers, extracts each variant function's
14
49
  assembly, and prints side-by-side listings plus a summary of instruction
15
- counts, outbound calls, and loop spans.
50
+ counts, loop spans, and outbound calls.
16
51
 
17
52
  Compilers and flags are configured per project through named targets in an
18
53
  `asmdiff.toml` file, and any GNU-as ELF assembly is parsed.
@@ -76,9 +111,9 @@ endbr64 | endbr64
76
111
  mulss .LC0(%rip), %xmm0 | movl $-5, %edi
77
112
  ret | jmp ldexpf@PLT
78
113
 
79
- function role insns calls loop spans
80
- old_scale baseline 3 - -
81
- new_scale candidate 3 ldexpf -
114
+ function role insns loop spans calls
115
+ old_scale baseline 3 - -
116
+ new_scale candidate 3 - ldexpf
82
117
  ```
83
118
 
84
119
  Read the `calls` column first: `-` means the construct lowered to inline
@@ -139,9 +174,9 @@ call8 __udivdi3 |
139
174
  mov.n a2, a10 |
140
175
  retw.n |
141
176
 
142
- function role insns calls loop spans
143
- old_elapsed_ms baseline 10 __udivdi3 -
144
- new_elapsed_ms candidate 6 - -
177
+ function role insns loop spans calls
178
+ old_elapsed_ms baseline 10 - __udivdi3
179
+ new_elapsed_ms candidate 6 - -
145
180
  ```
146
181
 
147
182
  The candidate is six inline instructions ending in a multiply-high by
@@ -198,8 +233,8 @@ apply_gain:
198
233
  .L1:
199
234
  retw.n
200
235
 
201
- function insns calls loop spans
202
- apply_gain 13 - .L3_LEND:4
236
+ function insns loop spans calls
237
+ apply_gain 13 .L3_LEND:4 -
203
238
  ```
204
239
 
205
240
  The span `.L3_LEND:4` is the four-instruction body of the Xtensa
@@ -228,11 +263,11 @@ $ asmdiff build/S3-Amysynth.elf --filter '^render_' # stats table only
228
263
  ```
229
264
 
230
265
  ```
231
- function insns calls loop spans
232
- render_lut_cub 98 - .L32:74
233
- render_lut 51 - .L7e_LEND:32
234
- render_external_audio_in 32 - .L52_LEND:10
235
- render_partial 112 exp2f, __divsf3, ... .L66:31
266
+ function insns loop spans calls
267
+ render_lut_cub 98 .L32:74 -
268
+ render_lut 51 .L7e_LEND:32 -
269
+ render_external_audio_in 32 .L52_LEND:10 -
270
+ render_partial 112 .L66:31 exp2f, __divsf3, ...
236
271
  ```
237
272
 
238
273
  (Four of the eleven matched rows shown.) The columns are the same as
@@ -255,23 +290,29 @@ everywhere else; two things are ELF-specific:
255
290
  evidence is missing or the register is overwritten in between -
256
291
  a genuine function-pointer dispatch still reads as indirect.
257
292
 
258
- What this looks like on real firmware: the same binary's sequencer
259
- core (project code, not a synth library) surfaces three findings in
260
- one row scan.
293
+ What this looks like on real firmware: this binary's sequencer path -
294
+ here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
295
+ with the divide sourced from `amy_sysclock()` in `api.c`) - surfaces
296
+ three findings in one row scan.
261
297
 
262
298
  ```
263
299
  $ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
264
300
 
265
- function insns calls loop spans
266
- sequencer_timer_callback$lto_priv$0 77 __divsf3, __udivdi3 .L21:41
267
- sequencer_recompute 46 __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3 -
268
- sequencer_process_tick$lto_priv$0 117 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8 .L2e:96 .L48:85 .L8d:6
301
+ function insns loop spans calls
302
+ sequencer_timer_callback$lto_priv$0 77 .L21:41 __divsf3, __udivdi3
303
+ sequencer_recompute 46 - __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
304
+ sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8
269
305
  ```
270
306
 
271
307
  A timer callback pays a software float divide *and* a 64-bit
272
308
  `__udivdi3` inside its 41-instruction loop - the
273
309
  [silent software divide](#example-catching-a-silent-software-divide)
274
- pattern, caught in shipped firmware instead of a harness.
310
+ pattern, caught in shipped firmware instead of a harness. asmdiff was
311
+ built to hunt exactly this in AMY, and this callback was the payoff: the
312
+ pairing was confirmed as both a codegen cost and an upstream correctness
313
+ bug - `amy_sysclock()`'s `uint32_t` sample count wraps far sooner than
314
+ its comment claims and loses precision on long uptimes - and is fixed
315
+ locally with a PR pending to shorepine/amy.
275
316
  `sequencer_recompute`'s `__extendsfdf2 -> __divdf3 -> __muldf3` chain
276
317
  is the double-promotion smell (typically an unsuffixed `60.0`-style
277
318
  literal) on a chip whose FPU is single-precision only. And the
@@ -535,6 +576,35 @@ Details that make it robust:
535
576
 
536
577
  ### Auto-discovering the database
537
578
 
579
+ <details>
580
+
581
+ <summary>Real Project Example</summary>
582
+
583
+ #### All Esp32 Xtensa toolchains in one call from the same compile_commands
584
+
585
+ Below, `-db` (short for `--compile-commands`) does this on a real ESP-IDF
586
+ synth firmware: one `synth_core` effects source, compiled across the
587
+ esp32 / esp32s2 / esp32s3 cross-toolchains in a single run, with each
588
+ chip's include/define flags auto-borrowed from `build/compile_commands.json`
589
+ so the component actually compiles. No `--target` and no path are given -
590
+ the config's `default` names the three chips and `-db` walks up to find the
591
+ database. Reading it left to right: the three `==` banners are the three
592
+ toolchains; each table is that chip's per-function `insns` / loop spans /
593
+ `calls`. The tell is in the `calls` column - the esp32-S2 (the one variant
594
+ with no hardware FPU) picks up softfloat conversion libcalls
595
+ `__floatsisf`, `__floatunsisf`, `__ltsf2` that its FPU-equipped siblings
596
+ never emit, so the same source is measurably heavier there:
597
+
598
+ [![asmdiff running a three-chip xtensa matrix (esp32/s2/s3) on one firmware source via -db](demo/esp32-matrix.gif)](demo/esp32-matrix.gif)
599
+
600
+ *One source, three real chip toolchains, build flags borrowed
601
+ automatically - the kind of comparison that otherwise needs three separate
602
+ build trees. (Click to enlarge.)*
603
+
604
+ ---
605
+
606
+ </details>
607
+
538
608
  When you run asmdiff from inside the project anyway, the path is
539
609
  redundant. Two opt-ins skip it:
540
610
 
@@ -550,6 +620,7 @@ asmdiff biquad.c --compile-commands # same, for any matrix
550
620
  asmdiff biquad.c --compile-commands path/to/compile_commands.json
551
621
  ```
552
622
 
623
+
553
624
  Both walk up from the current directory, checking each level for
554
625
  `compile_commands.json` and then `build/compile_commands.json` (where
555
626
  CMake and idf.py leave it) — first hit wins. The walk stops at the
@@ -607,14 +678,14 @@ $ asmdiff old/delay.c new/delay.c
607
678
 
608
679
  -- old/delay.c --
609
680
 
610
- function insns calls loop spans
611
- stereo_reverb 437 - .L108:327
681
+ function insns loop spans calls
682
+ stereo_reverb 437 .L108:327 -
612
683
  ...
613
- TOTAL (13 functions) 956 malloc_caps, free, ... -
684
+ TOTAL (13 functions) 956 - malloc_caps, free, ...
614
685
 
615
686
  -- new/delay.c --
616
687
  ...
617
- TOTAL (13 functions) 1028 malloc_caps, free, ... -
688
+ TOTAL (13 functions) 1028 - malloc_caps, free, ...
618
689
  ```
619
690
 
620
691
  The TOTAL row is a coarse sanity check — did this refactor move the file's
@@ -625,8 +696,13 @@ measurement, and per-function rows are where the real information is.
625
696
  Only labels the assembler types as functions are listed — global data
626
697
  (string constants, state structs, lookup tables) gets column-0 labels too
627
698
  but is not code. A `calls` list longer than 8 symbols is truncated to
628
- `..., +N more`; real firmware dispatch functions call dozens of distinct
629
- symbols and would otherwise make rows thousands of characters wide.
699
+ `..., Total Calls:N`; real firmware dispatch functions call dozens of
700
+ distinct symbols and would otherwise make rows thousands of characters
701
+ wide. When stdout is a terminal, rows are additionally trimmed to the
702
+ terminal width: callees are dropped from the end of the `calls` column
703
+ (never the first one) behind the same `Total Calls:N` summary, so every
704
+ row stays on one line. Piped or redirected output skips the width trim
705
+ and keeps the full capped list, so it stays stable and greppable.
630
706
 
631
707
  ## Comparing the same function across two builds (`--across`)
632
708
 
@@ -687,9 +763,9 @@ movl %r14d, %r11d | sarl $11, %ebp
687
763
  call SMULR6 | leal 1024(%rax), %edx
688
764
  [... 60 rows omitted ...]
689
765
 
690
- function role insns calls loop spans
691
- dsps_biquad_f32_ansi [cc#1] baseline 59 SMULR6 .L27:32
692
- dsps_biquad_f32_ansi [cc#2] candidate 89 - .L26:54
766
+ function role insns loop spans calls
767
+ dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
768
+ dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
693
769
  ```
694
770
 
695
771
  (The listing is abridged here; the tool prints all 92 rows. The columns
@@ -725,9 +801,9 @@ call ldexpf@PLT |
725
801
  leave |
726
802
  ret |
727
803
 
728
- function role insns calls loop spans
729
- new_rt [cc#1] baseline 13 ldexpf -
730
- new_rt [cc#2] candidate 2 ldexpf -
804
+ function role insns loop spans calls
805
+ new_rt [cc#1] baseline 13 - ldexpf
806
+ new_rt [cc#2] candidate 2 - ldexpf
731
807
  ```
732
808
 
733
809
  **Two files** — before/after versions of a source file (e.g. from a git
@@ -1,25 +1,24 @@
1
- Metadata-Version: 2.4
2
- Name: asmdiff
3
- Version: 0.1.6
4
- Summary: Compare per-function assembly between paired C implementations
5
- Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
- Project-URL: Repository, https://github.com/rt-rtos/asmdiff
7
- Author: Rasmus Tikkanen
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
11
- Classifier: Environment :: Console
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Topic :: Software Development :: Compilers
15
- Classifier: Topic :: Software Development :: Debuggers
16
- Requires-Python: >=3.8
17
- Description-Content-Type: text/markdown
18
-
19
1
  # [asmdiff](https://pypi.org/project/asmdiff/)
20
- ## per-function assembly comparison for paired C implementations
2
+ ## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
3
+
4
+ > asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for
5
+ investigating compiler code generation rather than benchmarking runtime performance.
6
+
7
+ <details>
8
+
9
+ <summary>Quickstart Example</summary>
10
+
11
+ [![asmdiff comparing gcc and clang assembly for paired C functions, with a fold-vs-libcall summary](demo/quickstart.gif)](demo/quickstart.gif)
12
+
13
+ *Each function appears twice - `old_*` and `new_*`, the same routine
14
+ before and after a rewrite - compiled across a gcc/clang matrix and diffed
15
+ side by side. The summary's `insns` and `calls` columns are the payoff:
16
+ `old_rt`'s 9-instruction `exp2f` call collapses to `new_rt`'s 2-instruction
17
+ `ldexpf`. (Click to enlarge.)*
21
18
 
22
- > asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
19
+ ---
20
+
21
+ </details>
23
22
 
24
23
  ### Try it yourself:
25
24
 
@@ -30,7 +29,7 @@ Description-Content-Type: text/markdown
30
29
  does the compiler actually emit - before and after?** It compiles a small
31
30
  harness file across a matrix of compilers, extracts each variant function's
32
31
  assembly, and prints side-by-side listings plus a summary of instruction
33
- counts, outbound calls, and loop spans.
32
+ counts, loop spans, and outbound calls.
34
33
 
35
34
  Compilers and flags are configured per project through named targets in an
36
35
  `asmdiff.toml` file, and any GNU-as ELF assembly is parsed.
@@ -94,9 +93,9 @@ endbr64 | endbr64
94
93
  mulss .LC0(%rip), %xmm0 | movl $-5, %edi
95
94
  ret | jmp ldexpf@PLT
96
95
 
97
- function role insns calls loop spans
98
- old_scale baseline 3 - -
99
- new_scale candidate 3 ldexpf -
96
+ function role insns loop spans calls
97
+ old_scale baseline 3 - -
98
+ new_scale candidate 3 - ldexpf
100
99
  ```
101
100
 
102
101
  Read the `calls` column first: `-` means the construct lowered to inline
@@ -157,9 +156,9 @@ call8 __udivdi3 |
157
156
  mov.n a2, a10 |
158
157
  retw.n |
159
158
 
160
- function role insns calls loop spans
161
- old_elapsed_ms baseline 10 __udivdi3 -
162
- new_elapsed_ms candidate 6 - -
159
+ function role insns loop spans calls
160
+ old_elapsed_ms baseline 10 - __udivdi3
161
+ new_elapsed_ms candidate 6 - -
163
162
  ```
164
163
 
165
164
  The candidate is six inline instructions ending in a multiply-high by
@@ -216,8 +215,8 @@ apply_gain:
216
215
  .L1:
217
216
  retw.n
218
217
 
219
- function insns calls loop spans
220
- apply_gain 13 - .L3_LEND:4
218
+ function insns loop spans calls
219
+ apply_gain 13 .L3_LEND:4 -
221
220
  ```
222
221
 
223
222
  The span `.L3_LEND:4` is the four-instruction body of the Xtensa
@@ -246,11 +245,11 @@ $ asmdiff build/S3-Amysynth.elf --filter '^render_' # stats table only
246
245
  ```
247
246
 
248
247
  ```
249
- function insns calls loop spans
250
- render_lut_cub 98 - .L32:74
251
- render_lut 51 - .L7e_LEND:32
252
- render_external_audio_in 32 - .L52_LEND:10
253
- render_partial 112 exp2f, __divsf3, ... .L66:31
248
+ function insns loop spans calls
249
+ render_lut_cub 98 .L32:74 -
250
+ render_lut 51 .L7e_LEND:32 -
251
+ render_external_audio_in 32 .L52_LEND:10 -
252
+ render_partial 112 .L66:31 exp2f, __divsf3, ...
254
253
  ```
255
254
 
256
255
  (Four of the eleven matched rows shown.) The columns are the same as
@@ -273,23 +272,29 @@ everywhere else; two things are ELF-specific:
273
272
  evidence is missing or the register is overwritten in between -
274
273
  a genuine function-pointer dispatch still reads as indirect.
275
274
 
276
- What this looks like on real firmware: the same binary's sequencer
277
- core (project code, not a synth library) surfaces three findings in
278
- one row scan.
275
+ What this looks like on real firmware: this binary's sequencer path -
276
+ here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
277
+ with the divide sourced from `amy_sysclock()` in `api.c`) - surfaces
278
+ three findings in one row scan.
279
279
 
280
280
  ```
281
281
  $ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
282
282
 
283
- function insns calls loop spans
284
- sequencer_timer_callback$lto_priv$0 77 __divsf3, __udivdi3 .L21:41
285
- sequencer_recompute 46 __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3 -
286
- sequencer_process_tick$lto_priv$0 117 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8 .L2e:96 .L48:85 .L8d:6
283
+ function insns loop spans calls
284
+ sequencer_timer_callback$lto_priv$0 77 .L21:41 __divsf3, __udivdi3
285
+ sequencer_recompute 46 - __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
286
+ sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8
287
287
  ```
288
288
 
289
289
  A timer callback pays a software float divide *and* a 64-bit
290
290
  `__udivdi3` inside its 41-instruction loop - the
291
291
  [silent software divide](#example-catching-a-silent-software-divide)
292
- pattern, caught in shipped firmware instead of a harness.
292
+ pattern, caught in shipped firmware instead of a harness. asmdiff was
293
+ built to hunt exactly this in AMY, and this callback was the payoff: the
294
+ pairing was confirmed as both a codegen cost and an upstream correctness
295
+ bug - `amy_sysclock()`'s `uint32_t` sample count wraps far sooner than
296
+ its comment claims and loses precision on long uptimes - and is fixed
297
+ locally with a PR pending to shorepine/amy.
293
298
  `sequencer_recompute`'s `__extendsfdf2 -> __divdf3 -> __muldf3` chain
294
299
  is the double-promotion smell (typically an unsuffixed `60.0`-style
295
300
  literal) on a chip whose FPU is single-precision only. And the
@@ -553,6 +558,35 @@ Details that make it robust:
553
558
 
554
559
  ### Auto-discovering the database
555
560
 
561
+ <details>
562
+
563
+ <summary>Real Project Example</summary>
564
+
565
+ #### All Esp32 Xtensa toolchains in one call from the same compile_commands
566
+
567
+ Below, `-db` (short for `--compile-commands`) does this on a real ESP-IDF
568
+ synth firmware: one `synth_core` effects source, compiled across the
569
+ esp32 / esp32s2 / esp32s3 cross-toolchains in a single run, with each
570
+ chip's include/define flags auto-borrowed from `build/compile_commands.json`
571
+ so the component actually compiles. No `--target` and no path are given -
572
+ the config's `default` names the three chips and `-db` walks up to find the
573
+ database. Reading it left to right: the three `==` banners are the three
574
+ toolchains; each table is that chip's per-function `insns` / loop spans /
575
+ `calls`. The tell is in the `calls` column - the esp32-S2 (the one variant
576
+ with no hardware FPU) picks up softfloat conversion libcalls
577
+ `__floatsisf`, `__floatunsisf`, `__ltsf2` that its FPU-equipped siblings
578
+ never emit, so the same source is measurably heavier there:
579
+
580
+ [![asmdiff running a three-chip xtensa matrix (esp32/s2/s3) on one firmware source via -db](demo/esp32-matrix.gif)](demo/esp32-matrix.gif)
581
+
582
+ *One source, three real chip toolchains, build flags borrowed
583
+ automatically - the kind of comparison that otherwise needs three separate
584
+ build trees. (Click to enlarge.)*
585
+
586
+ ---
587
+
588
+ </details>
589
+
556
590
  When you run asmdiff from inside the project anyway, the path is
557
591
  redundant. Two opt-ins skip it:
558
592
 
@@ -568,6 +602,7 @@ asmdiff biquad.c --compile-commands # same, for any matrix
568
602
  asmdiff biquad.c --compile-commands path/to/compile_commands.json
569
603
  ```
570
604
 
605
+
571
606
  Both walk up from the current directory, checking each level for
572
607
  `compile_commands.json` and then `build/compile_commands.json` (where
573
608
  CMake and idf.py leave it) — first hit wins. The walk stops at the
@@ -625,14 +660,14 @@ $ asmdiff old/delay.c new/delay.c
625
660
 
626
661
  -- old/delay.c --
627
662
 
628
- function insns calls loop spans
629
- stereo_reverb 437 - .L108:327
663
+ function insns loop spans calls
664
+ stereo_reverb 437 .L108:327 -
630
665
  ...
631
- TOTAL (13 functions) 956 malloc_caps, free, ... -
666
+ TOTAL (13 functions) 956 - malloc_caps, free, ...
632
667
 
633
668
  -- new/delay.c --
634
669
  ...
635
- TOTAL (13 functions) 1028 malloc_caps, free, ... -
670
+ TOTAL (13 functions) 1028 - malloc_caps, free, ...
636
671
  ```
637
672
 
638
673
  The TOTAL row is a coarse sanity check — did this refactor move the file's
@@ -643,8 +678,13 @@ measurement, and per-function rows are where the real information is.
643
678
  Only labels the assembler types as functions are listed — global data
644
679
  (string constants, state structs, lookup tables) gets column-0 labels too
645
680
  but is not code. A `calls` list longer than 8 symbols is truncated to
646
- `..., +N more`; real firmware dispatch functions call dozens of distinct
647
- symbols and would otherwise make rows thousands of characters wide.
681
+ `..., Total Calls:N`; real firmware dispatch functions call dozens of
682
+ distinct symbols and would otherwise make rows thousands of characters
683
+ wide. When stdout is a terminal, rows are additionally trimmed to the
684
+ terminal width: callees are dropped from the end of the `calls` column
685
+ (never the first one) behind the same `Total Calls:N` summary, so every
686
+ row stays on one line. Piped or redirected output skips the width trim
687
+ and keeps the full capped list, so it stays stable and greppable.
648
688
 
649
689
  ## Comparing the same function across two builds (`--across`)
650
690
 
@@ -705,9 +745,9 @@ movl %r14d, %r11d | sarl $11, %ebp
705
745
  call SMULR6 | leal 1024(%rax), %edx
706
746
  [... 60 rows omitted ...]
707
747
 
708
- function role insns calls loop spans
709
- dsps_biquad_f32_ansi [cc#1] baseline 59 SMULR6 .L27:32
710
- dsps_biquad_f32_ansi [cc#2] candidate 89 - .L26:54
748
+ function role insns loop spans calls
749
+ dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
750
+ dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
711
751
  ```
712
752
 
713
753
  (The listing is abridged here; the tool prints all 92 rows. The columns
@@ -743,9 +783,9 @@ call ldexpf@PLT |
743
783
  leave |
744
784
  ret |
745
785
 
746
- function role insns calls loop spans
747
- new_rt [cc#1] baseline 13 ldexpf -
748
- new_rt [cc#2] candidate 2 ldexpf -
786
+ function role insns loop spans calls
787
+ new_rt [cc#1] baseline 13 - ldexpf
788
+ new_rt [cc#2] candidate 2 - ldexpf
749
789
  ```
750
790
 
751
791
  **Two files** — before/after versions of a source file (e.g. from a git