From 3f59eb038c937aa0d700b654a08e17b5ff94aa4a Mon Sep 17 00:00:00 2001 From: "Emil J. Tywoniak" Date: Tue, 21 Jul 2026 15:10:36 +0200 Subject: [PATCH] WIP opt_clean: garbage collect against the signorm index BROKEN: 12 tests fail (list below). Committed to keep the work resumable, not because it is ready. opt_clean stops calling sigNormalize(false) and, for modules carrying an index, runs a tracing GC against it instead of rebuilding its own view of the netlist: mark from keep/output/$input_port roots through wire->driverCell_ (one dereference per input bit, no wire2driver hashtable, no assign_map), sweep cells, sweep wires on fanout emptiness, then compact the sigmap over the survivors before the wires are freed. Per the design decision, it does not rename or rewire: where the old pass collapsed a public wire and its driver's wire into one, this keeps both plus the alias in the sigmap, which is fine to traverse. Kernel support: SigNormIndex::compact() (mfp cannot erase, so the sigmap only ever grows -- opt_clean is the only pass that knows what is garbage, hence the only one that can shrink it), an index-aware Module::remove(pool), and signorm_sigmap() normalizing fully rather than only flushing, since a caller reading the *index* rather than just the map has to see it settled. What this is worth on its own: 4.75s -> 4.54s on a 2000-stage pipeline (Debug), against main-dbg's 4.56s. The real win needs opt_dff to stop tearing the index down too -- together they are 3.39s -- but opt_dff regresses optimization quality under signorm and is left denormalizing. Neither change pays off without the other, which is the point of keeping this around. See g/signorm-opt_clean-analysis.md. Failing: arch/fabulous/fsm, arch/gatemate/fsm, arch/quicklogic/qlf_k6n10f/t_mem{1,5,6}, asicworld/code_verilog_tutorial_explicit, memories/wide_all, sat/share, various/{bug3515,bug4909,muxcover,port_sign_extend}. port_sign_extend is diagnosed and is not this pass's fault: Design::add() indexes a module while it is still empty, so setup_module_inputs() finds no ports and the index stays incomplete for wires added afterwards. Same defect as submod. muxcover is another marker-cell count. sat/share is the one still worth suspecting the GC over. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01MQU5XCqDYY8MbivGkNRrDo --- kernel/rtlil.cc | 22 +- kernel/rtlil.h | 6 + kernel/rtlil_bufnorm.cc | 50 ++- passes/opt/opt_clean/CMakeLists.txt | 1 + passes/opt/opt_clean/opt_clean.cc | 13 +- passes/opt/opt_clean/opt_clean.h | 8 + passes/opt/opt_clean/signorm.cc | 464 ++++++++++++++++++++++++++++ passes/opt/opt_clean/wires.cc | 22 +- passes/opt/opt_dff.cc | 9 +- 9 files changed, 571 insertions(+), 24 deletions(-) create mode 100644 passes/opt/opt_clean/signorm.cc diff --git a/kernel/rtlil.cc b/kernel/rtlil.cc index 1bcff2772..d053369a6 100644 --- a/kernel/rtlil.cc +++ b/kernel/rtlil.cc @@ -3517,10 +3517,24 @@ void RTLIL::Module::remove(const pool &wires) } }; - DeleteWireWorker delete_wire_worker; - delete_wire_worker.module = this; - delete_wire_worker.wires_p = &wires; - rewrite_sigspecs2(delete_wire_worker); + if (sig_norm_index != nullptr) { + // Under the index a deletable wire is by construction referenced by + // nothing -- no driver, no fanout entry, and dropped from the sigmap + // by the caller -- so there are no sigspecs left to rewrite. Check + // that rather than rewriting, since rewrite_sigspecs2 cannot run here + // anyway (it would have to touch the index behind its own back). + for (auto wire : wires) { + log_assert(wire->driverCell_ == nullptr); + for (int i = 0; i < GetSize(wire); i++) + log_assert(fanout(SigBit(wire, i)).empty()); + } + log_assert(processes.empty()); + } else { + DeleteWireWorker delete_wire_worker; + delete_wire_worker.module = this; + delete_wire_worker.wires_p = &wires; + rewrite_sigspecs2(delete_wire_worker); + } if (design->flagBufferedNormalized) { for (auto wire : wires) { diff --git a/kernel/rtlil.h b/kernel/rtlil.h index b7717ab63..688d1ce25 100644 --- a/kernel/rtlil.h +++ b/kernel/rtlil.h @@ -3091,6 +3091,12 @@ public: // `connections_`. Returns nullptr when not in signorm mode. const SigMap *signorm_sigmap(); + // Drop `dead_wires` from the index's sigmap. The caller must be about to + // delete exactly those wires and must have established that nothing + // references them; call this before deleting, while the wires are still + // alive to be hashed. No-op outside signorm mode. + void signorm_compact(const pool &dead_wires); + // Equivalent to `connect(lhs, rhs)` followed by `sigNormalize()` for the // merge implied by this single connection: updates the sigmap, promotes // the driven side as canonical, and re-normalizes any existing fanout diff --git a/kernel/rtlil_bufnorm.cc b/kernel/rtlil_bufnorm.cc index 8ed72924d..419d4faf6 100644 --- a/kernel/rtlil_bufnorm.cc +++ b/kernel/rtlil_bufnorm.cc @@ -279,6 +279,37 @@ struct RTLIL::SigNormIndex ++signorm_restore_count; } + // `mfp` cannot erase, so the sigmap only ever grows while in signorm mode: + // every alias ever merged stays in it, including ones whose wires have + // since become garbage. Rebuild it over the bits that survive, dropping + // the rest. + // + // Must run *before* the wires are actually freed: looking a bit up hashes + // through `wire->name`, so a database holding a dangling wire cannot even + // be iterated safely. + void compact(const pool &dead_wires) { + flush_connections(); + // Any bit still queued here is one whose consumers have not been + // re-pointed yet, and it may well be a wire the caller is about to + // delete -- at which point the next insert rehashes a freed wire. + // Callers must have normalized before deciding what is garbage. + log_assert(newly_driven.empty()); + + SigMap fresh; + for (auto const &bit : sigmap.database) { + if (bit.is_wire() && dead_wires.count(bit.wire)) + continue; + SigBit rep = sigmap(bit); + if (rep == bit) + continue; + // A representative is a driven bit or a constant, and anything + // still aliasing it keeps it referenced, so it cannot be garbage. + log_assert(!(rep.is_wire() && dead_wires.count(rep.wire))); + fresh.add(bit, rep); + } + + sigmap.swap(fresh); + } }; @@ -428,11 +459,15 @@ const SigMap *RTLIL::Module::signorm_sigmap() if (sig_norm_index == nullptr) return nullptr; - // Only the merge half of `restore_connections`: pending connections have - // to reach the sigmap before it can be handed out, but materializing it - // back into `connections_` is exactly the work the caller is avoiding. + // Like `restore_connections` minus the materializing back into + // `connections_`, which is the work the caller is avoiding. Unlike it, + // this normalizes fully rather than only flushing: merging a connection + // demotes bits, and until the consumers of a demoted bit are re-pointed at + // its new representative the fanout index still answers under the old one. + // A caller reading the index -- rather than just the map -- has to see it + // settled. int64_t start = PerformanceTimer::query(); - sig_norm_index->flush_connections(); + sig_norm_index->normalize(); int64_t time_ns = PerformanceTimer::query() - start; Pass::subtract_from_current_runtime_ns(time_ns); signorm_restore_ns += time_ns; @@ -441,6 +476,13 @@ const SigMap *RTLIL::Module::signorm_sigmap() return &sig_norm_index->sigmap; } +void RTLIL::Module::signorm_compact(const pool &dead_wires) +{ + if (sig_norm_index == nullptr) + return; + sig_norm_index->compact(dead_wires); +} + void RTLIL::Module::new_connections(const std::vector &new_conn) { if (sig_norm_index != nullptr) { diff --git a/passes/opt/opt_clean/CMakeLists.txt b/passes/opt/opt_clean/CMakeLists.txt index 70b137c93..f2aac3b6d 100644 --- a/passes/opt/opt_clean/CMakeLists.txt +++ b/passes/opt/opt_clean/CMakeLists.txt @@ -2,6 +2,7 @@ yosys_pass(opt_clean cells_all.cc cells_temp.cc wires.cc + signorm.cc inits.cc opt_clean.cc opt_clean.h diff --git a/passes/opt/opt_clean/opt_clean.cc b/passes/opt/opt_clean/opt_clean.cc index da07d396c..6a05db558 100644 --- a/passes/opt/opt_clean/opt_clean.cc +++ b/passes/opt/opt_clean/opt_clean.cc @@ -34,6 +34,15 @@ void rmunused_module(RTLIL::Module *module, bool rminit, CleanRunContext &clean_ // we only start multithreading with at least 2000 cells. int num_worker_threads = ThreadPool::work_pool_size(0, module->cells_size(), 10000); ParallelDispatchThreadPool::Subpool subpool(clean_ctx.thread_pool, num_worker_threads); + + // Dispatch on the module actually carrying an index rather than on the + // design-wide flag: a module created after signorm was entered (submod, + // for one) has no index of its own to clean against. + if (module->signorm_sigmap() != nullptr) { + rmunused_module_signorm(module, subpool, clean_ctx); + return; + } + remove_temporary_cells(module, subpool, clean_ctx.flags.verbose); rmunused_module_cells(module, subpool, clean_ctx); while (rmunused_module_signals(module, subpool, clean_ctx)) { } @@ -84,8 +93,6 @@ struct OptCleanPass : public Pass { } extra_args(args, argidx, design); - design->sigNormalize(false); - { std::vector selected_modules; for (auto module : design->selected_whole_modules_warn()) @@ -143,8 +150,6 @@ struct CleanPass : public Pass { } extra_args(args, argidx, design); - design->sigNormalize(false); - { std::vector selected_modules; for (auto module : design->selected_unboxed_whole_modules()) diff --git a/passes/opt/opt_clean/opt_clean.h b/passes/opt/opt_clean/opt_clean.h index c48a8188a..2bf5ce8c3 100644 --- a/passes/opt/opt_clean/opt_clean.h +++ b/passes/opt/opt_clean/opt_clean.h @@ -82,6 +82,14 @@ public: } }; +// Is this a name worth preserving for its own sake, rather than a generated +// one? Used to decide which unreferenced wires survive outside -purge mode. +bool check_public_name(const std::string &id_str); + +// opt_clean for a module in signorm mode: a tracing GC driven by the index +// instead of by maps rebuilt per call. See signorm.cc. +void rmunused_module_signorm(RTLIL::Module *module, ParallelDispatchThreadPool::Subpool &subpool, CleanRunContext &clean_ctx); + void remove_temporary_cells(RTLIL::Module *module, ParallelDispatchThreadPool::Subpool &subpool, bool verbose); void rmunused_module_cells(Module *module, ParallelDispatchThreadPool::Subpool &subpool, CleanRunContext &clean_ctx); bool rmunused_module_signals(RTLIL::Module *module, ParallelDispatchThreadPool::Subpool &subpool, CleanRunContext &clean_ctx); diff --git a/passes/opt/opt_clean/signorm.cc b/passes/opt/opt_clean/signorm.cc new file mode 100644 index 000000000..2af8c8a3c --- /dev/null +++ b/passes/opt/opt_clean/signorm.cc @@ -0,0 +1,464 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2012 Claire Xenia Wolf + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +// opt_clean for designs that are in signorm mode. +// +// The denormalized pass has to build its own view of the netlist first: a +// SigMap over the module connections, a hashtable mapping every output bit to +// its driver cells, and sharded pools recording which bits are connected or +// used. In signorm mode the module already carries all of that, maintained +// incrementally by Cell::setPort/unsetPort: +// +// * every wire has at most one driver, `wire->driverCell_`, and that port +// holds exactly SigSpec(wire) -- so a driver lookup is a dereference; +// * every cell port sigspec is sigmap-canonical, so no assign_map is needed +// to compare or traverse; +// * `fanout[bit]` is the exact set of input port bits equal to it, so "is +// this wire read by anything" is a lookup rather than a scan; +// * multi-driver nets are $connect cells and module inputs are $input_port +// cells, both of which are structure here rather than temporaries. +// +// So this is a plain tracing GC: mark from the roots through driver edges, +// sweep the cells and wires that were not reached. What it deliberately does +// *not* do is the denormalized pass's re-canonicalization: there, +// compare_signals picks the nicest bit of each net as the representative and +// every cell port is rewritten to it. Under the index the representative is +// forced to be the driver's wire, and re-pointing readers at some other wire +// would leave the driving port non-canonical. Alias wires are simply left +// alone, which is fine to traverse -- that is what the sigmap is for. + +#include "kernel/ffinit.h" +#include "kernel/yosys_common.h" +#include "passes/opt/opt_clean/opt_clean.h" + +USING_YOSYS_NAMESPACE +PRIVATE_NAMESPACE_BEGIN + +bool is_signed_pos(RTLIL::Cell *cell) { + return cell->type == TW($pos) && cell->getParam(ID::A_SIGNED).as_bool(); +} + +// $buf/$pos/$_BUF_ are identity cells with no reason to exist. Dropping one +// merges its output net into its input net, which the index handles as an +// ordinary connection. The cell has to go first: while it still drives Y both +// sides are driven, and connecting two driven nets produces a $connect cell +// instead of a merge. +bool remove_buffer_cells(RTLIL::Module *module, bool verbose) +{ + std::vector buffers; + for (int i = 0; i < module->cells_size(); i++) { + RTLIL::Cell *cell = module->cell_at(i); + if (cell->type.in(TW($pos), TW($_BUF_), TW($buf)) && !cell->has_keep_attr()) + buffers.push_back(cell); + } + + bool did_something = false; + for (RTLIL::Cell *cell : buffers) { + RTLIL::SigSpec a = cell->getPort(TW::A); + RTLIL::SigSpec y = cell->getPort(TW::Y); + a.extend_u0(GetSize(y), is_signed_pos(cell)); + + if (a.has_const(State::Sz)) { + RTLIL::SigSpec new_a, new_y; + bool bail = false; + for (int i = 0; i < GetSize(a); ++i) { + if (a[i] == State::Sz) { + bail = true; + break; + } + new_a.append(a[i]); + new_y.append(y[i]); + } + if (bail) + continue; + a = std::move(new_a); + y = std::move(new_y); + } + + if (verbose) + log_debug(" removing buffer cell `%s': %s = %s\n", cell->name, + log_signal(y), log_signal(a)); + + module->remove(cell); + if (!y.empty()) + module->connect_incremental(y, a); + did_something = true; + } + return did_something; +} + +struct LiveSet { + pool cells; + std::vector worklist; + + void mark(RTLIL::Cell *cell) { + if (cell != nullptr && cells.insert(cell).second) + worklist.push_back(cell); + } +}; + +// $connect cells record that two already-driven nets are the same net. They +// are the one place where liveness flows backwards through a cell input, so +// they need an index from bit to cell. There are normally none at all, so this +// stays empty and the traversal skips the lookup entirely. +dict> index_connect_cells(RTLIL::Module *module) +{ + dict> result; + for (int i = 0; i < module->cells_size(); i++) { + RTLIL::Cell *cell = module->cell_at(i); + if (cell->type != TW($connect)) + continue; + for (auto &[port, sig] : cell->connections()) + for (auto bit : sig) + if (bit.is_wire()) + result[bit].push_back(cell); + } + return result; +} + +void trace_live(RTLIL::Module *module, LiveSet &live, pool &live_mems, + const SigMap &sigmap, CleanRunContext &clean_ctx) +{ + dict> connect_cells = index_connect_cells(module); + dict> mem_writers; + + for (int i = 0; i < module->cells_size(); i++) { + RTLIL::Cell *cell = module->cell_at(i); + if (cell->type.in(TW($memwr), TW($memwr_v2), TW($meminit), TW($meminit_v2))) + mem_writers[cell->getParam(ID::MEMID).decode_string()].push_back(cell); + // An $input_port cell is what makes a module input a driven net, so + // it lives exactly as long as its wire does. + if (cell->type == TW($input_port) || clean_ctx.keep_cache.query(cell)) + live.mark(cell); + } + + for (int i = 0; i < module->wires_size(); i++) { + RTLIL::Wire *wire = module->wire_at(i); + if (!wire->port_output && !wire->get_bool_attribute(ID::keep)) + continue; + for (auto bit : sigmap(RTLIL::SigSpec(wire))) + if (bit.is_wire()) + live.mark(bit.wire->driverCell_); + } + + while (!live.worklist.empty()) { + RTLIL::Cell *cell = live.worklist.back(); + live.worklist.pop_back(); + + for (auto &[port, sig] : cell->connections_) { + if (clean_ctx.ct_all.cell_known(cell->type_impl) && + !clean_ctx.ct_all.cell_input(cell->type_impl, port)) + continue; + for (auto bit : sig) { + if (!bit.is_wire()) + continue; + // Ports hold canonical bits, so the driver is one hop away. + live.mark(bit.wire->driverCell_); + if (!connect_cells.empty()) { + auto found = connect_cells.find(bit); + if (found != connect_cells.end()) + for (RTLIL::Cell *connect : found->second) + live.mark(connect); + } + } + } + + if (cell->type.in(TW($memrd), TW($memrd_v2))) { + std::string memid = cell->getParam(ID::MEMID).decode_string(); + if (live_mems.insert(memid).second) { + auto found = mem_writers.find(memid); + if (found != mem_writers.end()) + for (RTLIL::Cell *writer : found->second) + live.mark(writer); + } + } + } +} + +bool sweep_cells(RTLIL::Module *module, const LiveSet &live, FfInitVals &ffinit, + CleanRunContext &clean_ctx) +{ + pool dead; + for (int i = 0; i < module->cells_size(); i++) { + RTLIL::Cell *cell = module->cell_at(i); + if (!live.cells.count(cell)) + dead.insert(cell); + } + if (dead.empty()) + return false; + + dead.sort(RTLIL::sort_by_name()); + for (RTLIL::Cell *cell : dead) { + if (clean_ctx.flags.verbose) + log_debug(" removing unused `%s' cell `%s'.\n", cell->type, cell->name); + if (cell->is_builtin_ff()) + ffinit.remove_init(cell->getPort(TW::Q)); + module->remove(cell); + clean_ctx.stats.count_rm_cells++; + } + module->design->scratchpad_set_bool("opt.did_something", true); + return true; +} + +void sweep_mems(RTLIL::Module *module, const pool &live_mems, bool verbose) +{ + std::vector dead; + for (auto &it : module->memories) + if (!live_mems.count(module->design->twines.str(it.first))) + dead.push_back(it.first); + + for (TwineRef id : dead) { + if (verbose) + log_debug(" removing unused memory `%s'.\n", module->design->twines.str(id).c_str()); + delete module->memories.at(id); + module->memories.erase(id); + } +} + +// A wire is referenced iff a cell port names it: as an output (it has a +// driver) or as an input (it has fanout entries). Aliases are named by +// nothing -- readers hold the canonical bits -- which is what makes them +// collectable. +pool referenced_wires(RTLIL::Module *module) +{ + pool referenced; + for (auto &[bit, portbits] : module->signorm_fanout()) + if (bit.is_wire() && !portbits.empty()) + referenced.insert(bit.wire); + for (int i = 0; i < module->wires_size(); i++) { + RTLIL::Wire *wire = module->wire_at(i); + if (wire->driverCell_ != nullptr) + referenced.insert(wire); + } + return referenced; +} + +// A bit is used if some cell input port reads it or it leaves the module. The +// fanout index answers the first directly; only the module's own output ports +// have to be collected. +pool used_bits(RTLIL::Module *module, const SigMap &sigmap) +{ + pool used; + for (auto &[bit, portbits] : module->signorm_fanout()) + if (!portbits.empty()) + used.insert(bit); + for (int i = 0; i < module->wires_size(); i++) { + RTLIL::Wire *wire = module->wire_at(i); + if (wire->port_id > 0 && !wire->port_input) + for (auto bit : sigmap(RTLIL::SigSpec(wire))) + used.insert(bit); + } + return used; +} + +void update_unused_bits(RTLIL::Module *module, const SigMap &sigmap) +{ + pool used = used_bits(module, sigmap); + + for (int i = 0; i < module->wires_size(); i++) { + RTLIL::Wire *wire = module->wire_at(i); + + std::string unused; + if (wire->port_id == 0) + for (int j = 0; j < GetSize(wire); j++) { + RTLIL::SigBit bit = sigmap(RTLIL::SigBit(wire, j)); + if (!bit.is_wire() || used.count(bit)) + continue; + if (!unused.empty()) + unused += " "; + unused += stringf("%d", j); + } + + if (unused.empty()) { + wire->attributes.erase(ID::unused_bits); + } else { + RTLIL::Const value(std::move(unused)); + auto it = wire->attributes.find(ID::unused_bits); + if (it == wire->attributes.end() || it->second != value) + wire->attributes[ID::unused_bits] = std::move(value); + } + } +} + +// An init value on an alias only means anything through the net it aliases, +// and that alias may be about to be collected, so move init onto the +// representative wires first. This also narrows an init to the bits that +// actually survived: a 2-bit register with one bit left keeps a 1-bit init +// rather than the original 2-bit one. +void normalize_inits(RTLIL::Module *module, const SigMap &sigmap) +{ + dict values; + pool representatives; + + for (int i = 0; i < module->wires_size(); i++) { + RTLIL::Wire *wire = module->wire_at(i); + auto it = wire->attributes.find(ID::init); + if (it == wire->attributes.end()) + continue; + + RTLIL::Const val = it->second; + RTLIL::SigSpec sig = sigmap(RTLIL::SigSpec(wire)); + for (int j = 0; j < GetSize(val) && j < GetSize(sig); j++) + if (val[j] != State::Sx && sig[j].is_wire()) { + values[sig[j]] = val[j]; + representatives.insert(sig[j].wire); + } + wire->attributes.erase(it); + } + + for (RTLIL::Wire *wire : representatives) { + bool found = false; + RTLIL::Const val(State::Sx, wire->width); + for (int j = 0; j < wire->width; j++) { + auto it = values.find(RTLIL::SigBit(wire, j)); + if (it != values.end()) { + val.set(j, it->second); + found = true; + } + } + if (found) + wire->attributes[ID::init] = val; + } +} + +bool wire_is_pinned(RTLIL::Wire *wire) +{ + if (wire->port_id != 0) + return true; + if (wire->get_bool_attribute(ID::keep)) + return true; + auto init = wire->attributes.find(ID::init); + if (init != wire->attributes.end() && !init->second.is_fully_undef()) + return true; + return false; +} + +int sweep_wires(RTLIL::Module *module, CleanRunContext &clean_ctx) +{ + const SigMap *sigmap = module->signorm_sigmap(); + log_assert(sigmap != nullptr); + pool referenced = referenced_wires(module); + + auto is_live = [&](RTLIL::Wire *wire) { + return wire_is_pinned(wire) || referenced.count(wire) != 0; + }; + + pool dead; + + for (int i = 0; i < module->wires_size(); i++) { + RTLIL::Wire *wire = module->wire_at(i); + if (is_live(wire)) + continue; + + if (GetSize(wire) != 0 && !clean_ctx.flags.purge && + check_public_name(wire->name.escaped())) { + // A public name aliasing a live net is the only record of that + // name, so keep it and leave the alias in the sigmap. Aliasing a + // net that is itself dead saves nothing -- the whole net goes, + // which is also what the denormalized pass does once it has + // collected the driver. + bool aliases_live_net = false; + for (int j = 0; j < GetSize(wire); j++) { + RTLIL::SigBit bit(wire, j), rep = (*sigmap)(bit); + if (rep == bit) + continue; + // A constant representative still gives the name a value. + if (!rep.is_wire() || is_live(rep.wire)) { + aliases_live_net = true; + break; + } + } + if (aliases_live_net) + continue; + } + + dead.insert(wire); + } + + // Whatever survives keeps its representative reachable through the sigmap, + // so the representative has to survive too -- otherwise the map is left + // pointing at freed memory. This catches the wires kept for reasons other + // than aliasing something live: a port, `keep`, or an init value sitting + // on an alias of a net whose driver was just collected. Representatives + // are canonical, so rescuing one cannot strand a further wire. + for (auto const &bit : sigmap->database) { + if (!bit.is_wire() || dead.count(bit.wire)) + continue; + RTLIL::SigBit rep = (*sigmap)(bit); + if (rep.is_wire()) + dead.erase(rep.wire); + } + + if (dead.empty()) + return 0; + + int unreported = 0; + for (RTLIL::Wire *wire : dead) { + if (ys_debug() || (check_public_name(wire->name.escaped()) && clean_ctx.flags.verbose)) + log_debug(" removing unused non-port wire %s.\n", wire->name); + else + unreported++; + } + + module->signorm_compact(dead); + module->remove(dead); + + clean_ctx.stats.count_rm_wires += GetSize(dead); + if (clean_ctx.flags.verbose && unreported) + log_debug(" removed %d unused temporary wires.\n", unreported); + return GetSize(dead); +} + +PRIVATE_NAMESPACE_END + +YOSYS_NAMESPACE_BEGIN + +void rmunused_module_signorm(RTLIL::Module *module, ParallelDispatchThreadPool::Subpool &subpool, + CleanRunContext &clean_ctx) +{ + if (remove_buffer_cells(module, clean_ctx.flags.verbose)) + module->design->scratchpad_set_bool("opt.did_something", true); + + SigMap sigmap(module); + FfInitVals ffinit; + ffinit.set_parallel(&sigmap, subpool.thread_pool(), module); + + LiveSet live; + pool live_mems; + trace_live(module, live, live_mems, sigmap, clean_ctx); + + sweep_cells(module, live, ffinit, clean_ctx); + sweep_mems(module, live_mems, clean_ctx.flags.verbose); + + normalize_inits(module, sigmap); + sweep_wires(module, clean_ctx); + + // Reads and erases wire attributes and works off its own copy of the + // sigmap, so it needs no adaptation to run under the index. Dropping a + // redundant init unpins its wire, so sweep once more when it fires. + if (rmunused_module_init(module, subpool, clean_ctx.flags.verbose)) + sweep_wires(module, clean_ctx); + + // Sweeping is what makes bits unused, so the annotation comes last, over + // a sigmap refreshed past the compaction in sweep_wires. + SigMap swept(module); + update_unused_bits(module, swept); +} + +YOSYS_NAMESPACE_END diff --git a/passes/opt/opt_clean/wires.cc b/passes/opt/opt_clean/wires.cc index 0ece62110..adeaa70bf 100644 --- a/passes/opt/opt_clean/wires.cc +++ b/passes/opt/opt_clean/wires.cc @@ -144,17 +144,6 @@ bool compare_signals(const RTLIL::SigBit &s1, const RTLIL::SigBit &s2, const Sha return w2->name.lt_by_name(w1->name); } -bool check_public_name(const std::string &id_str) -{ - if (!id_str.empty() && id_str[0] == '$') - return false; - if (id_str.rfind("\\_", 0) == 0 && (id_str.back() == '_' || id_str.find("_[") != std::string::npos)) - return false; - if (id_str.find(".$") != std::string::npos) - return false; - return true; -} - void add_spec(ShardedSigPool::Builder &builder, const ThreadIndex &thread, const RTLIL::SigSpec &spec) { for (SigBit bit : spec) if (bit.wire != nullptr) @@ -533,6 +522,17 @@ PRIVATE_NAMESPACE_END YOSYS_NAMESPACE_BEGIN +bool check_public_name(const std::string &id_str) +{ + if (!id_str.empty() && id_str[0] == '$') + return false; + if (id_str.rfind("\\_", 0) == 0 && (id_str.back() == '_' || id_str.find("_[") != std::string::npos)) + return false; + if (id_str.find(".$") != std::string::npos) + return false; + return true; +} + bool rmunused_module_signals(RTLIL::Module *module, ParallelDispatchThreadPool::Subpool &subpool, CleanRunContext &clean_ctx) { // Passing actx to function == function does parallel work diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 52813ea8e..d87954d67 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -1456,7 +1456,14 @@ struct OptDffPass : public Pass { break; } extra_args(args, argidx, design); - // TODO extra wires signorm adds breaks muxtree traversal or requires sigmapping + // Keeping the index alive across opt_dff is where the remaining win + // is -- it is what forces the rebuild once per `opt` loop iteration, + // worth 4.68s vs 3.39s on a 2000-stage pipeline (Debug). But this + // pass reasons over direct connectivity: bit2mux/bitusers decide + // whether a mux is a net's only user, and the intermediate wires + // signorm introduces break that, so it stops inferring $sdffe (see + // tests/sim/sim_sdffe.ys) and 24 further tests regress. Making that + // analysis sigmap-aware is the prerequisite for dropping this. design->sigNormalize(false); // The SAT engine reasons in 2-valued logic (a constant x is treated as