asmdiff 0.2.0__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asmdiff-0.2.0 → asmdiff-0.3.3}/PKG-INFO +126 -8
- {asmdiff-0.2.0 → asmdiff-0.3.3}/README.md +124 -6
- {asmdiff-0.2.0 → asmdiff-0.3.3}/asmdiff.example.toml +6 -0
- {asmdiff-0.2.0 → asmdiff-0.3.3}/asmdiff.py +543 -81
- {asmdiff-0.2.0 → asmdiff-0.3.3}/pyproject.toml +16 -1
- {asmdiff-0.2.0 → asmdiff-0.3.3}/test_asmdiff.py +596 -2
- asmdiff-0.2.0/.claude/settings.local.json +0 -12
- asmdiff-0.2.0/2026-07-06-inspect-mode.md +0 -754
- asmdiff-0.2.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
- asmdiff-0.2.0/2026-07-07-elf-mode-handoff.md +0 -156
- asmdiff-0.2.0/2026-07-07-table-layout-handoff.md +0 -90
- asmdiff-0.2.0/CLAUDE.md +0 -33
- asmdiff-0.2.0/FIELD-TEST-REPORT.md +0 -123
- asmdiff-0.2.0/Screenshot 2026-07-07 225145.jpg +0 -0
- asmdiff-0.2.0/amy-hotspot-analysis.md +0 -214
- asmdiff-0.2.0/demo/esp32-matrix.gif +0 -0
- asmdiff-0.2.0/demo/quickstart.gif +0 -0
- asmdiff-0.2.0/dist_1/.gitignore +0 -1
- asmdiff-0.2.0/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- asmdiff-0.2.0/dist_1/asmdiff-0.1.4.tar.gz +0 -0
- asmdiff-0.2.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +0 -277
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -97
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +0 -147
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +0 -16
- asmdiff-0.2.0/gif-plan.md +0 -248
- asmdiff-0.2.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +0 -3
- {asmdiff-0.2.0 → asmdiff-0.3.3}/.gitignore +0 -0
- {asmdiff-0.2.0 → asmdiff-0.3.3}/LICENSE +0 -0
- {asmdiff-0.2.0 → asmdiff-0.3.3}/asmdiff_example.c +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -358,9 +358,11 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
|
|
|
358
358
|
```
|
|
359
359
|
asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
|
|
360
360
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
361
|
-
[--compile-commands [PATH]] [--flags-like PATH]
|
|
361
|
+
[--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
|
|
362
|
+
[--summary-only] [--collapse] [--span-stats]
|
|
362
363
|
[--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
|
|
363
364
|
asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
365
|
+
[-l list] [--summary-only] [--span-stats]
|
|
364
366
|
```
|
|
365
367
|
|
|
366
368
|
| Option | Meaning |
|
|
@@ -368,14 +370,20 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
|
368
370
|
| `SOURCE.c` | C file to compile — a purpose-built harness or a real project source. A second file may be given: with `--across` to compare a function, without it for a whole-file A/B summary. Bare names after the file are functions to inspect (see [Quick inspect](#quick-inspect-one-function-no-comparison)). An ELF binary (any name — detected by magic bytes) switches to [ELF input](#elf-input-what-actually-shipped-after-lto) instead of compiling. |
|
|
369
371
|
| `-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. |
|
|
370
372
|
| `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
|
|
371
|
-
| `-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).
|
|
372
|
-
| `--filter REGEX` |
|
|
373
|
+
| `-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
|
+
| `--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`. |
|
|
373
375
|
| `--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. |
|
|
374
376
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
375
377
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
376
378
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
377
379
|
| `-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). |
|
|
378
380
|
| `--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
|
+
| `--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
|
+
| `-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
|
+
| `--collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
|
|
385
|
+
| `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
|
|
386
|
+
| `--version` | Print the version and exit. |
|
|
379
387
|
| `-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. |
|
|
380
388
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
381
389
|
|
|
@@ -399,9 +407,103 @@ asmdiff h.c -- -fno-math-errno
|
|
|
399
407
|
```
|
|
400
408
|
|
|
401
409
|
Compilers missing from `PATH` are skipped with a warning; the run fails only
|
|
402
|
-
if none are usable
|
|
403
|
-
(
|
|
404
|
-
|
|
410
|
+
if none are usable, and that error names each missed binary
|
|
411
|
+
(`no-such-gcc: not found on PATH`) so the fix is in the message, not in a
|
|
412
|
+
warning that scrolled away. Exit status is non-zero only for operational
|
|
413
|
+
failures (compile error — the compiler's stderr is shown — unknown `--pair`
|
|
414
|
+
name, no usable compiler). Differing assembly is the expected result, never
|
|
415
|
+
an error.
|
|
416
|
+
|
|
417
|
+
## Shaping the output for reading vs deciding
|
|
418
|
+
|
|
419
|
+
A two-file compare over a three-target matrix prints three full
|
|
420
|
+
side-by-side listings with the summary table at the *end* of each — the
|
|
421
|
+
right shape for reading an unfamiliar delta, and roughly 100 KB of the
|
|
422
|
+
wrong shape when the table is all the decision needs. Three flags cut the
|
|
423
|
+
output to purpose:
|
|
424
|
+
|
|
425
|
+
- **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
|
|
426
|
+
the scripted/agent view: the table is the decision input, and a listing
|
|
427
|
+
is pulled with a second, narrower run only when a delta needs
|
|
428
|
+
explaining.
|
|
429
|
+
- **`--collapse`** elides runs of identical line pairs in every
|
|
430
|
+
side-by-side listing, keeping 3 lines of context around each difference
|
|
431
|
+
plus a `... N identical lines ...` marker. Two ~500-instruction
|
|
432
|
+
functions differing by five instructions render as a few readable hunks
|
|
433
|
+
instead of ~1000 lines.
|
|
434
|
+
- **`--span-stats`** follows each stats table with a per-loop-span
|
|
435
|
+
instruction mix, one row per span:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
function span insns load store mul branch other
|
|
439
|
+
stereo_reverb .L108 327 96 31 14 12 174
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
This weighs the span rather than the whole function — the number that
|
|
443
|
+
actually decides a hot-loop comparison (is the rewrite trading loads
|
|
444
|
+
for stores? did the multiplies move?) without hand-counting a listing
|
|
445
|
+
and accidentally tallying past the loop end into the epilogue.
|
|
446
|
+
Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
|
|
447
|
+
memory-operand heuristic for x86; *branch* means any control
|
|
448
|
+
transfer — conditional and unconditional branches, calls, and
|
|
449
|
+
returns (outbound calls are already itemised by name in the `calls`
|
|
450
|
+
column) — and anything the tables don't know lands in *other*.
|
|
451
|
+
|
|
452
|
+
Reading *branch* inside a span: a software loop's own backedge is
|
|
453
|
+
one of them, because the span runs from the label to the last
|
|
454
|
+
backward branch inclusive. A Xtensa zero-overhead loop instead
|
|
455
|
+
encloses only the body between `loop` and its end label, so the loop
|
|
456
|
+
machinery contributes zero and any branch counted there is real
|
|
457
|
+
per-iteration control flow — early exits, per-sample `if`/`else`,
|
|
458
|
+
wrap-around checks. Track it across variants: branches replaced by
|
|
459
|
+
conditional moves (`movnez`, `csel`, `cmov` — counted under *other*)
|
|
460
|
+
show as *branch* falling while *insns* stays roughly flat, and a
|
|
461
|
+
call appearing inside a hot span is the ZOL killer the `calls`
|
|
462
|
+
column already flagged.
|
|
463
|
+
|
|
464
|
+
All three combine with every compile mode; `--summary-only` and
|
|
465
|
+
`--span-stats` also apply to ELF input.
|
|
466
|
+
|
|
467
|
+
For scripted callers, `--json` replaces the tables entirely with one
|
|
468
|
+
JSON document on stdout: a flat `results` list holding one record per
|
|
469
|
+
function per compiler — `cc`, `tag` (source label in two-file runs),
|
|
470
|
+
`role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
|
|
471
|
+
`calls`, and `span_stats` when `--span-stats` is given. Flat records
|
|
472
|
+
keep it one `jq` expression away from any question the tables answer.
|
|
473
|
+
There is no built-in jq subcommand and there won't be: the tool prints
|
|
474
|
+
JSON on stdout, so a plain `| jq` pipe *is* the integration, and jq
|
|
475
|
+
already does the filtering better than a wrapper flag could.
|
|
476
|
+
|
|
477
|
+
A few recipes that cover the common questions:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Instruction count per function per side, as a table
|
|
481
|
+
asmdiff old.c new.c --json \
|
|
482
|
+
| jq -r '.results[] | "\(.tag)\t\(.function)\t\(.insns)"'
|
|
483
|
+
|
|
484
|
+
# Only the functions that emit libcalls (soft-float, __divdi3, memcpy…)
|
|
485
|
+
asmdiff old.c new.c --json \
|
|
486
|
+
| jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
|
|
487
|
+
|
|
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
|
+
# The full record for one hot function, spans and all
|
|
499
|
+
asmdiff old.c new.c -a stereo_reverb --json --span-stats \
|
|
500
|
+
| jq '.results[] | {role, insns, spans: .loop_spans}'
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The top level carries `asmdiff` (version) and `mode`
|
|
504
|
+
(`pairs`/`across`/`inspect`/`summary`/`elf`; ELF runs add the binary's
|
|
505
|
+
path as `elf`). Warnings and errors stay plain text on stderr, so a
|
|
506
|
+
failed run never emits half a document.
|
|
405
507
|
|
|
406
508
|
## Config file: named targets
|
|
407
509
|
|
|
@@ -573,6 +675,17 @@ Details that make it robust:
|
|
|
573
675
|
- **`compile_commands` expands `~` and `$VARS`.** The `==` header always
|
|
574
676
|
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
575
677
|
every include path the compiler actually received.
|
|
678
|
+
- **`--db-includes` restricts the borrow to header-search paths**,
|
|
679
|
+
dropping `-D`/`-U`, forced includes, and `-specs`/`--sysroot`. Use it
|
|
680
|
+
when the *target* doesn't match the arch the database was built for —
|
|
681
|
+
host gcc given an ESP-IDF database would otherwise inherit
|
|
682
|
+
`-specs=picolibc.specs` and cross-only defines it cannot compile
|
|
683
|
+
with, yet still needs the project's include paths to find its
|
|
684
|
+
headers. The kept paths are re-emitted as `-idirafter`, searched
|
|
685
|
+
*after* the compiler's own system directories: ESP-IDF's include set
|
|
686
|
+
contains a libc-overlay directory (`esp_libc/platform_include`) whose
|
|
687
|
+
`stdio.h` would shadow the host's under plain `-I`; demoted, the
|
|
688
|
+
project's own headers still resolve and the host's libc wins.
|
|
576
689
|
|
|
577
690
|
### Auto-discovering the database
|
|
578
691
|
|
|
@@ -665,6 +778,11 @@ baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
|
665
778
|
path the database has never heard of. The absent-source error and the
|
|
666
779
|
soft-miss note both point at this flag when a same-name entry exists.
|
|
667
780
|
|
|
781
|
+
To contrast the same pair on a host compiler (does gcc-on-x86 make the
|
|
782
|
+
same choice?), add `--db-includes`: the borrow then carries only the
|
|
783
|
+
project's include paths, which are target-portable, and none of the
|
|
784
|
+
cross-only defines or `-specs` a host gcc would choke on.
|
|
785
|
+
|
|
668
786
|
## Whole-file summary
|
|
669
787
|
|
|
670
788
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -340,9 +340,11 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
|
|
|
340
340
|
```
|
|
341
341
|
asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
|
|
342
342
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
343
|
-
[--compile-commands [PATH]] [--flags-like PATH]
|
|
343
|
+
[--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
|
|
344
|
+
[--summary-only] [--collapse] [--span-stats]
|
|
344
345
|
[--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
|
|
345
346
|
asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
347
|
+
[-l list] [--summary-only] [--span-stats]
|
|
346
348
|
```
|
|
347
349
|
|
|
348
350
|
| Option | Meaning |
|
|
@@ -350,14 +352,20 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
|
350
352
|
| `SOURCE.c` | C file to compile — a purpose-built harness or a real project source. A second file may be given: with `--across` to compare a function, without it for a whole-file A/B summary. Bare names after the file are functions to inspect (see [Quick inspect](#quick-inspect-one-function-no-comparison)). An ELF binary (any name — detected by magic bytes) switches to [ELF input](#elf-input-what-actually-shipped-after-lto) instead of compiling. |
|
|
351
353
|
| `-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. |
|
|
352
354
|
| `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
|
|
353
|
-
| `-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).
|
|
354
|
-
| `--filter REGEX` |
|
|
355
|
+
| `-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. |
|
|
356
|
+
| `--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`. |
|
|
355
357
|
| `--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. |
|
|
356
358
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
357
359
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
358
360
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
359
361
|
| `-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). |
|
|
360
362
|
| `--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. |
|
|
363
|
+
| `--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. |
|
|
364
|
+
| `-s`, `--summary-only` | Print only the summary/stats tables, suppressing every assembly listing (see [Shaping the output](#shaping-the-output-for-reading-vs-deciding)). |
|
|
365
|
+
| `--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. |
|
|
366
|
+
| `--collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
|
|
367
|
+
| `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
|
|
368
|
+
| `--version` | Print the version and exit. |
|
|
361
369
|
| `-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. |
|
|
362
370
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
363
371
|
|
|
@@ -381,9 +389,103 @@ asmdiff h.c -- -fno-math-errno
|
|
|
381
389
|
```
|
|
382
390
|
|
|
383
391
|
Compilers missing from `PATH` are skipped with a warning; the run fails only
|
|
384
|
-
if none are usable
|
|
385
|
-
(
|
|
386
|
-
|
|
392
|
+
if none are usable, and that error names each missed binary
|
|
393
|
+
(`no-such-gcc: not found on PATH`) so the fix is in the message, not in a
|
|
394
|
+
warning that scrolled away. Exit status is non-zero only for operational
|
|
395
|
+
failures (compile error — the compiler's stderr is shown — unknown `--pair`
|
|
396
|
+
name, no usable compiler). Differing assembly is the expected result, never
|
|
397
|
+
an error.
|
|
398
|
+
|
|
399
|
+
## Shaping the output for reading vs deciding
|
|
400
|
+
|
|
401
|
+
A two-file compare over a three-target matrix prints three full
|
|
402
|
+
side-by-side listings with the summary table at the *end* of each — the
|
|
403
|
+
right shape for reading an unfamiliar delta, and roughly 100 KB of the
|
|
404
|
+
wrong shape when the table is all the decision needs. Three flags cut the
|
|
405
|
+
output to purpose:
|
|
406
|
+
|
|
407
|
+
- **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
|
|
408
|
+
the scripted/agent view: the table is the decision input, and a listing
|
|
409
|
+
is pulled with a second, narrower run only when a delta needs
|
|
410
|
+
explaining.
|
|
411
|
+
- **`--collapse`** elides runs of identical line pairs in every
|
|
412
|
+
side-by-side listing, keeping 3 lines of context around each difference
|
|
413
|
+
plus a `... N identical lines ...` marker. Two ~500-instruction
|
|
414
|
+
functions differing by five instructions render as a few readable hunks
|
|
415
|
+
instead of ~1000 lines.
|
|
416
|
+
- **`--span-stats`** follows each stats table with a per-loop-span
|
|
417
|
+
instruction mix, one row per span:
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
function span insns load store mul branch other
|
|
421
|
+
stereo_reverb .L108 327 96 31 14 12 174
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
This weighs the span rather than the whole function — the number that
|
|
425
|
+
actually decides a hot-loop comparison (is the rewrite trading loads
|
|
426
|
+
for stores? did the multiplies move?) without hand-counting a listing
|
|
427
|
+
and accidentally tallying past the loop end into the epilogue.
|
|
428
|
+
Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
|
|
429
|
+
memory-operand heuristic for x86; *branch* means any control
|
|
430
|
+
transfer — conditional and unconditional branches, calls, and
|
|
431
|
+
returns (outbound calls are already itemised by name in the `calls`
|
|
432
|
+
column) — and anything the tables don't know lands in *other*.
|
|
433
|
+
|
|
434
|
+
Reading *branch* inside a span: a software loop's own backedge is
|
|
435
|
+
one of them, because the span runs from the label to the last
|
|
436
|
+
backward branch inclusive. A Xtensa zero-overhead loop instead
|
|
437
|
+
encloses only the body between `loop` and its end label, so the loop
|
|
438
|
+
machinery contributes zero and any branch counted there is real
|
|
439
|
+
per-iteration control flow — early exits, per-sample `if`/`else`,
|
|
440
|
+
wrap-around checks. Track it across variants: branches replaced by
|
|
441
|
+
conditional moves (`movnez`, `csel`, `cmov` — counted under *other*)
|
|
442
|
+
show as *branch* falling while *insns* stays roughly flat, and a
|
|
443
|
+
call appearing inside a hot span is the ZOL killer the `calls`
|
|
444
|
+
column already flagged.
|
|
445
|
+
|
|
446
|
+
All three combine with every compile mode; `--summary-only` and
|
|
447
|
+
`--span-stats` also apply to ELF input.
|
|
448
|
+
|
|
449
|
+
For scripted callers, `--json` replaces the tables entirely with one
|
|
450
|
+
JSON document on stdout: a flat `results` list holding one record per
|
|
451
|
+
function per compiler — `cc`, `tag` (source label in two-file runs),
|
|
452
|
+
`role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
|
|
453
|
+
`calls`, and `span_stats` when `--span-stats` is given. Flat records
|
|
454
|
+
keep it one `jq` expression away from any question the tables answer.
|
|
455
|
+
There is no built-in jq subcommand and there won't be: the tool prints
|
|
456
|
+
JSON on stdout, so a plain `| jq` pipe *is* the integration, and jq
|
|
457
|
+
already does the filtering better than a wrapper flag could.
|
|
458
|
+
|
|
459
|
+
A few recipes that cover the common questions:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
# Instruction count per function per side, as a table
|
|
463
|
+
asmdiff old.c new.c --json \
|
|
464
|
+
| jq -r '.results[] | "\(.tag)\t\(.function)\t\(.insns)"'
|
|
465
|
+
|
|
466
|
+
# Only the functions that emit libcalls (soft-float, __divdi3, memcpy…)
|
|
467
|
+
asmdiff old.c new.c --json \
|
|
468
|
+
| jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
|
|
469
|
+
|
|
470
|
+
# Per-function old→new instruction delta (regressions have a positive delta)
|
|
471
|
+
asmdiff old.c new.c --json \
|
|
472
|
+
| jq -r '.results | group_by(.function)[] | select(length == 2)
|
|
473
|
+
| "\(.[0].function): \(.[0].insns) -> \(.[1].insns) (\(.[1].insns - .[0].insns))"'
|
|
474
|
+
|
|
475
|
+
# Fail CI if any function grew, using jq's exit status
|
|
476
|
+
asmdiff old.c new.c --json \
|
|
477
|
+
| jq -e '.results | group_by(.function)
|
|
478
|
+
| all(length < 2 or .[1].insns <= .[0].insns)' > /dev/null
|
|
479
|
+
|
|
480
|
+
# The full record for one hot function, spans and all
|
|
481
|
+
asmdiff old.c new.c -a stereo_reverb --json --span-stats \
|
|
482
|
+
| jq '.results[] | {role, insns, spans: .loop_spans}'
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The top level carries `asmdiff` (version) and `mode`
|
|
486
|
+
(`pairs`/`across`/`inspect`/`summary`/`elf`; ELF runs add the binary's
|
|
487
|
+
path as `elf`). Warnings and errors stay plain text on stderr, so a
|
|
488
|
+
failed run never emits half a document.
|
|
387
489
|
|
|
388
490
|
## Config file: named targets
|
|
389
491
|
|
|
@@ -555,6 +657,17 @@ Details that make it robust:
|
|
|
555
657
|
- **`compile_commands` expands `~` and `$VARS`.** The `==` header always
|
|
556
658
|
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
557
659
|
every include path the compiler actually received.
|
|
660
|
+
- **`--db-includes` restricts the borrow to header-search paths**,
|
|
661
|
+
dropping `-D`/`-U`, forced includes, and `-specs`/`--sysroot`. Use it
|
|
662
|
+
when the *target* doesn't match the arch the database was built for —
|
|
663
|
+
host gcc given an ESP-IDF database would otherwise inherit
|
|
664
|
+
`-specs=picolibc.specs` and cross-only defines it cannot compile
|
|
665
|
+
with, yet still needs the project's include paths to find its
|
|
666
|
+
headers. The kept paths are re-emitted as `-idirafter`, searched
|
|
667
|
+
*after* the compiler's own system directories: ESP-IDF's include set
|
|
668
|
+
contains a libc-overlay directory (`esp_libc/platform_include`) whose
|
|
669
|
+
`stdio.h` would shadow the host's under plain `-I`; demoted, the
|
|
670
|
+
project's own headers still resolve and the host's libc wins.
|
|
558
671
|
|
|
559
672
|
### Auto-discovering the database
|
|
560
673
|
|
|
@@ -647,6 +760,11 @@ baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
|
647
760
|
path the database has never heard of. The absent-source error and the
|
|
648
761
|
soft-miss note both point at this flag when a same-name entry exists.
|
|
649
762
|
|
|
763
|
+
To contrast the same pair on a host compiler (does gcc-on-x86 make the
|
|
764
|
+
same choice?), add `--db-includes`: the borrow then carries only the
|
|
765
|
+
project's include paths, which are target-portable, and none of the
|
|
766
|
+
cross-only defines or `-specs` a host gcc would choke on.
|
|
767
|
+
|
|
650
768
|
## Whole-file summary
|
|
651
769
|
|
|
652
770
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -26,6 +26,12 @@ flags = [
|
|
|
26
26
|
cc = "clang"
|
|
27
27
|
flags = ["-O3", "-Wall", "-Wextra"]
|
|
28
28
|
|
|
29
|
+
# The name the docs use for "your native compiler" (--target host): an
|
|
30
|
+
# alias of [gcc] kept as its own table so the examples work verbatim.
|
|
31
|
+
[host]
|
|
32
|
+
cc = "gcc"
|
|
33
|
+
flags = ["-O3", "-Wall", "-Wextra"]
|
|
34
|
+
|
|
29
35
|
# cc values may use ~, $VARS, and glob patterns. A glob that matches
|
|
30
36
|
# several installed toolchains resolves to the highest version-sorted
|
|
31
37
|
# one (announced on stderr); pin the exact esp-NN directory instead if
|