awk Part IV · Module 13 · Real-World AWK Engineering Projects
Project: Synthesis Area Analyzer
The fourth script compares two synthesis runs and lists what grew, which sounds like subtraction and is not. Two areas that print identically can differ at the twelfth decimal, so grew needs a threshold — and this chapter establishes the threshold the honest way, by measuring how often an exact comparison would have worked. On this pair of reports it works perfectly; on 58.3% of comparable value sets it does not. That is a better argument for a tolerance than any single failure would be, because it says the exactness you observe is luck.
Advanced18 min readawkPart IVProjectSynthesisFloating Point
Part IV · Module 13 · Chapter 13.4 · Project: Synthesis Area Analyzer
1. What the Script Has to Do
Four things, and only the first is arithmetic:
- Per-module area and cell counts from each of two reports.
- A delta per module, with a defensible definition of "grew".
- Modules present in one report and absent from the other — added or removed, not treated as zero.
- A reconciliation against each report's own
TOTAL, because a dropped row changes every conclusion.
2. Two Runs
awk 'BEGIN {
n = split("u_regfile:466:4980.000|u_alu:178:1310.000|u_add:420:2840.000|u_mul:642:5210.000|u_shift:190:1130.000|u_ctrl:170:1240.000|u_io:100:810.000|u_dma:83:690.000|u_fpu:27:1950.000|u_debug:18:183.000", R, "|")
print "****************************************"; print "Report : area -hierarchy"; print "Design : soc_top rev1"
print "****************************************"; print ""
print "Module Cells Cell Area"; print "-------------------------------------------"
for (i = 1; i <= n; i++) { split(R[i], f, ":"); printf "%-20s %5d %10.3f\n", f[1], f[2], f[3]; c += f[2]; a += f[3] }
print "-------------------------------------------"; printf "%-20s %5d %10.3f\n", "TOTAL", c, a
}' > area_old.rpt
awk 'BEGIN {
n = split("u_regfile:466:4980.000|u_alu:178:1310.000|u_add:468:3122.500|u_mul:642:5210.000|u_shift:190:1130.000|u_ctrl:214:1476.000|u_io:100:810.000|u_dma:83:690.000|u_fpu:27:1950.000|u_debug:18:183.000", R, "|")
print "****************************************"; print "Report : area -hierarchy"; print "Design : soc_top rev2"
print "****************************************"; print ""
print "Module Cells Cell Area"; print "-------------------------------------------"
for (i = 1; i <= n; i++) { split(R[i], f, ":"); printf "%-20s %5d %10.3f\n", f[1], f[2], f[3]; c += f[2]; a += f[3] }
print "-------------------------------------------"; printf "%-20s %5d %10.3f\n", "TOTAL", c, a
}' > area_new.rpt
awk '$1 == "TOTAL" { printf " %s: %d cells, %.3f area\n", FILENAME, $2, $3 }' area_old.rpt area_new.rpt area_old.rpt: 2294 cells, 20343.000 area
area_new.rpt: 2386 cells, 20861.500 areaTen modules in each. Two of them changed.
3. What "Grew" Means
The obvious test is new > old. Whether that is safe depends on arithmetic nobody checks:
awk 'FNR == NR { if (/^[a-z_]/ && NF == 3 && $1 != "TOTAL") oa[$1] = $3; if ($1 == "TOTAL") ota = $3; next }
/^[a-z_]/ && NF == 3 && $1 != "TOTAL" { da += $3 - oa[$1] } $1 == "TOTAL" { nta = $3 }
END { printf " total area delta %.20f\n", nta - ota
printf " sum of module deltas %.20f\n", da
printf " equal with == %s\n", (nta - ota == da ? "yes" : "NO") }' area_old.rpt area_new.rpt total area delta 518.50000000000000000000
sum of module deltas 518.50000000000000000000
equal with == yesExact. On this pair of reports the reconciliation holds with == and no tolerance is needed.
That is not a licence to use ==, and the reason is measurable:
awk 'BEGIN {
srand(7); n = 20000
for (k = 1; k <= n; k++) {
a1 = int(rand()*900000)/1000; a2 = int(rand()*900000)/1000
b1 = int(rand()*900000)/1000; b2 = int(rand()*900000)/1000
if ((a2 + b2) - (a1 + b1) == (a2 - a1) + (b2 - b1)) exact++
}
printf " %d of %d random 3-decimal pairs reconcile EXACTLY with == (%.1f%%)\n", exact, n, 100*exact/n
printf " so %d of %d (%.1f%%) do NOT\n", n - exact, n, 100*(n-exact)/n
}' 8347 of 20000 random 3-decimal pairs reconcile EXACTLY with == (41.7%)
so 11653 of 20000 (58.3%) do NOTAnd a second threshold, which is an engineering decision rather than an arithmetic one:
awk 'FNR == NR { if (/^[a-z_]/ && NF == 3 && $1 != "TOTAL") oa[$1] = $3; next }
/^[a-z_]/ && NF == 3 && $1 != "TOTAL" {
d = $3 - oa[$1]
if (d > 1e-6) printf " %-11s %+9.3f absolute, %+6.2f%% relative\n", $1, d, 100*d/oa[$1] }' area_old.rpt area_new.rpt u_add +282.500 absolute, +9.95% relative
u_ctrl +236.000 absolute, +19.03% relativeu_add grew more in absolute terms and u_ctrl grew more as a fraction of itself. Both are reported, because a large module growing 0.05% and a small one growing 20% are different events — Chapter 11.2 section 7's point about ranking by the quantity you will act on.
4. Modules That Appear or Disappear
A module in one report and not the other is not a delta of zero:
grep -v '^u_debug' area_old.rpt | awk '$1 == "TOTAL" { printf "%-20s %5d %10.3f\n", "TOTAL", 2294-18, 20343.000-183.000; next } { print }' > old2.rpt
awk '$1 == "u_fpu" { print; printf "%-20s %5d %10.3f\n", "u_pcie", 310, 2400.000; next } $1 == "TOTAL" { printf "%-20s %5d %10.3f\n", "TOTAL", 2386+310, 20861.500+2400.000; next } { print }' area_new.rpt > new2.rpt
awk 'FNR == 1 { n++ } /^[a-z_]/ && NF == 3 && $1 != "TOTAL" { if (n == 1) o[$1] = 1; else nw[$1] = 1 }
END { for (m in o) if (!(m in nw)) r = r " " m
for (m in nw) if (!(m in o)) a = a " " m
printf " removed:%s\n added:%s\n", (r == "" ? " none" : r), (a == "" ? " none" : a) }' old2.rpt new2.rpt removed: none
added: u_debug u_pcieTwo modules appear in the new report and none disappears — u_debug was removed from the old one for this demonstration, so from the comparison's point of view it is new. The for (m in …) loops here accumulate into a string whose content does not depend on order, but the script uses a file-order array for its own output, because two added modules would otherwise print in an undefined sequence.
5. The Complete Script
# areadiff.awk — synthesis area analyzer.
#
# Usage: awk -f areadiff.awk area_old.rpt area_new.rpt
# awk -v tol=1e-6 -v rel=0.1 -f areadiff.awk area_old.rpt area_new.rpt
# Exit: 0 nothing grew beyond the tolerance
# 1 at least one module grew
# 2 the per-module rows do not reconcile with a report's TOTAL row
#
# Design notes, each from a measured failure in Module 11:
# - Compares floating-point areas with a TOLERANCE, never ==. On this pair of
# reports == happens to be exact; on 58.3% of random 3-decimal value sets
# it is not, so exactness here is luck. (11.1)
# - Integer cell counts DO compare with ==; the exemption runs the other
# way. (11.1)
# - Reconciles the per-module rows against each report's own TOTAL -- the
# only check that detects a dropped or misparsed row. (11.8)
# - Reports a module present in one report and absent from the other, rather
# than treating a missing key as zero. (9.5)
# - Reports both an absolute and a relative threshold, because a large module
# growing 0.05% and a small one doubling are different events. (11.2)
BEGIN { if (tol == "") tol = 1e-6; if (rel == "") rel = 0.1 }
FNR == 1 { nfile++ }
/^[a-z_]/ && NF == 3 && $1 != "TOTAL" {
if (nfile == 1) { oc[$1] = $2 + 0; oa[$1] = $3 + 0; osum_c += $2; osum_a += $3
if (!($1 in oseen)) { oseen[$1] = 1; order[++nmod] = $1 } }
else { nc[$1] = $2 + 0; na[$1] = $3 + 0; nsum_c += $2; nsum_a += $3
if (!($1 in nseen)) { nseen[$1] = 1; norder[++nnew] = $1 } }
}
$1 == "TOTAL" { if (nfile == 1) { otc = $2 + 0; ota = $3 + 0 } else { ntc = $2 + 0; nta = $3 + 0 } }
function near(x, y) { return (x - y < tol && y - x < tol) }
END {
if (nfile < 2) { print "ERROR: need two reports" > "/dev/stderr"; exit 2 }
# reconcile each report against its own TOTAL before comparing them
if (!near(osum_a, ota) || osum_c != otc) {
printf "ERROR: old rows sum to %d cells / %.3f area, its TOTAL says %d / %.3f\n", osum_c, osum_a, otc, ota > "/dev/stderr"; exit 2 }
if (!near(nsum_a, nta) || nsum_c != ntc) {
printf "ERROR: new rows sum to %d cells / %.3f area, its TOTAL says %d / %.3f\n", nsum_c, nsum_a, ntc, nta > "/dev/stderr"; exit 2 }
printf "%-12s %7s %7s %7s %11s %11s %10s %8s\n", "module", "cells-", "cells+", "dcells", "area-", "area+", "darea", "drel%"
for (i = 1; i <= nmod; i++) {
m = order[i]
if (!(m in nseen)) { printf "%-12s %7d %7s %7s %11.3f %11s %10s %8s REMOVED\n", m, oc[m], "-", "-", oa[m], "-", "-", "-"; gone++; continue }
dc = nc[m] - oc[m]; da = na[m] - oa[m]
dr = (oa[m] > 0 ? 100 * da / oa[m] : 0)
note = "[same]"
if (da > tol) { note = "GREW"; ngrew++ }
else if (da < -tol) { note = "shrank" }
if (da > tol && dr > rel) note = note " (" sprintf("%.2f%%", dr) " > " rel "%)"
printf "%-12s %7d %7d %+7d %11.3f %11.3f %+10.3f %+8.2f %s\n", m, oc[m], nc[m], dc, oa[m], na[m], da, dr, note
}
for (i = 1; i <= nnew; i++) { m = norder[i]
if (!(m in oseen)) { printf "%-12s %7s %7d %7s %11s %11.3f %10s %8s ADDED\n", m, "-", nc[m], "-", "-", na[m], "-", "-"; added++ } }
print ""
printf "TOTALS cells %d -> %d (%+d) area %.3f -> %.3f (%+.3f, %+.2f%%)\n",
otc, ntc, ntc - otc, ota, nta, nta - ota, 100 * (nta - ota) / ota
printf "#\n# %d module(s); %d grew beyond %g; %d added; %d removed\n", nmod, ngrew + 0, tol, added + 0, gone + 0
printf "# area deltas compared with a tolerance of %g, cell counts with == (integers are exact)\n", tol
if (ngrew) exit 1
}function near(x, y) is two one-sided comparisons rather than an absolute value, because POSIX awk has no abs() — Chapter 11.1 section 8. And both order[] and norder[] exist so that the main table and the ADDED rows each print in file order rather than in whatever sequence for (m in …) returns.
6. Running It
awk -f areadiff.awk area_old.rpt area_new.rpt
echo "status: $?"module cells- cells+ dcells area- area+ darea drel%
u_regfile 466 466 +0 4980.000 4980.000 +0.000 +0.00 [same]
u_alu 178 178 +0 1310.000 1310.000 +0.000 +0.00 [same]
u_add 420 468 +48 2840.000 3122.500 +282.500 +9.95 GREW (9.95% > 0.1%)
u_mul 642 642 +0 5210.000 5210.000 +0.000 +0.00 [same]
u_shift 190 190 +0 1130.000 1130.000 +0.000 +0.00 [same]
u_ctrl 170 214 +44 1240.000 1476.000 +236.000 +19.03 GREW (19.03% > 0.1%)
u_io 100 100 +0 810.000 810.000 +0.000 +0.00 [same]
u_dma 83 83 +0 690.000 690.000 +0.000 +0.00 [same]
u_fpu 27 27 +0 1950.000 1950.000 +0.000 +0.00 [same]
u_debug 18 18 +0 183.000 183.000 +0.000 +0.00 [same]
TOTALS cells 2294 -> 2386 (+92) area 20343.000 -> 20861.500 (+518.500, +2.55%)
#
# 10 module(s); 2 grew beyond 1e-06; 0 added; 0 removed
# area deltas compared with a tolerance of 1e-06, cell counts with == (integers are exact)
status: 1Two modules grew, eight are unchanged, and the footer states the comparison method — which is Chapter 11.8's rule that an aggregation decision belongs in the output. [same] is printed rather than left blank so that no row ends in whitespace and so that "unchanged" is an explicit verdict rather than an absence.
7. The Refusals
awk '$1 == "TOTAL" { sub(/20343.000/, "99999.000") } { print }' area_old.rpt > badtot.rpt
awk -f areadiff.awk badtot.rpt area_new.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: old rows sum to 2294 cells / 20343.000 area, its TOTAL says 2294 / 99999.000
status: 2The guard fires on the report whose rows do not add up to its own total — and it refuses before printing a single delta, because a comparison against a report with a dropped row is meaningless.
awk -f areadiff.awk old2.rpt new2.rpt 2>/dev/null | grep -E "ADDED|^# [0-9]"u_pcie - 310 - - 2400.000 - - ADDED
u_debug - 18 - - 183.000 - - ADDED
# 9 module(s); 2 grew beyond 1e-06; 2 added; 0 removedTwo added modules, in file order. An earlier version of the script iterated for (m in nseen) here and the two rows came out in an undefined sequence — the fix is the norder[] array, and the reason to care is that a diff of two nights' reports must not show spurious changes.
8. Every Decision and Its Source
| decision | alternative that fails | measured in |
|---|---|---|
| tolerance for area comparisons | == is exact here and fails on 58.3% of comparable data | 11.1 |
== for cell counts | integers are exact; a tolerance would mask a real ±1 | 11.1 |
near() as two one-sided tests | POSIX awk has no abs() | 11.1 |
reconcile each report against its own TOTAL | a dropped row changes every delta | 11.8 |
| refuse before emitting any row | a partial diff invites use | 10.2 |
| report added and removed modules | a missing key is not a delta of zero | 9.5 |
| absolute and relative thresholds | a small module doubling and a large one drifting differ | 11.2 |
| file-order arrays for both tables | for (m in …) has no defined order | 10.4 |
[same] rather than a blank | no trailing whitespace, and unchanged is a verdict | — |
| state the comparison method in the footer | a decision that changes the answer is part of it | 11.8 |
9. Common Mistakes
== on two areas. Exact here, and wrong on a majority of comparable inputs — section 3.
A tolerance on cell counts. It would hide a genuine difference of one cell.
abs(). POSIX awk has none; write two comparisons.
Only an absolute threshold. A 0.05% drift on a large module swamps a 20% jump on a small one.
Treating a missing module as zero. It was added or removed, which is a different fact — section 4.
Comparing without reconciling each report first. A dropped row silently changes every delta.
for (m in …) for output. The added-module rows came out in an undefined order.
Leaving an unchanged row blank. It trails whitespace and says nothing.
10. Exercises
Exercise 1 — Only what grew
awk -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/GREW/ { printf " %-11s %+10.3f %+7.2f%%\n", $1, $7, $8 }' u_add +282.500 +9.95%
u_ctrl +236.000 +19.03%Exercise 2 — A stricter relative threshold
awk -v rel=15 -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/GREW/ { print " " $0 }' u_add 420 468 +48 2840.000 3122.500 +282.500 +9.95 GREW
u_ctrl 170 214 +44 1240.000 1476.000 +236.000 +19.03 GREW (19.03% > 15%)Both still grew in absolute terms, and only u_ctrl clears a 15% relative bar — the annotation appears on exactly the row that passes both tests.
Exercise 3 — Debug a reconciliation that fails on good data
A comparison script reports that a report's modules do not sum to its total.
awk '/^[a-z_]/ && NF == 3 && $1 != "TOTAL" { s += $3 } $1 == "TOTAL" { t = $3 }
END { printf " rows sum to %.20f\n TOTAL says %.20f\n equal with == : %s\n within 1e-6 : %s\n", s, t, (s == t ? "yes" : "NO"), (s - t < 1e-6 && t - s < 1e-6 ? "yes" : "no") }' area_old.rpt rows sum to 20343.00000000000000000000
TOTAL says 20343.00000000000000000000
equal with == : yes
within 1e-6 : yesHere both agree. The point of the exercise is that this is not guaranteed — section 3 measured == holding on only 41.7% of comparable value sets, so a check written this way passes its test case and fails later on data that is equally correct.
Exercise 4 — Debug added modules in a changing order
for r in 1 2 3; do printf " run %d: " $r; awk -f areadiff.awk old2.rpt new2.rpt 2>/dev/null | awk '/ADDED/ { printf "[%s]", $1 } END { print "" }'; done run 1: [u_pcie][u_debug]
run 2: [u_pcie][u_debug]
run 3: [u_pcie][u_debug]Stable across three runs, because the script walks norder[] — the order the modules appeared in the file. With for (m in nseen) the two rows would come out in whatever order the implementation chose, and a nightly diff of the analyzer's own output would show changes that are not changes.
Exercise 5 — Engineering application
#!/bin/sh
# check_area.sh — compare two area reports and route the outcome.
awk -v tol=1e-6 -v rel=5 -f areadiff.awk area_old.rpt area_new.rpt > area_report.txt 2> area_errors.txt
status=$?
# the footer's count is $4 and the TOTALS line's area delta is $11, both
# established by printing the fields rather than guessing -- Chapter 12.4
ngrew=$(awk '/grew beyond/ { print $4 }' area_report.txt)
dtot=$(awk '/^TOTALS/ { d = $11; sub(/^\(/, "", d); sub(/,$/, "", d); print d }' area_report.txt)
case $status in
0) echo "AREA STABLE: nothing grew beyond the tolerance" ;;
1) echo "AREA GREW: $ngrew module(s), total $dtot; see area_report.txt" ;;
2) echo "AREA REPORT UNUSABLE:"; cat area_errors.txt ;;
*) echo "ANALYZER FAILED (status $status)" ;;
esac
exit $statussh check_area.sh
echo "status: $?"AREA GREW: 2 module(s), total +518.500; see area_report.txt
status: 1Both values come out clean, and getting there took printing the fields first:
awk -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/grew beyond/ { printf "NF=%d ", NF; for (i=1;i<=NF;i++) printf "[%d:%s]", i, $i; print "" }'NF=11 [1:#][2:10][3:module(s);][4:2][5:grew][6:beyond][7:1e-06;][8:0][9:added;][10:0][11:removed]$3 is module(s); and $4 is the count — a first draft of this exercise used $3 and printed module(s); module(s). The TOTALS line needs $11 with its leading parenthesis and trailing comma stripped, for the same reason.
That is the fourth time in Part IV that an index-based re-parse of a formatted line has taken the wrong field, and the structural answer is the one Chapter 13.3 section 6 built: a delimited sidecar with a fixed field count. This script does not have one, which is a genuine gap — Chapter 13.5 is where the metrics row gets written properly.
Exercise 6 — Challenge: show that the tolerance cannot be chosen from this report
Section 3 chose 10⁻⁶ because the report prints three decimals. Show that no observation of these two reports could have determined it.
awk 'FNR == NR { if (/^[a-z_]/ && NF == 3 && $1 != "TOTAL") oa[$1] = $3; next }
/^[a-z_]/ && NF == 3 && $1 != "TOTAL" { d = $3 - oa[$1]; if (d != 0) { n++; if (ab == "" || (d < 0 ? -d : d) < ab) ab = (d < 0 ? -d : d) } }
END { printf " %d non-zero delta(s); smallest magnitude %.3f\n", n, ab }' area_old.rpt area_new.rpt 2 non-zero delta(s); smallest magnitude 236.000The smallest non-zero delta in this comparison is 236.000. Any tolerance between 0 and 236 gives the same two GREW rows, so the data is consistent with a tolerance of 10⁻¹², 10⁻⁶, 1, or 100:
for t in 1e-12 1e-6 1 100 235 237; do
printf " tol=%-6s " "$t"
awk -v tol=$t -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/grew beyond/ { printf "%s module(s) grew\n", $4 }'
done tol=1e-12 2 module(s) grew
tol=1e-6 2 module(s) grew
tol=1 2 module(s) grew
tol=100 2 module(s) grew
tol=235 2 module(s) grew
tol=237 1 module(s) grewSix tolerances spanning fourteen orders of magnitude and five of them give the identical answer. The report cannot distinguish them, so the choice has to come from outside it — from the fact that the report prints three decimals, which makes anything below 10⁻³ unexpressible and anything below 10⁻⁶ certainly noise.
That is the general shape of a threshold decision and it is worth stating because it is easy to get backwards: a threshold is not calibrated on the data it will be applied to. Chapter 11.7's unconstrained-slack cutoff of 1e29 was chosen the same way — from what the quantity physically is, not from the values in one report — and both are kept in a named BEGIN variable so that the choice is visible rather than buried in a comparison.
11. Summary
- Four outputs: per-module counts, a delta with a defined threshold, added and removed modules, and a reconciliation.
==on areas is exact on these reports and fails on 58.3% of comparable value sets. The exactness you observe is luck.- Module 11.1's pair differs at the twelfth decimal and prints identically, including its difference as
-0.000. - A tolerance is a policy, not a bug fix — the failure depends on the values, so testing does not reveal it.
- Integer cell counts compare with
==. The exemption runs the opposite way from intuition. near()is two one-sided comparisons; POSIX awk has noabs().- Report absolute and relative growth.
u_addgrew more in area andu_ctrlmore as a fraction. - A module in one report and not the other is added or removed, not a delta of zero.
- Reconcile each report against its own
TOTALbefore comparing, and refuse before emitting any row. - Walk file-order arrays for output.
for (m in …)made the added rows appear in an undefined sequence. - The threshold cannot be calibrated on the data. Five tolerances spanning fourteen orders of magnitude give the identical answer here.
Chapter 13.5 — Project: Regression Metrics Exporter closes Module 13 by combining all four scripts into one dated row a dashboard can accumulate — and the design question becomes what a row must contain to still be comparable in six months.
Where this fits
Part of the awk curriculum.
