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.
- asmdiff-0.2.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
- asmdiff-0.2.0/2026-07-07-table-layout-handoff.md +90 -0
- asmdiff-0.1.6/README.md → asmdiff-0.2.0/PKG-INFO +112 -36
- asmdiff-0.1.6/PKG-INFO → asmdiff-0.2.0/README.md +94 -54
- asmdiff-0.2.0/Screenshot 2026-07-07 225145.jpg +0 -0
- asmdiff-0.2.0/amy-hotspot-analysis.md +214 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/asmdiff.py +76 -31
- asmdiff-0.2.0/demo/esp32-matrix.gif +0 -0
- asmdiff-0.2.0/demo/quickstart.gif +0 -0
- asmdiff-0.2.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +277 -0
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +147 -0
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +16 -0
- asmdiff-0.2.0/gif-plan.md +248 -0
- asmdiff-0.2.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +3 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/pyproject.toml +2 -2
- {asmdiff-0.1.6 → asmdiff-0.2.0}/test_asmdiff.py +110 -8
- {asmdiff-0.1.6 → asmdiff-0.2.0}/.claude/settings.local.json +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/.gitignore +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/2026-07-06-inspect-mode.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/2026-07-07-elf-mode-handoff.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/CLAUDE.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/FIELD-TEST-REPORT.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/LICENSE +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/asmdiff.example.toml +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/asmdiff_example.c +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/.gitignore +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.2.0}/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -0
|
File without changes
|
|
@@ -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
|
|
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
|
+
[](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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
202
|
-
apply_gain 13
|
|
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
|
|
232
|
-
render_lut_cub 98
|
|
233
|
-
render_lut 51
|
|
234
|
-
render_external_audio_in 32
|
|
235
|
-
render_partial 112 exp2f, __divsf3, ...
|
|
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:
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
266
|
-
sequencer_timer_callback$lto_priv$0 77 __divsf3, __udivdi3
|
|
267
|
-
sequencer_recompute 46 __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
|
|
268
|
-
sequencer_process_tick$lto_priv$0 117
|
|
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
|
+
[](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
|
|
611
|
-
stereo_reverb 437
|
|
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
|
-
`...,
|
|
629
|
-
symbols and would otherwise make rows thousands of characters
|
|
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
|
|
691
|
-
dsps_biquad_f32_ansi [cc#1] baseline 59
|
|
692
|
-
dsps_biquad_f32_ansi [cc#2] candidate 89
|
|
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
|
|
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
|
|
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
|
+
[](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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
220
|
-
apply_gain 13
|
|
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
|
|
250
|
-
render_lut_cub 98
|
|
251
|
-
render_lut 51
|
|
252
|
-
render_external_audio_in 32
|
|
253
|
-
render_partial 112 exp2f, __divsf3, ...
|
|
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:
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
284
|
-
sequencer_timer_callback$lto_priv$0 77 __divsf3, __udivdi3
|
|
285
|
-
sequencer_recompute 46 __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
|
|
286
|
-
sequencer_process_tick$lto_priv$0 117
|
|
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
|
+
[](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
|
|
629
|
-
stereo_reverb 437
|
|
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
|
-
`...,
|
|
647
|
-
symbols and would otherwise make rows thousands of characters
|
|
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
|
|
709
|
-
dsps_biquad_f32_ansi [cc#1] baseline 59
|
|
710
|
-
dsps_biquad_f32_ansi [cc#2] candidate 89
|
|
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
|
|
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
|
|
Binary file
|