asmdiff 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.dist-info → asmdiff-0.3.0.dist-info}/METADATA +86 -5
- asmdiff-0.3.0.dist-info/RECORD +6 -0
- asmdiff.py +429 -54
- asmdiff-0.2.0.dist-info/RECORD +0 -6
- {asmdiff-0.2.0.dist-info → asmdiff-0.3.0.dist-info}/WHEEL +0 -0
- {asmdiff-0.2.0.dist-info → asmdiff-0.3.0.dist-info}/entry_points.txt +0 -0
- {asmdiff-0.2.0.dist-info → asmdiff-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -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
|
+
[--summary-only] [--span-stats]
|
|
364
366
|
```
|
|
365
367
|
|
|
366
368
|
| Option | Meaning |
|
|
@@ -376,6 +378,12 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
|
|
|
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, elide 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,66 @@ 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* includes calls (outbound
|
|
448
|
+
calls are already itemised in the `calls` column), and anything the
|
|
449
|
+
tables don't know lands in *other*.
|
|
450
|
+
|
|
451
|
+
All three combine with every compile mode; `--summary-only` and
|
|
452
|
+
`--span-stats` also apply to ELF input.
|
|
453
|
+
|
|
454
|
+
For scripted callers, `--json` replaces the tables entirely with one
|
|
455
|
+
JSON document on stdout: a flat `results` list holding one record per
|
|
456
|
+
function per compiler — `cc`, `tag` (source label in two-file runs),
|
|
457
|
+
`role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
|
|
458
|
+
`calls`, and `span_stats` when `--span-stats` is given. Flat records
|
|
459
|
+
keep it one `jq` expression away from any question the tables answer:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
asmdiff old.c new.c -a stereo_reverb --json --span-stats \
|
|
463
|
+
| jq '.results[] | {role, insns, spans: .loop_spans}'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The top level carries `asmdiff` (version) and `mode`
|
|
467
|
+
(`pairs`/`across`/`inspect`/`summary`/`elf`; ELF runs add the binary's
|
|
468
|
+
path as `elf`). Warnings and errors stay plain text on stderr, so a
|
|
469
|
+
failed run never emits half a document.
|
|
405
470
|
|
|
406
471
|
## Config file: named targets
|
|
407
472
|
|
|
@@ -573,6 +638,17 @@ Details that make it robust:
|
|
|
573
638
|
- **`compile_commands` expands `~` and `$VARS`.** The `==` header always
|
|
574
639
|
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
575
640
|
every include path the compiler actually received.
|
|
641
|
+
- **`--db-includes` restricts the borrow to header-search paths**,
|
|
642
|
+
dropping `-D`/`-U`, forced includes, and `-specs`/`--sysroot`. Use it
|
|
643
|
+
when the *target* doesn't match the arch the database was built for —
|
|
644
|
+
host gcc given an ESP-IDF database would otherwise inherit
|
|
645
|
+
`-specs=picolibc.specs` and cross-only defines it cannot compile
|
|
646
|
+
with, yet still needs the project's include paths to find its
|
|
647
|
+
headers. The kept paths are re-emitted as `-idirafter`, searched
|
|
648
|
+
*after* the compiler's own system directories: ESP-IDF's include set
|
|
649
|
+
contains a libc-overlay directory (`esp_libc/platform_include`) whose
|
|
650
|
+
`stdio.h` would shadow the host's under plain `-I`; demoted, the
|
|
651
|
+
project's own headers still resolve and the host's libc wins.
|
|
576
652
|
|
|
577
653
|
### Auto-discovering the database
|
|
578
654
|
|
|
@@ -665,6 +741,11 @@ baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
|
665
741
|
path the database has never heard of. The absent-source error and the
|
|
666
742
|
soft-miss note both point at this flag when a same-name entry exists.
|
|
667
743
|
|
|
744
|
+
To contrast the same pair on a host compiler (does gcc-on-x86 make the
|
|
745
|
+
same choice?), add `--db-includes`: the borrow then carries only the
|
|
746
|
+
project's include paths, which are target-portable, and none of the
|
|
747
|
+
cross-only defines or `-specs` a host gcc would choke on.
|
|
748
|
+
|
|
668
749
|
## Whole-file summary
|
|
669
750
|
|
|
670
751
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
asmdiff.py,sha256=JRhmdQKEuaT2pnqMDoFVr7CKhFCACHb8i2HljBfO8k4,84779
|
|
2
|
+
asmdiff-0.3.0.dist-info/METADATA,sha256=Kg2z3K5V7VlM7agNMTz13yNNQvEHwt1TtcHubC5nnx0,60801
|
|
3
|
+
asmdiff-0.3.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
+
asmdiff-0.3.0.dist-info/entry_points.txt,sha256=bVeIyjLm3KgWALbgRSeQo16pZ89GXZZ4sFTSWJUMdTI,41
|
|
5
|
+
asmdiff-0.3.0.dist-info/licenses/LICENSE,sha256=Y8JW9wzNeVnfyw62X3qC9cuwZCv_U67rNSF9QUE0HJs,1072
|
|
6
|
+
asmdiff-0.3.0.dist-info/RECORD,,
|
asmdiff.py
CHANGED
|
@@ -13,12 +13,14 @@ Usage:
|
|
|
13
13
|
[--pair OLD:NEW]... [--across FUNC]...
|
|
14
14
|
[--cc 'CC FLAGS']... [--target NAME]...
|
|
15
15
|
[--config PATH] [--compile-commands [PATH]]
|
|
16
|
-
[--flags-like PATH]
|
|
17
|
-
[--
|
|
16
|
+
[--flags-like PATH] [--db-includes]
|
|
17
|
+
[--summary-only] [--collapse] [--span-stats]
|
|
18
|
+
[--json] [--layout list|side-by-side] [-v]
|
|
18
19
|
[-- EXTRA_FLAGS...]
|
|
19
20
|
tools/asmdiff/asmdiff.py FIRMWARE.elf [FUNC...] [--filter REGEX]
|
|
20
|
-
[--objdump PATH]
|
|
21
|
-
|
|
21
|
+
[--objdump PATH] [--summary-only]
|
|
22
|
+
[--span-stats] [--json]
|
|
23
|
+
tools/asmdiff/asmdiff.py --edit-config | --example-config | --version
|
|
22
24
|
|
|
23
25
|
Five modes:
|
|
24
26
|
SOURCE.c FUNC inspect: print the named function's assembly, no
|
|
@@ -44,6 +46,14 @@ Five modes:
|
|
|
44
46
|
(neither) whole-file summary: per-function counts plus a file
|
|
45
47
|
total, for one file or side by side for two.
|
|
46
48
|
|
|
49
|
+
Four flags shape the output: --summary-only prints only the stats
|
|
50
|
+
tables, --collapse elides identical runs in side-by-side listings
|
|
51
|
+
keeping context around each difference, --span-stats follows each
|
|
52
|
+
stats table with a per-loop-span instruction mix (load/store/mul/
|
|
53
|
+
branch/other counts; branch includes calls), and --json replaces the
|
|
54
|
+
tables with one JSON document of per-function records (implies
|
|
55
|
+
--summary-only) for scripted callers.
|
|
56
|
+
|
|
47
57
|
Compilers come from --cc strings, from named targets in an asmdiff.toml
|
|
48
58
|
config file (--target NAME), from the config's `default` entry, or —
|
|
49
59
|
failing all of those — plain `gcc -O3` and `clang -O3`.
|
|
@@ -62,7 +72,11 @@ the CWD up to the repository root, and its build/ subdirectory; a source
|
|
|
62
72
|
absent from a database found that way is compiled without borrowed flags
|
|
63
73
|
(with a note) rather than being an error. --flags-like PATH lets such a
|
|
64
74
|
source borrow the flags recorded for PATH, so a modified copy compares
|
|
65
|
-
against its original under one header environment.
|
|
75
|
+
against its original under one header environment. --db-includes limits
|
|
76
|
+
any borrow to the header-search paths, re-emitted as -idirafter so they
|
|
77
|
+
cannot shadow the host's own system headers: a host target can then
|
|
78
|
+
compile a cross project's source without inheriting cross-only defines,
|
|
79
|
+
-specs, or a libc-overlay include directory.
|
|
66
80
|
"""
|
|
67
81
|
import argparse
|
|
68
82
|
import difflib
|
|
@@ -84,6 +98,10 @@ try:
|
|
|
84
98
|
except ModuleNotFoundError:
|
|
85
99
|
tomllib = None
|
|
86
100
|
|
|
101
|
+
# Single source of truth for the package version: pyproject.toml reads
|
|
102
|
+
# it from here (hatchling dynamic version).
|
|
103
|
+
__version__ = "0.3.0"
|
|
104
|
+
|
|
87
105
|
DEFAULT_COMPILERS = ["gcc", "clang"]
|
|
88
106
|
FALLBACK_FLAGS = "-O3"
|
|
89
107
|
CONFIG_NAME = "asmdiff.toml"
|
|
@@ -120,6 +138,12 @@ flags = [
|
|
|
120
138
|
cc = "clang"
|
|
121
139
|
flags = ["-O3", "-Wall", "-Wextra"]
|
|
122
140
|
|
|
141
|
+
# The name the docs use for "your native compiler" (--target host): an
|
|
142
|
+
# alias of [gcc] kept as its own table so the examples work verbatim.
|
|
143
|
+
[host]
|
|
144
|
+
cc = "gcc"
|
|
145
|
+
flags = ["-O3", "-Wall", "-Wextra"]
|
|
146
|
+
|
|
123
147
|
# cc values may use ~, $VARS, and glob patterns. A glob that matches
|
|
124
148
|
# several installed toolchains resolves to the highest version-sorted
|
|
125
149
|
# one (announced on stderr); pin the exact esp-NN directory instead if
|
|
@@ -213,6 +237,13 @@ _CC_DB_EQ_FLAGS = ("-specs", "--specs", "--sysroot")
|
|
|
213
237
|
_DB_CACHE = {}
|
|
214
238
|
_MISS_NOTED = set()
|
|
215
239
|
_BORROW_NOTED = set()
|
|
240
|
+
# --db-includes: borrow only the header-search paths from the database.
|
|
241
|
+
# A project's -I paths are valid on any target; its -D defines, forced
|
|
242
|
+
# includes, and -specs/--sysroot are tied to the arch the database was
|
|
243
|
+
# built for — this is how a host target compiles a cross project's
|
|
244
|
+
# source without inheriting cross-only flags.
|
|
245
|
+
DB_INCLUDES = False
|
|
246
|
+
_DB_SEARCH_PATH_FLAGS = ("-I", "-iquote", "-isystem", "-idirafter")
|
|
216
247
|
# --flags-like PATH: a source with no database entry borrows the flags
|
|
217
248
|
# recorded for PATH. This is how a modified copy of a project source gets
|
|
218
249
|
# the same header environment as its original, so an --across between them
|
|
@@ -383,6 +414,32 @@ def load_compile_commands(path):
|
|
|
383
414
|
return data
|
|
384
415
|
|
|
385
416
|
|
|
417
|
+
def _borrowed(flags):
|
|
418
|
+
"""Apply --db-includes to one borrowed flag list: keep the
|
|
419
|
+
header-search pairs (-I/-iquote/-isystem/-idirafter PATH), drop
|
|
420
|
+
defines, forced includes, and driver-level flags. Identity when
|
|
421
|
+
the option is off.
|
|
422
|
+
|
|
423
|
+
Kept paths are re-emitted as -idirafter — searched AFTER the
|
|
424
|
+
compiler's own system directories — because a cross project's
|
|
425
|
+
include set can overlay libc headers (ESP-IDF ships an esp_libc/
|
|
426
|
+
platform_include whose stdio.h would otherwise shadow the host's);
|
|
427
|
+
project-only headers still resolve, the host's libc wins.
|
|
428
|
+
"""
|
|
429
|
+
if not DB_INCLUDES:
|
|
430
|
+
return flags
|
|
431
|
+
kept, i = [], 0
|
|
432
|
+
while i < len(flags):
|
|
433
|
+
f = flags[i]
|
|
434
|
+
if f in _CC_DB_PATH_FLAGS: # split "-I path" pair
|
|
435
|
+
if f in _DB_SEARCH_PATH_FLAGS:
|
|
436
|
+
kept += ["-idirafter", flags[i + 1]]
|
|
437
|
+
i += 2
|
|
438
|
+
else: # -DFOO / -specs=... token
|
|
439
|
+
i += 1
|
|
440
|
+
return kept
|
|
441
|
+
|
|
442
|
+
|
|
386
443
|
def compile_commands_flags(db_path, source, missing_ok=False):
|
|
387
444
|
"""Header/define flags for ``source`` taken from a compile_commands.json.
|
|
388
445
|
|
|
@@ -401,7 +458,7 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
401
458
|
want = Path(source).resolve()
|
|
402
459
|
flags, name_seen = _lookup_entry(entries, want)
|
|
403
460
|
if flags is not None:
|
|
404
|
-
return flags
|
|
461
|
+
return _borrowed(flags)
|
|
405
462
|
if FLAGS_LIKE:
|
|
406
463
|
like = Path(FLAGS_LIKE).resolve()
|
|
407
464
|
like_flags, _ = _lookup_entry(entries, like)
|
|
@@ -413,7 +470,7 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
413
470
|
_BORROW_NOTED.add(key)
|
|
414
471
|
print(f"note: {source} not in {db_path}; borrowing the flags "
|
|
415
472
|
f"recorded for {FLAGS_LIKE}", file=sys.stderr)
|
|
416
|
-
return like_flags
|
|
473
|
+
return _borrowed(like_flags)
|
|
417
474
|
suggest = ("; --flags-like PATH can borrow another entry's flags"
|
|
418
475
|
if name_seen else "")
|
|
419
476
|
if missing_ok:
|
|
@@ -685,8 +742,8 @@ def analyze(lines):
|
|
|
685
742
|
LABEL_REF = re.compile(r"\.L[\w$.]+")
|
|
686
743
|
|
|
687
744
|
|
|
688
|
-
def
|
|
689
|
-
"""Return [(label,
|
|
745
|
+
def loop_span_ranges(lines):
|
|
746
|
+
"""Return [(label, lo, hi)] line-index ranges for loop spans.
|
|
690
747
|
|
|
691
748
|
A span is the run of instructions from a local label to the last
|
|
692
749
|
instruction that references it from below — a backward branch, which
|
|
@@ -694,9 +751,7 @@ def loop_spans(lines):
|
|
|
694
751
|
Xtensa zero-overhead loops (loop/loopnez/loopgt) reference their END
|
|
695
752
|
label instead; there the span is the instructions the loop encloses.
|
|
696
753
|
Spans are reported in order of appearance, one per label; nested
|
|
697
|
-
labels yield nested spans.
|
|
698
|
-
lie in the span — nothing about trip count or hotness, which the
|
|
699
|
-
reader must judge from the source.
|
|
754
|
+
labels yield nested spans.
|
|
700
755
|
"""
|
|
701
756
|
label_at = {ln[:-1]: i for i, ln in enumerate(lines)
|
|
702
757
|
if ln.endswith(":")}
|
|
@@ -719,13 +774,129 @@ def loop_spans(lines):
|
|
|
719
774
|
lo = min(lo, spans[ref][0])
|
|
720
775
|
hi = max(hi, spans[ref][1])
|
|
721
776
|
spans[ref] = (lo, hi)
|
|
777
|
+
return [(ref, lo, hi) for ref, (lo, hi)
|
|
778
|
+
in sorted(spans.items(), key=lambda kv: kv[1])]
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def loop_spans(lines):
|
|
782
|
+
"""Return [(label, insns)] spans for cleaned asm lines.
|
|
783
|
+
|
|
784
|
+
The count states how many instructions lie in the span — nothing
|
|
785
|
+
about trip count or hotness, which the reader must judge from the
|
|
786
|
+
source.
|
|
787
|
+
"""
|
|
722
788
|
result = []
|
|
723
|
-
for ref,
|
|
789
|
+
for ref, lo, hi in loop_span_ranges(lines):
|
|
724
790
|
insns = sum(1 for ln in lines[lo:hi + 1] if not ln.endswith(":"))
|
|
725
791
|
result.append((ref, insns))
|
|
726
792
|
return result
|
|
727
793
|
|
|
728
794
|
|
|
795
|
+
# --span-stats buckets, by mnemonic table. Covers the targets the tool
|
|
796
|
+
# routinely meets: Xtensa, RISC-V, and ARM by exact (or dot-stripped)
|
|
797
|
+
# mnemonic; x86 AT&T by the memory-operand heuristic in classify_insn,
|
|
798
|
+
# since there loads and stores are mostly spellings of mov.
|
|
799
|
+
_LOAD_MNEMONICS = frozenset("""
|
|
800
|
+
l8ui l16ui l16si l32i l32i.n l32r l32e l32ai lsi lsiu lsx lsxu
|
|
801
|
+
lb lbu lh lhu lw lwu ld flw fld c.lw c.lwsp c.ld c.ldsp c.flw c.fld
|
|
802
|
+
ldr ldrb ldrh ldrsb ldrsh ldrd ldm ldmia vldr vldm
|
|
803
|
+
pop popl popq
|
|
804
|
+
""".split())
|
|
805
|
+
_STORE_MNEMONICS = frozenset("""
|
|
806
|
+
s8i s16i s32i s32i.n s32e s32ri s32c1i ssi ssiu ssx ssxu
|
|
807
|
+
sb sh sw sd fsw fsd c.sw c.swsp c.sd c.sdsp c.fsw c.fsd
|
|
808
|
+
str strb strh strd stm stmia vstr vstm
|
|
809
|
+
push pushl pushq
|
|
810
|
+
""".split())
|
|
811
|
+
# Any multiply, including multiply-accumulate and FP forms (mull,
|
|
812
|
+
# mulsh, mula.dd.*, fmadd.s, vmla.f32, imull, mulss, ...).
|
|
813
|
+
_MUL_PREFIXES = ("mul", "imul", "fmul", "fmadd", "fmsub", "fnmadd",
|
|
814
|
+
"fnmsub", "madd", "msub", "mla", "mls", "smul", "umul",
|
|
815
|
+
"smla", "umla", "vmul", "vmla", "vmls", "vfma", "vfms")
|
|
816
|
+
_BRANCH_MNEMONICS = frozenset("""
|
|
817
|
+
beq bne blt bge bltu bgeu beqz bnez beqi bnei blti bgei bltui bgeui
|
|
818
|
+
bbci bbsi bbc bbs bany bnone ball bnall bt bf
|
|
819
|
+
blez bgez bltz bgtz bgt ble bgtu bleu
|
|
820
|
+
b bl bx blx cbz cbnz bcs bcc bmi bpl bvs bvc bhi bls bal
|
|
821
|
+
jr jal jalr ret retw tail
|
|
822
|
+
c.j c.jr c.jal c.jalr c.beqz c.bnez
|
|
823
|
+
""".split())
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def classify_insn(line):
|
|
827
|
+
"""Bucket one cleaned instruction: load/store/mul/branch/other.
|
|
828
|
+
|
|
829
|
+
Branch means any control transfer, calls included (a span's
|
|
830
|
+
outbound calls are already itemised in the calls column). For
|
|
831
|
+
mnemonics no table knows, an AT&T-style memory operand decides
|
|
832
|
+
load (source side) vs store (destination, the last operand);
|
|
833
|
+
lea and alignment nops are excluded from that heuristic first.
|
|
834
|
+
"""
|
|
835
|
+
parts = line.split(None, 1)
|
|
836
|
+
mnem = parts[0].lower()
|
|
837
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
838
|
+
base = mnem.split(".", 1)[0]
|
|
839
|
+
if mnem in _LOAD_MNEMONICS or base in _LOAD_MNEMONICS:
|
|
840
|
+
return "load"
|
|
841
|
+
if mnem in _STORE_MNEMONICS or base in _STORE_MNEMONICS:
|
|
842
|
+
return "store"
|
|
843
|
+
if base.startswith(_MUL_PREFIXES):
|
|
844
|
+
return "mul"
|
|
845
|
+
if mnem in _BRANCH_MNEMONICS or base in _BRANCH_MNEMONICS:
|
|
846
|
+
return "branch"
|
|
847
|
+
if base.startswith(("j", "call", "loop")):
|
|
848
|
+
return "branch"
|
|
849
|
+
if base.startswith(("lea", "nop")):
|
|
850
|
+
return "other"
|
|
851
|
+
if "(" in rest:
|
|
852
|
+
depth, cut = 0, -1
|
|
853
|
+
for i, ch in enumerate(rest):
|
|
854
|
+
if ch == "(":
|
|
855
|
+
depth += 1
|
|
856
|
+
elif ch == ")":
|
|
857
|
+
depth -= 1
|
|
858
|
+
elif ch == "," and depth == 0:
|
|
859
|
+
cut = i
|
|
860
|
+
return "store" if "(" in rest[cut + 1:] else "load"
|
|
861
|
+
return "other"
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
_MIX_KEYS = ("load", "store", "mul", "branch", "other")
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def span_mix(lines):
|
|
868
|
+
"""[{label, insns, load, store, mul, branch, other}] per loop span
|
|
869
|
+
of one function — the data behind --span-stats, table and JSON."""
|
|
870
|
+
result = []
|
|
871
|
+
for label, lo, hi in loop_span_ranges(lines):
|
|
872
|
+
body = [ln for ln in lines[lo:hi + 1] if not ln.endswith(":")]
|
|
873
|
+
counts = dict.fromkeys(_MIX_KEYS, 0)
|
|
874
|
+
for ln in body:
|
|
875
|
+
counts[classify_insn(ln)] += 1
|
|
876
|
+
entry = {"label": label, "insns": len(body)}
|
|
877
|
+
entry.update(counts)
|
|
878
|
+
result.append(entry)
|
|
879
|
+
return result
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def span_stats_table(fn_names, funcs, max_width=None):
|
|
883
|
+
"""Per-span instruction mix for the named functions.
|
|
884
|
+
|
|
885
|
+
One row per loop span: how many of its instructions load, store,
|
|
886
|
+
multiply, or branch. This weighs the span the way the doctrine
|
|
887
|
+
asks — the hand-written awk this replaces kept counting past the
|
|
888
|
+
loop end into the epilogue.
|
|
889
|
+
"""
|
|
890
|
+
rows = [("function", "span", "insns") + _MIX_KEYS]
|
|
891
|
+
for name in fn_names:
|
|
892
|
+
for mix in span_mix(funcs[name]):
|
|
893
|
+
rows.append((name, mix["label"], str(mix["insns"]))
|
|
894
|
+
+ tuple(str(mix[k]) for k in _MIX_KEYS))
|
|
895
|
+
if len(rows) == 1:
|
|
896
|
+
return "(no loop spans)"
|
|
897
|
+
return render_table(rows, max_width)
|
|
898
|
+
|
|
899
|
+
|
|
729
900
|
def auto_pairs(names):
|
|
730
901
|
"""Pair old_X with new_X for every X present in both."""
|
|
731
902
|
names = list(names)
|
|
@@ -941,10 +1112,62 @@ def build_matrix(cc_args, target_args, config, config_path, db_arg=None,
|
|
|
941
1112
|
|
|
942
1113
|
# --verbose: print full compiler command lines and untrimmed stderr.
|
|
943
1114
|
VERBOSE = False
|
|
1115
|
+
# --summary-only: print the stats tables and skip every listing. The
|
|
1116
|
+
# tables are the decision input; a listing is pulled on a second run
|
|
1117
|
+
# when a delta needs explaining.
|
|
1118
|
+
SUMMARY_ONLY = False
|
|
1119
|
+
# --span-stats: follow each stats table with the per-loop-span
|
|
1120
|
+
# instruction mix (see span_stats_table).
|
|
1121
|
+
SPAN_STATS = False
|
|
1122
|
+
# --json: collect summary records here instead of printing tables;
|
|
1123
|
+
# None means normal table output. main() dumps the collected list.
|
|
1124
|
+
JSON_OUT = None
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def span_stats_block(fn_names, funcs):
|
|
1128
|
+
"""Print the per-span mix table after a stats table when
|
|
1129
|
+
--span-stats is on; a silent no-op otherwise."""
|
|
1130
|
+
if SPAN_STATS and JSON_OUT is None:
|
|
1131
|
+
print()
|
|
1132
|
+
print(span_stats_table(fn_names, funcs, table_width()))
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def json_record(name, lines, cc=None, tag=None, role=None):
|
|
1136
|
+
"""One --json result: a summary-table row as data. cc is the
|
|
1137
|
+
resolved compiler command, tag the source/side label of a two-file
|
|
1138
|
+
comparison, role baseline/candidate for paired rows; each is
|
|
1139
|
+
omitted where the mode has no such notion."""
|
|
1140
|
+
insns, calls = analyze(lines)
|
|
1141
|
+
rec = {"function": name}
|
|
1142
|
+
if cc is not None:
|
|
1143
|
+
rec["cc"] = str(cc)
|
|
1144
|
+
if tag is not None:
|
|
1145
|
+
rec["tag"] = tag
|
|
1146
|
+
if role is not None:
|
|
1147
|
+
rec["role"] = role
|
|
1148
|
+
rec["insns"] = insns
|
|
1149
|
+
rec["loop_spans"] = [{"label": label, "insns": n}
|
|
1150
|
+
for label, n in loop_spans(lines)]
|
|
1151
|
+
rec["calls"] = calls
|
|
1152
|
+
if SPAN_STATS:
|
|
1153
|
+
rec["span_stats"] = span_mix(lines)
|
|
1154
|
+
return rec
|
|
944
1155
|
# Without --verbose, a failed compile shows this many stderr lines — enough
|
|
945
1156
|
# for the include chain plus the first error, which is the actionable part.
|
|
946
1157
|
MAX_STDERR_LINES = 20
|
|
947
1158
|
|
|
1159
|
+
# Compilers skipped because their binary was missing, in matrix order.
|
|
1160
|
+
# The final no-usable-compiler error repeats them (via _skipped_suffix)
|
|
1161
|
+
# so the fix is visible in the error itself, not only in an earlier
|
|
1162
|
+
# stderr warning that may have scrolled away.
|
|
1163
|
+
_SKIPPED_CCS = []
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _skipped_suffix():
|
|
1167
|
+
"""" (cc: not found on PATH; ...)" naming every skipped compiler,
|
|
1168
|
+
empty when nothing was skipped."""
|
|
1169
|
+
return f" ({'; '.join(_SKIPPED_CCS)})" if _SKIPPED_CCS else ""
|
|
1170
|
+
|
|
948
1171
|
|
|
949
1172
|
def _compile_failure(cmd, stderr):
|
|
950
1173
|
"""Exit for a failed compile.
|
|
@@ -989,6 +1212,9 @@ def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
|
989
1212
|
if shutil.which(argv[0]) is None:
|
|
990
1213
|
print(f"warning: {argv[0]} not found on PATH, skipping",
|
|
991
1214
|
file=sys.stderr)
|
|
1215
|
+
miss = f"{argv[0]}: not found on PATH"
|
|
1216
|
+
if miss not in _SKIPPED_CCS:
|
|
1217
|
+
_SKIPPED_CCS.append(miss)
|
|
992
1218
|
return None
|
|
993
1219
|
db = getattr(cc_cmd, "compile_commands", None)
|
|
994
1220
|
db_flags = (compile_commands_flags(
|
|
@@ -1039,14 +1265,53 @@ def mangled_pair_hint(names):
|
|
|
1039
1265
|
return any(MANGLED_PAIR.match(n) for n in names)
|
|
1040
1266
|
|
|
1041
1267
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1268
|
+
# --collapse keeps this many line pairs of context around each
|
|
1269
|
+
# differing pair; longer identical runs are elided with a count.
|
|
1270
|
+
COLLAPSE_CONTEXT = 3
|
|
1271
|
+
# --collapse: elide identical runs in side-by-side listings.
|
|
1272
|
+
COLLAPSE = False
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def side_by_side(left, right, ltitle, rtitle, width=44, collapse=False):
|
|
1276
|
+
"""Two-column view of a pair's asm lines.
|
|
1277
|
+
|
|
1278
|
+
With collapse, runs of identical line pairs shrink to the
|
|
1279
|
+
COLLAPSE_CONTEXT pairs around each difference plus an elision
|
|
1280
|
+
marker — two ~500-insn functions differing by a handful of
|
|
1281
|
+
instructions render as a few hunks instead of ~1000 lines. The
|
|
1282
|
+
sides are aligned first (difflib), not paired by position: an
|
|
1283
|
+
inserted instruction gets a gap opposite it instead of desyncing
|
|
1284
|
+
every following pair.
|
|
1285
|
+
"""
|
|
1044
1286
|
rows = [f"{ltitle:<{width}} | {rtitle}",
|
|
1045
1287
|
f"{'-' * width}-+-{'-' * width}"]
|
|
1046
|
-
|
|
1288
|
+
if collapse:
|
|
1289
|
+
pairs = []
|
|
1290
|
+
matcher = difflib.SequenceMatcher(a=left, b=right, autojunk=False)
|
|
1291
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
1292
|
+
pairs += zip_longest(left[i1:i2], right[j1:j2], fillvalue="")
|
|
1293
|
+
keep = [False] * len(pairs)
|
|
1294
|
+
for i, (l, r) in enumerate(pairs):
|
|
1295
|
+
if l != r:
|
|
1296
|
+
for j in range(max(0, i - COLLAPSE_CONTEXT),
|
|
1297
|
+
min(len(pairs), i + COLLAPSE_CONTEXT + 1)):
|
|
1298
|
+
keep[j] = True
|
|
1299
|
+
else:
|
|
1300
|
+
pairs = list(zip_longest(left, right, fillvalue=""))
|
|
1301
|
+
keep = [True] * len(pairs)
|
|
1302
|
+
elided = 0
|
|
1303
|
+
for (l, r), kept in zip(pairs, keep):
|
|
1304
|
+
if not kept:
|
|
1305
|
+
elided += 1
|
|
1306
|
+
continue
|
|
1307
|
+
if elided:
|
|
1308
|
+
rows.append(f" ... {elided} identical lines ...")
|
|
1309
|
+
elided = 0
|
|
1047
1310
|
l = l.expandtabs(8)[:width]
|
|
1048
1311
|
r = r.expandtabs(8)[:width]
|
|
1049
1312
|
rows.append(f"{l:<{width}} | {r}")
|
|
1313
|
+
if elided:
|
|
1314
|
+
rows.append(f" ... {elided} identical lines ...")
|
|
1050
1315
|
return "\n".join(rows)
|
|
1051
1316
|
|
|
1052
1317
|
|
|
@@ -1187,7 +1452,8 @@ def file_tags(a, b):
|
|
|
1187
1452
|
return str(a), str(b)
|
|
1188
1453
|
|
|
1189
1454
|
|
|
1190
|
-
def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag
|
|
1455
|
+
def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag,
|
|
1456
|
+
ccs=(None, None)):
|
|
1191
1457
|
"""Side-by-side + summary for the same functions from two compilations."""
|
|
1192
1458
|
missing = sorted({f for f in fn_names
|
|
1193
1459
|
if f not in left_funcs or f not in right_funcs})
|
|
@@ -1195,15 +1461,25 @@ def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag):
|
|
|
1195
1461
|
sys.exit("error: function(s) not in asm: " + ", ".join(missing)
|
|
1196
1462
|
+ f"; {left_tag} has: " + (", ".join(left_funcs) or "none")
|
|
1197
1463
|
+ f"; {right_tag} has: " + (", ".join(right_funcs) or "none"))
|
|
1464
|
+
if JSON_OUT is not None:
|
|
1465
|
+
for f in fn_names:
|
|
1466
|
+
JSON_OUT.append(json_record(f, left_funcs[f], cc=ccs[0],
|
|
1467
|
+
tag=left_tag, role="baseline"))
|
|
1468
|
+
JSON_OUT.append(json_record(f, right_funcs[f], cc=ccs[1],
|
|
1469
|
+
tag=right_tag, role="candidate"))
|
|
1470
|
+
return
|
|
1198
1471
|
decorated, pairs = {}, []
|
|
1199
1472
|
for f in fn_names:
|
|
1200
1473
|
lt, rt = f"{f} [{left_tag}]", f"{f} [{right_tag}]"
|
|
1201
1474
|
decorated[lt], decorated[rt] = left_funcs[f], right_funcs[f]
|
|
1202
1475
|
pairs.append((lt, rt))
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1476
|
+
if not SUMMARY_ONLY:
|
|
1477
|
+
for lt, rt in pairs:
|
|
1478
|
+
print(side_by_side(decorated[lt], decorated[rt], lt, rt,
|
|
1479
|
+
collapse=COLLAPSE))
|
|
1480
|
+
print()
|
|
1206
1481
|
print(summary_table(pairs, decorated, table_width()))
|
|
1482
|
+
span_stats_block([n for p in pairs for n in p], decorated)
|
|
1207
1483
|
|
|
1208
1484
|
|
|
1209
1485
|
def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
@@ -1227,10 +1503,13 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
|
1227
1503
|
ran_any = True
|
|
1228
1504
|
tags = mark_db_misses(cc_cmd, sources,
|
|
1229
1505
|
file_tags(sources[0], sources[1]))
|
|
1230
|
-
|
|
1231
|
-
|
|
1506
|
+
if JSON_OUT is None:
|
|
1507
|
+
print(f"\n== {cc_cmd} ==\n")
|
|
1508
|
+
report_across(fn_names, sides[0], sides[1], *tags,
|
|
1509
|
+
ccs=(cc_cmd, cc_cmd))
|
|
1232
1510
|
if not ran_any:
|
|
1233
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1511
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1512
|
+
+ _skipped_suffix())
|
|
1234
1513
|
return 0
|
|
1235
1514
|
|
|
1236
1515
|
usable = []
|
|
@@ -1240,14 +1519,17 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
|
1240
1519
|
usable.append((f"cc#{idx}", cc_cmd, extract_functions(asm)))
|
|
1241
1520
|
if len(usable) < 2:
|
|
1242
1521
|
sys.exit("error: --across needs at least two usable compilers "
|
|
1243
|
-
"in the matrix")
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1522
|
+
"in the matrix" + _skipped_suffix())
|
|
1523
|
+
if JSON_OUT is None:
|
|
1524
|
+
print()
|
|
1525
|
+
for tag, cc_cmd, _ in usable:
|
|
1526
|
+
print(f"{tag}: {cc_cmd}")
|
|
1527
|
+
base_tag, base_cc, base_funcs = usable[0]
|
|
1528
|
+
for tag, cc_cmd, funcs in usable[1:]:
|
|
1529
|
+
if JSON_OUT is None:
|
|
1530
|
+
print(f"\n== {base_tag} vs {tag} ==\n")
|
|
1531
|
+
report_across(fn_names, base_funcs, funcs, base_tag, tag,
|
|
1532
|
+
ccs=(base_cc, cc_cmd))
|
|
1251
1533
|
return 0
|
|
1252
1534
|
|
|
1253
1535
|
|
|
@@ -1268,15 +1550,25 @@ def run_summary(sources, matrix, extra_flags, tmp):
|
|
|
1268
1550
|
ran_any = True
|
|
1269
1551
|
shown = (mark_db_misses(cc_cmd, sources, tags)
|
|
1270
1552
|
if len(sources) == 2 else tags)
|
|
1271
|
-
|
|
1553
|
+
if JSON_OUT is None:
|
|
1554
|
+
print(f"\n== {cc_cmd} ==")
|
|
1272
1555
|
for tag, funcs in zip(shown, sections):
|
|
1556
|
+
if JSON_OUT is not None:
|
|
1557
|
+
JSON_OUT.extend(
|
|
1558
|
+
json_record(name, lines, cc=cc_cmd,
|
|
1559
|
+
tag=tag if len(sections) > 1 else None)
|
|
1560
|
+
for name, lines in funcs.items())
|
|
1561
|
+
continue
|
|
1273
1562
|
if len(sections) > 1:
|
|
1274
1563
|
print(f"\n-- {tag} --")
|
|
1275
1564
|
print()
|
|
1276
1565
|
print(file_summary_table(funcs, table_width()) if funcs
|
|
1277
1566
|
else "(no functions found)")
|
|
1567
|
+
if funcs:
|
|
1568
|
+
span_stats_block(list(funcs), funcs)
|
|
1278
1569
|
if not ran_any:
|
|
1279
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1570
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1571
|
+
+ _skipped_suffix())
|
|
1280
1572
|
return 0
|
|
1281
1573
|
|
|
1282
1574
|
|
|
@@ -1300,22 +1592,39 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
|
|
|
1300
1592
|
print('note: C++ mangling defeats old_*/new_* auto-pairing; '
|
|
1301
1593
|
'declare the pairs extern "C" or use --pair with the '
|
|
1302
1594
|
'mangled names', file=sys.stderr)
|
|
1595
|
+
if JSON_OUT is not None:
|
|
1596
|
+
JSON_OUT.extend(json_record(name, lines, cc=cc_cmd)
|
|
1597
|
+
for name, lines in funcs.items())
|
|
1598
|
+
continue
|
|
1303
1599
|
print(f"\n== {cc_cmd} ==\n")
|
|
1304
1600
|
print(file_summary_table(funcs, table_width()) if funcs
|
|
1305
1601
|
else "(no functions found)")
|
|
1602
|
+
if funcs:
|
|
1603
|
+
span_stats_block(list(funcs), funcs)
|
|
1306
1604
|
continue
|
|
1307
1605
|
missing = sorted({n for p in pairs for n in p if n not in funcs})
|
|
1308
1606
|
if missing:
|
|
1309
1607
|
sys.exit("error: function(s) not in asm: "
|
|
1310
1608
|
+ ", ".join(missing)
|
|
1311
1609
|
+ "; functions seen: " + ", ".join(funcs))
|
|
1610
|
+
if JSON_OUT is not None:
|
|
1611
|
+
for old, new in pairs:
|
|
1612
|
+
JSON_OUT.append(json_record(old, funcs[old], cc=cc_cmd,
|
|
1613
|
+
role="baseline"))
|
|
1614
|
+
JSON_OUT.append(json_record(new, funcs[new], cc=cc_cmd,
|
|
1615
|
+
role="candidate"))
|
|
1616
|
+
continue
|
|
1312
1617
|
print(f"\n== {cc_cmd} ==\n")
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1618
|
+
if not SUMMARY_ONLY:
|
|
1619
|
+
for old, new in pairs:
|
|
1620
|
+
print(side_by_side(funcs[old], funcs[new], old, new,
|
|
1621
|
+
collapse=COLLAPSE))
|
|
1622
|
+
print()
|
|
1316
1623
|
print(summary_table(pairs, funcs, table_width()))
|
|
1624
|
+
span_stats_block([n for p in pairs for n in p], funcs)
|
|
1317
1625
|
if not ran_any:
|
|
1318
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1626
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1627
|
+
+ _skipped_suffix())
|
|
1319
1628
|
return 0
|
|
1320
1629
|
|
|
1321
1630
|
|
|
@@ -1333,13 +1642,19 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
|
|
|
1333
1642
|
if asm is not None:
|
|
1334
1643
|
usable.append((f"cc#{idx}", cc_cmd, extract_functions(asm)))
|
|
1335
1644
|
if not usable:
|
|
1336
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1645
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1646
|
+
+ _skipped_suffix())
|
|
1337
1647
|
for _, cc_cmd, funcs in usable:
|
|
1338
1648
|
missing = sorted({f for f in fn_names if f not in funcs})
|
|
1339
1649
|
if missing:
|
|
1340
1650
|
sys.exit("error: function(s) not in asm: " + ", ".join(missing)
|
|
1341
1651
|
+ f" under {cc_cmd}; functions seen: "
|
|
1342
1652
|
+ ", ".join(funcs))
|
|
1653
|
+
if JSON_OUT is not None:
|
|
1654
|
+
for _, cc_cmd, funcs in usable:
|
|
1655
|
+
JSON_OUT.extend(json_record(name, funcs[name], cc=cc_cmd)
|
|
1656
|
+
for name in fn_names)
|
|
1657
|
+
return 0
|
|
1343
1658
|
if layout == "side-by-side" and len(usable) < 2:
|
|
1344
1659
|
sys.exit("error: --layout side-by-side needs at least two usable "
|
|
1345
1660
|
"compilers in the matrix")
|
|
@@ -1357,11 +1672,13 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
|
|
|
1357
1672
|
for _, cc_cmd, funcs in usable:
|
|
1358
1673
|
if len(usable) > 1:
|
|
1359
1674
|
print(f"\n== {cc_cmd} ==")
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1675
|
+
if not SUMMARY_ONLY:
|
|
1676
|
+
for name in fn_names:
|
|
1677
|
+
print()
|
|
1678
|
+
print(listing(name, funcs[name]))
|
|
1363
1679
|
print()
|
|
1364
1680
|
print(inspect_table(fn_names, funcs, table_width()))
|
|
1681
|
+
span_stats_block(fn_names, funcs)
|
|
1365
1682
|
return 0
|
|
1366
1683
|
|
|
1367
1684
|
|
|
@@ -1420,11 +1737,16 @@ def run_elf(elf, fn_names, filter_regex, objdump):
|
|
|
1420
1737
|
if not selected:
|
|
1421
1738
|
sys.exit(f"error: --filter {filter_regex!r} matched no function "
|
|
1422
1739
|
f"among the {len(funcs)} in {elf}")
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1740
|
+
if JSON_OUT is not None:
|
|
1741
|
+
JSON_OUT.extend(json_record(name, funcs[name]) for name in selected)
|
|
1742
|
+
return 0
|
|
1743
|
+
if not SUMMARY_ONLY:
|
|
1744
|
+
for name in fn_names:
|
|
1745
|
+
print()
|
|
1746
|
+
print(listing(name, funcs[name]))
|
|
1426
1747
|
print()
|
|
1427
1748
|
print(inspect_table(selected, funcs, table_width()))
|
|
1749
|
+
span_stats_block(selected, funcs)
|
|
1428
1750
|
return 0
|
|
1429
1751
|
|
|
1430
1752
|
|
|
@@ -1487,6 +1809,14 @@ def main(argv=None):
|
|
|
1487
1809
|
help=f"config file; default search: {CONFIG_NAME} "
|
|
1488
1810
|
"next to SOURCE.c, in the current directory, "
|
|
1489
1811
|
"then in ~/.config/")
|
|
1812
|
+
parser.add_argument("--db-includes", action="store_true",
|
|
1813
|
+
help="borrow only the header-search paths from the "
|
|
1814
|
+
"database (re-emitted as -idirafter, so they "
|
|
1815
|
+
"never shadow the host's system headers), "
|
|
1816
|
+
"dropping its defines, forced includes, and "
|
|
1817
|
+
"-specs/--sysroot - lets a host/foreign-arch "
|
|
1818
|
+
"target resolve a cross project's headers "
|
|
1819
|
+
"without inheriting cross-only flags")
|
|
1490
1820
|
parser.add_argument("--flags-like", metavar="PATH",
|
|
1491
1821
|
help="when a source has no compile_commands entry, "
|
|
1492
1822
|
"borrow the include/define flags recorded for "
|
|
@@ -1502,14 +1832,45 @@ def main(argv=None):
|
|
|
1502
1832
|
help="print the built-in example config (the "
|
|
1503
1833
|
"repository's asmdiff.example.toml) to stdout, "
|
|
1504
1834
|
"ready to redirect into a config file")
|
|
1835
|
+
parser.add_argument("--span-stats", action="store_true",
|
|
1836
|
+
help="follow each stats table with a per-loop-span "
|
|
1837
|
+
"instruction mix (load/store/mul/branch/other "
|
|
1838
|
+
"counts) - weighs the span instead of the "
|
|
1839
|
+
"whole function")
|
|
1840
|
+
parser.add_argument("--collapse", action="store_true",
|
|
1841
|
+
help="in side-by-side listings, elide runs of "
|
|
1842
|
+
"identical line pairs, keeping "
|
|
1843
|
+
f"{COLLAPSE_CONTEXT} lines of context around "
|
|
1844
|
+
"each difference - near-identical functions "
|
|
1845
|
+
"render as a few hunks")
|
|
1846
|
+
parser.add_argument("--json", action="store_true",
|
|
1847
|
+
help="emit the summary as JSON on stdout instead "
|
|
1848
|
+
"of tables: one record per function per "
|
|
1849
|
+
"compiler (insns, loop spans, calls; span "
|
|
1850
|
+
"mix with --span-stats). Implies "
|
|
1851
|
+
"--summary-only; errors stay plain text on "
|
|
1852
|
+
"stderr")
|
|
1853
|
+
parser.add_argument("-s", "--summary-only", action="store_true",
|
|
1854
|
+
help="print only the summary/stats tables, "
|
|
1855
|
+
"suppressing every assembly listing - the "
|
|
1856
|
+
"scripted-caller view (pull a listing with a "
|
|
1857
|
+
"second run when a delta needs explaining)")
|
|
1858
|
+
parser.add_argument("--version", action="version",
|
|
1859
|
+
version=f"asmdiff {__version__}")
|
|
1505
1860
|
parser.add_argument("-v", "--verbose", action="store_true",
|
|
1506
1861
|
help="on compile failure, print the full compiler "
|
|
1507
1862
|
"command and complete error output instead of "
|
|
1508
1863
|
"the first lines")
|
|
1509
1864
|
args = parser.parse_args(argv)
|
|
1510
|
-
global VERBOSE, FLAGS_LIKE
|
|
1865
|
+
global VERBOSE, FLAGS_LIKE, SUMMARY_ONLY, COLLAPSE, SPAN_STATS
|
|
1866
|
+
global DB_INCLUDES, JSON_OUT
|
|
1511
1867
|
VERBOSE = args.verbose
|
|
1512
1868
|
FLAGS_LIKE = args.flags_like
|
|
1869
|
+
SUMMARY_ONLY = args.summary_only or args.json
|
|
1870
|
+
COLLAPSE = args.collapse
|
|
1871
|
+
SPAN_STATS = args.span_stats
|
|
1872
|
+
DB_INCLUDES = args.db_includes
|
|
1873
|
+
JSON_OUT = [] if args.json else None
|
|
1513
1874
|
|
|
1514
1875
|
if args.example_config:
|
|
1515
1876
|
sys.stdout.write(EXAMPLE_CONFIG)
|
|
@@ -1542,7 +1903,12 @@ def main(argv=None):
|
|
|
1542
1903
|
sys.exit("error: no gcc in the matrix to derive an objdump "
|
|
1543
1904
|
"from (" + "; ".join(matrix) + "); pass --objdump "
|
|
1544
1905
|
"PATH or a gcc-based --cc/--target")
|
|
1545
|
-
|
|
1906
|
+
status = run_elf(sources[0], fn_names, args.filter, objdump)
|
|
1907
|
+
if JSON_OUT is not None:
|
|
1908
|
+
print(json.dumps({"asmdiff": __version__, "mode": "elf",
|
|
1909
|
+
"elf": sources[0], "results": JSON_OUT},
|
|
1910
|
+
indent=2))
|
|
1911
|
+
return status
|
|
1546
1912
|
if args.filter or args.objdump:
|
|
1547
1913
|
parser.error("--filter/--objdump apply to ELF input only")
|
|
1548
1914
|
if len(sources) > 2:
|
|
@@ -1573,15 +1939,24 @@ def main(argv=None):
|
|
|
1573
1939
|
|
|
1574
1940
|
with tempfile.TemporaryDirectory(prefix="asmdiff") as tmp:
|
|
1575
1941
|
if fn_names:
|
|
1576
|
-
|
|
1942
|
+
mode = "inspect"
|
|
1943
|
+
status = run_inspect(sources[0], matrix, fn_names, args.layout,
|
|
1944
|
+
extra_flags, tmp)
|
|
1945
|
+
elif args.across:
|
|
1946
|
+
mode = "across"
|
|
1947
|
+
status = run_across(sources, matrix, args.across,
|
|
1948
|
+
extra_flags, tmp)
|
|
1949
|
+
elif len(sources) == 2:
|
|
1950
|
+
mode = "summary"
|
|
1951
|
+
status = run_summary(sources, matrix, extra_flags, tmp)
|
|
1952
|
+
else:
|
|
1953
|
+
mode = "pairs"
|
|
1954
|
+
status = run_pairs(sources[0], matrix, args.pair,
|
|
1577
1955
|
extra_flags, tmp)
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
return run_summary(sources, matrix, extra_flags, tmp)
|
|
1583
|
-
return run_pairs(sources[0], matrix, args.pair,
|
|
1584
|
-
extra_flags, tmp)
|
|
1956
|
+
if JSON_OUT is not None:
|
|
1957
|
+
print(json.dumps({"asmdiff": __version__, "mode": mode,
|
|
1958
|
+
"results": JSON_OUT}, indent=2))
|
|
1959
|
+
return status
|
|
1585
1960
|
|
|
1586
1961
|
|
|
1587
1962
|
if __name__ == "__main__":
|
asmdiff-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
asmdiff.py,sha256=cJrdwqqlG4ejalcvBHlSSSCylFRVIAKNYmKsiaCIer4,68504
|
|
2
|
-
asmdiff-0.2.0.dist-info/METADATA,sha256=Gxk-QQA7YDZsmB0WPq2lKHJIg1FxZIuWIkz8TyYotKc,56049
|
|
3
|
-
asmdiff-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
-
asmdiff-0.2.0.dist-info/entry_points.txt,sha256=bVeIyjLm3KgWALbgRSeQo16pZ89GXZZ4sFTSWJUMdTI,41
|
|
5
|
-
asmdiff-0.2.0.dist-info/licenses/LICENSE,sha256=Y8JW9wzNeVnfyw62X3qC9cuwZCv_U67rNSF9QUE0HJs,1072
|
|
6
|
-
asmdiff-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|