awk Part III · Module 11 · Synthesis and Coverage Report Analysis
Hierarchy Statistics
A hierarchical area report prints two area columns per module and they mean different things: one is the silicon that module's own cells occupy, the other is that plus everything nested inside it. Adding up the second gives two and a half times the design's real area, and this chapter shows that the factor is not arbitrary — it decomposes exactly, module by module, from the nesting depth. It also covers the two ways the nesting itself gets lost: awk's default field splitting discards the indentation that encodes it, and the obvious fix returns zero for every module on a report indented with tabs.
Advanced18 min readawkPart IIISynthesisHierarchyDouble Counting
Part III · Module 11 · Chapter 11.3 · Hierarchy Statistics
1. Why This Matters
The per-module table is what turns an area number into a decision. 27793.500 says the block is too big; a table saying u_mul is 5210.000 of it says where to look.
The report that provides it is also the first one in this module whose structure carries information outside the field values. The nesting is in the indentation, which awk's default field splitting throws away, and the area is in two columns whose difference is the entire subject of the chapter.
2. Two Area Columns
hier.rpt is listed in full in Chapter 11.2 section 4. Its header names the columns:
sed -n '6,9p' hier.rptInstance Module Cells Cell Area Total Area
--------------------------------------------------------------------
alu_core alu_core 204 1420.500 27793.500
u_regfile regfile_m 466 4980.000 4980.000Cell Areais the area of the leaf cells this module owns directly.Total Areais that plus everything in every module nested inside it.
On alu_core the two differ enormously — 1420.500 against 27793.500 — because the top level owns almost nothing and contains everything. On u_regfile they are equal, because it has no children.
Summing each column:
awk 'NR>7 && !/^-|^TOTAL/ { own += $(NF-1); tot += $NF } END { printf "sum of Cell Area (own) = %12.3f\nsum of Total Area (cumulative) = %12.3f <- %.2fx\n", own, tot, tot/own }' hier.rptsum of Cell Area (own) = 27793.500
sum of Total Area (cumulative) = 69376.500 <- 2.50xThe own-area column sums to the design's real total. The cumulative column sums to two and a half times it.
$(NF-1) and $NF rather than $4 and $5, because the instance column is indented and the module names vary in length — Chapter 7.1's point about counting fields from the right when the left is ragged.
3. The Factor Is Not Arbitrary
Each module's own area appears in its own Total Area, and again in the Total Area of every ancestor. So a module at depth d is counted d + 1 times:
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; a[d] += $(NF-1) }
END { for (i = 0; i <= 2; i++) { m = i + 1; sub_ = a[i] * m; s += sub_; printf " depth %d own %10.3f counted %d time(s) -> %11.3f\n", i, a[i], m, sub_ }
printf " predicted sum of Total Area = %.3f\n", s }' hier.rpt
awk 'NR>7 && !/^-|^TOTAL/ { tot += $NF } END { printf " measured sum of Total Area = %.3f\n", tot }' hier.rpt depth 0 own 1420.500 counted 1 time(s) -> 1420.500
depth 1 own 11163.000 counted 2 time(s) -> 22326.000
depth 2 own 15210.000 counted 3 time(s) -> 45630.000
predicted sum of Total Area = 69376.500
measured sum of Total Area = 69376.500Predicted equals measured to the last digit.
That makes the 2.50× a property of this design's shape rather than a rule of thumb. A flat design would give 1.00×; a deeply nested one gives more. The factor is the average nesting depth plus one, weighted by area — which means you cannot correct for it with a constant, and a "divide by about two" heuristic is wrong for every design but this one.
4. The Report's Own Arithmetic
The relation that makes the cumulative column meaningful, checked on a parent with three children:
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; name = $1
if (name == "u_ctrl") { pown = $(NF-1); ptot = $NF; pd = d; inp = 1; next }
if (inp && d == pd + 1) { ksum += $NF; printf " child %-12s Total Area %9.3f\n", name, $NF; next }
if (inp && d <= pd) inp = 0 }
END { printf " u_ctrl own %9.3f + children %9.3f = %9.3f reported Total Area %9.3f -> %s\n", pown, ksum, pown+ksum, ptot, (ptot-(pown+ksum) < 1e-6 && (pown+ksum)-ptot < 1e-6 ? "MATCH" : "MISMATCH") }' hier.rpt child u_decode Total Area 740.000
child u_fsm Total Area 520.000
child u_hazard Total Area 226.000
u_ctrl own 1240.000 + children 1486.000 = 2726.000 reported Total Area 2726.000 -> MATCHAnd on a parent with four:
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2
if ($1 == "u_alu") { pown = $(NF-1); ptot = $NF; pd = d; inp = 1; next }
if (inp && d == pd + 1) { ksum += $NF; n++; next }
if (inp && d <= pd) inp = 0 }
END { printf " u_alu own %.3f + %d children %.3f = %.3f reported %.3f -> %s\n", pown, n, ksum, pown+ksum, ptot, (ptot-(pown+ksum) < 1e-6 && (pown+ksum)-ptot < 1e-6 ? "MATCH" : "MISMATCH") }' hier.rpt u_alu own 1310.000 + 4 children 10040.000 = 11350.000 reported 11350.000 -> MATCHThe inp flag with d == pd + 1 for children and d <= pd to stop is the state-carrying pattern from Chapter 10.2 applied to a tree: a sibling or an uncle ends the parent's child list, and only a direct child counts.
Both comparisons use a tolerance rather than ==, for the reason Chapter 11.1 section 8 measured.
5. The Indentation Carries the Depth, and $1 Loses It
awk 'NR>7 && !/^-|^TOTAL/ { printf "[%s]", $1 } END { print "" }' hier.rpt[alu_core][u_regfile][u_alu][u_add][u_mul][u_shift][u_logic][u_ctrl][u_decode][u_fsm][u_hazard][u_io][u_pad_ring][u_sync][u_dma][u_desc][u_fifo][u_arb][u_fpu][u_norm][u_round][u_debug]Twenty-two names and no structure at all. awk's default field splitting treats runs of blanks as one separator and ignores leading blanks, so the two- and four-space indents vanish with them.
The depth is recoverable from the raw record, which the splitting does not touch:
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; printf "[%d:%s]", d, $1 } END { print "" }' hier.rpt[0:alu_core][1:u_regfile][1:u_alu][2:u_add][2:u_mul][2:u_shift][2:u_logic][1:u_ctrl][2:u_decode][2:u_fsm][2:u_hazard][1:u_io][2:u_pad_ring][2:u_sync][1:u_dma][2:u_desc][2:u_fifo][2:u_arb][1:u_fpu][2:u_norm][2:u_round][1:u_debug]match($0, /[^ ]/) returns the 1-based position of the first non-blank, so - 1 is the indent width and / 2 converts it to levels. $0 is the untouched record — Chapter 3.2's point that $0 and the field list are two views of the same line and only one of them has been normalised.
The per-depth rollup that section 3 relied on:
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; a[d] += $(NF-1); c[d]++ } END { for (i = 0; i <= 2; i++) printf " depth %d: %2d module(s), own area %10.3f (%.1f%%)\n", i, c[i], a[i], 100*a[i]/27793.5 }' hier.rpt depth 0: 1 module(s), own area 1420.500 (5.1%)
depth 1: 7 module(s), own area 11163.000 (40.2%)
depth 2: 14 module(s), own area 15210.000 (54.7%)Three depths, 5.1% / 40.2% / 54.7%, adding to 100% because own areas partition the design.
6. Tabs Return Zero for Everything
The depth rule counts spaces. On a report indented with tabs:
sed 's/^ /\t\t/; s/^ /\t/' hier.rpt > hier_tab.rpt
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; printf "%d", d } END { print "" }' hier_tab.rpt0000000000000000000000Every module reads as depth 0. A tab is not a space, so /[^ ]/ matches it and match returns 1 for every line.
Expanding the tabs first restores the real depths:
awk 'NR>7 && !/^-|^TOTAL/ { s = $0; sub(/[^ \t].*$/, "", s); gsub(/\t/, " ", s); d = length(s) / 2; printf "%d", d } END { print "" }' hier_tab.rpt0112222122212212221221Which is the sequence section 5 measured on the space-indented original.
7. A Bare Instance Name Is Not a Key
Two modules can instantiate submodules with the same name:
Instance Module Cells Cell Area Total Area
--------------------------------------------------------------------
top top_m 12 100.000 1300.000
u_tx tx_m 40 300.000 700.000
u_fifo fifo_m 30 400.000 400.000
u_rx rx_m 50 200.000 500.000
u_fifo fifo_m 45 300.000 300.000
--------------------------------------------------------------------awk 'NR>2 && !/^-/ { n[$1]++ } END { for (k in n) if (n[k] > 1) printf " %s appears %d times\n", k, n[k] }' dup.rpt
awk 'NR>2 && !/^-/ { a[$1] += $(NF-1) } END { printf " u_fifo = %.3f (two distinct instances of 400.000 and 300.000)\n", a["u_fifo"] }' dup.rpt u_fifo appears 2 times
u_fifo = 700.000 (two distinct instances of 400.000 and 300.000)The two merge into one 700.000 row — a module that does not exist, with an area neither instance has.
Rebuilding the full path from the depth keeps them apart:
awk 'NR>2 && !/^-/ { d = (match($0, /[^ ]/) - 1) / 2; path[d] = $1; p = path[0]; for (i = 1; i <= d; i++) p = p "/" path[i]; a[p] += $(NF-1); if (!(p in seen)) { seen[p]=1; o[++n]=p } } END { for (i=1;i<=n;i++) printf " %-22s %9.3f\n", o[i], a[o[i]] }' dup.rpt top 100.000
top/u_tx 300.000
top/u_tx/u_fifo 400.000
top/u_rx 200.000
top/u_rx/u_fifo 300.000path[d] = $1 keeps the current name at each level and the loop joins levels 0 to d. Because the report is depth-first, the ancestors of any row are the last names seen at the shallower depths — which is the same insight as Chapter 10.2's carried header, with one slot per level instead of one variable.
Note that the duplicate check is worth running even when you expect unique names. u_fifo is a completely ordinary thing to call two FIFOs, and a design with a transmit and a receive path will usually have a matched pair.
8. Common Mistakes
Summing Total Area. It gives 2.50× the real total here — section 2.
Correcting for it with a constant. The factor is this design's area-weighted nesting depth plus one, not a universal — section 3.
$4 and $5 for the area columns. The instance column is indented and the module names vary; count from NF — section 2.
Reading depth from $1. Default field splitting discards the indentation — section 5.
A space-based depth rule on a tab-indented report. Every module reads as depth 0 and the hierarchy collapses silently — section 6.
Expanding tabs across the whole line. It breaks the column alignment $(NF-1) depends on; normalise the leading whitespace only.
Keying a table on the bare instance name. Two u_fifo instances merge into one 700.000 row — section 7.
Comparing a parent's total against the children's sum with ==. Use a tolerance — Chapter 11.1 section 8.
9. Exercises
Exercise 1 — The per-module own-area table
awk 'NR>7 && !/^-|^TOTAL/ { printf "%-14s %5d %10.3f\n", $1, $(NF-2), $(NF-1) }' hier.rpt | head -6alu_core 204 1420.500
u_regfile 466 4980.000
u_alu 178 1310.000
u_add 420 2840.000
u_mul 642 5210.000
u_shift 190 1130.000Exercise 2 — Depth and full path
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; path[d] = $1; p = path[0]; for (i = 1; i <= d; i++) p = p "/" path[i]; printf "%d %s\n", d, p }' hier.rpt | head -70 alu_core
1 alu_core/u_regfile
1 alu_core/u_alu
2 alu_core/u_alu/u_add
2 alu_core/u_alu/u_mul
2 alu_core/u_alu/u_shift
2 alu_core/u_alu/u_logicExercise 3 — Debug a total that is 2.5× too large
A per-module script reports the design at 69376.500 where synthesis says 27793.500.
awk 'NR>7 && !/^-|^TOTAL/ { own += $(NF-1); tot += $NF } END { printf "own column %.3f, cumulative column %.3f, ratio %.2f\n", own, tot, tot/own }' hier.rptown column 27793.500, cumulative column 69376.500, ratio 2.50The cumulative column was summed. Each module's area is in its own row and in every ancestor's:
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; a[d] += $(NF-1) } END { for (i=0;i<=2;i++) s += a[i] * (i+1); printf "predicted from depth multiplicity: %.3f\n", s }' hier.rptpredicted from depth multiplicity: 69376.500Exercise 4 — Debug a hierarchy that came out flat
A per-module table shows twenty-two modules all at the top level.
sed 's/^ /\t\t/; s/^ /\t/' hier.rpt > hier_tab.rpt
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; if (d > md) md = d; rows++ } END { printf "%d module rows, maximum depth %d -> %s\n", rows, md, (md == 0 && rows > 1 ? "SUSPECT: a multi-module design with no nesting" : "plausible") }' hier_tab.rpt22 module rows, maximum depth 0 -> SUSPECT: a multi-module design with no nestingsed -n '9,11p' hier_tab.rpt | cat -t^Iu_regfile regfile_m 466 4980.000 4980.000
^Iu_alu alu_m 178 1310.000 11350.000
^I^Iu_add add_m 420 2840.000 2840.000cat -t shows ^I — the indentation is tabs, which /[^ ]/ matches, so match returns 1 on every line. Everything else about the report is intact: u_alu still reports its own 1310.000 and its cumulative 11350.000, and the columns still line up. Only the depth is lost.
awk 'NR>7 && !/^-|^TOTAL/ { s = $0; sub(/[^ \t].*$/, "", s); gsub(/\t/, " ", s); d = length(s) / 2; if (d > md) md = d; rows++ } END { printf "%d module rows, maximum depth %d\n", rows, md }' hier_tab.rpt22 module rows, maximum depth 2Exercise 5 — Engineering application
The per-module table a synthesis review reads: full paths so nothing merges, own area so nothing double-counts, the parent/child arithmetic checked, and the flat-hierarchy precondition enforced.
# hierstats.awk — per-module statistics from a hierarchical area report.
#
# Usage: awk -f hierstats.awk hier.rpt
# awk -v top=5 -f hierstats.awk hier.rpt
# Exit: 0 ok
# 1 the own-area column does not match the top row's Total Area
# 2 the hierarchy looks collapsed (indentation was not understood)
BEGIN { TOL = 1e-6 }
# the column header and the rules are the only lines that are not modules
/^Instance|^-|^TOTAL|^\*|^Report|^Design|^$/ { next }
{
lead = $0; sub(/[^ \t].*$/, "", lead); gsub(/\t/, " ", lead)
d = length(lead) / 2
if (d > maxd) maxd = d
path[d] = $1
p = path[0]
for (i = 1; i <= d; i++) p = p "/" path[i]
rows++
own[p] = $(NF-1); cum[p] = $NF; cells[p] = $(NF-2); depth[p] = d
order[rows] = p
ownsum += $(NF-1)
bydepth[d] += $(NF-1); ndepth[d]++
if (d == 0) topcum = $NF
}
END {
if (maxd == 0 && rows > 1) {
printf "ERROR: %d module rows and maximum depth 0 -- the indentation was not understood\n", rows > "/dev/stderr"
exit 2
}
if (!(ownsum - topcum < TOL && topcum - ownsum < TOL)) {
printf "ERROR: own areas sum to %.3f but the top row reports %.3f\n", ownsum, topcum > "/dev/stderr"
exit 1
}
printf "%-26s %2s %6s %11s %11s %7s\n", "path", "d", "cells", "own area", "total area", "share"
lim = (top == "" ? rows : top)
shown = 0
for (i = 1; i <= rows && shown < lim; i++) {
p = order[i]; shown++
printf "%-26s %2d %6d %11.3f %11.3f %6.1f%%\n", p, depth[p], cells[p], own[p], cum[p], 100 * own[p] / ownsum
}
if (shown < rows) printf "... %d more module(s)\n", rows - shown
printf "#\n# own area by depth:\n"
for (i = 0; i <= maxd; i++) printf "# depth %d: %2d module(s) %11.3f (%.1f%%)\n", i, ndepth[i], bydepth[i], 100 * bydepth[i] / ownsum
printf "# %d module(s), own areas sum to %.3f = the top row's Total Area\n", rows, ownsum
}awk -v top=6 -f hierstats.awk hier.rpt
echo "status: $?"path d cells own area total area share
alu_core 0 204 1420.500 27793.500 5.1%
alu_core/u_regfile 1 466 4980.000 4980.000 17.9%
alu_core/u_alu 1 178 1310.000 11350.000 4.7%
alu_core/u_alu/u_add 2 420 2840.000 2840.000 10.2%
alu_core/u_alu/u_mul 2 642 5210.000 5210.000 18.7%
alu_core/u_alu/u_shift 2 190 1130.000 1130.000 4.1%
... 16 more module(s)
#
# own area by depth:
# depth 0: 1 module(s) 1420.500 (5.1%)
# depth 1: 7 module(s) 11163.000 (40.2%)
# depth 2: 14 module(s) 15210.000 (54.7%)
# 22 module(s), own areas sum to 27793.500 = the top row's Total Area
status: 0The share column is each module's own area as a fraction of the design — which sums to 100% because own areas partition it, and which the cumulative column could never give.
The reconciliation is the exit-1 condition: own areas must sum to the top row's Total Area. That single equation catches a dropped row, a misread column and a double-counted parent at once, and it is available in the report itself rather than from an expectation.
And the tab-indented variant, which is the case section 6 showed defeats a naive depth rule:
awk -v top=3 -f hierstats.awk hier_tab.rpt
echo "status: $?"path d cells own area total area share
alu_core 0 204 1420.500 27793.500 5.1%
alu_core/u_regfile 1 466 4980.000 4980.000 17.9%
alu_core/u_alu 1 178 1310.000 11350.000 4.7%
... 19 more module(s)
#
# own area by depth:
# depth 0: 1 module(s) 1420.500 (5.1%)
# depth 1: 7 module(s) 11163.000 (40.2%)
# depth 2: 14 module(s) 15210.000 (54.7%)
# 22 module(s), own areas sum to 27793.500 = the top row's Total Area
status: 0Identical to the space-indented run, down to the per-depth breakdown — because the script normalises the leading whitespace before measuring it and reads the columns from NF rather than by position. The depth rule in section 6 that returned all zeros is the one this script does not use, and the difference is the gsub(/\t/, " ").
That is the whole value of the normalisation, and it is worth seeing it succeed rather than only seeing the naive version fail. The collapse guard never fires here — which is correct, and is also why Exercise 6 exists: a guard that passes is only reassuring once you know what would make it fire.
Exercise 6 — Challenge: show that the flat-hierarchy check is not enough
Exercise 5's maxd == 0 && rows > 1 guard catches a total collapse. Construct a report where the indentation is misread but the guard passes, and find what catches it.
sed 's/^ \(u_regfile\)/\1/' hier.rpt > shift.rpt
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; if (d > md) md = d; rows++ } END { printf "%d rows, max depth %d -> %s\n", rows, md, (md == 0 && rows > 1 ? "SUSPECT" : "passes the collapse check") }' shift.rpt22 rows, max depth 2 -> passes the collapse checkOne module's indentation was removed, so u_regfile now reads as a second top-level module. The maximum depth is still 2, so the collapse check sees nothing.
awk 'NR>7 && !/^-|^TOTAL/ { d = (match($0, /[^ ]/) - 1) / 2; if (d == 0) { n0++; printf " depth 0: %s\n", $1 } } END { printf " %d module(s) at depth 0 -> %s\n", n0, (n0 == 1 ? "ok" : "A REPORT HAS EXACTLY ONE TOP MODULE") }' shift.rpt depth 0: alu_core
depth 0: u_regfile
2 module(s) at depth 0 -> A REPORT HAS EXACTLY ONE TOP MODULETwo modules at depth 0, which a single-top report cannot have. That is the check, and it is a structural fact about the report rather than a threshold.
The own-area reconciliation does not catch this one:
awk 'NR>7 && !/^-|^TOTAL/ { own += $(NF-1); if ((match($0, /[^ ]/) - 1) / 2 == 0 && !seen++) topcum = $NF } END { printf "own sum %.3f vs top Total Area %.3f -> %s\n", own, topcum, (own - topcum < 1e-6 && topcum - own < 1e-6 ? "RECONCILES" : "mismatch") }' shift.rptown sum 27793.500 vs top Total Area 27793.500 -> RECONCILESIt reconciles perfectly, because de-indenting a row changes no area — only the tree. So the two checks are independent and both necessary: the reconciliation validates the numbers and the single-top rule validates the structure, and each is blind to the other's failure.
That pairing is the module's recurring shape. Chapter 11.1 needed a residual check and an anchored pattern; Chapter 11.2 needed a type assertion and the leaf/instance reconciliation; here it is areas and topology. One check is a property; two independent checks are a parser.
10. Summary
Cell Areais own area;Total Areaincludes everything nested inside. On the top row they are 1420.500 and 27793.500.- Summing
Total Areagives 2.50× the real total — section 2. - The factor decomposes exactly — depth 0 counted once, depth 1 twice, depth 2 three times, predicting 69376.500 against a measured 69376.500. It is this design's shape, not a constant.
- A parent's
Total Area= ownCell Area+ children'sTotal Area— verified on a three-child and a four-child parent. - Own areas partition the design, so a per-module share column sums to 100%.
- Count fields from
NF. The instance column is indented and module names vary in width. - Default field splitting discards the indentation. Recover depth with
(match($0, /[^ ]/) - 1) / 2on$0. - On a tab-indented report that rule returns 0 for every module and the hierarchy collapses silently, with every number still correct.
- A bare instance name is not a key — two
u_fifoinstances merge into a 700.000 row that does not exist. Rebuild the path withpath[d] = $1. - Two independent checks are needed: own areas must sum to the top's total, and exactly one module may sit at depth 0. Each is blind to the other's failure.
Chapter 11.4 — Line Coverage leaves synthesis for verification, and starts with the difference that makes coverage reports harder than area reports: an area is a measurement, and a coverage percentage is a ratio whose denominator the report may not print.
Where this fits
Part of the awk curriculum.
