|
| 1 | +/* |
| 2 | + * yosys -- Yosys Open SYnthesis Suite |
| 3 | + * |
| 4 | + * Copyright (C) 2026 Akash Levy <akash@silimate.com> |
| 5 | + * |
| 6 | + * Permission to use, copy, modify, and/or distribute this software for any |
| 7 | + * purpose with or without fee is hereby granted, provided that the above |
| 8 | + * copyright notice and this permission notice appear in all copies. |
| 9 | + * |
| 10 | + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| 11 | + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| 12 | + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| 13 | + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| 14 | + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| 15 | + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| 16 | + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| 17 | + * |
| 18 | + */ |
| 19 | + |
| 20 | +#include "kernel/yosys.h" |
| 21 | +#include "kernel/sigtools.h" |
| 22 | +#include "kernel/celltypes.h" |
| 23 | +#include <cmath> |
| 24 | + |
| 25 | +USING_YOSYS_NAMESPACE |
| 26 | +PRIVATE_NAMESPACE_BEGIN |
| 27 | + |
| 28 | +// Marks the incrementer/mux a carry-select rewrite emits so peepopt -muxorder |
| 29 | +// (which would otherwise fold `s ? (a+1) : a` back into `a + s`, putting the |
| 30 | +// late select on a wide ripple carry) leaves them alone. |
| 31 | +static const IdString kAttrCarrySelect = ID(carry_select); |
| 32 | + |
| 33 | +static inline double log2p1(int n) { return std::log2(double(std::max(1, n)) + 1.0); } |
| 34 | + |
| 35 | +// Heuristic, model-free per-cell delay used only to rank "early" vs "late" |
| 36 | +// operands. Mirrors opt_timing_balance's heuristic so the two passes agree on |
| 37 | +// which arrivals dominate. |
| 38 | +static double estimate_cell_delay(const Cell *cell, int out_width) |
| 39 | +{ |
| 40 | + if (cell == nullptr) |
| 41 | + return 1.0; |
| 42 | + IdString t = cell->type; |
| 43 | + if (t.in(ID($add), ID($sub), ID($neg), ID($alu), ID($shl), ID($shr), ID($sshl), ID($sshr))) |
| 44 | + return log2p1(out_width); |
| 45 | + if (t.in(ID($mul), ID($div), ID($mod))) |
| 46 | + return out_width; |
| 47 | + if (t == ID($pmux)) { |
| 48 | + int s_width = cell->hasParam(ID::S_WIDTH) ? cell->getParam(ID::S_WIDTH).as_int() : 1; |
| 49 | + return log2p1(s_width); |
| 50 | + } |
| 51 | + if (t.in(ID($and), ID($or))) |
| 52 | + return 0.5; |
| 53 | + // $mux, $xor, $xnor, $not, gate-level $_*_ and everything else: one level. |
| 54 | + return 1.0; |
| 55 | +} |
| 56 | + |
| 57 | +struct OptCarrySelectWorker { |
| 58 | + Module *module; |
| 59 | + SigMap sigmap; |
| 60 | + CellTypes cell_types; |
| 61 | + |
| 62 | + dict<SigBit, Cell*> driver; // sigmap'd output bit -> driving cell |
| 63 | + dict<SigBit, double> arrival_cache; // memoized arrival per bit |
| 64 | + pool<SigBit> on_stack; // combinational-loop guard |
| 65 | + |
| 66 | + int max_narrow; |
| 67 | + int min_wide; |
| 68 | + double margin; |
| 69 | + |
| 70 | + int converted = 0; |
| 71 | + |
| 72 | + OptCarrySelectWorker(Module *m, int max_narrow, int min_wide, double margin) |
| 73 | + : module(m), sigmap(m), max_narrow(max_narrow), min_wide(min_wide), margin(margin) |
| 74 | + { |
| 75 | + cell_types.setup(); |
| 76 | + for (auto cell : module->cells()) |
| 77 | + for (auto &conn : cell->connections()) |
| 78 | + if (cell->output(conn.first)) |
| 79 | + for (auto bit : sigmap(conn.second)) |
| 80 | + if (bit.wire) |
| 81 | + driver[bit] = cell; |
| 82 | + } |
| 83 | + |
| 84 | + bool is_boundary(Cell *cell) { |
| 85 | + if (cell == nullptr) |
| 86 | + return true; |
| 87 | + if (cell->get_bool_attribute(ID::keep) || cell->get_bool_attribute(ID::blackbox)) |
| 88 | + return true; |
| 89 | + if (cell->is_builtin_ff()) |
| 90 | + return true; |
| 91 | + if (cell->type.in(ID($dlatch), ID($adlatch), ID($dlatchsr), |
| 92 | + ID($mem), ID($mem_v2), ID($memrd), ID($memrd_v2), |
| 93 | + ID($memwr), ID($memwr_v2), ID($meminit), ID($meminit_v2))) |
| 94 | + return true; |
| 95 | + return !cell_types.cell_known(cell->type); |
| 96 | + } |
| 97 | + |
| 98 | + Cell *driver_of(SigBit bit) { |
| 99 | + auto it = driver.find(bit); |
| 100 | + return (it == driver.end()) ? nullptr : it->second; |
| 101 | + } |
| 102 | + |
| 103 | + // Explicit-stack post-order traversal so arbitrarily deep combinational cones |
| 104 | + // cannot overflow the C++ stack (worker threads can have small stacks). |
| 105 | + double arrival_bit(SigBit start) { |
| 106 | + start = sigmap(start); |
| 107 | + if (!start.wire) |
| 108 | + return 0.0; |
| 109 | + if (auto it = arrival_cache.find(start); it != arrival_cache.end()) |
| 110 | + return it->second; |
| 111 | + |
| 112 | + struct Frame { SigBit bit; bool finalize; }; |
| 113 | + std::vector<Frame> stack; |
| 114 | + stack.push_back({start, false}); |
| 115 | + |
| 116 | + while (!stack.empty()) { |
| 117 | + Frame &top = stack.back(); |
| 118 | + SigBit bit = top.bit; |
| 119 | + if (!bit.wire || arrival_cache.count(bit)) { |
| 120 | + stack.pop_back(); |
| 121 | + continue; |
| 122 | + } |
| 123 | + Cell *drv = driver_of(bit); |
| 124 | + if (drv == nullptr || is_boundary(drv)) { |
| 125 | + arrival_cache[bit] = 0.0; |
| 126 | + stack.pop_back(); |
| 127 | + continue; |
| 128 | + } |
| 129 | + |
| 130 | + if (!top.finalize) { |
| 131 | + // Expand: schedule finalize, then push not-yet-resolved inputs. |
| 132 | + top.finalize = true; |
| 133 | + on_stack.insert(bit); |
| 134 | + for (auto &conn : drv->connections()) |
| 135 | + if (drv->input(conn.first)) |
| 136 | + for (auto in_bit : sigmap(conn.second)) { |
| 137 | + if (!in_bit.wire || arrival_cache.count(in_bit)) |
| 138 | + continue; |
| 139 | + if (on_stack.count(in_bit)) // combinational loop: break it |
| 140 | + continue; |
| 141 | + stack.push_back({in_bit, false}); |
| 142 | + } |
| 143 | + continue; |
| 144 | + } |
| 145 | + |
| 146 | + // Finalize: all resolvable inputs are now cached. |
| 147 | + double max_in = 0.0; |
| 148 | + int out_width = 1; |
| 149 | + for (auto &conn : drv->connections()) |
| 150 | + if (drv->output(conn.first)) |
| 151 | + out_width = std::max(out_width, GetSize(conn.second)); |
| 152 | + for (auto &conn : drv->connections()) |
| 153 | + if (drv->input(conn.first)) |
| 154 | + for (auto in_bit : sigmap(conn.second)) { |
| 155 | + auto it = arrival_cache.find(in_bit); |
| 156 | + if (it != arrival_cache.end()) |
| 157 | + max_in = std::max(max_in, it->second); |
| 158 | + } |
| 159 | + arrival_cache[bit] = max_in + estimate_cell_delay(drv, out_width); |
| 160 | + on_stack.erase(bit); |
| 161 | + stack.pop_back(); |
| 162 | + } |
| 163 | + return arrival_cache.at(start); |
| 164 | + } |
| 165 | + |
| 166 | + double arrival(const SigSpec &sig) { |
| 167 | + double t = 0.0; |
| 168 | + for (auto bit : sigmap(sig)) |
| 169 | + t = std::max(t, arrival_bit(bit)); |
| 170 | + return t; |
| 171 | + } |
| 172 | + |
| 173 | + // Decision record so we never iterate over a mutating cell list. |
| 174 | + struct Plan { |
| 175 | + Cell *add; |
| 176 | + bool wide_is_a; |
| 177 | + int k; // narrow width / split point |
| 178 | + int w; // result width |
| 179 | + }; |
| 180 | + |
| 181 | + bool qualifies(Cell *c, Plan &plan) { |
| 182 | + if (c->type != ID($add)) |
| 183 | + return false; |
| 184 | + if (c->get_bool_attribute(kAttrCarrySelect)) |
| 185 | + return false; |
| 186 | + if (c->getParam(ID::A_SIGNED).as_bool() || c->getParam(ID::B_SIGNED).as_bool()) |
| 187 | + return false; // v1: unsigned operands only (exact zero-extension) |
| 188 | + |
| 189 | + int a_w = c->getParam(ID::A_WIDTH).as_int(); |
| 190 | + int b_w = c->getParam(ID::B_WIDTH).as_int(); |
| 191 | + int w = c->getParam(ID::Y_WIDTH).as_int(); |
| 192 | + |
| 193 | + bool wide_is_a = a_w >= b_w; |
| 194 | + int narrow_w = std::min(a_w, b_w); |
| 195 | + int k = narrow_w; |
| 196 | + |
| 197 | + if (k < 1 || k >= w) |
| 198 | + return false; |
| 199 | + if (k > max_narrow) |
| 200 | + return false; |
| 201 | + if (w - k < min_wide) |
| 202 | + return false; |
| 203 | + |
| 204 | + SigSpec wide_sig = c->getPort(wide_is_a ? ID::A : ID::B); |
| 205 | + SigSpec narrow_sig = c->getPort(wide_is_a ? ID::B : ID::A); |
| 206 | + |
| 207 | + double arr_wide = arrival(wide_sig); |
| 208 | + double arr_narrow = arrival(narrow_sig); |
| 209 | + if (arr_narrow <= arr_wide + margin) |
| 210 | + return false; // no timing benefit: narrow operand is not the late one |
| 211 | + |
| 212 | + plan.add = c; |
| 213 | + plan.wide_is_a = wide_is_a; |
| 214 | + plan.k = k; |
| 215 | + plan.w = w; |
| 216 | + return true; |
| 217 | + } |
| 218 | + |
| 219 | + void apply(const Plan &plan) { |
| 220 | + Cell *cell = plan.add; |
| 221 | + int k = plan.k; |
| 222 | + int w = plan.w; |
| 223 | + std::string src = cell->get_src_attribute(); |
| 224 | + |
| 225 | + SigSpec wide_sig = cell->getPort(plan.wide_is_a ? ID::A : ID::B); |
| 226 | + SigSpec narrow_sig = cell->getPort(plan.wide_is_a ? ID::B : ID::A); |
| 227 | + SigSpec y = cell->getPort(ID::Y); |
| 228 | + |
| 229 | + // Zero-extend the wide operand to the full result width. |
| 230 | + SigSpec wext = wide_sig; |
| 231 | + wext.extend_u0(w, /*is_signed=*/false); |
| 232 | + SigSpec wlow = wext.extract(0, k); |
| 233 | + SigSpec whigh = wext.extract(k, w - k); |
| 234 | + |
| 235 | + // Low add: produces the low k sum bits plus the carry into bit k. |
| 236 | + Wire *losum = module->addWire(NEW_ID2_SUFFIX("cs_lo"), k + 1); |
| 237 | + module->addAdd(NEW_ID2_SUFFIX("cs_lo_add"), wlow, narrow_sig, SigSpec(losum), /*is_signed=*/false, src); |
| 238 | + SigSpec losum_low = SigSpec(losum).extract(0, k); |
| 239 | + SigBit carry = SigSpec(losum)[k]; |
| 240 | + |
| 241 | + // High part precomputed from the (early) wide operand, then selected by |
| 242 | + // the (late) low carry: hi = carry ? (whigh + 1) : whigh. |
| 243 | + Wire *hiinc = module->addWire(NEW_ID2_SUFFIX("cs_hi_inc"), w - k); |
| 244 | + Cell *inc = module->addAdd(NEW_ID2_SUFFIX("cs_hi_inc_add"), whigh, SigSpec(State::S1), SigSpec(hiinc), /*is_signed=*/false, src); |
| 245 | + inc->set_bool_attribute(kAttrCarrySelect); |
| 246 | + |
| 247 | + Wire *hi = module->addWire(NEW_ID2_SUFFIX("cs_hi"), w - k); |
| 248 | + Cell *mux = module->addMux(NEW_ID2_SUFFIX("cs_hi_mux"), /*A=*/whigh, /*B=*/SigSpec(hiinc), /*S=*/carry, SigSpec(hi), src); |
| 249 | + mux->set_bool_attribute(kAttrCarrySelect); |
| 250 | + |
| 251 | + // Reassemble result and drive the original adder output. |
| 252 | + SigSpec result = losum_low; |
| 253 | + result.append(SigSpec(hi)); |
| 254 | + module->connect(y, result); |
| 255 | + module->remove(cell); |
| 256 | + converted++; |
| 257 | + } |
| 258 | + |
| 259 | + void run() { |
| 260 | + vector<Cell*> adds; |
| 261 | + for (auto cell : module->cells()) |
| 262 | + if (cell->type == ID($add)) |
| 263 | + adds.push_back(cell); |
| 264 | + |
| 265 | + vector<Plan> plans; |
| 266 | + for (auto c : adds) { |
| 267 | + Plan plan; |
| 268 | + if (qualifies(c, plan)) |
| 269 | + plans.push_back(plan); |
| 270 | + } |
| 271 | + for (auto &plan : plans) |
| 272 | + apply(plan); |
| 273 | + } |
| 274 | +}; |
| 275 | + |
| 276 | +struct OptCarrySelectPass : public Pass { |
| 277 | + OptCarrySelectPass() : Pass("opt_carry_select", |
| 278 | + "decompose wide-early + narrow-late adders into carry-select form") {} |
| 279 | + |
| 280 | + void help() override { |
| 281 | + // |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---| |
| 282 | + log("\n"); |
| 283 | + log(" opt_carry_select [options] [selection]\n"); |
| 284 | + log("\n"); |
| 285 | + log("Rewrites a $add of the shape `wide_early + narrow_late` into a low add plus\n"); |
| 286 | + log("a high carry-select. The low add absorbs the late, narrow operand and emits a\n"); |
| 287 | + log("carry; the wide high half is precomputed from the early operand (as `hi` and\n"); |
| 288 | + log("`hi + 1`) and selected by that carry through a single mux. This moves the wide\n"); |
| 289 | + log("carry propagation onto the early operand, so the late operand only sees a\n"); |
| 290 | + log("small add plus one mux instead of a full-width ripple carry.\n"); |
| 291 | + log("\n"); |
| 292 | + log("Only unsigned adders whose narrow operand arrives later (per a heuristic\n"); |
| 293 | + log("logic-depth model) than the wide operand are rewritten, so adders that would\n"); |
| 294 | + log("not benefit are left untouched. Emitted cells are tagged so peepopt -muxorder\n"); |
| 295 | + log("does not fold the carry-select back into a late-carry ripple adder.\n"); |
| 296 | + log("\n"); |
| 297 | + log(" -max-narrow N\n"); |
| 298 | + log(" only rewrite when the narrow operand width is <= N (default 16).\n"); |
| 299 | + log("\n"); |
| 300 | + log(" -min-wide N\n"); |
| 301 | + log(" only rewrite when the wide (high) part width is >= N (default 8).\n"); |
| 302 | + log("\n"); |
| 303 | + log(" -margin F\n"); |
| 304 | + log(" require narrow_arrival > wide_arrival + F (default 0.0).\n"); |
| 305 | + log("\n"); |
| 306 | + } |
| 307 | + |
| 308 | + void execute(std::vector<std::string> args, RTLIL::Design *design) override { |
| 309 | + log_header(design, "Executing OPT_CARRY_SELECT pass (late-operand carry-select).\n"); |
| 310 | + |
| 311 | + int max_narrow = 16; |
| 312 | + int min_wide = 8; |
| 313 | + double margin = 0.0; |
| 314 | + |
| 315 | + size_t argidx; |
| 316 | + for (argidx = 1; argidx < args.size(); argidx++) { |
| 317 | + if (args[argidx] == "-max-narrow" && argidx + 1 < args.size()) { |
| 318 | + max_narrow = atoi(args[++argidx].c_str()); |
| 319 | + continue; |
| 320 | + } |
| 321 | + if (args[argidx] == "-min-wide" && argidx + 1 < args.size()) { |
| 322 | + min_wide = atoi(args[++argidx].c_str()); |
| 323 | + continue; |
| 324 | + } |
| 325 | + if (args[argidx] == "-margin" && argidx + 1 < args.size()) { |
| 326 | + margin = atof(args[++argidx].c_str()); |
| 327 | + continue; |
| 328 | + } |
| 329 | + break; |
| 330 | + } |
| 331 | + extra_args(args, argidx, design); |
| 332 | + |
| 333 | + int total = 0; |
| 334 | + for (auto module : design->selected_modules()) { |
| 335 | + OptCarrySelectWorker worker(module, max_narrow, min_wide, margin); |
| 336 | + worker.run(); |
| 337 | + total += worker.converted; |
| 338 | + } |
| 339 | + log("Converted %d adder(s) to carry-select form.\n", total); |
| 340 | + } |
| 341 | +} OptCarrySelectPass; |
| 342 | + |
| 343 | +PRIVATE_NAMESPACE_END |
0 commit comments