asmdiff 0.3.3__tar.gz → 0.4.1__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.1}/PKG-INFO +539 -136
- {asmdiff-0.3.3 → asmdiff-0.4.1}/README.md +538 -135
- {asmdiff-0.3.3 → asmdiff-0.4.1}/asmdiff.example.toml +54 -6
- asmdiff-0.4.1/asmdiff.py +3869 -0
- asmdiff-0.4.1/golden/across-two-files.txt +14 -0
- asmdiff-0.4.1/golden/elf-span-stats.txt +17 -0
- asmdiff-0.4.1/golden/elf.txt +14 -0
- asmdiff-0.4.1/golden/inspect-span-stats.txt +14 -0
- asmdiff-0.4.1/golden/inspect-two-cc.txt +15 -0
- asmdiff-0.4.1/golden/inspect.txt +11 -0
- asmdiff-0.4.1/golden/pairs-cost.txt +5 -0
- asmdiff-0.4.1/golden/pairs-s.txt +11 -0
- asmdiff-0.4.1/golden/pairs-two-cc.txt +25 -0
- asmdiff-0.4.1/golden/pairs.txt +10 -0
- asmdiff-0.4.1/golden/summary-two-files.txt +8 -0
- {asmdiff-0.3.3 → asmdiff-0.4.1}/pyproject.toml +7 -0
- asmdiff-0.4.1/test_asmdiff.py +4173 -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.1}/.gitignore +0 -0
- {asmdiff-0.3.3 → asmdiff-0.4.1}/LICENSE +0 -0
- {asmdiff-0.3.3 → asmdiff-0.4.1}/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.1
|
|
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
|
|
@@ -277,18 +339,28 @@ everywhere else; two things are ELF-specific:
|
|
|
277
339
|
targets, not labels, so in-function targets are rewritten to
|
|
278
340
|
`.L<hex-offset>` labels (the offset from the function start - stable
|
|
279
341
|
across rebuilds of an unchanged function, unlike link addresses). An
|
|
280
|
-
Xtensa zero-overhead loop's end address
|
|
281
|
-
hardware loop is distinguishable from a
|
|
282
|
-
spans` column at a glance: `render_lut`
|
|
283
|
-
body as a ZOL, `render_lut_cub` fell back
|
|
284
|
-
branch loop.
|
|
342
|
+
Xtensa zero-overhead loop's end address (a pi32v2 `rep` block's too)
|
|
343
|
+
becomes `.L<off>_LEND`, so a hardware loop is distinguishable from a
|
|
344
|
+
branch loop in the `loop spans` column at a glance: `render_lut`
|
|
345
|
+
shipped its 32-instruction body as a ZOL, `render_lut_cub` fell back
|
|
346
|
+
to a 74-instruction 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:
|
|
@@ -343,6 +415,22 @@ cue to pass a matching target.
|
|
|
343
415
|
A whole firmware has over a thousand functions, so a bare ELF with no
|
|
344
416
|
function names and no `--filter` is an error, not a thousand-row table.
|
|
345
417
|
|
|
418
|
+
**Other toolchains.** Besides GNU objdump output, the JieLi pi32v2
|
|
419
|
+
objdump's dialect is read (the AC79 family, e.g. the M-VAVE FM-1 and
|
|
420
|
+
its open firmwares): bare `name:` headers, the space-padded byte column,
|
|
421
|
+
`<sym+0x96 : addr >` targets and the decimal displacement before
|
|
422
|
+
`call`/`goto` are normalised into the GNU shape, `.rodata` dumps are
|
|
423
|
+
skipped, and the algebraic instruction form (`r5_r4 = r4 * r14 (s)`,
|
|
424
|
+
`[r6+0] += r4`) is bucketed by shape for `--span-stats`. The
|
|
425
|
+
toolchain's objdump sits beside its clang, so the bundled `pi32v2`
|
|
426
|
+
profile (or any `--cc` naming that clang by path) derives it; otherwise
|
|
427
|
+
pass `--objdump`. Compile mode works with
|
|
428
|
+
`--cc '$JIELI_TOOLCHAIN/pi32v2/bin/clang -target pi32v2 -Os'`. The
|
|
429
|
+
compiler does not emit the `rep` hardware loop from C, but vendor inline
|
|
430
|
+
asm uses it (`rep n rN { ... }`); a `rep` block gets a span over its
|
|
431
|
+
body, named `.L<off>_LEND` in ELF input like an Xtensa zero-overhead
|
|
432
|
+
loop.
|
|
433
|
+
|
|
346
434
|
If the question behind the sweep is "did my hot loops ship as
|
|
347
435
|
zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
|
|
348
436
|
`-O2`) are worth knowing before blaming the compiler:
|
|
@@ -357,12 +445,16 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
|
|
|
357
445
|
|
|
358
446
|
```
|
|
359
447
|
asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
|
|
360
|
-
[--cc 'CC FLAGS']... [--
|
|
448
|
+
[--target NAME]... [--cc 'CC FLAGS']... [--config PATH]
|
|
361
449
|
[--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
|
|
362
|
-
[--summary-only] [--collapse] [--span-stats]
|
|
450
|
+
[--filter REGEX] [--summary-only] [--collapse] [--span-stats]
|
|
451
|
+
[--cost] [--costs NAME] [--fail-on-growth] [--json] [--width N]
|
|
363
452
|
[--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
|
|
364
453
|
asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
365
|
-
[-l list] [--summary-only] [--span-stats]
|
|
454
|
+
[-l list] [--summary-only] [--span-stats] [--cost] [--costs NAME]
|
|
455
|
+
[--json] [--width N]
|
|
456
|
+
asmdiff --edit-config | --example-config | --list-targets | --version
|
|
457
|
+
asmdiff --completion bash|zsh|fish | --install-completion [SHELL]
|
|
366
458
|
```
|
|
367
459
|
|
|
368
460
|
| Option | Meaning |
|
|
@@ -371,18 +463,25 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
|
371
463
|
| `-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
464
|
| `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
|
|
373
465
|
| `-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
|
-
|
|
|
466
|
+
| `-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
467
|
| `--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
468
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
377
|
-
|
|
|
469
|
+
| `-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
470
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
471
|
+
| `--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
472
|
| `-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
473
|
| `--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
474
|
| `--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
475
|
| `-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
|
-
| `--
|
|
476
|
+
| `--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. |
|
|
477
|
+
| `-C, --collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
|
|
478
|
+
| `--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. |
|
|
479
|
+
| `--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. |
|
|
480
|
+
| `--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). |
|
|
481
|
+
| `--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). |
|
|
482
|
+
| `--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`). |
|
|
483
|
+
| `--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). |
|
|
484
|
+
| `--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
485
|
| `--version` | Print the version and exit. |
|
|
387
486
|
| `-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
487
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
@@ -409,45 +508,72 @@ asmdiff h.c -- -fno-math-errno
|
|
|
409
508
|
Compilers missing from `PATH` are skipped with a warning; the run fails only
|
|
410
509
|
if none are usable, and that error names each missed binary
|
|
411
510
|
(`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
|
-
|
|
511
|
+
warning that scrolled away. A row whose compile *fails* is dropped the same
|
|
512
|
+
way: the remaining rows are compiled, their table and listings print, and
|
|
513
|
+
the failing row's compiler output follows on stderr under its label
|
|
514
|
+
(`error: [esp32s3] xtensa-esp32s3-elf-gcc failed on biquad.c`) once the
|
|
515
|
+
run has printed everything it could. One broken target no longer costs
|
|
516
|
+
you the others; the run still exits 1.
|
|
517
|
+
|
|
518
|
+
| Status | Meaning |
|
|
519
|
+
|---|---|
|
|
520
|
+
| 0 | The run printed what was asked for. Differing assembly is the expected result, never an error. |
|
|
521
|
+
| 1 | The tool failed: a compile error, an unknown `--pair` name, a config that does not load, no usable compiler in the matrix. |
|
|
522
|
+
| 2 | argparse's usage error - an unknown flag, a missing value, a negative `--width`. |
|
|
523
|
+
| 3 | `--fail-on-growth` found a candidate with more instructions than its baseline. |
|
|
416
524
|
|
|
417
525
|
## Shaping the output for reading vs deciding
|
|
418
526
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
527
|
+
Every run leads with the table: the legend, one summary table for the
|
|
528
|
+
whole matrix, the `--span-stats` and provenance lines that belong to it,
|
|
529
|
+
and then the listings, grouped per target under `== LABEL ==`. A
|
|
530
|
+
two-file compare over a three-target matrix still prints three full
|
|
531
|
+
side-by-side listings under that table, roughly 100 KB of them when the
|
|
532
|
+
table was all the decision needed. Four flags cut the output to purpose:
|
|
424
533
|
|
|
425
534
|
- **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
|
|
426
535
|
the scripted/agent view: the table is the decision input, and a listing
|
|
427
536
|
is pulled with a second, narrower run only when a delta needs
|
|
428
|
-
explaining.
|
|
429
|
-
- **`--collapse`** elides runs of identical line pairs in every
|
|
537
|
+
explaining. What is left is the legend, the table, and its footer.
|
|
538
|
+
- **`--collapse`** (`-C`) elides runs of identical line pairs in every
|
|
430
539
|
side-by-side listing, keeping 3 lines of context around each difference
|
|
431
540
|
plus a `... N identical lines ...` marker. Two ~500-instruction
|
|
432
541
|
functions differing by five instructions render as a few readable hunks
|
|
433
|
-
instead of ~1000 lines.
|
|
434
|
-
|
|
542
|
+
instead of ~1000 lines. Two sides that are equal line for line collapse
|
|
543
|
+
to their header with `(identical)` on the left title and no body at
|
|
544
|
+
all.
|
|
545
|
+
|
|
546
|
+
What it cannot collapse is the case where the two sides differ on every
|
|
547
|
+
line without differing in substance: a changed register allocation, a
|
|
548
|
+
reordered but equivalent schedule, one extra spill slot shifting every
|
|
549
|
+
offset. There is no identical run to elide, so the listing prints in
|
|
550
|
+
full. The summary table and `--span-stats` are the reading unit there:
|
|
551
|
+
the instruction classes, the calls, and the per-span mix say what
|
|
552
|
+
moved, where a line-by-line read of two differently allocated bodies
|
|
553
|
+
does not.
|
|
554
|
+
- **`--span-stats`** follows the stats table with a per-loop-span
|
|
435
555
|
instruction mix, one row per span:
|
|
436
556
|
|
|
437
557
|
```
|
|
438
|
-
function span insns load store mul branch other
|
|
439
|
-
stereo_reverb .L108 327 96 31 14 12
|
|
558
|
+
function span depth insns load store mul div branch other
|
|
559
|
+
stereo_reverb .L108 0 327 96 31 14 0 12 174
|
|
440
560
|
```
|
|
441
561
|
|
|
442
562
|
This weighs the span rather than the whole function — the number that
|
|
443
563
|
actually decides a hot-loop comparison (is the rewrite trading loads
|
|
444
564
|
for stores? did the multiplies move?) without hand-counting a listing
|
|
445
565
|
and accidentally tallying past the loop end into the epilogue.
|
|
566
|
+
*depth* is how many other spans of the same function contain this
|
|
567
|
+
one, so an inner loop is told from the loop around it without reading
|
|
568
|
+
the listing ([How a span is found](#how-a-span-is-found)).
|
|
446
569
|
Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
|
|
447
570
|
memory-operand heuristic for x86; *branch* means any control
|
|
448
571
|
transfer — conditional and unconditional branches, calls, and
|
|
449
572
|
returns (outbound calls are already itemised by name in the `calls`
|
|
450
573
|
column) — and anything the tables don't know lands in *other*.
|
|
574
|
+
*div* is a hardware divide or square root; a division done by a
|
|
575
|
+
libcall (`__divsf3`, `__udivdi3`) is a call, and the Xtensa FPU
|
|
576
|
+
divide sequence counts as the several instructions it is.
|
|
451
577
|
|
|
452
578
|
Reading *branch* inside a span: a software loop's own backedge is
|
|
453
579
|
one of them, because the span runs from the label to the last
|
|
@@ -460,21 +586,54 @@ output to purpose:
|
|
|
460
586
|
show as *branch* falling while *insns* stays roughly flat, and a
|
|
461
587
|
call appearing inside a hot span is the ZOL killer the `calls`
|
|
462
588
|
column already flagged.
|
|
589
|
+
- **`--cost`** adds a `cost` column to the stats tables saying what
|
|
590
|
+
each function is made of: the same instruction classes over the
|
|
591
|
+
whole body, and the tier of every call site with the sites a loop
|
|
592
|
+
span holds counted apart. `--costs NAME` prices those counts with a
|
|
593
|
+
measured profile from the config. Both are described under [Cost
|
|
594
|
+
column](#cost-column) below.
|
|
595
|
+
|
|
596
|
+
All four combine with every compile mode; `--summary-only`,
|
|
597
|
+
`--span-stats`, `--cost` and `--costs` also apply to ELF input.
|
|
463
598
|
|
|
464
|
-
|
|
465
|
-
|
|
599
|
+
Tables and side-by-side listings take their width from the terminal,
|
|
600
|
+
falling back off one to `$COLUMNS` and then to 120 columns; `--width N`
|
|
601
|
+
sets the budget directly, and `--width 0` means unlimited, which is the
|
|
602
|
+
untrimmed callee column a script greps.
|
|
466
603
|
|
|
467
604
|
For scripted callers, `--json` replaces the tables entirely with one
|
|
468
605
|
JSON document on stdout: a flat `results` list holding one record per
|
|
469
|
-
function per compiler — `
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
606
|
+
function per compiler — `target` (the matrix row's label, the same
|
|
607
|
+
string the table's `target` column prints), `cc`, `tag` (source label
|
|
608
|
+
in two-file runs), `role` (`baseline`/`candidate` in paired runs),
|
|
609
|
+
`insns`, `loop_spans` (each `{label, insns, depth}`), `calls`, `delta`
|
|
610
|
+
(on candidate records), `cost` when `--cost` or `--costs` is given, and
|
|
611
|
+
`span_stats` when `--span-stats` is given. An ELF run compiles nothing,
|
|
612
|
+
so its records have no matrix row to name and carry neither `target`
|
|
613
|
+
nor `cc`. Flat records keep it one `jq` expression away from any
|
|
614
|
+
question the tables answer.
|
|
615
|
+
|
|
616
|
+
The pairing questions the tool answers itself, because the pairing is
|
|
617
|
+
its own: every summary table closes a pair with a `delta` row (signed
|
|
618
|
+
instruction count, per-span `before -> after`, callees gained with `+`
|
|
619
|
+
and lost with `-`, and under `--cost` the classes and tiers that
|
|
620
|
+
moved), each candidate record carries the same as a `delta` object,
|
|
621
|
+
and `--fail-on-growth` makes it an exit status:
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# Fail the job when a rewrite that was meant to shrink did not
|
|
625
|
+
asmdiff old.c new.c -a stereo_reverb -t esp32s3 --fail-on-growth -s
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Exit 3 and one `growth: FUNC +N insns (BASELINE -> CANDIDATE)` line per
|
|
629
|
+
offender on stderr if any candidate has more instructions than its
|
|
630
|
+
baseline, exit 0 otherwise; status 1 is left meaning the tool itself
|
|
631
|
+
failed and 2 is argparse's usage error, so a job can tell "grew" from
|
|
632
|
+
"did not compile" from "misused the flags".
|
|
476
633
|
|
|
477
|
-
|
|
634
|
+
Arbitrary selection stays jq's: the tool prints JSON on stdout, so a
|
|
635
|
+
plain `| jq` pipe *is* the integration, and jq does that filtering
|
|
636
|
+
better than a wrapper flag could. A few recipes:
|
|
478
637
|
|
|
479
638
|
```bash
|
|
480
639
|
# Instruction count per function per side, as a table
|
|
@@ -485,19 +644,14 @@ asmdiff old.c new.c --json \
|
|
|
485
644
|
asmdiff old.c new.c --json \
|
|
486
645
|
| jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
|
|
487
646
|
|
|
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
647
|
# The full record for one hot function, spans and all
|
|
499
648
|
asmdiff old.c new.c -a stereo_reverb --json --span-stats \
|
|
500
649
|
| jq '.results[] | {role, insns, spans: .loop_spans}'
|
|
650
|
+
|
|
651
|
+
# Functions reaching a soft-float helper from inside a loop
|
|
652
|
+
asmdiff old.c new.c --json --cost \
|
|
653
|
+
| jq '.results[] | select(.cost.tiers_in_loop.softfp > 0)
|
|
654
|
+
| {function, tag, in_loop: .cost.tiers_in_loop}'
|
|
501
655
|
```
|
|
502
656
|
|
|
503
657
|
The top level carries `asmdiff` (version) and `mode`
|
|
@@ -505,6 +659,99 @@ The top level carries `asmdiff` (version) and `mode`
|
|
|
505
659
|
path as `elf`). Warnings and errors stay plain text on stderr, so a
|
|
506
660
|
failed run never emits half a document.
|
|
507
661
|
|
|
662
|
+
### Cost column
|
|
663
|
+
|
|
664
|
+
`--cost` adds a `cost` column before `calls`, saying what each function
|
|
665
|
+
is made of rather than how many lines long it is:
|
|
666
|
+
|
|
667
|
+
```
|
|
668
|
+
$ asmdiff asmdiff_example.c --cost -s --cc 'gcc -O2'
|
|
669
|
+
|
|
670
|
+
function role insns loop spans cost calls
|
|
671
|
+
old_const baseline 3 - mul:1 | br:1 | oth:1 -
|
|
672
|
+
new_const candidate 3 - br:1 | oth:2 | libm:1 ldexpf
|
|
673
|
+
delta 0 - mul:-1 | oth:+1 | libm:+1 +ldexpf
|
|
674
|
+
old_rt baseline 9 - st:1 | mul:1 | br:2 | oth:5 | libm:1 exp2f
|
|
675
|
+
new_rt candidate 2 - br:1 | oth:1 | libm:1 ldexpf
|
|
676
|
+
delta -7 - st:-1 | mul:-1 | br:-1 | oth:-4 +ldexpf -exp2f
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Instructions fall into the six classes `--span-stats` uses, abbreviated
|
|
680
|
+
here (`ld`, `st`, `mul`, `div`, `br`, `oth`). Every call site is
|
|
681
|
+
additionally tiered by the callee's name:
|
|
682
|
+
|
|
683
|
+
| Tier | Callees |
|
|
684
|
+
|---|---|
|
|
685
|
+
| `softfp` | soft-float add, subtract, multiply, compare, convert: `__addsf3`, `__muldf3`, `__floatsidf`, `__aeabi_dmul`, `__aeabi_i2d` |
|
|
686
|
+
| `softfp-div` | soft-float divide: `__divsf3`, `__divdf3`, `__aeabi_fdiv`, `__aeabi_ddiv` |
|
|
687
|
+
| `int-div` | integer divide and modulo helpers: `__udivdi3`, `__moddi3`, `__aeabi_idivmod`, `__aeabi_uldivmod` |
|
|
688
|
+
| `libm` | the math families, float and double: `sqrtf`, `sin`, `exp2f`, `ldexpf`, `pow`, `fmod` |
|
|
689
|
+
| `mem` | `memcpy`, `memset`, `memmove`, `memcmp` |
|
|
690
|
+
| `call` | everything else, `indirect(<reg>)` included |
|
|
691
|
+
|
|
692
|
+
The libgcc and the ARM EABI spelling of one helper land in the same
|
|
693
|
+
tier, so a `__muldf3` target and a `__aeabi_dmul` target read alike. A
|
|
694
|
+
name no pattern knows lands in `call`, where the `calls` column already
|
|
695
|
+
spells it out.
|
|
696
|
+
|
|
697
|
+
Each tier is followed by how many of its call sites a loop span holds -
|
|
698
|
+
`softfp:3 (2 in loop)` - since a soft-float helper reached once per
|
|
699
|
+
iteration is a different finding from one on an error path. The counts
|
|
700
|
+
are per call site, not per distinct callee: two calls to `__muldf3`
|
|
701
|
+
count twice, where the `calls` column names it once.
|
|
702
|
+
|
|
703
|
+
The delta row's cost cell lists only what moved. A tier whose count
|
|
704
|
+
came through the rewrite unchanged is absent from it even when the
|
|
705
|
+
callee changed: `old_rt` and `new_rt` above each hold one `libm` call,
|
|
706
|
+
so the cell says nothing about `libm` while the `calls` column carries
|
|
707
|
+
the swap as `+ldexpf -exp2f`. The two cells are read together.
|
|
708
|
+
|
|
709
|
+
With a profile (`--costs NAME`, or a target's `costs = "NAME"`), the
|
|
710
|
+
cell opens with a score and the number of instructions and call sites
|
|
711
|
+
no weight covered, and the table is followed by the line saying where
|
|
712
|
+
the weights came from:
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
score 399 (1 unweighted) | ld:40 | st:12 | mul:14 | ...
|
|
716
|
+
...
|
|
717
|
+
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
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
The weights behind that score are the commented template in
|
|
721
|
+
`asmdiff --example-config`, not a measurement; no measured profile
|
|
722
|
+
ships with the tool, and [Cost profiles](#cost-profiles) below is how
|
|
723
|
+
one is written. The delta row leads with the signed score. A cell is
|
|
724
|
+
capped at 56 characters and closes with `...` when the mix does not fit
|
|
725
|
+
(as it does here), so the table stays aligned; `--json` carries all of
|
|
726
|
+
it:
|
|
727
|
+
|
|
728
|
+
```json
|
|
729
|
+
"cost": {
|
|
730
|
+
"classes": {"load": 40, "store": 12, "mul": 14, "div": 1,
|
|
731
|
+
"branch": 9, "other": 61},
|
|
732
|
+
"tiers": {"softfp": 3, "call": 1},
|
|
733
|
+
"tiers_in_loop": {"softfp": 2},
|
|
734
|
+
"score": 399,
|
|
735
|
+
"unweighted": 1,
|
|
736
|
+
"profile": {"name": "esp32s3-iram",
|
|
737
|
+
"measured_on": "ESP32-S3 rev 0.2, code in IRAM, ...",
|
|
738
|
+
"method": "esp_cpu_get_cycle_count harness, ..."}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
`classes` always carries all six keys. `tiers` and `tiers_in_loop` list
|
|
743
|
+
only what the function reaches, so `.cost.tiers.softfp` on a function
|
|
744
|
+
with no soft-float is `null` rather than `0` - `// 0` in the jq
|
|
745
|
+
expression if you want a number. Without a profile, `score` and
|
|
746
|
+
`profile` are `null` and `unweighted` is every instruction and call
|
|
747
|
+
site the function has. Candidate records also gain `delta.cost`
|
|
748
|
+
(`classes`, `tiers`, `score`), again signed and again only what moved.
|
|
749
|
+
|
|
750
|
+
ELF input takes `--cost` and `--costs` like a compile run does. A
|
|
751
|
+
target's `costs = "NAME"` is not consulted there: nothing is compiled,
|
|
752
|
+
so no matrix row stands behind the binary's instructions. Name the
|
|
753
|
+
profile with `--costs`.
|
|
754
|
+
|
|
508
755
|
## Config file: named targets
|
|
509
756
|
|
|
510
757
|
Retyping a cross-compiler path and ten flags per run is the enemy of actually
|
|
@@ -532,8 +779,9 @@ flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
|
|
|
532
779
|
survives toolchain upgrades (`esp-14` → `esp-15`) without editing. A
|
|
533
780
|
pattern matching several installed toolchains resolves to the highest
|
|
534
781
|
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
|
|
782
|
+
choice is printed to stderr; the legend line above the table always
|
|
783
|
+
shows the fully resolved command that actually ran, so the short
|
|
784
|
+
`target` label never hides which binary it was. No match is an error. Pin
|
|
537
785
|
the exact directory instead when reproducibility matters more than
|
|
538
786
|
convenience. Flags expand `$VARS` only (no globbing).
|
|
539
787
|
|
|
@@ -542,16 +790,122 @@ shapes:
|
|
|
542
790
|
|
|
543
791
|
```bash
|
|
544
792
|
asmdiff h.c # config default target(s)
|
|
545
|
-
asmdiff h.c
|
|
546
|
-
asmdiff h.c
|
|
793
|
+
asmdiff h.c -t esp32s3 -t host # two-target matrix
|
|
794
|
+
asmdiff h.c -t esp32s3,host # same, as a comma list
|
|
795
|
+
asmdiff h.c --across f -t esp32s3 --cc 'gcc -O2' # mix freely
|
|
796
|
+
asmdiff --list-targets # what does my config define?
|
|
547
797
|
```
|
|
548
798
|
|
|
549
799
|
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
|
-
|
|
800
|
+
naming a source in that directory finds it, from any CWD.
|
|
801
|
+
|
|
802
|
+
`default` takes whatever `-t` takes: a target name, a `[groups]` name, a
|
|
803
|
+
comma list, a glob over target names, or an array mixing those
|
|
804
|
+
(`default = ["native", "esp32s3"]`). A name it cannot resolve is the
|
|
805
|
+
same error `-t` would give. One idiom worth knowing is the one-member
|
|
806
|
+
group: `[groups] ship = ["esp32s3"]` with `default = "ship"` gives the
|
|
807
|
+
standing matrix a name of its own, so widening the run later is editing
|
|
808
|
+
the group rather than every `default` and script that mentions the
|
|
809
|
+
target.
|
|
810
|
+
|
|
811
|
+
The included `asmdiff.example.toml` is a starting point. If a flag or
|
|
812
|
+
include path must vary per machine, that's what per-machine config files
|
|
813
|
+
are for — nothing lives in the tool.
|
|
814
|
+
|
|
815
|
+
### Target groups
|
|
816
|
+
|
|
817
|
+
A `[groups]` table names matrices of targets so a whole family runs
|
|
818
|
+
from one `-t`, without editing `default`:
|
|
819
|
+
|
|
820
|
+
```toml
|
|
821
|
+
[groups]
|
|
822
|
+
riscv32-esp = ["esp32c3", "esp32c6", "esp32h2", "esp32p4"]
|
|
823
|
+
native = ["gcc", "clang"]
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
asmdiff h.c -t riscv32-esp # four RISC-V targets
|
|
828
|
+
asmdiff h.c -t native -t esp32s3 # a group plus a single target
|
|
829
|
+
asmdiff h.c -t 'esp32c*' # glob over target names (quote it)
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
A `-t` value is resolved as an exact target name first, then as a group
|
|
833
|
+
name, then as a glob (`*`, `?`, `[...]`) over target names in config
|
|
834
|
+
order. A group naming an undefined target, or an empty group, is an
|
|
835
|
+
error. `--list-targets` prints what the resolved config defines.
|
|
836
|
+
|
|
837
|
+
### Cost profiles
|
|
838
|
+
|
|
839
|
+
A `[costs.NAME]` table holds measured weights and where they were
|
|
840
|
+
measured. `--cost` alone counts; with a profile in hand it also scores:
|
|
841
|
+
|
|
842
|
+
```toml
|
|
843
|
+
[costs.esp32s3-iram]
|
|
844
|
+
measured_on = "ESP32-S3 rev 0.2, code in IRAM, esp-15.2.0 libgcc"
|
|
845
|
+
method = "esp_cpu_get_cycle_count harness, median of 1000 runs"
|
|
846
|
+
other = 1
|
|
847
|
+
load = 2
|
|
848
|
+
store = 1
|
|
849
|
+
mul = 2
|
|
850
|
+
div = 20
|
|
851
|
+
branch = 2
|
|
852
|
+
softfp = 60
|
|
853
|
+
softfp-div = 200
|
|
854
|
+
int-div = 40
|
|
855
|
+
"__muldf3" = 90
|
|
856
|
+
|
|
857
|
+
[esp32s3]
|
|
858
|
+
cc = "..."
|
|
859
|
+
flags = ["-O2", "-mlongcalls"]
|
|
860
|
+
costs = "esp32s3-iram"
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
A key's value decides what it is. A number is a weight, keyed by an
|
|
864
|
+
instruction class (`load`, `store`, `mul`, `div`, `branch`, `other`),
|
|
865
|
+
a libcall tier (`softfp`, `softfp-div`, `int-div`, `libm`), or a call
|
|
866
|
+
symbol in quotes (`"__muldf3" = 90`). A string is provenance:
|
|
867
|
+
`measured_on` and `method` are required and the load fails naming the
|
|
868
|
+
one that is missing, and any further string key is kept and printed
|
|
869
|
+
after them. Anything else is an error naming the key. `mem` and `call`
|
|
870
|
+
are refused a weight, since a `memcpy`'s cost is its size argument and
|
|
871
|
+
an unknown callee's is its body, neither of which the assembly shows;
|
|
872
|
+
both stay counted and land in the unweighted total. `extends = "OTHER"`
|
|
873
|
+
copies another profile's weights before this table's own apply, one
|
|
874
|
+
level deep - a chain is an error.
|
|
875
|
+
|
|
876
|
+
Weights resolve in this order. An instruction costs its class weight,
|
|
877
|
+
or counts as unweighted. A call site costs its callee's own weight
|
|
878
|
+
where the profile names that symbol, else its tier's weight, else it
|
|
879
|
+
counts as unweighted. The score is therefore always printed with the
|
|
880
|
+
number of instructions and call sites it left out. It is an integer
|
|
881
|
+
while every weight is; one fractional weight anywhere rounds the total
|
|
882
|
+
to a decimal.
|
|
883
|
+
|
|
884
|
+
Which profile a row uses: `--costs NAME` sets it for every matrix row
|
|
885
|
+
and is the only route for a `--cc` row, which has no target to read one
|
|
886
|
+
from; otherwise each target brings the one its `costs = "NAME"` names,
|
|
887
|
+
so a two-target matrix scores each target under its own. A name that
|
|
888
|
+
does not resolve fails before the first compile. `--list-targets`
|
|
889
|
+
prints the profiles a config defines, with their `measured_on`.
|
|
890
|
+
|
|
891
|
+
What a score claims, and what it does not:
|
|
892
|
+
|
|
893
|
+
- **A weight is a static issue cost.** Dual issue, pipeline stalls,
|
|
894
|
+
cache and flash-fetch latency, and the operand-dependence of helpers
|
|
895
|
+
like `__divdf3` are not in it. Two scores that differ by a few
|
|
896
|
+
percent say nothing.
|
|
897
|
+
- **Scores compare within one target.** Cycle costs differ per core,
|
|
898
|
+
per memory placement, and per libgcc build, so a score under
|
|
899
|
+
`esp32s3-iram` and a score under some other profile are two different
|
|
900
|
+
units. That is why the provenance line prints under every scored
|
|
901
|
+
table.
|
|
902
|
+
- **Depth is reported, not multiplied.** A span at depth 2 scores like
|
|
903
|
+
one at depth 0; the trip count is not in the assembly. `--span-stats`
|
|
904
|
+
gives the depth and the per-span mix, and the weighing is yours.
|
|
905
|
+
|
|
906
|
+
No measured profile ships with the tool. `asmdiff --example-config`
|
|
907
|
+
carries the template above as comments, with the fields a reader needs
|
|
908
|
+
in order to judge how approximate the numbers are.
|
|
555
909
|
|
|
556
910
|
### Bundled ESP profiles
|
|
557
911
|
|
|
@@ -578,7 +932,10 @@ flags = ["-O2", "-march=rv32imac_zicsr_zifencei", "-mabi=ilp32"]
|
|
|
578
932
|
A profile is nothing more than a curated group of targets — the `esp-*`
|
|
579
933
|
glob finds the toolchains `idf_tools.py install` left in `~/.espressif`
|
|
580
934
|
(the newest, by the version-sort rule above, when several are
|
|
581
|
-
installed)
|
|
935
|
+
installed). The example config defines each profile as a `[groups]`
|
|
936
|
+
entry, so `-t riscv32-esp` or `-t xtensa-esp` runs it as one matrix and
|
|
937
|
+
`-t esp` runs every ESP chip; set `default` to a group's list to make it
|
|
938
|
+
the no-argument matrix:
|
|
582
939
|
|
|
583
940
|
```toml
|
|
584
941
|
default = ["esp32c3", "esp32c6", "esp32h2", "esp32p4"]
|
|
@@ -672,7 +1029,7 @@ Details that make it robust:
|
|
|
672
1029
|
- **Absent source is an error**, not a silent empty flag set — otherwise
|
|
673
1030
|
you'd just hit the missing-header failure this feature exists to prevent.
|
|
674
1031
|
The message flags a same-basename entry recorded under a different path.
|
|
675
|
-
- **`compile_commands` expands `~` and `$VARS`.** The
|
|
1032
|
+
- **`compile_commands` expands `~` and `$VARS`.** The legend line always
|
|
676
1033
|
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
677
1034
|
every include path the compiler actually received.
|
|
678
1035
|
- **`--db-includes` restricts the borrow to header-search paths**,
|
|
@@ -739,10 +1096,25 @@ Both walk up from the current directory, checking each level for
|
|
|
739
1096
|
CMake and idf.py leave it) — first hit wins. The walk stops at the
|
|
740
1097
|
repository root (the first directory with a `.git`), so running from any
|
|
741
1098
|
depth of component directory finds the project database, but an unrelated
|
|
742
|
-
one further up the filesystem is never picked up.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1099
|
+
one further up the filesystem is never picked up. It is never on by
|
|
1100
|
+
default — a target without `compile_commands` and no
|
|
1101
|
+
`--compile-commands` flag borrows nothing.
|
|
1102
|
+
|
|
1103
|
+
The two opt-ins differ in what happens when the walk finds nothing:
|
|
1104
|
+
|
|
1105
|
+
- **A bare `--compile-commands` is an error.** You asked for a database
|
|
1106
|
+
in this run and there is none; compiling anyway would silently drop
|
|
1107
|
+
the flags you came for.
|
|
1108
|
+
- **`compile_commands = true` in a target is a note.** The config
|
|
1109
|
+
describes where that target is usually compiled, not what this run
|
|
1110
|
+
is. A harness file compiled from a directory with no `build/` under
|
|
1111
|
+
it gets `target [esp32s3]: no compile_commands.json found near the
|
|
1112
|
+
current directory; compiling without borrowed flags` on stderr and
|
|
1113
|
+
compiles with the target's own `cc`/`flags`, which is what a
|
|
1114
|
+
standalone harness wants.
|
|
1115
|
+
|
|
1116
|
+
`compile_commands = "PATH"` naming a database that is not there stays an
|
|
1117
|
+
error under both: a path that does not resolve is a typo, not a context.
|
|
746
1118
|
|
|
747
1119
|
The precedence is what you'd hope: a target that names its own
|
|
748
1120
|
`compile_commands` path always keeps it; `--compile-commands` (with or
|
|
@@ -787,25 +1159,23 @@ cross-only defines or `-specs` a host gcc would choke on.
|
|
|
787
1159
|
|
|
788
1160
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
789
1161
|
auto-pair, the tool prints what it parsed instead of erroring: every
|
|
790
|
-
function's counts plus a file total. With two files,
|
|
1162
|
+
function's counts plus a file total. With two files, a leading `file`
|
|
1163
|
+
column tells the two apart inside the one table:
|
|
791
1164
|
|
|
792
1165
|
```
|
|
793
1166
|
$ asmdiff old/delay.c new/delay.c
|
|
794
1167
|
|
|
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, ...
|
|
1168
|
+
file function insns loop spans calls
|
|
1169
|
+
old/delay.c stereo_reverb 437 .L108:327 -
|
|
1170
|
+
old/delay.c ...
|
|
1171
|
+
old/delay.c TOTAL (13 functions) 956 - malloc_caps, free, ...
|
|
1172
|
+
new/delay.c ...
|
|
1173
|
+
new/delay.c TOTAL (13 functions) 1028 - malloc_caps, free, ...
|
|
807
1174
|
```
|
|
808
1175
|
|
|
1176
|
+
Over a matrix of several targets a `target` column leads the `file` one,
|
|
1177
|
+
so the whole run is still one table.
|
|
1178
|
+
|
|
809
1179
|
The TOTAL row is a coarse sanity check — did this refactor move the file's
|
|
810
1180
|
weight, did a call appear that shouldn't have? It sums parsed function
|
|
811
1181
|
bodies only (no literal pools, data, or alignment), so it is not a size
|
|
@@ -814,13 +1184,15 @@ measurement, and per-function rows are where the real information is.
|
|
|
814
1184
|
Only labels the assembler types as functions are listed — global data
|
|
815
1185
|
(string constants, state structs, lookup tables) gets column-0 labels too
|
|
816
1186
|
but is not code. A `calls` list longer than 8 symbols is truncated to
|
|
817
|
-
`...,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1187
|
+
`..., ... (N total)` (the leading `...` marks the elision, so the
|
|
1188
|
+
marker never reads as one more callee); real firmware dispatch
|
|
1189
|
+
functions call dozens of distinct symbols and would otherwise make
|
|
1190
|
+
rows thousands of characters wide. Rows are additionally trimmed to the
|
|
1191
|
+
column budget - the terminal, else `$COLUMNS`, else 120: callees are
|
|
1192
|
+
dropped from the end of the `calls` column (never the first one) behind
|
|
1193
|
+
the same `... (N total)` marker, so every row stays on one line.
|
|
1194
|
+
`--width 0` turns the trim off and keeps the full capped list, which is
|
|
1195
|
+
the shape to pipe into a grep.
|
|
824
1196
|
|
|
825
1197
|
## Comparing the same function across two builds (`--across`)
|
|
826
1198
|
|
|
@@ -852,6 +1224,11 @@ asmdiff src/filters.c --across dsps_biquad_f32_ansi \
|
|
|
852
1224
|
cc#1: gcc -Os
|
|
853
1225
|
cc#2: gcc -O3
|
|
854
1226
|
|
|
1227
|
+
function role insns loop spans calls
|
|
1228
|
+
dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
|
|
1229
|
+
dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
|
|
1230
|
+
delta +30 32 -> 54 -SMULR6
|
|
1231
|
+
|
|
855
1232
|
== cc#1 vs cc#2 ==
|
|
856
1233
|
|
|
857
1234
|
dsps_biquad_f32_ansi [cc#1] | dsps_biquad_f32_ansi [cc#2]
|
|
@@ -880,10 +1257,6 @@ addl %eax, %edx | sarl $11, %r12d
|
|
|
880
1257
|
movl %r14d, %r11d | sarl $11, %ebp
|
|
881
1258
|
call SMULR6 | leal 1024(%rax), %edx
|
|
882
1259
|
[... 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
1260
|
```
|
|
888
1261
|
|
|
889
1262
|
(The listing is abridged here; the tool prints all 92 rows. The columns
|
|
@@ -892,8 +1265,8 @@ the listing shows why — `SMULR6` inlined into the loop body, vector setup
|
|
|
892
1265
|
around it. Whether that trade is good is your call.)
|
|
893
1266
|
|
|
894
1267
|
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:
|
|
1268
|
+
invocations, then the table, then one listing section per
|
|
1269
|
+
baseline/candidate pairing. Runnable against the bundled example file:
|
|
897
1270
|
|
|
898
1271
|
```
|
|
899
1272
|
$ asmdiff asmdiff_example.c --across new_rt --cc 'gcc -O0' --cc 'gcc -O3'
|
|
@@ -901,6 +1274,11 @@ $ asmdiff asmdiff_example.c --across new_rt --cc 'gcc -O0' --cc 'gcc -O3'
|
|
|
901
1274
|
cc#1: gcc -O0
|
|
902
1275
|
cc#2: gcc -O3
|
|
903
1276
|
|
|
1277
|
+
function role insns loop spans calls
|
|
1278
|
+
new_rt [cc#1] baseline 13 - ldexpf
|
|
1279
|
+
new_rt [cc#2] candidate 2 - ldexpf
|
|
1280
|
+
delta -11 - -
|
|
1281
|
+
|
|
904
1282
|
== cc#1 vs cc#2 ==
|
|
905
1283
|
|
|
906
1284
|
new_rt [cc#1] | new_rt [cc#2]
|
|
@@ -918,15 +1296,12 @@ movd %eax, %xmm0 |
|
|
|
918
1296
|
call ldexpf@PLT |
|
|
919
1297
|
leave |
|
|
920
1298
|
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
1299
|
```
|
|
926
1300
|
|
|
927
1301
|
**Two files** — before/after versions of a source file (e.g. from a git
|
|
928
|
-
worktree, a branch checkout, or a patched copy).
|
|
929
|
-
|
|
1302
|
+
worktree, a branch checkout, or a patched copy). Every compiler in the
|
|
1303
|
+
matrix lands in the one table under its own `target` label, and each
|
|
1304
|
+
gets its own listing section:
|
|
930
1305
|
|
|
931
1306
|
```bash
|
|
932
1307
|
git worktree add ../baseline main
|
|
@@ -978,8 +1353,10 @@ The summary makes the trade-off immediate:
|
|
|
978
1353
|
function role insns calls
|
|
979
1354
|
exp2_lut [amy-baseline/log2_exp2.c] baseline 65 exp2f
|
|
980
1355
|
exp2_lut [amy/log2_exp2.c] candidate 59 ldexpf
|
|
1356
|
+
delta -6 +ldexpf -exp2f
|
|
981
1357
|
log2_lut [amy-baseline/log2_exp2.c] baseline 58 -
|
|
982
1358
|
log2_lut [amy/log2_exp2.c] candidate 64 ldexpf
|
|
1359
|
+
delta +6 +ldexpf
|
|
983
1360
|
```
|
|
984
1361
|
|
|
985
1362
|
The runtime site improves (a leaner libcall replaces `exp2f` + multiply),
|
|
@@ -1000,6 +1377,8 @@ probed the same way.
|
|
|
1000
1377
|
x86 (`call`, `jmp` tail calls), ARM (`bl`, `blx`), RISC-V (`call`,
|
|
1001
1378
|
`tail`, `jal`), and Xtensa (`call0/4/8/12`, `callx*`, `j`). Local-label
|
|
1002
1379
|
branches and register-indirect x86 jumps are not counted as calls.
|
|
1380
|
+
Register-indirect call mnemonics (`callx8 a8`, single-operand `jalr`,
|
|
1381
|
+
`blx r3`) are reported as `indirect(<reg>)`.
|
|
1003
1382
|
4. Loop spans come from label references alone — no mnemonic tables, no
|
|
1004
1383
|
control-flow analysis. The next section walks through it.
|
|
1005
1384
|
|
|
@@ -1033,6 +1412,16 @@ into that label's single span. Nested labels report separately — the
|
|
|
1033
1412
|
outer span simply contains the inner one. Forward references (loop exits
|
|
1034
1413
|
like `jle .L24`) are ignored.
|
|
1035
1414
|
|
|
1415
|
+
Containment is what the `depth` column of `--span-stats` reports: a
|
|
1416
|
+
span's depth is the number of other spans of the same function that
|
|
1417
|
+
start at or before it and end at or after it. An outermost loop is
|
|
1418
|
+
depth 0, a loop nested in it 1, and a span at depth 1 runs its body
|
|
1419
|
+
once per iteration of the span at depth 0. Two labels covering exactly
|
|
1420
|
+
the same lines (one loop body reached by two edges) contain each other
|
|
1421
|
+
under no reading, so both keep the depth of whatever encloses them.
|
|
1422
|
+
Depth is a nesting fact and nothing more - the trip count is not in the
|
|
1423
|
+
assembly, so nothing in the tool multiplies a count by it.
|
|
1424
|
+
|
|
1036
1425
|
The one arch-specific case is Xtensa zero-overhead loops, where the
|
|
1037
1426
|
hardware — not a branch — repeats the body, and the `loop` instruction
|
|
1038
1427
|
names its *end* label, forward:
|
|
@@ -1045,6 +1434,9 @@ names its *end* label, forward:
|
|
|
1045
1434
|
retw.n
|
|
1046
1435
|
```
|
|
1047
1436
|
|
|
1437
|
+
pi32v2's `rep` hardware loop (`rep .Ltmp8 r7` in `-S` output, `rep 6 r7 {`
|
|
1438
|
+
in objdump) is the same case, with its body ending at the named label.
|
|
1439
|
+
|
|
1048
1440
|
That is the entire mechanism. There is no CFG, no trip count, and no
|
|
1049
1441
|
notion of "the" loop: a backward `goto` produces a span exactly like a
|
|
1050
1442
|
`for` loop, and an unrolled loop's span is the unrolled body. The column
|
|
@@ -1063,6 +1455,11 @@ misreadings to avoid are few and predictable:
|
|
|
1063
1455
|
cost more than the rest of the function. Treat the count as a *size*
|
|
1064
1456
|
and *structure* fact - a call appearing, a loop body growing, a
|
|
1065
1457
|
softfloat sequence materializing - and measure time on the target.
|
|
1458
|
+
`--cost` splits the same body into instruction classes and libcall
|
|
1459
|
+
tiers, so that one `__udivdi3` line reads as an `int-div` call
|
|
1460
|
+
instead of as one more instruction; a `[costs.NAME]` profile prices
|
|
1461
|
+
those counts in measured cycles, which is still a static issue cost
|
|
1462
|
+
and still not a measurement of your program.
|
|
1066
1463
|
|
|
1067
1464
|
- **The cost of a call is in the callee.** The listing shows only the
|
|
1068
1465
|
call site. The `-O3` version of `new_rt` above is two instructions,
|
|
@@ -1071,7 +1468,9 @@ misreadings to avoid are few and predictable:
|
|
|
1071
1468
|
|
|
1072
1469
|
- **Weigh the span, not the function.** One instruction added inside a
|
|
1073
1470
|
loop that runs per sample outweighs twenty added to setup code. The
|
|
1074
|
-
whole-function count charges both the same
|
|
1471
|
+
whole-function count charges both the same; `--span-stats` gives the
|
|
1472
|
+
per-span mix and its nesting depth, and `--cost` counts the call
|
|
1473
|
+
sites a span holds apart from the rest.
|
|
1075
1474
|
|
|
1076
1475
|
- **Bigger is often faster.** Unrolling and vectorization raise every
|
|
1077
1476
|
count on purpose - the `-Os` vs `-O3` biquad above is bigger by every
|
|
@@ -1122,15 +1521,19 @@ standard library, and contains no project-specific constants. To port:
|
|
|
1122
1521
|
supported — on a Mac, compare inside a Linux container or with a
|
|
1123
1522
|
cross-toolchain.
|
|
1124
1523
|
- 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
|
-
|
|
1524
|
+
loaded address (other than x86 `jmp *reg`) are reported as
|
|
1525
|
+
`indirect(<reg>)` (e.g. Xtensa `callx8 a10` -> `indirect(a10)`), which
|
|
1526
|
+
errs toward visibility rather than silence. ELF input narrows this:
|
|
1527
|
+
`-mlongcalls` sequences are resolved to their real callee when objdump's
|
|
1528
|
+
literal annotation names one; genuine function-pointer dispatch still
|
|
1529
|
+
reads as indirect.
|
|
1530
|
+
- Side-by-side columns truncate long instruction lines to keep pairs
|
|
1531
|
+
aligned, and drop a trailing assembler comment (clang's `# TAILCALL`,
|
|
1532
|
+
`# 8-byte Reload`) before truncating, so the operands survive at half
|
|
1533
|
+
a terminal's width. An ARM `#4` immediate has no space after the hash
|
|
1534
|
+
and is untouched. Each side gets half the column budget, so `--width`
|
|
1535
|
+
widens them; `-l list`, the single-column listings and `--json` keep
|
|
1536
|
+
the line as extracted, comment included.
|
|
1134
1537
|
- Loop spans are layout facts, not loop analysis. Label numbers are
|
|
1135
1538
|
compiler-assigned, so a baseline's `.L27` and a candidate's `.L26` may
|
|
1136
1539
|
or may not be "the same" loop — match them through the listing, not by
|