3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2025-04-06 17:44:09 +00:00

cxxrtl: implement debug information outlining.

Aggressive wire localization and inlining is necessary for CXXRTL to
achieve high performance. However, that comes with a cost: reduced
debug information coverage. Previously, as a workaround, the `-Og`
option could have been used to guarantee complete coverage, at a cost
of a significant performance penalty.

This commit introduces debug information outlining. The main eval()
function is compiled with the user-specified optimization settings.
In tandem, an auxiliary debug_eval() function, compiled from the same
netlist, can be used to reconstruct the values of localized/inlined
signals on demand. To the extent that it is possible, debug_eval()
reuses the results of computations performed by eval(), only filling
in the missing values.

Benchmarking a representative design (Minerva SoC SRAM) shows that:
  * Switching from `-O4`/`-Og` to `-O6` reduces runtime by ~40%.
  * Switching from `-g1` to `-g2`, both used with `-O6`, increases
    compile time by ~25%.
  * Although `-g2` increases the resident size of generated modules,
    this has no effect on runtime.

Because the impact of `-g2` is minimal and the benefits of having
unconditional 100% debug information coverage (and the performance
improvement as well) are major, this commit removes `-Og` and changes
the defaults to `-O6 -g2`.

We'll have our cake and eat it too!
This commit is contained in:
whitequark 2020-12-13 07:03:16 +00:00
parent 3b5a1314cd
commit ece25a45d4
5 changed files with 278 additions and 71 deletions

View file

@ -36,6 +36,7 @@
#include <map> #include <map>
#include <algorithm> #include <algorithm>
#include <memory> #include <memory>
#include <functional>
#include <sstream> #include <sstream>
#include <backends/cxxrtl/cxxrtl_capi.h> #include <backends/cxxrtl/cxxrtl_capi.h>
@ -843,6 +844,9 @@ typedef std::map<std::string, metadata> metadata_map;
// Tag class to disambiguate values/wires and their aliases. // Tag class to disambiguate values/wires and their aliases.
struct debug_alias {}; struct debug_alias {};
// Tag declaration to disambiguate values and debug outlines.
using debug_outline = ::_cxxrtl_outline;
// This structure is intended for consumption via foreign function interfaces, like Python's ctypes. // This structure is intended for consumption via foreign function interfaces, like Python's ctypes.
// Because of this it uses a C-style layout that is easy to parse rather than more idiomatic C++. // Because of this it uses a C-style layout that is easy to parse rather than more idiomatic C++.
// //
@ -851,10 +855,11 @@ struct debug_alias {};
struct debug_item : ::cxxrtl_object { struct debug_item : ::cxxrtl_object {
// Object types. // Object types.
enum : uint32_t { enum : uint32_t {
VALUE = CXXRTL_VALUE, VALUE = CXXRTL_VALUE,
WIRE = CXXRTL_WIRE, WIRE = CXXRTL_WIRE,
MEMORY = CXXRTL_MEMORY, MEMORY = CXXRTL_MEMORY,
ALIAS = CXXRTL_ALIAS, ALIAS = CXXRTL_ALIAS,
OUTLINE = CXXRTL_OUTLINE,
}; };
// Object flags. // Object flags.
@ -881,6 +886,7 @@ struct debug_item : ::cxxrtl_object {
zero_at = 0; zero_at = 0;
curr = item.data; curr = item.data;
next = item.data; next = item.data;
outline = nullptr;
} }
template<size_t Bits> template<size_t Bits>
@ -895,6 +901,7 @@ struct debug_item : ::cxxrtl_object {
zero_at = 0; zero_at = 0;
curr = const_cast<chunk_t*>(item.data); curr = const_cast<chunk_t*>(item.data);
next = nullptr; next = nullptr;
outline = nullptr;
} }
template<size_t Bits> template<size_t Bits>
@ -910,6 +917,7 @@ struct debug_item : ::cxxrtl_object {
zero_at = 0; zero_at = 0;
curr = item.curr.data; curr = item.curr.data;
next = item.next.data; next = item.next.data;
outline = nullptr;
} }
template<size_t Width> template<size_t Width>
@ -924,6 +932,7 @@ struct debug_item : ::cxxrtl_object {
zero_at = zero_offset; zero_at = zero_offset;
curr = item.data.empty() ? nullptr : item.data[0].data; curr = item.data.empty() ? nullptr : item.data[0].data;
next = nullptr; next = nullptr;
outline = nullptr;
} }
template<size_t Bits> template<size_t Bits>
@ -938,6 +947,7 @@ struct debug_item : ::cxxrtl_object {
zero_at = 0; zero_at = 0;
curr = const_cast<chunk_t*>(item.data); curr = const_cast<chunk_t*>(item.data);
next = nullptr; next = nullptr;
outline = nullptr;
} }
template<size_t Bits> template<size_t Bits>
@ -953,6 +963,22 @@ struct debug_item : ::cxxrtl_object {
zero_at = 0; zero_at = 0;
curr = const_cast<chunk_t*>(item.curr.data); curr = const_cast<chunk_t*>(item.curr.data);
next = nullptr; next = nullptr;
outline = nullptr;
}
template<size_t Bits>
debug_item(debug_outline &group, const value<Bits> &item, size_t lsb_offset = 0) {
static_assert(sizeof(item) == value<Bits>::chunks * sizeof(chunk_t),
"value<Bits> is not compatible with C layout");
type = OUTLINE;
flags = DRIVEN_COMB;
width = Bits;
lsb_at = lsb_offset;
depth = 1;
zero_at = 0;
curr = const_cast<chunk_t*>(item.data);
next = nullptr;
outline = &group;
} }
}; };
static_assert(std::is_standard_layout<debug_item>::value, "debug_item is not compatible with C layout"); static_assert(std::is_standard_layout<debug_item>::value, "debug_item is not compatible with C layout");
@ -1029,11 +1055,16 @@ struct module {
} // namespace cxxrtl } // namespace cxxrtl
// Internal structure used to communicate with the implementation of the C interface. // Internal structures used to communicate with the implementation of the C interface.
typedef struct _cxxrtl_toplevel { typedef struct _cxxrtl_toplevel {
std::unique_ptr<cxxrtl::module> module; std::unique_ptr<cxxrtl::module> module;
} *cxxrtl_toplevel; } *cxxrtl_toplevel;
typedef struct _cxxrtl_outline {
std::function<void()> eval;
} *cxxrtl_outline;
// Definitions of internal Yosys cells. Other than the functions in this namespace, CXXRTL is fully generic // Definitions of internal Yosys cells. Other than the functions in this namespace, CXXRTL is fully generic
// and indepenent of Yosys implementation details. // and indepenent of Yosys implementation details.
// //

View file

@ -539,6 +539,7 @@ struct CxxrtlWorker {
bool inline_public = false; bool inline_public = false;
bool debug_info = false; bool debug_info = false;
bool debug_eval = false;
std::ostringstream f; std::ostringstream f;
std::string indent; std::string indent;
@ -553,8 +554,9 @@ struct CxxrtlWorker {
pool<const RTLIL::Wire*> unbuffered_wires; pool<const RTLIL::Wire*> unbuffered_wires;
pool<const RTLIL::Wire*> localized_wires; pool<const RTLIL::Wire*> localized_wires;
dict<const RTLIL::Wire*, FlowGraph::Node> inlined_wires; dict<const RTLIL::Wire*, FlowGraph::Node> inlined_wires;
dict<const RTLIL::Wire*, const RTLIL::Wire*> debug_alias_wires;
dict<const RTLIL::Wire*, RTLIL::Const> debug_const_wires; dict<const RTLIL::Wire*, RTLIL::Const> debug_const_wires;
dict<const RTLIL::Wire*, const RTLIL::Wire*> debug_alias_wires;
pool<const RTLIL::Wire*> debug_outlined_wires;
dict<RTLIL::SigBit, bool> bit_has_state; dict<RTLIL::SigBit, bool> bit_has_state;
dict<const RTLIL::Module*, pool<std::string>> blackbox_specializations; dict<const RTLIL::Module*, pool<std::string>> blackbox_specializations;
dict<const RTLIL::Module*, bool> eval_converges; dict<const RTLIL::Module*, bool> eval_converges;
@ -786,22 +788,22 @@ struct CxxrtlWorker {
dump_const(data, data.size()); dump_const(data, data.size());
} }
bool dump_sigchunk(const RTLIL::SigChunk &chunk, bool is_lhs) bool dump_sigchunk(const RTLIL::SigChunk &chunk, bool is_lhs, bool for_debug = false)
{ {
if (chunk.wire == NULL) { if (chunk.wire == NULL) {
dump_const(chunk.data, chunk.width, chunk.offset); dump_const(chunk.data, chunk.width, chunk.offset);
return false; return false;
} else { } else {
if (inlined_wires.count(chunk.wire)) { if (inlined_wires.count(chunk.wire) && (!for_debug || !debug_outlined_wires[chunk.wire])) {
log_assert(!is_lhs); log_assert(!is_lhs);
const FlowGraph::Node &node = inlined_wires[chunk.wire]; const FlowGraph::Node &node = inlined_wires[chunk.wire];
switch (node.type) { switch (node.type) {
case FlowGraph::Node::Type::CONNECT: case FlowGraph::Node::Type::CONNECT:
dump_connect_expr(node.connect); dump_connect_expr(node.connect, for_debug);
break; break;
case FlowGraph::Node::Type::CELL_EVAL: case FlowGraph::Node::Type::CELL_EVAL:
log_assert(is_inlinable_cell(node.cell->type)); log_assert(is_inlinable_cell(node.cell->type));
dump_cell_expr(node.cell); dump_cell_expr(node.cell, for_debug);
break; break;
default: default:
log_assert(false); log_assert(false);
@ -821,36 +823,36 @@ struct CxxrtlWorker {
} }
} }
bool dump_sigspec(const RTLIL::SigSpec &sig, bool is_lhs) bool dump_sigspec(const RTLIL::SigSpec &sig, bool is_lhs, bool for_debug = false)
{ {
if (sig.empty()) { if (sig.empty()) {
f << "value<0>()"; f << "value<0>()";
return false; return false;
} else if (sig.is_chunk()) { } else if (sig.is_chunk()) {
return dump_sigchunk(sig.as_chunk(), is_lhs); return dump_sigchunk(sig.as_chunk(), is_lhs, for_debug);
} else { } else {
dump_sigchunk(*sig.chunks().rbegin(), is_lhs); dump_sigchunk(*sig.chunks().rbegin(), is_lhs, for_debug);
for (auto it = sig.chunks().rbegin() + 1; it != sig.chunks().rend(); ++it) { for (auto it = sig.chunks().rbegin() + 1; it != sig.chunks().rend(); ++it) {
f << ".concat("; f << ".concat(";
dump_sigchunk(*it, is_lhs); dump_sigchunk(*it, is_lhs, for_debug);
f << ")"; f << ")";
} }
return true; return true;
} }
} }
void dump_sigspec_lhs(const RTLIL::SigSpec &sig) void dump_sigspec_lhs(const RTLIL::SigSpec &sig, bool for_debug = false)
{ {
dump_sigspec(sig, /*is_lhs=*/true); dump_sigspec(sig, /*is_lhs=*/true, for_debug);
} }
void dump_sigspec_rhs(const RTLIL::SigSpec &sig) void dump_sigspec_rhs(const RTLIL::SigSpec &sig, bool for_debug = false)
{ {
// In the contexts where we want template argument deduction to occur for `template<size_t Bits> ... value<Bits>`, // In the contexts where we want template argument deduction to occur for `template<size_t Bits> ... value<Bits>`,
// it is necessary to have the argument to already be a `value<N>`, since template argument deduction and implicit // it is necessary to have the argument to already be a `value<N>`, since template argument deduction and implicit
// type conversion are mutually exclusive. In these contexts, we use dump_sigspec_rhs() to emit an explicit // type conversion are mutually exclusive. In these contexts, we use dump_sigspec_rhs() to emit an explicit
// type conversion, but only if the expression needs it. // type conversion, but only if the expression needs it.
bool is_complex = dump_sigspec(sig, /*is_lhs=*/false); bool is_complex = dump_sigspec(sig, /*is_lhs=*/false, for_debug);
if (is_complex) if (is_complex)
f << ".val()"; f << ".val()";
} }
@ -875,9 +877,9 @@ struct CxxrtlWorker {
} }
} }
void dump_connect_expr(const RTLIL::SigSig &conn) void dump_connect_expr(const RTLIL::SigSig &conn, bool for_debug = false)
{ {
dump_sigspec_rhs(conn.second); dump_sigspec_rhs(conn.second, for_debug);
} }
bool is_connect_inlined(const RTLIL::SigSig &conn) bool is_connect_inlined(const RTLIL::SigSig &conn)
@ -885,6 +887,14 @@ struct CxxrtlWorker {
return conn.first.is_wire() && inlined_wires.count(conn.first.as_wire()); return conn.first.is_wire() && inlined_wires.count(conn.first.as_wire());
} }
bool is_connect_outlined(const RTLIL::SigSig &conn)
{
for (auto chunk : conn.first.chunks())
if (debug_outlined_wires.count(chunk.wire))
return true;
return false;
}
void collect_connect(const RTLIL::SigSig &conn, std::vector<RTLIL::IdString> &cells) void collect_connect(const RTLIL::SigSig &conn, std::vector<RTLIL::IdString> &cells)
{ {
if (!is_connect_inlined(conn)) if (!is_connect_inlined(conn))
@ -893,16 +903,18 @@ struct CxxrtlWorker {
collect_sigspec_rhs(conn.second, cells); collect_sigspec_rhs(conn.second, cells);
} }
void dump_connect(const RTLIL::SigSig &conn) void dump_connect(const RTLIL::SigSig &conn, bool for_debug = false)
{ {
if (is_connect_inlined(conn)) if (!for_debug && is_connect_inlined(conn))
return;
if (for_debug && !is_connect_outlined(conn))
return; return;
f << indent << "// connection\n"; f << indent << "// connection\n";
f << indent; f << indent;
dump_sigspec_lhs(conn.first); dump_sigspec_lhs(conn.first, for_debug);
f << " = "; f << " = ";
dump_connect_expr(conn); dump_connect_expr(conn, for_debug);
f << ";\n"; f << ";\n";
} }
@ -919,7 +931,7 @@ struct CxxrtlWorker {
} }
} }
void dump_cell_expr(const RTLIL::Cell *cell) void dump_cell_expr(const RTLIL::Cell *cell, bool for_debug = false)
{ {
// Unary cells // Unary cells
if (is_unary_cell(cell->type)) { if (is_unary_cell(cell->type)) {
@ -927,7 +939,7 @@ struct CxxrtlWorker {
if (is_extending_cell(cell->type)) if (is_extending_cell(cell->type))
f << '_' << (cell->getParam(ID::A_SIGNED).as_bool() ? 's' : 'u'); f << '_' << (cell->getParam(ID::A_SIGNED).as_bool() ? 's' : 'u');
f << "<" << cell->getParam(ID::Y_WIDTH).as_int() << ">("; f << "<" << cell->getParam(ID::Y_WIDTH).as_int() << ">(";
dump_sigspec_rhs(cell->getPort(ID::A)); dump_sigspec_rhs(cell->getPort(ID::A), for_debug);
f << ")"; f << ")";
// Binary cells // Binary cells
} else if (is_binary_cell(cell->type)) { } else if (is_binary_cell(cell->type)) {
@ -936,18 +948,18 @@ struct CxxrtlWorker {
f << '_' << (cell->getParam(ID::A_SIGNED).as_bool() ? 's' : 'u') << f << '_' << (cell->getParam(ID::A_SIGNED).as_bool() ? 's' : 'u') <<
(cell->getParam(ID::B_SIGNED).as_bool() ? 's' : 'u'); (cell->getParam(ID::B_SIGNED).as_bool() ? 's' : 'u');
f << "<" << cell->getParam(ID::Y_WIDTH).as_int() << ">("; f << "<" << cell->getParam(ID::Y_WIDTH).as_int() << ">(";
dump_sigspec_rhs(cell->getPort(ID::A)); dump_sigspec_rhs(cell->getPort(ID::A), for_debug);
f << ", "; f << ", ";
dump_sigspec_rhs(cell->getPort(ID::B)); dump_sigspec_rhs(cell->getPort(ID::B), for_debug);
f << ")"; f << ")";
// Muxes // Muxes
} else if (cell->type == ID($mux)) { } else if (cell->type == ID($mux)) {
f << "("; f << "(";
dump_sigspec_rhs(cell->getPort(ID::S)); dump_sigspec_rhs(cell->getPort(ID::S), for_debug);
f << " ? "; f << " ? ";
dump_sigspec_rhs(cell->getPort(ID::B)); dump_sigspec_rhs(cell->getPort(ID::B), for_debug);
f << " : "; f << " : ";
dump_sigspec_rhs(cell->getPort(ID::A)); dump_sigspec_rhs(cell->getPort(ID::A), for_debug);
f << ")"; f << ")";
// Parallel (one-hot) muxes // Parallel (one-hot) muxes
} else if (cell->type == ID($pmux)) { } else if (cell->type == ID($pmux)) {
@ -955,24 +967,24 @@ struct CxxrtlWorker {
int s_width = cell->getParam(ID::S_WIDTH).as_int(); int s_width = cell->getParam(ID::S_WIDTH).as_int();
for (int part = 0; part < s_width; part++) { for (int part = 0; part < s_width; part++) {
f << "("; f << "(";
dump_sigspec_rhs(cell->getPort(ID::S).extract(part)); dump_sigspec_rhs(cell->getPort(ID::S).extract(part), for_debug);
f << " ? "; f << " ? ";
dump_sigspec_rhs(cell->getPort(ID::B).extract(part * width, width)); dump_sigspec_rhs(cell->getPort(ID::B).extract(part * width, width), for_debug);
f << " : "; f << " : ";
} }
dump_sigspec_rhs(cell->getPort(ID::A)); dump_sigspec_rhs(cell->getPort(ID::A), for_debug);
for (int part = 0; part < s_width; part++) { for (int part = 0; part < s_width; part++) {
f << ")"; f << ")";
} }
// Concats // Concats
} else if (cell->type == ID($concat)) { } else if (cell->type == ID($concat)) {
dump_sigspec_rhs(cell->getPort(ID::B)); dump_sigspec_rhs(cell->getPort(ID::B), for_debug);
f << ".concat("; f << ".concat(";
dump_sigspec_rhs(cell->getPort(ID::A)); dump_sigspec_rhs(cell->getPort(ID::A), for_debug);
f << ").val()"; f << ").val()";
// Slices // Slices
} else if (cell->type == ID($slice)) { } else if (cell->type == ID($slice)) {
dump_sigspec_rhs(cell->getPort(ID::A)); dump_sigspec_rhs(cell->getPort(ID::A), for_debug);
f << ".slice<"; f << ".slice<";
f << cell->getParam(ID::OFFSET).as_int() + cell->getParam(ID::Y_WIDTH).as_int() - 1; f << cell->getParam(ID::OFFSET).as_int() + cell->getParam(ID::Y_WIDTH).as_int() - 1;
f << ","; f << ",";
@ -989,6 +1001,17 @@ struct CxxrtlWorker {
inlined_wires.count(cell->getPort(ID::Y).as_wire()); inlined_wires.count(cell->getPort(ID::Y).as_wire());
} }
bool is_cell_outlined(const RTLIL::Cell *cell)
{
if (is_internal_cell(cell->type))
for (auto conn : cell->connections())
if (cell->output(conn.first))
for (auto chunk : conn.second.chunks())
if (debug_outlined_wires.count(chunk.wire))
return true;
return false;
}
void collect_cell_eval(const RTLIL::Cell *cell, std::vector<RTLIL::IdString> &cells) void collect_cell_eval(const RTLIL::Cell *cell, std::vector<RTLIL::IdString> &cells)
{ {
if (!is_cell_inlined(cell)) if (!is_cell_inlined(cell))
@ -1000,9 +1023,11 @@ struct CxxrtlWorker {
collect_sigspec_rhs(port.second, cells); collect_sigspec_rhs(port.second, cells);
} }
void dump_cell_eval(const RTLIL::Cell *cell) void dump_cell_eval(const RTLIL::Cell *cell, bool for_debug = false)
{ {
if (is_cell_inlined(cell)) if (!for_debug && is_cell_inlined(cell))
return;
if (for_debug && !is_cell_outlined(cell))
return; return;
if (cell->type == ID($meminit)) if (cell->type == ID($meminit))
return; // Handled elsewhere. return; // Handled elsewhere.
@ -1026,9 +1051,9 @@ struct CxxrtlWorker {
// Elidable cells // Elidable cells
if (is_inlinable_cell(cell->type)) { if (is_inlinable_cell(cell->type)) {
f << indent; f << indent;
dump_sigspec_lhs(cell->getPort(ID::Y)); dump_sigspec_lhs(cell->getPort(ID::Y), for_debug);
f << " = "; f << " = ";
dump_cell_expr(cell); dump_cell_expr(cell, for_debug);
f << ";\n"; f << ";\n";
// Flip-flops // Flip-flops
} else if (is_ff_cell(cell->type)) { } else if (is_ff_cell(cell->type)) {
@ -1460,14 +1485,11 @@ struct CxxrtlWorker {
void dump_wire(const RTLIL::Wire *wire, bool is_local) void dump_wire(const RTLIL::Wire *wire, bool is_local)
{ {
if (inlined_wires.count(wire)) if (is_local && localized_wires[wire] && !inlined_wires.count(wire)) {
return;
if (localized_wires[wire] && is_local) {
dump_attrs(wire); dump_attrs(wire);
f << indent << "value<" << wire->width << "> " << mangle(wire) << ";\n"; f << indent << "value<" << wire->width << "> " << mangle(wire) << ";\n";
} }
if (!localized_wires[wire] && !is_local) { if (!is_local && !localized_wires[wire]) {
std::string width; std::string width;
if (wire->module->has_attribute(ID(cxxrtl_blackbox)) && wire->has_attribute(ID(cxxrtl_width))) { if (wire->module->has_attribute(ID(cxxrtl_blackbox)) && wire->has_attribute(ID(cxxrtl_width))) {
width = wire->get_string_attribute(ID(cxxrtl_width)); width = wire->get_string_attribute(ID(cxxrtl_width));
@ -1530,6 +1552,23 @@ struct CxxrtlWorker {
} }
} }
void dump_debug_wire(const RTLIL::Wire *wire, bool is_local)
{
if (!debug_outlined_wires[wire])
return;
bool is_outlined_member = wire->name.isPublic() &&
!(debug_const_wires.count(wire) || debug_alias_wires.count(wire));
if (is_local && !is_outlined_member) {
dump_attrs(wire);
f << indent << "value<" << wire->width << "> " << mangle(wire) << ";\n";
}
if (!is_local && is_outlined_member) {
dump_attrs(wire);
f << indent << "/*outline*/ value<" << wire->width << "> " << mangle(wire) << ";\n";
}
}
void dump_memory(RTLIL::Module *module, const RTLIL::Memory *memory) void dump_memory(RTLIL::Module *module, const RTLIL::Memory *memory)
{ {
vector<const RTLIL::Cell*> init_cells; vector<const RTLIL::Cell*> init_cells;
@ -1619,6 +1658,27 @@ struct CxxrtlWorker {
dec_indent(); dec_indent();
} }
void dump_debug_eval_method(RTLIL::Module *module)
{
inc_indent();
for (auto wire : module->wires())
dump_debug_wire(wire, /*is_local=*/true);
for (auto node : schedule[module]) {
switch (node.type) {
case FlowGraph::Node::Type::CONNECT:
dump_connect(node.connect, /*for_debug=*/true);
break;
case FlowGraph::Node::Type::CELL_EVAL:
dump_cell_eval(node.cell, /*for_debug=*/true);
break;
case FlowGraph::Node::Type::CELL_SYNC:
case FlowGraph::Node::Type::PROCESS:
break;
}
}
dec_indent();
}
void dump_commit_method(RTLIL::Module *module) void dump_commit_method(RTLIL::Module *module)
{ {
inc_indent(); inc_indent();
@ -1656,6 +1716,7 @@ struct CxxrtlWorker {
size_t count_public_wires = 0; size_t count_public_wires = 0;
size_t count_const_wires = 0; size_t count_const_wires = 0;
size_t count_alias_wires = 0; size_t count_alias_wires = 0;
size_t count_inline_wires = 0;
size_t count_member_wires = 0; size_t count_member_wires = 0;
size_t count_skipped_wires = 0; size_t count_skipped_wires = 0;
size_t count_driven_sync = 0; size_t count_driven_sync = 0;
@ -1685,6 +1746,12 @@ struct CxxrtlWorker {
f << ", debug_item(debug_alias(), " << mangle(debug_alias_wires[wire]) << ", "; f << ", debug_item(debug_alias(), " << mangle(debug_alias_wires[wire]) << ", ";
f << wire->start_offset << "));\n"; f << wire->start_offset << "));\n";
count_alias_wires++; count_alias_wires++;
} else if (debug_outlined_wires.count(wire)) {
// Inlined but rematerializable wire
f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire));
f << ", debug_item(debug_eval_outline, " << mangle(wire) << ", ";
f << wire->start_offset << "));\n";
count_inline_wires++;
} else if (!localized_wires.count(wire)) { } else if (!localized_wires.count(wire)) {
// Member wire // Member wire
std::vector<std::string> flags; std::vector<std::string> flags;
@ -1738,6 +1805,7 @@ struct CxxrtlWorker {
f << "));\n"; f << "));\n";
count_member_wires++; count_member_wires++;
} else { } else {
// Localized or inlined wire with no debug information
count_skipped_wires++; count_skipped_wires++;
} }
} }
@ -1761,14 +1829,16 @@ struct CxxrtlWorker {
log_debug("Debug information statistics for module `%s':\n", log_id(module)); log_debug("Debug information statistics for module `%s':\n", log_id(module));
log_debug(" Public wires: %zu, of which:\n", count_public_wires); log_debug(" Public wires: %zu, of which:\n", count_public_wires);
log_debug(" Const wires: %zu\n", count_const_wires);
log_debug(" Alias wires: %zu\n", count_alias_wires);
log_debug(" Member wires: %zu, of which:\n", count_member_wires); log_debug(" Member wires: %zu, of which:\n", count_member_wires);
log_debug(" Driven sync: %zu\n", count_driven_sync); log_debug(" Driven sync: %zu\n", count_driven_sync);
log_debug(" Driven comb: %zu\n", count_driven_comb); log_debug(" Driven comb: %zu\n", count_driven_comb);
log_debug(" Undriven: %zu\n", count_undriven);
log_debug(" Mixed driver: %zu\n", count_mixed_driver); log_debug(" Mixed driver: %zu\n", count_mixed_driver);
log_debug(" Other wires: %zu (no debug information)\n", count_skipped_wires); log_debug(" Undriven: %zu\n", count_undriven);
log_debug(" Inline wires: %zu\n", count_inline_wires);
log_debug(" Alias wires: %zu\n", count_alias_wires);
log_debug(" Const wires: %zu\n", count_const_wires);
log_debug(" Other wires: %zu%s\n", count_skipped_wires,
count_skipped_wires > 0 ? " (debug information unavailable)" : "");
} }
void dump_metadata_map(const dict<RTLIL::IdString, RTLIL::Const> &metadata_map) void dump_metadata_map(const dict<RTLIL::IdString, RTLIL::Const> &metadata_map)
@ -1855,7 +1925,8 @@ struct CxxrtlWorker {
inc_indent(); inc_indent();
for (auto wire : module->wires()) for (auto wire : module->wires())
dump_wire(wire, /*is_local=*/false); dump_wire(wire, /*is_local=*/false);
f << "\n"; for (auto wire : module->wires())
dump_debug_wire(wire, /*is_local=*/false);
bool has_memories = false; bool has_memories = false;
for (auto memory : module->memories) { for (auto memory : module->memories) {
dump_memory(module, memory.second); dump_memory(module, memory.second);
@ -1927,8 +1998,20 @@ struct CxxrtlWorker {
f << "\n"; f << "\n";
f << indent << "bool eval() override;\n"; f << indent << "bool eval() override;\n";
f << indent << "bool commit() override;\n"; f << indent << "bool commit() override;\n";
if (debug_info) if (debug_info) {
if (debug_eval) {
f << "\n";
f << indent << "void debug_eval();\n";
for (auto wire : module->wires())
if (debug_outlined_wires.count(wire)) {
f << indent << "debug_outline debug_eval_outline { std::bind(&"
<< mangle(module) << "::debug_eval, this) };\n";
break;
}
}
f << "\n";
f << indent << "void debug_info(debug_items &items, std::string path = \"\") override;\n"; f << indent << "void debug_info(debug_items &items, std::string path = \"\") override;\n";
}
dec_indent(); dec_indent();
f << indent << "}; // struct " << mangle(module) << "\n"; f << indent << "}; // struct " << mangle(module) << "\n";
f << "\n"; f << "\n";
@ -1948,6 +2031,12 @@ struct CxxrtlWorker {
f << indent << "}\n"; f << indent << "}\n";
f << "\n"; f << "\n";
if (debug_info) { if (debug_info) {
if (debug_eval) {
f << indent << "void " << mangle(module) << "::debug_eval() {\n";
dump_debug_eval_method(module);
f << indent << "}\n";
f << "\n";
}
f << indent << "void " << mangle(module) << "::debug_info(debug_items &items, std::string path) {\n"; f << indent << "void " << mangle(module) << "::debug_info(debug_items &items, std::string path) {\n";
dump_debug_info_method(module); dump_debug_info_method(module);
f << indent << "}\n"; f << indent << "}\n";
@ -2251,6 +2340,11 @@ struct CxxrtlWorker {
for (auto node : wire_comb_def.second) for (auto node : wire_comb_def.second)
node_defs[node].insert(wire_comb_def.first); node_defs[node].insert(wire_comb_def.first);
dict<FlowGraph::Node*, pool<const RTLIL::Wire*>, hash_ptr_ops> node_uses;
for (auto wire_use : flow.wire_uses)
for (auto node : wire_use.second)
node_uses[node].insert(wire_use.first);
Scheduler<FlowGraph::Node> scheduler; Scheduler<FlowGraph::Node> scheduler;
dict<FlowGraph::Node*, Scheduler<FlowGraph::Node>::Vertex*, hash_ptr_ops> node_map; dict<FlowGraph::Node*, Scheduler<FlowGraph::Node>::Vertex*, hash_ptr_ops> node_map;
for (auto node : flow.nodes) for (auto node : flow.nodes)
@ -2368,6 +2462,30 @@ struct CxxrtlWorker {
} }
} }
} }
if (debug_info && debug_eval) {
// Find wires that can be be outlined, i.e. whose values can be always recovered from
// the values of other wires. (This is the inverse of inlining--any wire that can be
// inlined can also be outlined.) Although this may seem strictly less efficient, since
// such values are computed at least twice, second-order effects make outlining useful.
pool<const RTLIL::Wire*> worklist, visited;
for (auto wire : module->wires()) {
if (!wire->name.isPublic())
continue; // only outline public wires
worklist.insert(wire);
}
while (!worklist.empty()) {
const RTLIL::Wire *wire = worklist.pop();
visited.insert(wire);
if (!localized_wires.count(wire) && !inlined_wires.count(wire))
continue; // member wire, doesn't need outlining
if (wire->name.isPublic() || !inlined_wires.count(wire))
debug_outlined_wires.insert(wire); // allow outlining of internal wires only
for (auto node : flow.wire_comb_defs[wire])
for (auto node_use : node_uses[node])
if (!visited.count(node_use))
worklist.insert(node_use);
}
}
} }
if (has_feedback_arcs || has_buffered_comb_wires) { if (has_feedback_arcs || has_buffered_comb_wires) {
// Although both non-feedback buffered combinatorial wires and apparent feedback wires may be eliminated // Although both non-feedback buffered combinatorial wires and apparent feedback wires may be eliminated
@ -2457,8 +2575,7 @@ struct CxxrtlWorker {
struct CxxrtlBackend : public Backend { struct CxxrtlBackend : public Backend {
static const int DEFAULT_OPT_LEVEL = 6; static const int DEFAULT_OPT_LEVEL = 6;
static const int OPT_LEVEL_DEBUG = 4; static const int DEFAULT_DEBUG_LEVEL = 2;
static const int DEFAULT_DEBUG_LEVEL = 1;
CxxrtlBackend() : Backend("cxxrtl", "convert design to C++ RTL simulation") { } CxxrtlBackend() : Backend("cxxrtl", "convert design to C++ RTL simulation") { }
void help() override void help() override
@ -2671,10 +2788,6 @@ struct CxxrtlBackend : public Backend {
log(" -O6\n"); log(" -O6\n");
log(" like -O5, and inline public wires not marked (*keep*) if possible.\n"); log(" like -O5, and inline public wires not marked (*keep*) if possible.\n");
log("\n"); log("\n");
log(" -Og\n");
log(" highest optimization level that provides debug information for all\n");
log(" public wires. currently, alias for -O%d.\n", OPT_LEVEL_DEBUG);
log("\n");
log(" -g <level>\n"); log(" -g <level>\n");
log(" set the debug level. the default is -g%d. higher debug levels provide\n", DEFAULT_DEBUG_LEVEL); log(" set the debug level. the default is -g%d. higher debug levels provide\n", DEFAULT_DEBUG_LEVEL);
log(" more visibility and generate more code, but do not pessimize evaluation.\n"); log(" more visibility and generate more code, but do not pessimize evaluation.\n");
@ -2686,6 +2799,10 @@ struct CxxrtlBackend : public Backend {
log(" debug information for non-optimized public wires. this also makes it\n"); log(" debug information for non-optimized public wires. this also makes it\n");
log(" possible to use the C API.\n"); log(" possible to use the C API.\n");
log("\n"); log("\n");
log(" -g2\n");
log(" like -g1, and compute debug information on demand for all public wires\n");
log(" that were optimized out.\n");
log("\n");
} }
void execute(std::ostream *&f, std::string filename, std::vector<std::string> args, RTLIL::Design *design) override void execute(std::ostream *&f, std::string filename, std::vector<std::string> args, RTLIL::Design *design) override
@ -2715,12 +2832,14 @@ struct CxxrtlBackend : public Backend {
continue; continue;
} }
if (args[argidx] == "-Og") { if (args[argidx] == "-Og") {
opt_level = OPT_LEVEL_DEBUG; log_warning("The `-Og` option has been removed. Use `-g2` instead for complete "
"design coverage regardless of optimization level.\n");
continue; continue;
} }
if (args[argidx] == "-O" && argidx+1 < args.size() && args[argidx+1] == "g") { if (args[argidx] == "-O" && argidx+1 < args.size() && args[argidx+1] == "g") {
argidx++; argidx++;
opt_level = OPT_LEVEL_DEBUG; log_warning("The `-Og` option has been removed. Use `-g2` instead for complete "
"design coverage regardless of optimization level.\n");
continue; continue;
} }
if (args[argidx] == "-O" && argidx+1 < args.size()) { if (args[argidx] == "-O" && argidx+1 < args.size()) {
@ -2781,6 +2900,9 @@ struct CxxrtlBackend : public Backend {
} }
switch (debug_level) { switch (debug_level) {
// the highest level here must match DEFAULT_DEBUG_LEVEL // the highest level here must match DEFAULT_DEBUG_LEVEL
case 2:
worker.debug_eval = true;
YS_FALLTHROUGH
case 1: case 1:
worker.debug_info = true; worker.debug_info = true;
YS_FALLTHROUGH YS_FALLTHROUGH

View file

@ -86,3 +86,7 @@ void cxxrtl_enum(cxxrtl_handle handle, void *data,
for (auto &it : handle->objects.table) for (auto &it : handle->objects.table)
callback(data, it.first.c_str(), static_cast<cxxrtl_object*>(&it.second[0]), it.second.size()); callback(data, it.first.c_str(), static_cast<cxxrtl_object*>(&it.second[0]), it.second.size());
} }
void cxxrtl_outline_eval(cxxrtl_outline outline) {
outline->eval();
}

View file

@ -128,6 +128,18 @@ enum cxxrtl_type {
// pointer is always NULL. // pointer is always NULL.
CXXRTL_ALIAS = 3, CXXRTL_ALIAS = 3,
// Outlines correspond to netlist nodes that were optimized in a way that makes them inaccessible
// outside of a module's `eval()` function. At the highest debug information level, every inlined
// node has a corresponding outline object.
//
// Outlines can be inspected via the `curr` pointer and can never be modified; the `next` pointer
// is always NULL. Unlike all other objects, the bits of an outline object are meaningful only
// after a call to `cxxrtl_outline_eval` and until any subsequent modification to the netlist.
// Observing this requirement is the responsibility of the caller; it is not enforced.
//
// Outlines always correspond to combinatorial netlist nodes that are not ports.
CXXRTL_OUTLINE = 4,
// More object types may be added in the future, but the existing ones will never change. // More object types may be added in the future, but the existing ones will never change.
}; };
@ -171,8 +183,8 @@ enum cxxrtl_flag {
// Node has bits that are driven by a combinatorial cell or another node. // Node has bits that are driven by a combinatorial cell or another node.
// //
// This flag can be set on objects of type `CXXRTL_VALUE` and `CXXRTL_WIRE`. It may be combined // This flag can be set on objects of type `CXXRTL_VALUE`, `CXXRTL_WIRE`, and `CXXRTL_OUTLINE`.
// with `CXXRTL_DRIVEN_SYNC` and `CXXRTL_UNDRIVEN`, as well as other flags. // It may be combined with `CXXRTL_DRIVEN_SYNC` and `CXXRTL_UNDRIVEN`, as well as other flags.
// //
// This flag is set on objects that have bits connected to the output of a combinatorial cell, // This flag is set on objects that have bits connected to the output of a combinatorial cell,
// or directly to another node. For designs without combinatorial loops, writing to such bits // or directly to another node. For designs without combinatorial loops, writing to such bits
@ -193,8 +205,8 @@ enum cxxrtl_flag {
// Description of a simulated object. // Description of a simulated object.
// //
// The `data` array can be accessed directly to inspect and, if applicable, modify the bits // The `curr` and `next` arrays can be accessed directly to inspect and, if applicable, modify
// stored in the object. // the bits stored in the object.
struct cxxrtl_object { struct cxxrtl_object {
// Type of the object. // Type of the object.
// //
@ -231,6 +243,12 @@ struct cxxrtl_object {
uint32_t *curr; uint32_t *curr;
uint32_t *next; uint32_t *next;
// Opaque reference to an outline. Only meaningful for outline objects.
//
// See the documentation of `cxxrtl_outline` for details. When creating a `cxxrtl_object`, set
// this field to NULL.
struct _cxxrtl_outline *outline;
// More description fields may be added in the future, but the existing ones will never change. // More description fields may be added in the future, but the existing ones will never change.
}; };
@ -272,6 +290,20 @@ void cxxrtl_enum(cxxrtl_handle handle, void *data,
void (*callback)(void *data, const char *name, void (*callback)(void *data, const char *name,
struct cxxrtl_object *object, size_t parts)); struct cxxrtl_object *object, size_t parts));
// Opaque reference to an outline.
//
// An outline is a group of outline objects that are evaluated simultaneously. The identity of
// an outline can be compared to determine whether any two objects belong to the same outline.
typedef struct _cxxrtl_outline *cxxrtl_outline;
// Evaluate an outline.
//
// After evaluating an outline, the bits of every outline object contained in it are consistent
// with the current state of the netlist. In general, any further modification to the netlist
// causes every outline object to become stale, after which the corresponding outline must be
// re-evaluated, otherwise the bits read from that object are meaningless.
void cxxrtl_outline_eval(cxxrtl_outline outline);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -28,10 +28,13 @@ class vcd_writer {
size_t ident; size_t ident;
size_t width; size_t width;
chunk_t *curr; chunk_t *curr;
size_t prev_off; size_t cache_offset;
debug_outline *outline;
bool *outline_warm;
}; };
std::vector<std::string> current_scope; std::vector<std::string> current_scope;
std::map<debug_outline*, bool> outlines;
std::vector<variable> variables; std::vector<variable> variables;
std::vector<chunk_t> cache; std::vector<chunk_t> cache;
std::map<chunk_t*, size_t> aliases; std::map<chunk_t*, size_t> aliases;
@ -112,16 +115,22 @@ class vcd_writer {
buffer += '\n'; buffer += '\n';
} }
const variable &register_variable(size_t width, chunk_t *curr, bool constant = false) { void reset_outlines() {
for (auto &outline_it : outlines)
outline_it.second = /*warm=*/(outline_it.first == nullptr);
}
variable &register_variable(size_t width, chunk_t *curr, bool constant = false, debug_outline *outline = nullptr) {
if (aliases.count(curr)) { if (aliases.count(curr)) {
return variables[aliases[curr]]; return variables[aliases[curr]];
} else { } else {
auto outline_it = outlines.emplace(outline, /*warm=*/(outline == nullptr)).first;
const size_t chunks = (width + (sizeof(chunk_t) * 8 - 1)) / (sizeof(chunk_t) * 8); const size_t chunks = (width + (sizeof(chunk_t) * 8 - 1)) / (sizeof(chunk_t) * 8);
aliases[curr] = variables.size(); aliases[curr] = variables.size();
if (constant) { if (constant) {
variables.emplace_back(variable { variables.size(), width, curr, (size_t)-1 }); variables.emplace_back(variable { variables.size(), width, curr, (size_t)-1, outline_it->first, &outline_it->second });
} else { } else {
variables.emplace_back(variable { variables.size(), width, curr, cache.size() }); variables.emplace_back(variable { variables.size(), width, curr, cache.size(), outline_it->first, &outline_it->second });
cache.insert(cache.end(), &curr[0], &curr[chunks]); cache.insert(cache.end(), &curr[0], &curr[chunks]);
} }
return variables.back(); return variables.back();
@ -129,13 +138,17 @@ class vcd_writer {
} }
bool test_variable(const variable &var) { bool test_variable(const variable &var) {
if (var.prev_off == (size_t)-1) if (var.cache_offset == (size_t)-1)
return false; // constant return false; // constant
if (!*var.outline_warm) {
var.outline->eval();
*var.outline_warm = true;
}
const size_t chunks = (var.width + (sizeof(chunk_t) * 8 - 1)) / (sizeof(chunk_t) * 8); const size_t chunks = (var.width + (sizeof(chunk_t) * 8 - 1)) / (sizeof(chunk_t) * 8);
if (std::equal(&var.curr[0], &var.curr[chunks], &cache[var.prev_off])) { if (std::equal(&var.curr[0], &var.curr[chunks], &cache[var.cache_offset])) {
return false; return false;
} else { } else {
std::copy(&var.curr[0], &var.curr[chunks], &cache[var.prev_off]); std::copy(&var.curr[0], &var.curr[chunks], &cache[var.cache_offset]);
return true; return true;
} }
} }
@ -197,6 +210,10 @@ public:
emit_var(register_variable(item.width, item.curr), emit_var(register_variable(item.width, item.curr),
"wire", name, item.lsb_at, multipart); "wire", name, item.lsb_at, multipart);
break; break;
case debug_item::OUTLINE:
emit_var(register_variable(item.width, item.curr, /*constant=*/false, item.outline),
"wire", name, item.lsb_at, multipart);
break;
} }
} }
@ -228,6 +245,7 @@ public:
emit_scope({}); emit_scope({});
emit_enddefinitions(); emit_enddefinitions();
} }
reset_outlines();
emit_time(timestamp); emit_time(timestamp);
for (auto var : variables) for (auto var : variables)
if (test_variable(var) || first_sample) { if (test_variable(var) || first_sample) {