Perl Part II · Module 20 · Netlist Processing
Counting and Classifying Cells
A cell census is the most common thing anybody asks a netlist script for: how many flops, how much combinational logic, how many clock gates, and is there anything in here that should not be. Counting is a hash. The two things worth getting right are that a per-type count of the text is not a count of the design, and that a cell type the classifier does not recognise has to be reported as unrecognised rather than quietly dropped into a bucket called other.
Advanced14 min readPerlPart IINetlists
Part II · Module 20 · Chapter 20.5 · Counting and Classifying Cells
1. Two Censuses From One Netlist
Counting is Chapter 6.8's hash, once per cell type. The interesting part is that there are two legitimate counts, and the script produces both.
use strict;
use warnings;
my @KEYWORDS = qw(module endmodule input output inout wire reg assign
parameter localparam always initial generate endgenerate
genvar begin end supply0 supply1);
my %IS_KW = map { $_ => 1 } @KEYWORDS;
sub read_netlist {
my ($file) = @_;
open my $fh, "<", $file or die "$file: $!";
my (@mods, @inst, $cur, $buf, $in_block);
while (my $raw = <$fh>) {
my $l = $raw;
if ($in_block) { if ($l =~ s{.*?\*/}{}) { $in_block = 0 } else { next } }
$l =~ s{/\*.*?\*/}{}g;
if ($l =~ s{/\*.*$}{}) { $in_block = 1 }
$l =~ s{//.*$}{};
next unless $l =~ /\S/;
if ($l =~ /^\s*module\s+(\w+)/) { push @mods, $1; $cur = $1; $buf = ""; next }
if ($l =~ /^\s*endmodule\b/) { undef $cur; $buf = ""; next }
next unless defined $cur;
$buf .= " $l";
next unless $buf =~ /;/;
my $stmt = $buf; $buf = "";
$stmt =~ s/;.*$//; $stmt =~ s/\s+/ /g; $stmt =~ s/^ | $//g;
next unless $stmt =~ /^(\w+)\s+(\w+)\s*\((.*)\)$/;
next if $IS_KW{$1};
push @inst, { type => $1, name => $2, parent => $cur };
}
close $fh;
return (\@mods, \@inst);
}
# ---- the family table. DATA, not logic (20.3's rule): retargeting to another
# library is an edit here. Order matters -- the first match wins.
my @FAMILY = (
[ qr/^DF/, "flop" ],
[ qr/^CKLN/, "clock gate" ],
[ qr/^CK/, "clock buffer" ],
[ qr/^(BUFF|INV)/, "buffer/inv" ],
[ qr/^(AN|ND|OR|NR|XOR|XNR|MUX|AO|OA)/, "combinational" ],
[ qr/^(FILL|TIEHI|TIELO|ANTENNA|DCAP)/, "physical-only" ],
);
sub family {
my ($type) = @_;
for my $f (@FAMILY) { return $f->[1] if $type =~ $f->[0] }
return undef; # UNKNOWN -- never silently "other"
}
my ($mods, $inst) = read_netlist("netlist/counter_syn.v");
my %IS_MODULE = map { $_ => 1 } @$mods;
# ---- how many times is each module instantiated, for the elaborated count
my %children;
push @{ $children{ $_->{parent} } }, $_ for @$inst;
my %instantiated = map { $_->{type} => 1 } @$inst;
my ($top) = grep { !$instantiated{$_} } @$mods;
my %mult; # module -> how many elaborated copies
sub walk_mult {
my ($mod, $times) = @_;
$mult{$mod} += $times;
for my $c (@{ $children{$mod} || [] }) {
walk_mult($c->{type}, $times) if $IS_MODULE{ $c->{type} };
}
}
walk_mult($top, 1);
# ---- two censuses: as WRITTEN, and as ELABORATED
my (%text, %elab);
for my $i (@$inst) {
next if $IS_MODULE{ $i->{type} };
$text{ $i->{type} }++;
$elab{ $i->{type} } += $mult{ $i->{parent} } // 1;
}
printf "top module: %s\n", $top;
printf "module copies after elaboration: %s\n\n",
join(", ", map { "$_ x$mult{$_}" } sort keys %mult);
printf "%-12s %-16s %-10s %-12s %s\n", "CELL TYPE", "FAMILY", "IN TEXT", "ELABORATED", "note";
my ($t_tot, $e_tot) = (0, 0);
for my $type (sort keys %text) {
my $fam = family($type);
$t_tot += $text{$type};
$e_tot += $elab{$type};
printf "%-12s %-16s %-10d %-12d %s\n", $type, ($fam // "*** UNKNOWN ***"),
$text{$type}, $elab{$type},
($elab{$type} != $text{$type} ? "x" . ($elab{$type} / $text{$type}) : "");
}
printf "%-12s %-16s %-10d %-12d\n", "TOTAL", "", $t_tot, $e_tot;
print "\nper family (elaborated):\n";
my %fam_count;
for my $type (keys %elab) {
my $f = family($type) // "*** UNKNOWN ***";
$fam_count{$f} += $elab{$type};
}
printf " %-16s %d\n", $_, $fam_count{$_} for sort keys %fam_count;
print "\nchecks worth running on a signoff netlist:\n";
my @physical = grep { (family($_) // "") eq "physical-only" } sort keys %elab;
printf " physical-only cells present: %s\n",
(@physical ? join(", ", map { "$_ x$elab{$_}" } @physical) : "none");
my @unknown = grep { !defined family($_) } sort keys %elab;
printf " UNRECOGNISED cell types: %s\n",
(@unknown ? join(", ", @unknown) : "none");
printf " flop count (elaborated): %d\n", $fam_count{flop} // 0;
printf " and in the TEXT it looks like: %d\n",
(grep { (family($_) // "") eq "flop" } keys %text)
? do { my $n = 0; $n += $text{$_} for grep { (family($_)//"") eq "flop" } keys %text; $n }
: 0;
# ---- is the UNKNOWN branch able to fire? A check that never fires on your
# input is not a check yet (the standing rule of this track).
print "\nproving the classifier can fail -- types it has never seen:\n";
my @PROBE = qw(DFCNQD1 INVD1 LVLHLD1 SDFFRPQ_X2 MY_CUSTOM_RAM retention_ff);
for my $t (@PROBE) {
my $f = family($t);
printf " %-14s -> %s\n", $t, ($f // "*** UNKNOWN *** (would be reported)");
}
my $unknown_probe = grep { !defined family($_) } @PROBE;
printf " %d of %d probe types are unrecognised, so the branch is live\n",
$unknown_probe, scalar @PROBE;
print "\nand why 'other' would be the wrong bucket:\n";
print " a retention flop classified as 'other' disappears from the flop\n";
print " count, and a flop count that is quietly low is exactly the number\n";
print " a reviewer trusts. UNKNOWN must be a named outcome, not a default.\n";top module: counter_top
module copies after elaboration: counter_bit x4, counter_top x1
CELL TYPE FAMILY IN TEXT ELABORATED note
AN2D1 combinational 2 2
BUFFD1 buffer/inv 1 1
CKBD1 clock buffer 1 1
CKLNQD1 clock gate 1 1
DFCNQD1 flop 1 4 x4
FILL1 physical-only 1 1
INVD1 buffer/inv 3 9 x3
TIELO physical-only 1 1
XOR2D1 combinational 3 3
TOTAL 14 23
per family (elaborated):
buffer/inv 10
clock buffer 1
clock gate 1
combinational 5
flop 4
physical-only 2
checks worth running on a signoff netlist:
physical-only cells present: FILL1 x1, TIELO x1
UNRECOGNISED cell types: none
flop count (elaborated): 4
and in the TEXT it looks like: 1
proving the classifier can fail -- types it has never seen:
DFCNQD1 -> flop
INVD1 -> buffer/inv
LVLHLD1 -> *** UNKNOWN *** (would be reported)
SDFFRPQ_X2 -> *** UNKNOWN *** (would be reported)
MY_CUSTOM_RAM -> *** UNKNOWN *** (would be reported)
retention_ff -> *** UNKNOWN *** (would be reported)
4 of 6 probe types are unrecognised, so the branch is live
and why 'other' would be the wrong bucket:
a retention flop classified as 'other' disappears from the flop
count, and a flop count that is quietly low is exactly the number
a reviewer trusts. UNKNOWN must be a named outcome, not a default.14 cell instantiations in the text; 23 cells in the elaborated design. The multiplier comes from Chapter 20.4's map: counter_bit is instantiated four times, so everything inside it counts four times.
The elaborated total of 23 is the same number Chapter 20.4 established three independent ways — a recursive expansion, an independent iterative one, and the simulator's own VCD scope list. Two chapters, two different methods, the same answer, which is the kind of agreement worth noticing when it happens.
The mechanism is one recursive walk that records how many copies of each module exist:
sub walk_mult {
my ($mod, $times) = @_;
$mult{$mod} += $times;
for my $c (@{ $children{$mod} || [] }) {
walk_mult($c->{type}, $times) if $IS_MODULE{ $c->{type} };
}
}
walk_mult($top, 1);Then each cell's elaborated count is its text count times the multiplier of the module it sits in. Measured: counter_bit x4, counter_top x1.
2. The Flop Count
Look at the one row that matters most:
DFCNQD1 flop 1 in text 4 elaborated x4The flop count is the single most-quoted number in any netlist census — it drives area estimates, power estimates, scan-chain planning and the "is this the netlist I think it is" sanity check. And a text census reports 1 where the design has 4.
The script says both, deliberately:
flop count (elaborated): 4
and in the TEXT it looks like: 1One level of hierarchy and four repeats is all it takes. A real design has ten levels and blocks repeated hundreds of times, so the gap is not 4× — it is whatever the hierarchy multiplies to, and it is always in the same direction: the text count is a floor.
3. The Family Table Is Data
Classification is a table of pattern to family, which is Chapter 20.3's rule — the varying part belongs in data, not in logic:
my @FAMILY = (
[ qr/^DF/, "flop" ],
[ qr/^CKLN/, "clock gate" ],
[ qr/^CK/, "clock buffer" ],
[ qr/^(BUFF|INV)/, "buffer/inv" ],
[ qr/^(AN|ND|OR|NR|XOR|XNR|MUX|AO|OA)/, "combinational" ],
[ qr/^(FILL|TIEHI|TIELO|ANTENNA|DCAP)/, "physical-only" ],
);Four properties that are easy to get wrong:
Order matters, first match wins. ^CKLN must come before ^CK, or every clock gate is counted as a clock buffer. The measurement confirms the order works: CKLNQD1 is a clock gate and CKBD1 a clock buffer.
Every pattern is anchored. ^DF and not DF, or MUXDFX1 becomes a flop. Chapter 11.8's anchors, and the same lesson Chapter 19.2 reached about filename filters.
Compiled patterns in a table, as Chapter 18.8 built for tool-version spellings and Chapter 7.11's dispatch tables do generally. Retargeting to a different library is an edit to this list.
It returns undef, not a default. That is section 4.
4. Unrecognised Is A Named Outcome
family() returns undef for a type it does not match, and the census prints *** UNKNOWN ***. On this netlist nothing is unrecognised — which means the branch is untested by the main measurement, so the script exercises it separately:
LVLHLD1 -> *** UNKNOWN *** (would be reported)
SDFFRPQ_X2 -> *** UNKNOWN *** (would be reported)
MY_CUSTOM_RAM -> *** UNKNOWN *** (would be reported)
retention_ff -> *** UNKNOWN *** (would be reported)4 of 6 probe types are unrecognised, and the other two still classify correctly. Both halves matter: a probe where everything comes back UNKNOWN would prove only that the function can return undef, not that it discriminates.
And look at what is in that list. SDFFRPQ_X2 is a scan flop — a perfectly ordinary name in another vendor's library — and ^DF does not match it. So the table is not merely incomplete in principle; it is demonstrably incomplete against a realistic name, which is the concrete form of "the family table is library-specific".
5. The Checks Worth Running
The census is the input to three checks that genuinely find things.
Physical-only cells in a functional netlist. Measured: FILL1 x1, TIELO x1. Fillers, tie cells, decaps and antenna diodes belong in a post-layout netlist and not in one you are about to simulate or hand to formal — they carry no function, and their presence usually means somebody passed the wrong file. This is the check that most often catches a real mistake, because the two netlists have similar names.
Unrecognised cell types. Covered above. In a real run this is the one that fires when a library is upgraded.
A count delta against the previous netlist. The census is a hash of type to count, so comparing two runs is a hash comparison — and this is Chapter 20.7's subject, because the useful form is not the delta itself but which deltas are worth reporting.
Two more that cost one line each, and are worth adding once the above exist:
# a netlist with no flops at all is almost always the wrong file
warn "no flops found -- is this a combinational block or the wrong netlist?\n"
unless $fam_count{flop};
# a netlist that is ALL buffers and inverters is usually a clock-tree file
warn "buffer/inv is " . int(100 * $fam_count{"buffer/inv"} / $e_tot) . "% of cells\n"
if $e_tot && $fam_count{"buffer/inv"} / $e_tot > 0.8;Both are range checks in the sense Chapter 18.8 used: a number you cannot sanity-check is a number you should not publish, and the cheapest sanity check is whether the shape of the design matches what you expected to be handed.
6. What To Remember
- Counting is a hash (6.8); the care goes into what is being counted.
- There are two legitimate censuses: as written in the text, and as elaborated.
- Measured: 14 cell instantiations in the text, 23 cells in the design.
- The elaborated total matches 20.4's independently verified 23 — two chapters, two methods, one number.
- One recursive walk gives each module's copy count; a cell's elaborated count is its text count times its module's multiplier.
- Measured: the flop count is 1 in the text and 4 in the design. A 4x error from one level of hierarchy.
- The text count is always a floor, never an estimate that might be high.
- The wrong number is plausible — "1,200 flops" looks fine to everybody. Label it
(elaborated)or(as written)every time. - If the hierarchy is unavailable, say so: "at least N", not N.
- The family table is data, not logic — retargeting a library is an edit to a list.
- Order matters and first match wins:
^CKLNbefore^CK, or every clock gate becomes a buffer. - Anchor every family pattern, or
MUXDFX1becomes a flop (11.8). family()returnsundef, never a default bucket.- Proven able to fire: 4 of 6 probe types unrecognised, with 2 still classified correctly — a probe where everything is UNKNOWN proves nothing.
SDFFRPQ_X2, an ordinary scan-flop name, is missed by^DF— the table is demonstrably library-specific.- A bucket called "other" converts an unknown into a known-looking zero (18.8).
- Flag physical-only cells — fillers and tie cells in a functional netlist usually mean the wrong file was passed.
- Flag no-flops and all-buffers as shape checks on the netlist you were handed.
Chapter 20.6 goes one level finer, into the connection lists themselves, and is careful about what it promises: reading what connects to a cell's pins at a single level of hierarchy is straightforward, and following a net across a boundary is not something a line scan can do.
Where this fits
Part of the Perl curriculum.
