MapAnalyzer
Delphi map file analyzer
- Active · last change 8-23-2026
- ★ 1 on GitHub
README
MapAnalyzer
A single-file Delphi console app that analyses .map files. No dependencies —
SysUtils, Classes, Math only, so it builds on old Delphi versions
(Delphi 6/7 and up — pointer-sized integers are shimmed for pre-2009
compilers) as well as current ones, and on FPC.
dcc32 MapAnalyzer.dpr # Win32
dcc64 MapAnalyzer.dpr # Win64
fpc -Mdelphi MapAnalyzer.dpr # if you want it outside the IDEPrerequisite
Project ▸ Options ▸ Building ▸ Delphi Compiler ▸ Linking ▸ Map file = Detailed.
The lines command additionally needs debug information enabled.
Start here
MapAnalyzer sections MyApp.mapPrints what the parser recognised, the detected bitness, and — importantly — any lines it failed to understand. If your Delphi version emits a shape I haven't anticipated, this is where it shows up. Send me those lines and I'll widen the parser.
Commands
units — the leaderboard
MapAnalyzer units MyApp.map --top=60
MapAnalyzer units MyApp.map --group=1 # roll up: Vcl.*, FireDAC.*, ...
MapAnalyzer units MyApp.map --exe=MyApp.exe # what the map does NOT explain
MapAnalyzer units MyApp.map --all --csv=units.csv--exe is worth running once. The gap between the map total and the real file
size is resources — DFMs, {$R} data, icons, manifests — plus PE headers and
padding. On form-heavy apps it is often larger than people expect, and no
amount of uses-clause work will touch it.
routines — per-symbol sizes
MapAnalyzer routines MyApp.map --top=60
MapAnalyzer routines MyApp.map --generics # generic instantiation bloat
MapAnalyzer routines MyApp.map --unit=System.SysUtils
MapAnalyzer routines MyApp.map --filter=TListSizes are the address gap to the next public symbol, clamped to the containing contribution. Private and local routines are not listed in the map, so their bytes fold into the public symbol above them: treat these as upper bounds, not exact measurements. They are still good enough to find the outliers.
--generics is where the surprises usually are — the same container
specialised a dozen times, each paying full freight.
lines — bytes per source line
MapAnalyzer lines MyApp.map --unit=MyApp.BigUnit --top=40Uses the map's line-number tables, so it needs a debug build. This is the one that connects back to the line count the IDE shows you: not "this unit is 400 KB" but "these 30 lines are 40 KB".
diff — the scoreboard
MapAnalyzer diff before.map after.map
MapAnalyzer diff before.map after.map --group=1Build, change one thing, rebuild, diff. Turns "I think that helped" into "that removed 180 KB and dropped 23 units". For a long grind this is probably the command you'll run most.
uses — cost per uses-clause entry
MapAnalyzer uses MyApp.map ^
--src=C:\Projects\MyApp ^
--src="C:\Program Files (x86)\Embarcadero\Studio\23.0\source" ^
--top=60 --csv=edges.csvFor every A uses B, reports how many bytes and how many units would actually
leave the build if that one entry disappeared.
Point --src at the Delphi RTL/VCL source as well as your own. Without it
the library units look like leaves, their dependencies are invisible, and every
cost is understated. The tool warns you when linked modules have no readable
source, and tells you how many.
An edge scoring 0 is reachable by another path too — cutting it alone frees nothing. That is usually most of them, and it is the point: it stops you spending a day on removals that change nothing.
Options: --root=<Unit> if the project file isn't in --src, --min=<bytes>
to cut off the tail, --csv=<file>.
Global
--out=<file> writes the report to a file instead of the console.
How the uses numbers are computed
Each uses entry becomes its own node in the dependency graph (A → E → B), and
the tool computes the dominator tree of that graph from the project root. The
dominator subtree of E is exactly the set of units that become unreachable if
that single edge is cut.
The edge-splitting matters. The obvious version — check whether A immediately
dominates B — is wrong, and wrong in a way that silently inflates results:
A dominating B means every path reaches node A, not that every path uses
the edge A→B. In testing that formulation claimed 40 KB for an edge whose
true cost was zero, because the target was still reachable another way.
The implementation is checked against brute force — delete the edge, recompute reachability, compare — over 60 randomised graphs containing cycles, shared paths, unreachable units, and units present in source but absent from the map.
Three things the numbers won't tell you
- Initialization side effects. A unit whose job is registration —
Vcl.Imaging.Jpeg, FireDAC driver links,MidasLib,System.Win.ComObj, anything callingRegisterClass— will compile away cleanly and fail at run time. No static analysis catches this. Keep a whitelist. - Interface vs implementation. The
sectcolumn matters. Anintfentry propagates to everyone who uses that unit; animplone doesn't. Moving a heavyintfentry down to the implementation section is usually cheaper and safer than removing it, and it's most of the compile-time win. - Identifier collisions. Uses order decides which of two same-named symbols wins. Removing a unit can quietly change resolution rather than fail to compile.
Known limits
- Resources aren't in the map at all (see
--exe). - Contributions are attributed to the unit that emitted the code. For generics and inlined routines that's the instantiating unit, not the declaring one, which reads as misattribution if you don't expect it.
IFDEFbranches are all treated as live when parsing uses clauses — deliberately conservative, so a conditional dependency is counted as real.- The graph only contains units the linker actually kept. Units dropped by the smart linker have no weight and no edges.

