asmdiff 0.3.3__tar.gz → 0.4.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.3.3 → asmdiff-0.4.0}/PKG-INFO +515 -131
- {asmdiff-0.3.3 → asmdiff-0.4.0}/README.md +514 -130
- {asmdiff-0.3.3 → asmdiff-0.4.0}/asmdiff.example.toml +45 -6
- asmdiff-0.4.0/asmdiff.py +3753 -0
- asmdiff-0.4.0/golden/across-two-files.txt +14 -0
- asmdiff-0.4.0/golden/elf-span-stats.txt +17 -0
- asmdiff-0.4.0/golden/elf.txt +14 -0
- asmdiff-0.4.0/golden/inspect-span-stats.txt +14 -0
- asmdiff-0.4.0/golden/inspect-two-cc.txt +15 -0
- asmdiff-0.4.0/golden/inspect.txt +11 -0
- asmdiff-0.4.0/golden/pairs-cost.txt +5 -0
- asmdiff-0.4.0/golden/pairs-s.txt +11 -0
- asmdiff-0.4.0/golden/pairs-two-cc.txt +25 -0
- asmdiff-0.4.0/golden/pairs.txt +10 -0
- asmdiff-0.4.0/golden/summary-two-files.txt +8 -0
- {asmdiff-0.3.3 → asmdiff-0.4.0}/pyproject.toml +7 -0
- asmdiff-0.4.0/test_asmdiff.py +3784 -0
- asmdiff-0.3.3/asmdiff.py +0 -2050
- asmdiff-0.3.3/test_asmdiff.py +0 -2406
- {asmdiff-0.3.3 → asmdiff-0.4.0}/.gitignore +0 -0
- {asmdiff-0.3.3 → asmdiff-0.4.0}/LICENSE +0 -0
- {asmdiff-0.3.3 → asmdiff-0.4.0}/asmdiff_example.c +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
|
|
5
5
|
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
6
|
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
@@ -76,6 +76,39 @@ the tool is a single stdlib-only file: `python3 asmdiff.py HARNESS.c`.
|
|
|
76
76
|
Requires Python >= 3.8; `asmdiff.toml` config files need >= 3.11
|
|
77
77
|
(stdlib `tomllib`).
|
|
78
78
|
|
|
79
|
+
### Shell completion
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
asmdiff --install-completion # bash, zsh or fish, taken from $SHELL
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This writes a completion script into the directory your shell already
|
|
86
|
+
autoloads from - `bash-completion`'s `completions/`,
|
|
87
|
+
`~/.config/fish/completions/`, or `~/.zfunc/` - and never touches an rc
|
|
88
|
+
file. A file asmdiff did not write is named and left alone rather than
|
|
89
|
+
replaced. zsh reads `~/.zfunc` only once it is on `fpath`, so the command
|
|
90
|
+
prints the two lines to add to `.zshrc` yourself:
|
|
91
|
+
|
|
92
|
+
```zsh
|
|
93
|
+
fpath=(~/.zfunc $fpath)
|
|
94
|
+
autoload -Uz compinit && compinit
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To keep everything in one rc file instead, `--completion SHELL` prints
|
|
98
|
+
the script to stdout:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
eval "$(asmdiff --completion bash)" # in .bashrc
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The flag list is read off the argument parser when the script is
|
|
105
|
+
generated, so a script regenerated after an upgrade cannot drift from the
|
|
106
|
+
tool. Completing `-t` offers the targets and groups from the config that
|
|
107
|
+
run would resolve - a `--config PATH` already on the line is honoured -
|
|
108
|
+
and a comma list is completed element by element. Each tab press starts
|
|
109
|
+
Python and parses the config: fine for a `pip`, `pipx` or `uv tool`
|
|
110
|
+
install, noticeably slow behind a cold `uvx` launcher.
|
|
111
|
+
|
|
79
112
|
**Portability:** pure-stdlib Python with nothing intentionally
|
|
80
113
|
platform-specific, developed and tested on Linux. On native Windows,
|
|
81
114
|
`HOME` is usually unset, so replace `$HOME` with `$USERPROFILE` (or
|
|
@@ -100,10 +133,21 @@ Run:
|
|
|
100
133
|
$ asmdiff myharness.c
|
|
101
134
|
```
|
|
102
135
|
|
|
103
|
-
Output
|
|
136
|
+
Output:
|
|
104
137
|
|
|
105
138
|
```
|
|
106
|
-
|
|
139
|
+
cc#1: gcc -O3
|
|
140
|
+
cc#2: clang -O3
|
|
141
|
+
|
|
142
|
+
target function role insns loop spans calls
|
|
143
|
+
cc#1 old_scale baseline 3 - -
|
|
144
|
+
cc#1 new_scale candidate 3 - ldexpf
|
|
145
|
+
cc#1 delta 0 - +ldexpf
|
|
146
|
+
cc#2 old_scale baseline 2 - -
|
|
147
|
+
cc#2 new_scale candidate 2 - ldexpf
|
|
148
|
+
cc#2 delta 0 - +ldexpf
|
|
149
|
+
|
|
150
|
+
== cc#1 ==
|
|
107
151
|
|
|
108
152
|
old_scale | new_scale
|
|
109
153
|
---------------------------------------------+---------------------------------------------
|
|
@@ -111,21 +155,36 @@ endbr64 | endbr64
|
|
|
111
155
|
mulss .LC0(%rip), %xmm0 | movl $-5, %edi
|
|
112
156
|
ret | jmp ldexpf@PLT
|
|
113
157
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
new_scale
|
|
158
|
+
== cc#2 ==
|
|
159
|
+
|
|
160
|
+
old_scale | new_scale
|
|
161
|
+
---------------------------------------------+---------------------------------------------
|
|
162
|
+
mulss .LCPI0_0(%rip), %xmm0 | movl $-5, %edi
|
|
163
|
+
retq | jmp ldexpf@PLT
|
|
117
164
|
```
|
|
118
165
|
|
|
166
|
+
Every run has the same three parts: a legend naming each row of the
|
|
167
|
+
compiler matrix, one table covering the whole matrix, then the listings
|
|
168
|
+
grouped under `== LABEL ==`. The label in the `target` column is the
|
|
169
|
+
config target a row came from; the two fallback compilers here are
|
|
170
|
+
unnamed, so they are numbered by their position in the matrix. A single
|
|
171
|
+
unnamed row (one `--cc`) is its own label, and then the legend and the
|
|
172
|
+
`target` column are both dropped - the blocks further down this README
|
|
173
|
+
show both shapes.
|
|
174
|
+
|
|
119
175
|
Read the `calls` column first: `-` means the construct lowered to inline
|
|
120
|
-
instructions; a symbol name means a libcall. The
|
|
121
|
-
the evidence.
|
|
176
|
+
instructions; a symbol name means a libcall. The listings under the table
|
|
177
|
+
are the evidence.
|
|
122
178
|
|
|
123
179
|
The `loop spans` column reports `label:N` for every local label that some
|
|
124
180
|
instruction branches back to: N instructions lie between the label and the
|
|
125
181
|
last backward branch targeting it. Whole-function `insns` charges loop-hoisting
|
|
126
182
|
changes for their one-time setup/writeback code; the span count is the part
|
|
127
183
|
that repeats. Which span is your hot loop — and how often it runs — the
|
|
128
|
-
listing and your source know, not the tool.
|
|
184
|
+
listing and your source know, not the tool. A function with more than
|
|
185
|
+
six spans shows the first six and a `+N more` tail, since past that the
|
|
186
|
+
column stops being readable and the count is what is left to say;
|
|
187
|
+
`--json` carries every span.
|
|
129
188
|
|
|
130
189
|
A worked example is included — `asmdiff_example.c` reproduces the
|
|
131
190
|
exp2f/ldexpf analysis for both constant and runtime shift amounts:
|
|
@@ -159,7 +218,10 @@ uint32_t new_elapsed_ms(uint64_t now, uint64_t then) {
|
|
|
159
218
|
```
|
|
160
219
|
$ asmdiff elapsed.c --cc 'xtensa-esp32s3-elf-gcc -O2 -mlongcalls'
|
|
161
220
|
|
|
162
|
-
|
|
221
|
+
function role insns loop spans calls
|
|
222
|
+
old_elapsed_ms baseline 10 - __udivdi3
|
|
223
|
+
new_elapsed_ms candidate 6 - -
|
|
224
|
+
delta -4 - -__udivdi3
|
|
163
225
|
|
|
164
226
|
old_elapsed_ms | new_elapsed_ms
|
|
165
227
|
---------------------------------------------+---------------------------------------------
|
|
@@ -173,12 +235,11 @@ sub a11, a3, a11 |
|
|
|
173
235
|
call8 __udivdi3 |
|
|
174
236
|
mov.n a2, a10 |
|
|
175
237
|
retw.n |
|
|
176
|
-
|
|
177
|
-
function role insns loop spans calls
|
|
178
|
-
old_elapsed_ms baseline 10 - __udivdi3
|
|
179
|
-
new_elapsed_ms candidate 6 - -
|
|
180
238
|
```
|
|
181
239
|
|
|
240
|
+
One `--cc` row is its own label, so this run prints neither a legend nor
|
|
241
|
+
a `target` column and the listing needs no `== LABEL ==` header.
|
|
242
|
+
|
|
182
243
|
The candidate is six inline instructions ending in a multiply-high by
|
|
183
244
|
the reciprocal constant in `.LC0`. The baseline hands the division to
|
|
184
245
|
`__udivdi3` - a software divide loop whose cost the visible 10
|
|
@@ -198,11 +259,12 @@ the file - no harness, no pairing:
|
|
|
198
259
|
|
|
199
260
|
$ asmdiff src/oscillators.c render_lut
|
|
200
261
|
|
|
201
|
-
With one usable compiler you get
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
262
|
+
With one usable compiler you get a stats row and the function's
|
|
263
|
+
listing under it; with exactly two (the default gcc + clang matrix)
|
|
264
|
+
the two listings are set side by side; with a bigger matrix the table
|
|
265
|
+
covers the whole matrix and each row's listing follows under its own
|
|
266
|
+
`== LABEL ==`. `-l list` / `-l side-by-side` forces a presentation.
|
|
267
|
+
Several function names can be given at once.
|
|
206
268
|
|
|
207
269
|
A bare name is inspected as a function; an argument that exists on
|
|
208
270
|
disk is a second source file, and a path-looking argument that does
|
|
@@ -215,6 +277,9 @@ like this:
|
|
|
215
277
|
```
|
|
216
278
|
$ asmdiff dsp_util.c apply_gain --cc 'xtensa-esp32s3-elf-gcc -O2 -mlongcalls'
|
|
217
279
|
|
|
280
|
+
function insns loop spans calls
|
|
281
|
+
apply_gain 13 .L3_LEND:4 -
|
|
282
|
+
|
|
218
283
|
apply_gain:
|
|
219
284
|
entry sp, 32
|
|
220
285
|
wfr f1, a4
|
|
@@ -232,9 +297,6 @@ apply_gain:
|
|
|
232
297
|
.L3_LEND:
|
|
233
298
|
.L1:
|
|
234
299
|
retw.n
|
|
235
|
-
|
|
236
|
-
function insns loop spans calls
|
|
237
|
-
apply_gain 13 .L3_LEND:4 -
|
|
238
300
|
```
|
|
239
301
|
|
|
240
302
|
The span `.L3_LEND:4` is the four-instruction body of the Xtensa
|
|
@@ -283,12 +345,22 @@ everywhere else; two things are ELF-specific:
|
|
|
283
345
|
body as a ZOL, `render_lut_cub` fell back to a 74-instruction
|
|
284
346
|
branch loop.
|
|
285
347
|
- **`-mlongcalls` calls are resolved.** An out-of-range call survives
|
|
286
|
-
linking as `l32r a8, <lit>` + `callx8 a8`, which
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
348
|
+
linking as `l32r a8, <lit>` + `callx8 a8`, which has no callee name
|
|
349
|
+
of its own. When the disassembly annotates the literal with its
|
|
350
|
+
value (`l32r a8, ... (40002274 <__divsf3>)`), the real callee is
|
|
351
|
+
reported instead. When the evidence is missing or the register is
|
|
352
|
+
overwritten in between, the listing keeps the raw `callx8 a8` and
|
|
353
|
+
the `calls` column reports `indirect(a8)` - a genuine
|
|
354
|
+
function-pointer dispatch still reads as indirect, without the
|
|
355
|
+
register name masquerading as a symbol.
|
|
356
|
+
- **Nearest-symbol noise is stripped.** objdump names every address
|
|
357
|
+
after the closest preceding symbol, so a literal pool word renders
|
|
358
|
+
as e.g. `l32r a8, <_stext+0x44> (<some_font_table+0x707ad9>)` - two
|
|
359
|
+
unrelated symbols decorating an address and a constant. Offset-form
|
|
360
|
+
annotations of foreign symbols become raw hex
|
|
361
|
+
(`l32r a8, 0x40370100 (0x3f8f5f80)`); bare-symbol annotations (a
|
|
362
|
+
real callee or object start) and offsets into the current function
|
|
363
|
+
itself are kept.
|
|
292
364
|
|
|
293
365
|
What this looks like on real firmware: this binary's sequencer path -
|
|
294
366
|
here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
|
|
@@ -301,7 +373,7 @@ $ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|time
|
|
|
301
373
|
function insns loop spans calls
|
|
302
374
|
sequencer_timer_callback$lto_priv$0 77 .L21:41 __divsf3, __udivdi3
|
|
303
375
|
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
|
|
376
|
+
sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, indirect(a8)
|
|
305
377
|
```
|
|
306
378
|
|
|
307
379
|
A timer callback pays a software float divide *and* a 64-bit
|
|
@@ -318,8 +390,8 @@ is the double-promotion smell (typically an unsuffixed `60.0`-style
|
|
|
318
390
|
literal) on a chip whose FPU is single-precision only. And the
|
|
319
391
|
`$lto_priv$0` suffixes show LTO renaming the survivors, which is why
|
|
320
392
|
`--filter` matters: you can't name symbols you don't know exist. The
|
|
321
|
-
lone `a8` is a genuine function-pointer dispatch, reported
|
|
322
|
-
|
|
393
|
+
lone `indirect(a8)` is a genuine function-pointer dispatch, reported
|
|
394
|
+
as such rather than guessed at.
|
|
323
395
|
|
|
324
396
|
Functions LTO inlined away don't table at all, and the error says so
|
|
325
397
|
usefully:
|
|
@@ -357,12 +429,16 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
|
|
|
357
429
|
|
|
358
430
|
```
|
|
359
431
|
asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
|
|
360
|
-
[--cc 'CC FLAGS']... [--
|
|
432
|
+
[--target NAME]... [--cc 'CC FLAGS']... [--config PATH]
|
|
361
433
|
[--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
|
|
362
|
-
[--summary-only] [--collapse] [--span-stats]
|
|
434
|
+
[--filter REGEX] [--summary-only] [--collapse] [--span-stats]
|
|
435
|
+
[--cost] [--costs NAME] [--fail-on-growth] [--json] [--width N]
|
|
363
436
|
[--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
|
|
364
437
|
asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
365
|
-
[-l list] [--summary-only] [--span-stats]
|
|
438
|
+
[-l list] [--summary-only] [--span-stats] [--cost] [--costs NAME]
|
|
439
|
+
[--json] [--width N]
|
|
440
|
+
asmdiff --edit-config | --example-config | --list-targets | --version
|
|
441
|
+
asmdiff --completion bash|zsh|fish | --install-completion [SHELL]
|
|
366
442
|
```
|
|
367
443
|
|
|
368
444
|
| Option | Meaning |
|
|
@@ -371,18 +447,25 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
|
371
447
|
| `-p, --pair OLD:NEW` | Compare two *different* functions within one compilation. Repeatable. Default: every `old_X` is auto-paired with its `new_X`; with no pairs at all, the whole-file summary is printed instead. |
|
|
372
448
|
| `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
|
|
373
449
|
| `-l, --layout list\|side-by-side` | Force the inspect-mode presentation instead of the adaptive default (1 usable compiler lists, 2 go side by side, more list). With ELF input, `list` also prints `--filter` matches' listings. |
|
|
374
|
-
|
|
|
450
|
+
| `-f, --filter REGEX` | Also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem, or reach compiler-generated clones (`$constprop$0`, `.isra.0`), without naming each function. In compile modes matches are full peers of named functions (lenient when a match exists under only part of the matrix) and narrow the whole-file summary; in ELF mode matches appear in the stats table but are not listed in full — add `-l list` to print their listings too (a trailing note reminds you when listings were withheld). Not combinable with `--pair`/`--across`. |
|
|
375
451
|
| `--objdump PATH` | Disassembler for ELF input. Default: derived from the first gcc in the matrix by swapping the trailing `gcc` for `objdump`, so `--target`/config globs locate it like they locate the compiler. |
|
|
376
452
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
377
|
-
|
|
|
453
|
+
| `-t, --target NAME` | A named target from the config file, resolved to a `--cc` entry. `NAME` may also be a `[groups]` entry, a comma-separated list, or a glob over target names (`-t 'esp32c*'`). Repeatable; appended to the matrix after `--cc` entries. See [Target groups](#target-groups). |
|
|
378
454
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
455
|
+
| `--list-targets` | Print the resolved config's `default`, groups, cost profiles (name and `measured_on`), and targets (name and `cc`), then exit. No source file needed. |
|
|
379
456
|
| `-db, --compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, walk up from the CWD checking each directory and its `build/` until the repository root. See [below](#borrowing-includes-from-compile_commandsjson). |
|
|
380
457
|
| `--flags-like PATH` | A source with no `compile_commands` entry borrows the flags recorded for `PATH` — the way to compare a modified copy of a project source under its original's header environment. |
|
|
381
458
|
| `--db-includes` | Borrow only the header-search paths from the database, dropping its defines, forced includes, and `-specs`/`--sysroot`; kept paths are re-emitted as `-idirafter` so they cannot shadow the host's own system headers. This is how a host target compiles a cross project's source. |
|
|
382
459
|
| `-s`, `--summary-only` | Print only the summary/stats tables, suppressing every assembly listing (see [Shaping the output](#shaping-the-output-for-reading-vs-deciding)). |
|
|
383
|
-
| `--json` | Emit the summary as JSON on stdout instead of tables — one record per function per compiler. Implies `--summary-only`; errors stay plain text on stderr. |
|
|
384
|
-
|
|
|
385
|
-
| `--
|
|
460
|
+
| `--json` | Emit the summary as JSON on stdout instead of tables — one record per function per compiler, each carrying the `target` label of the matrix row it came from. Implies `--summary-only`; errors stay plain text on stderr. |
|
|
461
|
+
| `-C, --collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
|
|
462
|
+
| `--width N` | Column budget for tables and side-by-side listings. Default: the terminal's width, else `$COLUMNS`, else 120. `0` is unlimited, leaving the callee column untrimmed for a script to grep; a negative `N` is a usage error. |
|
|
463
|
+
| `--span-stats` | Follow the stats table with a per-loop-span instruction mix: nesting depth and load/store/mul/div/branch/other counts per span. |
|
|
464
|
+
| `--cost` | Add a `cost` column to the stats tables: the instruction classes of each function and the tier of every call site (`softfp`, `softfp-div`, `int-div`, `libm`, `mem`, `call`), with the sites a loop span holds counted apart. Counts only; a score appears when a profile prices them. See [Cost column](#cost-column). |
|
|
465
|
+
| `--costs NAME` | Price the cost column with the config's `[costs.NAME]` profile on every matrix row, `--cc` rows and ELF input included; implies `--cost`. Overrides a `costs = "NAME"` the target names. See [Cost profiles](#cost-profiles). |
|
|
466
|
+
| `--fail-on-growth` | Exit 3, naming each offender on stderr, if any candidate has more instructions than its baseline; exit 0 otherwise. Needs paired functions (`--pair`, auto-paired `old_X`/`new_X`, or `--across`). |
|
|
467
|
+
| `--completion bash\|zsh\|fish` | Print a completion script for that shell on stdout and exit. Its flag list is read off the argument parser at generation time, so it cannot drift from the tool. See [Shell completion](#shell-completion). |
|
|
468
|
+
| `--install-completion [SHELL]` | Write that script to the shell's own user completion directory and exit; no rc file is touched. `SHELL` defaults to the basename of `$SHELL`. An existing file is replaced only if asmdiff wrote it. |
|
|
386
469
|
| `--version` | Print the version and exit. |
|
|
387
470
|
| `-v`, `--verbose` | On compile failure, print the full compiler command and complete error output. Default shows only the compiler, the source, and the first error lines. |
|
|
388
471
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
@@ -409,45 +492,72 @@ asmdiff h.c -- -fno-math-errno
|
|
|
409
492
|
Compilers missing from `PATH` are skipped with a warning; the run fails only
|
|
410
493
|
if none are usable, and that error names each missed binary
|
|
411
494
|
(`no-such-gcc: not found on PATH`) so the fix is in the message, not in a
|
|
412
|
-
warning that scrolled away.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
495
|
+
warning that scrolled away. A row whose compile *fails* is dropped the same
|
|
496
|
+
way: the remaining rows are compiled, their table and listings print, and
|
|
497
|
+
the failing row's compiler output follows on stderr under its label
|
|
498
|
+
(`error: [esp32s3] xtensa-esp32s3-elf-gcc failed on biquad.c`) once the
|
|
499
|
+
run has printed everything it could. One broken target no longer costs
|
|
500
|
+
you the others; the run still exits 1.
|
|
501
|
+
|
|
502
|
+
| Status | Meaning |
|
|
503
|
+
|---|---|
|
|
504
|
+
| 0 | The run printed what was asked for. Differing assembly is the expected result, never an error. |
|
|
505
|
+
| 1 | The tool failed: a compile error, an unknown `--pair` name, a config that does not load, no usable compiler in the matrix. |
|
|
506
|
+
| 2 | argparse's usage error - an unknown flag, a missing value, a negative `--width`. |
|
|
507
|
+
| 3 | `--fail-on-growth` found a candidate with more instructions than its baseline. |
|
|
416
508
|
|
|
417
509
|
## Shaping the output for reading vs deciding
|
|
418
510
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
511
|
+
Every run leads with the table: the legend, one summary table for the
|
|
512
|
+
whole matrix, the `--span-stats` and provenance lines that belong to it,
|
|
513
|
+
and then the listings, grouped per target under `== LABEL ==`. A
|
|
514
|
+
two-file compare over a three-target matrix still prints three full
|
|
515
|
+
side-by-side listings under that table, roughly 100 KB of them when the
|
|
516
|
+
table was all the decision needed. Four flags cut the output to purpose:
|
|
424
517
|
|
|
425
518
|
- **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
|
|
426
519
|
the scripted/agent view: the table is the decision input, and a listing
|
|
427
520
|
is pulled with a second, narrower run only when a delta needs
|
|
428
|
-
explaining.
|
|
429
|
-
- **`--collapse`** elides runs of identical line pairs in every
|
|
521
|
+
explaining. What is left is the legend, the table, and its footer.
|
|
522
|
+
- **`--collapse`** (`-C`) elides runs of identical line pairs in every
|
|
430
523
|
side-by-side listing, keeping 3 lines of context around each difference
|
|
431
524
|
plus a `... N identical lines ...` marker. Two ~500-instruction
|
|
432
525
|
functions differing by five instructions render as a few readable hunks
|
|
433
|
-
instead of ~1000 lines.
|
|
434
|
-
|
|
526
|
+
instead of ~1000 lines. Two sides that are equal line for line collapse
|
|
527
|
+
to their header with `(identical)` on the left title and no body at
|
|
528
|
+
all.
|
|
529
|
+
|
|
530
|
+
What it cannot collapse is the case where the two sides differ on every
|
|
531
|
+
line without differing in substance: a changed register allocation, a
|
|
532
|
+
reordered but equivalent schedule, one extra spill slot shifting every
|
|
533
|
+
offset. There is no identical run to elide, so the listing prints in
|
|
534
|
+
full. The summary table and `--span-stats` are the reading unit there:
|
|
535
|
+
the instruction classes, the calls, and the per-span mix say what
|
|
536
|
+
moved, where a line-by-line read of two differently allocated bodies
|
|
537
|
+
does not.
|
|
538
|
+
- **`--span-stats`** follows the stats table with a per-loop-span
|
|
435
539
|
instruction mix, one row per span:
|
|
436
540
|
|
|
437
541
|
```
|
|
438
|
-
function span insns load store mul branch other
|
|
439
|
-
stereo_reverb .L108 327 96 31 14 12
|
|
542
|
+
function span depth insns load store mul div branch other
|
|
543
|
+
stereo_reverb .L108 0 327 96 31 14 0 12 174
|
|
440
544
|
```
|
|
441
545
|
|
|
442
546
|
This weighs the span rather than the whole function — the number that
|
|
443
547
|
actually decides a hot-loop comparison (is the rewrite trading loads
|
|
444
548
|
for stores? did the multiplies move?) without hand-counting a listing
|
|
445
549
|
and accidentally tallying past the loop end into the epilogue.
|
|
550
|
+
*depth* is how many other spans of the same function contain this
|
|
551
|
+
one, so an inner loop is told from the loop around it without reading
|
|
552
|
+
the listing ([How a span is found](#how-a-span-is-found)).
|
|
446
553
|
Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
|
|
447
554
|
memory-operand heuristic for x86; *branch* means any control
|
|
448
555
|
transfer — conditional and unconditional branches, calls, and
|
|
449
556
|
returns (outbound calls are already itemised by name in the `calls`
|
|
450
557
|
column) — and anything the tables don't know lands in *other*.
|
|
558
|
+
*div* is a hardware divide or square root; a division done by a
|
|
559
|
+
libcall (`__divsf3`, `__udivdi3`) is a call, and the Xtensa FPU
|
|
560
|
+
divide sequence counts as the several instructions it is.
|
|
451
561
|
|
|
452
562
|
Reading *branch* inside a span: a software loop's own backedge is
|
|
453
563
|
one of them, because the span runs from the label to the last
|
|
@@ -460,21 +570,54 @@ output to purpose:
|
|
|
460
570
|
show as *branch* falling while *insns* stays roughly flat, and a
|
|
461
571
|
call appearing inside a hot span is the ZOL killer the `calls`
|
|
462
572
|
column already flagged.
|
|
573
|
+
- **`--cost`** adds a `cost` column to the stats tables saying what
|
|
574
|
+
each function is made of: the same instruction classes over the
|
|
575
|
+
whole body, and the tier of every call site with the sites a loop
|
|
576
|
+
span holds counted apart. `--costs NAME` prices those counts with a
|
|
577
|
+
measured profile from the config. Both are described under [Cost
|
|
578
|
+
column](#cost-column) below.
|
|
463
579
|
|
|
464
|
-
All
|
|
465
|
-
`--span-stats` also apply to ELF input.
|
|
580
|
+
All four combine with every compile mode; `--summary-only`,
|
|
581
|
+
`--span-stats`, `--cost` and `--costs` also apply to ELF input.
|
|
582
|
+
|
|
583
|
+
Tables and side-by-side listings take their width from the terminal,
|
|
584
|
+
falling back off one to `$COLUMNS` and then to 120 columns; `--width N`
|
|
585
|
+
sets the budget directly, and `--width 0` means unlimited, which is the
|
|
586
|
+
untrimmed callee column a script greps.
|
|
466
587
|
|
|
467
588
|
For scripted callers, `--json` replaces the tables entirely with one
|
|
468
589
|
JSON document on stdout: a flat `results` list holding one record per
|
|
469
|
-
function per compiler — `
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
590
|
+
function per compiler — `target` (the matrix row's label, the same
|
|
591
|
+
string the table's `target` column prints), `cc`, `tag` (source label
|
|
592
|
+
in two-file runs), `role` (`baseline`/`candidate` in paired runs),
|
|
593
|
+
`insns`, `loop_spans` (each `{label, insns, depth}`), `calls`, `delta`
|
|
594
|
+
(on candidate records), `cost` when `--cost` or `--costs` is given, and
|
|
595
|
+
`span_stats` when `--span-stats` is given. An ELF run compiles nothing,
|
|
596
|
+
so its records have no matrix row to name and carry neither `target`
|
|
597
|
+
nor `cc`. Flat records keep it one `jq` expression away from any
|
|
598
|
+
question the tables answer.
|
|
599
|
+
|
|
600
|
+
The pairing questions the tool answers itself, because the pairing is
|
|
601
|
+
its own: every summary table closes a pair with a `delta` row (signed
|
|
602
|
+
instruction count, per-span `before -> after`, callees gained with `+`
|
|
603
|
+
and lost with `-`, and under `--cost` the classes and tiers that
|
|
604
|
+
moved), each candidate record carries the same as a `delta` object,
|
|
605
|
+
and `--fail-on-growth` makes it an exit status:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
# Fail the job when a rewrite that was meant to shrink did not
|
|
609
|
+
asmdiff old.c new.c -a stereo_reverb -t esp32s3 --fail-on-growth -s
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Exit 3 and one `growth: FUNC +N insns (BASELINE -> CANDIDATE)` line per
|
|
613
|
+
offender on stderr if any candidate has more instructions than its
|
|
614
|
+
baseline, exit 0 otherwise; status 1 is left meaning the tool itself
|
|
615
|
+
failed and 2 is argparse's usage error, so a job can tell "grew" from
|
|
616
|
+
"did not compile" from "misused the flags".
|
|
476
617
|
|
|
477
|
-
|
|
618
|
+
Arbitrary selection stays jq's: the tool prints JSON on stdout, so a
|
|
619
|
+
plain `| jq` pipe *is* the integration, and jq does that filtering
|
|
620
|
+
better than a wrapper flag could. A few recipes:
|
|
478
621
|
|
|
479
622
|
```bash
|
|
480
623
|
# Instruction count per function per side, as a table
|
|
@@ -485,19 +628,14 @@ asmdiff old.c new.c --json \
|
|
|
485
628
|
asmdiff old.c new.c --json \
|
|
486
629
|
| jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
|
|
487
630
|
|
|
488
|
-
# Per-function old→new instruction delta (regressions have a positive delta)
|
|
489
|
-
asmdiff old.c new.c --json \
|
|
490
|
-
| jq -r '.results | group_by(.function)[] | select(length == 2)
|
|
491
|
-
| "\(.[0].function): \(.[0].insns) -> \(.[1].insns) (\(.[1].insns - .[0].insns))"'
|
|
492
|
-
|
|
493
|
-
# Fail CI if any function grew, using jq's exit status
|
|
494
|
-
asmdiff old.c new.c --json \
|
|
495
|
-
| jq -e '.results | group_by(.function)
|
|
496
|
-
| all(length < 2 or .[1].insns <= .[0].insns)' > /dev/null
|
|
497
|
-
|
|
498
631
|
# The full record for one hot function, spans and all
|
|
499
632
|
asmdiff old.c new.c -a stereo_reverb --json --span-stats \
|
|
500
633
|
| jq '.results[] | {role, insns, spans: .loop_spans}'
|
|
634
|
+
|
|
635
|
+
# Functions reaching a soft-float helper from inside a loop
|
|
636
|
+
asmdiff old.c new.c --json --cost \
|
|
637
|
+
| jq '.results[] | select(.cost.tiers_in_loop.softfp > 0)
|
|
638
|
+
| {function, tag, in_loop: .cost.tiers_in_loop}'
|
|
501
639
|
```
|
|
502
640
|
|
|
503
641
|
The top level carries `asmdiff` (version) and `mode`
|
|
@@ -505,6 +643,99 @@ The top level carries `asmdiff` (version) and `mode`
|
|
|
505
643
|
path as `elf`). Warnings and errors stay plain text on stderr, so a
|
|
506
644
|
failed run never emits half a document.
|
|
507
645
|
|
|
646
|
+
### Cost column
|
|
647
|
+
|
|
648
|
+
`--cost` adds a `cost` column before `calls`, saying what each function
|
|
649
|
+
is made of rather than how many lines long it is:
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
$ asmdiff asmdiff_example.c --cost -s --cc 'gcc -O2'
|
|
653
|
+
|
|
654
|
+
function role insns loop spans cost calls
|
|
655
|
+
old_const baseline 3 - mul:1 | br:1 | oth:1 -
|
|
656
|
+
new_const candidate 3 - br:1 | oth:2 | libm:1 ldexpf
|
|
657
|
+
delta 0 - mul:-1 | oth:+1 | libm:+1 +ldexpf
|
|
658
|
+
old_rt baseline 9 - st:1 | mul:1 | br:2 | oth:5 | libm:1 exp2f
|
|
659
|
+
new_rt candidate 2 - br:1 | oth:1 | libm:1 ldexpf
|
|
660
|
+
delta -7 - st:-1 | mul:-1 | br:-1 | oth:-4 +ldexpf -exp2f
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Instructions fall into the six classes `--span-stats` uses, abbreviated
|
|
664
|
+
here (`ld`, `st`, `mul`, `div`, `br`, `oth`). Every call site is
|
|
665
|
+
additionally tiered by the callee's name:
|
|
666
|
+
|
|
667
|
+
| Tier | Callees |
|
|
668
|
+
|---|---|
|
|
669
|
+
| `softfp` | soft-float add, subtract, multiply, compare, convert: `__addsf3`, `__muldf3`, `__floatsidf`, `__aeabi_dmul`, `__aeabi_i2d` |
|
|
670
|
+
| `softfp-div` | soft-float divide: `__divsf3`, `__divdf3`, `__aeabi_fdiv`, `__aeabi_ddiv` |
|
|
671
|
+
| `int-div` | integer divide and modulo helpers: `__udivdi3`, `__moddi3`, `__aeabi_idivmod`, `__aeabi_uldivmod` |
|
|
672
|
+
| `libm` | the math families, float and double: `sqrtf`, `sin`, `exp2f`, `ldexpf`, `pow`, `fmod` |
|
|
673
|
+
| `mem` | `memcpy`, `memset`, `memmove`, `memcmp` |
|
|
674
|
+
| `call` | everything else, `indirect(<reg>)` included |
|
|
675
|
+
|
|
676
|
+
The libgcc and the ARM EABI spelling of one helper land in the same
|
|
677
|
+
tier, so a `__muldf3` target and a `__aeabi_dmul` target read alike. A
|
|
678
|
+
name no pattern knows lands in `call`, where the `calls` column already
|
|
679
|
+
spells it out.
|
|
680
|
+
|
|
681
|
+
Each tier is followed by how many of its call sites a loop span holds -
|
|
682
|
+
`softfp:3 (2 in loop)` - since a soft-float helper reached once per
|
|
683
|
+
iteration is a different finding from one on an error path. The counts
|
|
684
|
+
are per call site, not per distinct callee: two calls to `__muldf3`
|
|
685
|
+
count twice, where the `calls` column names it once.
|
|
686
|
+
|
|
687
|
+
The delta row's cost cell lists only what moved. A tier whose count
|
|
688
|
+
came through the rewrite unchanged is absent from it even when the
|
|
689
|
+
callee changed: `old_rt` and `new_rt` above each hold one `libm` call,
|
|
690
|
+
so the cell says nothing about `libm` while the `calls` column carries
|
|
691
|
+
the swap as `+ldexpf -exp2f`. The two cells are read together.
|
|
692
|
+
|
|
693
|
+
With a profile (`--costs NAME`, or a target's `costs = "NAME"`), the
|
|
694
|
+
cell opens with a score and the number of instructions and call sites
|
|
695
|
+
no weight covered, and the table is followed by the line saying where
|
|
696
|
+
the weights came from:
|
|
697
|
+
|
|
698
|
+
```
|
|
699
|
+
score 399 (1 unweighted) | ld:40 | st:12 | mul:14 | ...
|
|
700
|
+
...
|
|
701
|
+
costs: esp32s3-iram - ESP32-S3 rev 0.2, code in IRAM, esp-15.2.0 libgcc; esp_cpu_get_cycle_count harness, median of 1000 runs
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The weights behind that score are the commented template in
|
|
705
|
+
`asmdiff --example-config`, not a measurement; no measured profile
|
|
706
|
+
ships with the tool, and [Cost profiles](#cost-profiles) below is how
|
|
707
|
+
one is written. The delta row leads with the signed score. A cell is
|
|
708
|
+
capped at 56 characters and closes with `...` when the mix does not fit
|
|
709
|
+
(as it does here), so the table stays aligned; `--json` carries all of
|
|
710
|
+
it:
|
|
711
|
+
|
|
712
|
+
```json
|
|
713
|
+
"cost": {
|
|
714
|
+
"classes": {"load": 40, "store": 12, "mul": 14, "div": 1,
|
|
715
|
+
"branch": 9, "other": 61},
|
|
716
|
+
"tiers": {"softfp": 3, "call": 1},
|
|
717
|
+
"tiers_in_loop": {"softfp": 2},
|
|
718
|
+
"score": 399,
|
|
719
|
+
"unweighted": 1,
|
|
720
|
+
"profile": {"name": "esp32s3-iram",
|
|
721
|
+
"measured_on": "ESP32-S3 rev 0.2, code in IRAM, ...",
|
|
722
|
+
"method": "esp_cpu_get_cycle_count harness, ..."}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
`classes` always carries all six keys. `tiers` and `tiers_in_loop` list
|
|
727
|
+
only what the function reaches, so `.cost.tiers.softfp` on a function
|
|
728
|
+
with no soft-float is `null` rather than `0` - `// 0` in the jq
|
|
729
|
+
expression if you want a number. Without a profile, `score` and
|
|
730
|
+
`profile` are `null` and `unweighted` is every instruction and call
|
|
731
|
+
site the function has. Candidate records also gain `delta.cost`
|
|
732
|
+
(`classes`, `tiers`, `score`), again signed and again only what moved.
|
|
733
|
+
|
|
734
|
+
ELF input takes `--cost` and `--costs` like a compile run does. A
|
|
735
|
+
target's `costs = "NAME"` is not consulted there: nothing is compiled,
|
|
736
|
+
so no matrix row stands behind the binary's instructions. Name the
|
|
737
|
+
profile with `--costs`.
|
|
738
|
+
|
|
508
739
|
## Config file: named targets
|
|
509
740
|
|
|
510
741
|
Retyping a cross-compiler path and ten flags per run is the enemy of actually
|
|
@@ -532,8 +763,9 @@ flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
|
|
|
532
763
|
survives toolchain upgrades (`esp-14` → `esp-15`) without editing. A
|
|
533
764
|
pattern matching several installed toolchains resolves to the highest
|
|
534
765
|
version-sorted one — numerically, so `esp-15` beats `esp-9` — and the
|
|
535
|
-
choice is printed to stderr; the
|
|
536
|
-
the fully resolved command that actually ran
|
|
766
|
+
choice is printed to stderr; the legend line above the table always
|
|
767
|
+
shows the fully resolved command that actually ran, so the short
|
|
768
|
+
`target` label never hides which binary it was. No match is an error. Pin
|
|
537
769
|
the exact directory instead when reproducibility matters more than
|
|
538
770
|
convenience. Flags expand `$VARS` only (no globbing).
|
|
539
771
|
|
|
@@ -542,16 +774,122 @@ shapes:
|
|
|
542
774
|
|
|
543
775
|
```bash
|
|
544
776
|
asmdiff h.c # config default target(s)
|
|
545
|
-
asmdiff h.c
|
|
546
|
-
asmdiff h.c
|
|
777
|
+
asmdiff h.c -t esp32s3 -t host # two-target matrix
|
|
778
|
+
asmdiff h.c -t esp32s3,host # same, as a comma list
|
|
779
|
+
asmdiff h.c --across f -t esp32s3 --cc 'gcc -O2' # mix freely
|
|
780
|
+
asmdiff --list-targets # what does my config define?
|
|
547
781
|
```
|
|
548
782
|
|
|
549
783
|
A config placed next to your harness files travels with them: any invocation
|
|
550
|
-
naming a source in that directory finds it, from any CWD.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
784
|
+
naming a source in that directory finds it, from any CWD.
|
|
785
|
+
|
|
786
|
+
`default` takes whatever `-t` takes: a target name, a `[groups]` name, a
|
|
787
|
+
comma list, a glob over target names, or an array mixing those
|
|
788
|
+
(`default = ["native", "esp32s3"]`). A name it cannot resolve is the
|
|
789
|
+
same error `-t` would give. One idiom worth knowing is the one-member
|
|
790
|
+
group: `[groups] ship = ["esp32s3"]` with `default = "ship"` gives the
|
|
791
|
+
standing matrix a name of its own, so widening the run later is editing
|
|
792
|
+
the group rather than every `default` and script that mentions the
|
|
793
|
+
target.
|
|
794
|
+
|
|
795
|
+
The included `asmdiff.example.toml` is a starting point. If a flag or
|
|
796
|
+
include path must vary per machine, that's what per-machine config files
|
|
797
|
+
are for — nothing lives in the tool.
|
|
798
|
+
|
|
799
|
+
### Target groups
|
|
800
|
+
|
|
801
|
+
A `[groups]` table names matrices of targets so a whole family runs
|
|
802
|
+
from one `-t`, without editing `default`:
|
|
803
|
+
|
|
804
|
+
```toml
|
|
805
|
+
[groups]
|
|
806
|
+
riscv32-esp = ["esp32c3", "esp32c6", "esp32h2", "esp32p4"]
|
|
807
|
+
native = ["gcc", "clang"]
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
asmdiff h.c -t riscv32-esp # four RISC-V targets
|
|
812
|
+
asmdiff h.c -t native -t esp32s3 # a group plus a single target
|
|
813
|
+
asmdiff h.c -t 'esp32c*' # glob over target names (quote it)
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
A `-t` value is resolved as an exact target name first, then as a group
|
|
817
|
+
name, then as a glob (`*`, `?`, `[...]`) over target names in config
|
|
818
|
+
order. A group naming an undefined target, or an empty group, is an
|
|
819
|
+
error. `--list-targets` prints what the resolved config defines.
|
|
820
|
+
|
|
821
|
+
### Cost profiles
|
|
822
|
+
|
|
823
|
+
A `[costs.NAME]` table holds measured weights and where they were
|
|
824
|
+
measured. `--cost` alone counts; with a profile in hand it also scores:
|
|
825
|
+
|
|
826
|
+
```toml
|
|
827
|
+
[costs.esp32s3-iram]
|
|
828
|
+
measured_on = "ESP32-S3 rev 0.2, code in IRAM, esp-15.2.0 libgcc"
|
|
829
|
+
method = "esp_cpu_get_cycle_count harness, median of 1000 runs"
|
|
830
|
+
other = 1
|
|
831
|
+
load = 2
|
|
832
|
+
store = 1
|
|
833
|
+
mul = 2
|
|
834
|
+
div = 20
|
|
835
|
+
branch = 2
|
|
836
|
+
softfp = 60
|
|
837
|
+
softfp-div = 200
|
|
838
|
+
int-div = 40
|
|
839
|
+
"__muldf3" = 90
|
|
840
|
+
|
|
841
|
+
[esp32s3]
|
|
842
|
+
cc = "..."
|
|
843
|
+
flags = ["-O2", "-mlongcalls"]
|
|
844
|
+
costs = "esp32s3-iram"
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
A key's value decides what it is. A number is a weight, keyed by an
|
|
848
|
+
instruction class (`load`, `store`, `mul`, `div`, `branch`, `other`),
|
|
849
|
+
a libcall tier (`softfp`, `softfp-div`, `int-div`, `libm`), or a call
|
|
850
|
+
symbol in quotes (`"__muldf3" = 90`). A string is provenance:
|
|
851
|
+
`measured_on` and `method` are required and the load fails naming the
|
|
852
|
+
one that is missing, and any further string key is kept and printed
|
|
853
|
+
after them. Anything else is an error naming the key. `mem` and `call`
|
|
854
|
+
are refused a weight, since a `memcpy`'s cost is its size argument and
|
|
855
|
+
an unknown callee's is its body, neither of which the assembly shows;
|
|
856
|
+
both stay counted and land in the unweighted total. `extends = "OTHER"`
|
|
857
|
+
copies another profile's weights before this table's own apply, one
|
|
858
|
+
level deep - a chain is an error.
|
|
859
|
+
|
|
860
|
+
Weights resolve in this order. An instruction costs its class weight,
|
|
861
|
+
or counts as unweighted. A call site costs its callee's own weight
|
|
862
|
+
where the profile names that symbol, else its tier's weight, else it
|
|
863
|
+
counts as unweighted. The score is therefore always printed with the
|
|
864
|
+
number of instructions and call sites it left out. It is an integer
|
|
865
|
+
while every weight is; one fractional weight anywhere rounds the total
|
|
866
|
+
to a decimal.
|
|
867
|
+
|
|
868
|
+
Which profile a row uses: `--costs NAME` sets it for every matrix row
|
|
869
|
+
and is the only route for a `--cc` row, which has no target to read one
|
|
870
|
+
from; otherwise each target brings the one its `costs = "NAME"` names,
|
|
871
|
+
so a two-target matrix scores each target under its own. A name that
|
|
872
|
+
does not resolve fails before the first compile. `--list-targets`
|
|
873
|
+
prints the profiles a config defines, with their `measured_on`.
|
|
874
|
+
|
|
875
|
+
What a score claims, and what it does not:
|
|
876
|
+
|
|
877
|
+
- **A weight is a static issue cost.** Dual issue, pipeline stalls,
|
|
878
|
+
cache and flash-fetch latency, and the operand-dependence of helpers
|
|
879
|
+
like `__divdf3` are not in it. Two scores that differ by a few
|
|
880
|
+
percent say nothing.
|
|
881
|
+
- **Scores compare within one target.** Cycle costs differ per core,
|
|
882
|
+
per memory placement, and per libgcc build, so a score under
|
|
883
|
+
`esp32s3-iram` and a score under some other profile are two different
|
|
884
|
+
units. That is why the provenance line prints under every scored
|
|
885
|
+
table.
|
|
886
|
+
- **Depth is reported, not multiplied.** A span at depth 2 scores like
|
|
887
|
+
one at depth 0; the trip count is not in the assembly. `--span-stats`
|
|
888
|
+
gives the depth and the per-span mix, and the weighing is yours.
|
|
889
|
+
|
|
890
|
+
No measured profile ships with the tool. `asmdiff --example-config`
|
|
891
|
+
carries the template above as comments, with the fields a reader needs
|
|
892
|
+
in order to judge how approximate the numbers are.
|
|
555
893
|
|
|
556
894
|
### Bundled ESP profiles
|
|
557
895
|
|
|
@@ -578,7 +916,10 @@ flags = ["-O2", "-march=rv32imac_zicsr_zifencei", "-mabi=ilp32"]
|
|
|
578
916
|
A profile is nothing more than a curated group of targets — the `esp-*`
|
|
579
917
|
glob finds the toolchains `idf_tools.py install` left in `~/.espressif`
|
|
580
918
|
(the newest, by the version-sort rule above, when several are
|
|
581
|
-
installed)
|
|
919
|
+
installed). The example config defines each profile as a `[groups]`
|
|
920
|
+
entry, so `-t riscv32-esp` or `-t xtensa-esp` runs it as one matrix and
|
|
921
|
+
`-t esp` runs every ESP chip; set `default` to a group's list to make it
|
|
922
|
+
the no-argument matrix:
|
|
582
923
|
|
|
583
924
|
```toml
|
|
584
925
|
default = ["esp32c3", "esp32c6", "esp32h2", "esp32p4"]
|
|
@@ -672,7 +1013,7 @@ Details that make it robust:
|
|
|
672
1013
|
- **Absent source is an error**, not a silent empty flag set — otherwise
|
|
673
1014
|
you'd just hit the missing-header failure this feature exists to prevent.
|
|
674
1015
|
The message flags a same-basename entry recorded under a different path.
|
|
675
|
-
- **`compile_commands` expands `~` and `$VARS`.** The
|
|
1016
|
+
- **`compile_commands` expands `~` and `$VARS`.** The legend line always
|
|
676
1017
|
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
677
1018
|
every include path the compiler actually received.
|
|
678
1019
|
- **`--db-includes` restricts the borrow to header-search paths**,
|
|
@@ -739,10 +1080,25 @@ Both walk up from the current directory, checking each level for
|
|
|
739
1080
|
CMake and idf.py leave it) — first hit wins. The walk stops at the
|
|
740
1081
|
repository root (the first directory with a `.git`), so running from any
|
|
741
1082
|
depth of component directory finds the project database, but an unrelated
|
|
742
|
-
one further up the filesystem is never picked up.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1083
|
+
one further up the filesystem is never picked up. It is never on by
|
|
1084
|
+
default — a target without `compile_commands` and no
|
|
1085
|
+
`--compile-commands` flag borrows nothing.
|
|
1086
|
+
|
|
1087
|
+
The two opt-ins differ in what happens when the walk finds nothing:
|
|
1088
|
+
|
|
1089
|
+
- **A bare `--compile-commands` is an error.** You asked for a database
|
|
1090
|
+
in this run and there is none; compiling anyway would silently drop
|
|
1091
|
+
the flags you came for.
|
|
1092
|
+
- **`compile_commands = true` in a target is a note.** The config
|
|
1093
|
+
describes where that target is usually compiled, not what this run
|
|
1094
|
+
is. A harness file compiled from a directory with no `build/` under
|
|
1095
|
+
it gets `target [esp32s3]: no compile_commands.json found near the
|
|
1096
|
+
current directory; compiling without borrowed flags` on stderr and
|
|
1097
|
+
compiles with the target's own `cc`/`flags`, which is what a
|
|
1098
|
+
standalone harness wants.
|
|
1099
|
+
|
|
1100
|
+
`compile_commands = "PATH"` naming a database that is not there stays an
|
|
1101
|
+
error under both: a path that does not resolve is a typo, not a context.
|
|
746
1102
|
|
|
747
1103
|
The precedence is what you'd hope: a target that names its own
|
|
748
1104
|
`compile_commands` path always keeps it; `--compile-commands` (with or
|
|
@@ -787,25 +1143,23 @@ cross-only defines or `-specs` a host gcc would choke on.
|
|
|
787
1143
|
|
|
788
1144
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
789
1145
|
auto-pair, the tool prints what it parsed instead of erroring: every
|
|
790
|
-
function's counts plus a file total. With two files,
|
|
1146
|
+
function's counts plus a file total. With two files, a leading `file`
|
|
1147
|
+
column tells the two apart inside the one table:
|
|
791
1148
|
|
|
792
1149
|
```
|
|
793
1150
|
$ asmdiff old/delay.c new/delay.c
|
|
794
1151
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
...
|
|
802
|
-
TOTAL (13 functions) 956 - malloc_caps, free, ...
|
|
803
|
-
|
|
804
|
-
-- new/delay.c --
|
|
805
|
-
...
|
|
806
|
-
TOTAL (13 functions) 1028 - malloc_caps, free, ...
|
|
1152
|
+
file function insns loop spans calls
|
|
1153
|
+
old/delay.c stereo_reverb 437 .L108:327 -
|
|
1154
|
+
old/delay.c ...
|
|
1155
|
+
old/delay.c TOTAL (13 functions) 956 - malloc_caps, free, ...
|
|
1156
|
+
new/delay.c ...
|
|
1157
|
+
new/delay.c TOTAL (13 functions) 1028 - malloc_caps, free, ...
|
|
807
1158
|
```
|
|
808
1159
|
|
|
1160
|
+
Over a matrix of several targets a `target` column leads the `file` one,
|
|
1161
|
+
so the whole run is still one table.
|
|
1162
|
+
|
|
809
1163
|
The TOTAL row is a coarse sanity check — did this refactor move the file's
|
|
810
1164
|
weight, did a call appear that shouldn't have? It sums parsed function
|
|
811
1165
|
bodies only (no literal pools, data, or alignment), so it is not a size
|
|
@@ -814,13 +1168,15 @@ measurement, and per-function rows are where the real information is.
|
|
|
814
1168
|
Only labels the assembler types as functions are listed — global data
|
|
815
1169
|
(string constants, state structs, lookup tables) gets column-0 labels too
|
|
816
1170
|
but is not code. A `calls` list longer than 8 symbols is truncated to
|
|
817
|
-
`...,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1171
|
+
`..., ... (N total)` (the leading `...` marks the elision, so the
|
|
1172
|
+
marker never reads as one more callee); real firmware dispatch
|
|
1173
|
+
functions call dozens of distinct symbols and would otherwise make
|
|
1174
|
+
rows thousands of characters wide. Rows are additionally trimmed to the
|
|
1175
|
+
column budget - the terminal, else `$COLUMNS`, else 120: callees are
|
|
1176
|
+
dropped from the end of the `calls` column (never the first one) behind
|
|
1177
|
+
the same `... (N total)` marker, so every row stays on one line.
|
|
1178
|
+
`--width 0` turns the trim off and keeps the full capped list, which is
|
|
1179
|
+
the shape to pipe into a grep.
|
|
824
1180
|
|
|
825
1181
|
## Comparing the same function across two builds (`--across`)
|
|
826
1182
|
|
|
@@ -852,6 +1208,11 @@ asmdiff src/filters.c --across dsps_biquad_f32_ansi \
|
|
|
852
1208
|
cc#1: gcc -Os
|
|
853
1209
|
cc#2: gcc -O3
|
|
854
1210
|
|
|
1211
|
+
function role insns loop spans calls
|
|
1212
|
+
dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
|
|
1213
|
+
dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
|
|
1214
|
+
delta +30 32 -> 54 -SMULR6
|
|
1215
|
+
|
|
855
1216
|
== cc#1 vs cc#2 ==
|
|
856
1217
|
|
|
857
1218
|
dsps_biquad_f32_ansi [cc#1] | dsps_biquad_f32_ansi [cc#2]
|
|
@@ -880,10 +1241,6 @@ addl %eax, %edx | sarl $11, %r12d
|
|
|
880
1241
|
movl %r14d, %r11d | sarl $11, %ebp
|
|
881
1242
|
call SMULR6 | leal 1024(%rax), %edx
|
|
882
1243
|
[... 60 rows omitted ...]
|
|
883
|
-
|
|
884
|
-
function role insns loop spans calls
|
|
885
|
-
dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
|
|
886
|
-
dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
|
|
887
1244
|
```
|
|
888
1245
|
|
|
889
1246
|
(The listing is abridged here; the tool prints all 92 rows. The columns
|
|
@@ -892,8 +1249,8 @@ the listing shows why — `SMULR6` inlined into the loop body, vector setup
|
|
|
892
1249
|
around it. Whether that trade is good is your call.)
|
|
893
1250
|
|
|
894
1251
|
The output prints a legend mapping `cc#N` tags to the full compiler
|
|
895
|
-
invocations, then one section per
|
|
896
|
-
against the bundled example file:
|
|
1252
|
+
invocations, then the table, then one listing section per
|
|
1253
|
+
baseline/candidate pairing. Runnable against the bundled example file:
|
|
897
1254
|
|
|
898
1255
|
```
|
|
899
1256
|
$ asmdiff asmdiff_example.c --across new_rt --cc 'gcc -O0' --cc 'gcc -O3'
|
|
@@ -901,6 +1258,11 @@ $ asmdiff asmdiff_example.c --across new_rt --cc 'gcc -O0' --cc 'gcc -O3'
|
|
|
901
1258
|
cc#1: gcc -O0
|
|
902
1259
|
cc#2: gcc -O3
|
|
903
1260
|
|
|
1261
|
+
function role insns loop spans calls
|
|
1262
|
+
new_rt [cc#1] baseline 13 - ldexpf
|
|
1263
|
+
new_rt [cc#2] candidate 2 - ldexpf
|
|
1264
|
+
delta -11 - -
|
|
1265
|
+
|
|
904
1266
|
== cc#1 vs cc#2 ==
|
|
905
1267
|
|
|
906
1268
|
new_rt [cc#1] | new_rt [cc#2]
|
|
@@ -918,15 +1280,12 @@ movd %eax, %xmm0 |
|
|
|
918
1280
|
call ldexpf@PLT |
|
|
919
1281
|
leave |
|
|
920
1282
|
ret |
|
|
921
|
-
|
|
922
|
-
function role insns loop spans calls
|
|
923
|
-
new_rt [cc#1] baseline 13 - ldexpf
|
|
924
|
-
new_rt [cc#2] candidate 2 - ldexpf
|
|
925
1283
|
```
|
|
926
1284
|
|
|
927
1285
|
**Two files** — before/after versions of a source file (e.g. from a git
|
|
928
|
-
worktree, a branch checkout, or a patched copy).
|
|
929
|
-
|
|
1286
|
+
worktree, a branch checkout, or a patched copy). Every compiler in the
|
|
1287
|
+
matrix lands in the one table under its own `target` label, and each
|
|
1288
|
+
gets its own listing section:
|
|
930
1289
|
|
|
931
1290
|
```bash
|
|
932
1291
|
git worktree add ../baseline main
|
|
@@ -978,8 +1337,10 @@ The summary makes the trade-off immediate:
|
|
|
978
1337
|
function role insns calls
|
|
979
1338
|
exp2_lut [amy-baseline/log2_exp2.c] baseline 65 exp2f
|
|
980
1339
|
exp2_lut [amy/log2_exp2.c] candidate 59 ldexpf
|
|
1340
|
+
delta -6 +ldexpf -exp2f
|
|
981
1341
|
log2_lut [amy-baseline/log2_exp2.c] baseline 58 -
|
|
982
1342
|
log2_lut [amy/log2_exp2.c] candidate 64 ldexpf
|
|
1343
|
+
delta +6 +ldexpf
|
|
983
1344
|
```
|
|
984
1345
|
|
|
985
1346
|
The runtime site improves (a leaner libcall replaces `exp2f` + multiply),
|
|
@@ -1000,6 +1361,8 @@ probed the same way.
|
|
|
1000
1361
|
x86 (`call`, `jmp` tail calls), ARM (`bl`, `blx`), RISC-V (`call`,
|
|
1001
1362
|
`tail`, `jal`), and Xtensa (`call0/4/8/12`, `callx*`, `j`). Local-label
|
|
1002
1363
|
branches and register-indirect x86 jumps are not counted as calls.
|
|
1364
|
+
Register-indirect call mnemonics (`callx8 a8`, single-operand `jalr`,
|
|
1365
|
+
`blx r3`) are reported as `indirect(<reg>)`.
|
|
1003
1366
|
4. Loop spans come from label references alone — no mnemonic tables, no
|
|
1004
1367
|
control-flow analysis. The next section walks through it.
|
|
1005
1368
|
|
|
@@ -1033,6 +1396,16 @@ into that label's single span. Nested labels report separately — the
|
|
|
1033
1396
|
outer span simply contains the inner one. Forward references (loop exits
|
|
1034
1397
|
like `jle .L24`) are ignored.
|
|
1035
1398
|
|
|
1399
|
+
Containment is what the `depth` column of `--span-stats` reports: a
|
|
1400
|
+
span's depth is the number of other spans of the same function that
|
|
1401
|
+
start at or before it and end at or after it. An outermost loop is
|
|
1402
|
+
depth 0, a loop nested in it 1, and a span at depth 1 runs its body
|
|
1403
|
+
once per iteration of the span at depth 0. Two labels covering exactly
|
|
1404
|
+
the same lines (one loop body reached by two edges) contain each other
|
|
1405
|
+
under no reading, so both keep the depth of whatever encloses them.
|
|
1406
|
+
Depth is a nesting fact and nothing more - the trip count is not in the
|
|
1407
|
+
assembly, so nothing in the tool multiplies a count by it.
|
|
1408
|
+
|
|
1036
1409
|
The one arch-specific case is Xtensa zero-overhead loops, where the
|
|
1037
1410
|
hardware — not a branch — repeats the body, and the `loop` instruction
|
|
1038
1411
|
names its *end* label, forward:
|
|
@@ -1063,6 +1436,11 @@ misreadings to avoid are few and predictable:
|
|
|
1063
1436
|
cost more than the rest of the function. Treat the count as a *size*
|
|
1064
1437
|
and *structure* fact - a call appearing, a loop body growing, a
|
|
1065
1438
|
softfloat sequence materializing - and measure time on the target.
|
|
1439
|
+
`--cost` splits the same body into instruction classes and libcall
|
|
1440
|
+
tiers, so that one `__udivdi3` line reads as an `int-div` call
|
|
1441
|
+
instead of as one more instruction; a `[costs.NAME]` profile prices
|
|
1442
|
+
those counts in measured cycles, which is still a static issue cost
|
|
1443
|
+
and still not a measurement of your program.
|
|
1066
1444
|
|
|
1067
1445
|
- **The cost of a call is in the callee.** The listing shows only the
|
|
1068
1446
|
call site. The `-O3` version of `new_rt` above is two instructions,
|
|
@@ -1071,7 +1449,9 @@ misreadings to avoid are few and predictable:
|
|
|
1071
1449
|
|
|
1072
1450
|
- **Weigh the span, not the function.** One instruction added inside a
|
|
1073
1451
|
loop that runs per sample outweighs twenty added to setup code. The
|
|
1074
|
-
whole-function count charges both the same
|
|
1452
|
+
whole-function count charges both the same; `--span-stats` gives the
|
|
1453
|
+
per-span mix and its nesting depth, and `--cost` counts the call
|
|
1454
|
+
sites a span holds apart from the rest.
|
|
1075
1455
|
|
|
1076
1456
|
- **Bigger is often faster.** Unrolling and vectorization raise every
|
|
1077
1457
|
count on purpose - the `-Os` vs `-O3` biquad above is bigger by every
|
|
@@ -1122,15 +1502,19 @@ standard library, and contains no project-specific constants. To port:
|
|
|
1122
1502
|
supported — on a Mac, compare inside a Linux container or with a
|
|
1123
1503
|
cross-toolchain.
|
|
1124
1504
|
- Call detection is a mnemonic heuristic. Register-indirect calls through a
|
|
1125
|
-
loaded address (other than x86 `jmp *reg`)
|
|
1126
|
-
|
|
1127
|
-
visibility rather than silence. ELF input narrows this:
|
|
1128
|
-
sequences are resolved to their real callee when objdump's
|
|
1129
|
-
annotation names one; genuine function-pointer dispatch still
|
|
1130
|
-
as
|
|
1131
|
-
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1505
|
+
loaded address (other than x86 `jmp *reg`) are reported as
|
|
1506
|
+
`indirect(<reg>)` (e.g. Xtensa `callx8 a10` -> `indirect(a10)`), which
|
|
1507
|
+
errs toward visibility rather than silence. ELF input narrows this:
|
|
1508
|
+
`-mlongcalls` sequences are resolved to their real callee when objdump's
|
|
1509
|
+
literal annotation names one; genuine function-pointer dispatch still
|
|
1510
|
+
reads as indirect.
|
|
1511
|
+
- Side-by-side columns truncate long instruction lines to keep pairs
|
|
1512
|
+
aligned, and drop a trailing assembler comment (clang's `# TAILCALL`,
|
|
1513
|
+
`# 8-byte Reload`) before truncating, so the operands survive at half
|
|
1514
|
+
a terminal's width. An ARM `#4` immediate has no space after the hash
|
|
1515
|
+
and is untouched. Each side gets half the column budget, so `--width`
|
|
1516
|
+
widens them; `-l list`, the single-column listings and `--json` keep
|
|
1517
|
+
the line as extracted, comment included.
|
|
1134
1518
|
- Loop spans are layout facts, not loop analysis. Label numbers are
|
|
1135
1519
|
compiler-assigned, so a baseline's `.L27` and a candidate's `.L26` may
|
|
1136
1520
|
or may not be "the same" loop — match them through the listing, not by
|