From 6f1c96d1379a14ba671c71b04288cb39f19cb103 Mon Sep 17 00:00:00 2001 From: Stefan Nitz Date: Thu, 30 Jul 2026 02:59:51 +0200 Subject: [PATCH 1/2] Skip recalculating nodes that no transistor connects to anything A third of recalcNode's calls are for a node that no turned-on transistor connects to anything, which can only take its own pullup or pulldown value, or keep the one it has. Count the turned-on transistors per node and those calls become a load and a branch instead of a group walk. The counters only move where a node value changes, which already walks the nodes behind that node's transistors, so maintaining them is nearly free. nodes_dependant cannot serve as that walk: it is deduplicated and drops vss and vcc, while a counter has to move once per endpoint and must see a node held to ground as connected. An undeduplicated endpoint list replaces it, with listout_add filtering the repeats as it already did. -DDEBUG_ON_DEGREE asserts every counter against a from-scratch recount. measure is byte-identical over all 256 opcodes with and without it. --- netlist_sim.c | 185 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 140 insertions(+), 45 deletions(-) diff --git a/netlist_sim.c b/netlist_sim.c index 62513e5..4f5d3ba 100644 --- a/netlist_sim.c +++ b/netlist_sim.c @@ -94,10 +94,15 @@ typedef struct { bitmap_t *nodes_value; c1c2_t *nodes_c1c2s; count_t *nodes_c1c2offset; - nodenum_t *nodes_dependant; nodenum_t *nodes_left_dependant; nodenum_t *dependent_block; + /* number of turned-on transistors that connect this node to something */ + count_t *nodes_on_degree; + /* the nodes touched by the transistors a node gates, NOT deduplicated */ + count_t *nodes_endpoint_offset; + nodenum_t *endpoint_block; + /* the nodes we are working with */ nodenum_t *list1; list_t listin; @@ -384,9 +389,72 @@ getGroupValue(group_value node_value) return NO; } +/* + * assign a new value to a node, switching the transistors it gates: + * keep the on-degree of the nodes they touch up to date, and collect + * the nodes behind them for the next run + */ +static inline void +changeNodeValue(state_t *state, nodenum_t nn, BOOL newv) +{ + set_nodes_value(state, nn, newv); + + const count_t ep_offset = state->nodes_endpoint_offset[nn]; + const count_t ep_end = state->nodes_endpoint_offset[nn+1]; + const nodenum_t *endpoint_block = state->endpoint_block; + count_t *on_degree = state->nodes_on_degree; + + if (newv) { + for (count_t g = ep_offset; g < ep_end; g++) + on_degree[endpoint_block[g]]++; + + /* + * the transistors are now on, so the nodes on either side of one + * end up in the same group - recalculating from one side is enough + */ + const nodenum_t dep_offset = state->nodes_left_dependant[nn]; + const nodenum_t dep_end = state->nodes_left_dependant[nn+1]; + for (count_t g = dep_offset; g < dep_end; g++) { + listout_add(state, state->dependent_block[g]); + } + } else { + /* + * the transistors are now off, so both sides come loose and have to + * be recalculated separately - which is every endpoint, exactly what + * we are already walking. listout_add filters the repeats out. + */ + for (count_t g = ep_offset; g < ep_end; g++) { + const nodenum_t e = endpoint_block[g]; + on_degree[e]--; + listout_add(state, e); + } + } +} + static inline void recalcNode(state_t *state, nodenum_t node) { + /* + * A node that no turned-on transistor connects to anything is a group of + * its own, so the whole group walk collapses to reading its on-degree. + * Its own pulldown or pullup then decides the value, and a node with + * neither of those keeps the value it already has - it cannot change at + * all. + */ + if (state->nodes_on_degree[node] == 0) { + BOOL newv; + if (get_nodes_pulldown(state, node)) + newv = NO; + else if (get_nodes_pullup(state, node)) + newv = YES; + else + return; /* provably inert */ + + if (get_nodes_value(state, node) != newv) + changeNodeValue(state, node, newv); + return; + } + /* * get all nodes that are connected through * transistors, starting with this one @@ -405,31 +473,43 @@ recalcNode(state_t *state, nodenum_t node) const count_t grp_count = group_count(state); for (count_t i = 0; i < grp_count; i++) { const nodenum_t nn = group_get(state, i); - if (get_nodes_value(state, nn) != newv) { - set_nodes_value(state, nn, newv); - - if (newv) { - const nodenum_t dep_offset = state->nodes_left_dependant[nn]; - const nodenum_t dep_end = state->nodes_left_dependant[nn+1]; - for (count_t g = dep_offset; g < dep_end; g++) { - listout_add(state, state->dependent_block[g]); - } - } else { - const nodenum_t dep_offset = state->nodes_dependant[nn]; - const nodenum_t dep_end = state->nodes_dependant[nn+1]; - for (count_t g = dep_offset; g < dep_end; g++) { - listout_add(state, state->dependent_block[g]); - } - } - } + if (get_nodes_value(state, nn) != newv) + changeNodeValue(state, nn, newv); } } +#ifdef DEBUG_ON_DEGREE +/* + * Recompute every on-degree the slow way and check it against the + * incrementally maintained counter. A desync silently skips work instead of + * crashing, so it is worth being able to catch it at the source. + */ +static void +verify_on_degree(state_t *state) +{ + for (nodenum_t n = 0; n < state->nodes; n++) { + if (n == state->vss || n == state->vcc) + continue; + count_t expected = 0; + const count_t start = state->nodes_c1c2offset[n]; + const count_t end = state->nodes_c1c2offset[n+1]; + for (count_t t = start; t < end; t++) + if (get_nodes_value(state, state->nodes_c1c2s[t].gate)) + expected++; + assert(state->nodes_on_degree[n] == expected); + } +} +#endif + void recalcNodeList(state_t *state) { const int max_iterations = 50; int j; + +#ifdef DEBUG_ON_DEGREE + verify_on_degree(state); +#endif for (j = 0; j < max_iterations; j++) { /* loop limiter */ /* @@ -492,10 +572,11 @@ add_nodes_dependant(state_t *state, nodenum_t a, nodenum_t b, nodenum_t *counts, 3288 transistors, 3239 used in simulation after duplicate removal 1725 entries in node list and used in simulation c1c2total = 6478 - block_dep_size = 7260 + block_dep_size = 3239 + endpoint_block_size = 4021 - Working set = 89 KB allocations, 220 KB binary, plus system libs and text buffering - = 604 KB in release build + Working set = 92 KB allocations, 220 KB binary, plus system libs and text buffering + = 607 KB in release build */ state_t * setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nodenum_t nodes, nodenum_t transistors, nodenum_t vss, nodenum_t vcc) @@ -514,6 +595,15 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod state->listout_bitmap = calloc(WORDS_FOR_BITS(state->nodes), sizeof(*state->listout_bitmap)); state->groupbitmap = calloc(WORDS_FOR_BITS(state->nodes), sizeof(*state->groupbitmap)); + /* All node values start out low, so every transistor is off and every + on-degree is zero. vss and vcc are deliberately left out of the endpoint + list and never have a value of their own assigned, so their counters + would sit at zero forever and wrongly qualify them as inert - park them + on a value that keeps them out of the fast path for good. */ + state->nodes_on_degree = calloc(state->nodes, sizeof(*state->nodes_on_degree)); + state->nodes_on_degree[vss] = 1; + state->nodes_on_degree[vcc] = 1; + /* group content depends on active state, not easy to predict actual size needed */ state->group = calloc(state->nodes, sizeof(*state->group)); @@ -527,7 +617,7 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod /* these are only used in initialization */ - nodenum_t *nodes_dep_count = calloc(state->nodes, sizeof(nodenum_t)); + nodenum_t *nodes_endpoint_count = calloc(state->nodes, sizeof(nodenum_t)); nodenum_t *nodes_left_dep_count = calloc(state->nodes, sizeof(nodenum_t)); count_t *nodes_gatecount = calloc(state->nodes, sizeof(count_t)); nodenum_t *transistors_gate = calloc(state->transistors, sizeof(nodenum_t)); @@ -624,22 +714,22 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod } nodes_gates[state->nodes] = node_index; /* fill the end entry, so we can calculate distances/counts */ - /* See how many dependent node entries we really need. + /* See how many endpoint and dependent node entries we really need. Must happen after gatecount and nodes_gates assignments! */ for (i = 0; i < state->nodes; i++) { - nodes_dep_count[i] = 0; + nodes_endpoint_count[i] = 0; nodes_left_dep_count[i] = 0; nodenum_t g_start = nodes_gates[i]; nodenum_t g_end = g_start + nodes_gatecount[i]; for (nodenum_t t = g_start; t < g_end; t++) { nodenum_t c1 = transistors_c1[t]; if (c1 != vss && c1 != vcc) { - nodes_dep_count[i]++; + nodes_endpoint_count[i]++; } nodenum_t c2 = transistors_c2[t]; if (c2 != vss && c2 != vcc) { - nodes_dep_count[i]++; + nodes_endpoint_count[i]++; } nodes_left_dep_count[i]++; } @@ -648,25 +738,15 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod /* Sum the counts to find total size of the dependents array */ size_t block_dep_size = 0; for (i = 0; i < state->nodes; i++) { - block_dep_size += nodes_dep_count[i]; block_dep_size += nodes_left_dep_count[i]; } /* Allocate the dependents block all at once */ - state->dependent_block = calloc( block_dep_size, sizeof(*state->nodes_dependant) ); - - /* Assign offsets from our block, using only counts needed */ - state->nodes_dependant = malloc((nodes+1) * sizeof(*state->nodes_dependant)); - nodenum_t dep_index = 0; - for (i = 0; i < state->nodes; i++) { - nodenum_t count = nodes_dep_count[i]; - state->nodes_dependant[i] = dep_index; - dep_index += count; - } - state->nodes_dependant[state->nodes] = dep_index; /* fill the end entry, so we can calculate distances/counts */ - + state->dependent_block = calloc( block_dep_size, sizeof(*state->dependent_block) ); + /* Assign offsets from our block, using only counts needed */ state->nodes_left_dependant = malloc((nodes+1) * sizeof(*state->nodes_left_dependant)); + nodenum_t dep_index = 0; for (i = 0; i < state->nodes; i++) { nodenum_t count = nodes_left_dep_count[i]; state->nodes_left_dependant[i] = dep_index; @@ -674,20 +754,32 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod } state->nodes_left_dependant[state->nodes] = dep_index; /* fill the end entry, so we can calculate distances/counts */ + /* Assign offsets for the endpoint list. Unlike the dependents it is not + deduplicated - the on-degree has to move once per transistor endpoint, + not once per distinct node - so its counts are exact. */ + state->nodes_endpoint_offset = malloc((nodes+1) * sizeof(*state->nodes_endpoint_offset)); + count_t endpoint_index = 0; + for (i = 0; i < state->nodes; i++) { + state->nodes_endpoint_offset[i] = endpoint_index; + endpoint_index += nodes_endpoint_count[i]; + } + state->nodes_endpoint_offset[state->nodes] = endpoint_index; /* fill the end entry, so we can calculate distances/counts */ + state->endpoint_block = calloc(endpoint_index, sizeof(*state->endpoint_block)); + endpoint_index = 0; + /* Copy dependencies into smaller data structures */ for (i = 0; i < state->nodes; i++) { - nodes_dep_count[i] = 0; nodes_left_dep_count[i] = 0; nodenum_t g_start = nodes_gates[i]; nodenum_t g_end = g_start + nodes_gatecount[i]; for (nodenum_t t = g_start; t < g_end; t++) { nodenum_t c1 = transistors_c1[t]; if (c1 != vss && c1 != vcc) { - add_nodes_dependant(state, i, c1, nodes_dep_count, state->nodes_dependant[i]); + state->endpoint_block[endpoint_index++] = c1; } nodenum_t c2 = transistors_c2[t]; if (c2 != vss && c2 != vcc) { - add_nodes_dependant(state, i, c2, nodes_dep_count, state->nodes_dependant[i]); + state->endpoint_block[endpoint_index++] = c2; } if (c1 != vss && c1 != vcc) { add_nodes_dependant(state, i, c1, nodes_left_dep_count, state->nodes_left_dependant[i]); @@ -696,10 +788,10 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod } } } - + /* these are unused after initialization */ - free(nodes_dep_count); - nodes_dep_count = NULL; + free(nodes_endpoint_count); + nodes_endpoint_count = NULL; free(nodes_left_dep_count); nodes_left_dep_count = NULL; free(nodes_gatecount); @@ -733,6 +825,9 @@ destroyNodesAndTransistors(state_t *state) free(state->nodes_c1c2s); free(state->nodes_c1c2offset); free(state->dependent_block); + free(state->nodes_on_degree); + free(state->nodes_endpoint_offset); + free(state->endpoint_block); free(state->list1); free(state->list2); free(state->listout_bitmap); From 34e4a12b15b8976bc4b25a7a91dd349932a509cd Mon Sep 17 00:00:00 2001 From: Stefan Nitz Date: Thu, 30 Jul 2026 02:59:51 +0200 Subject: [PATCH 2/2] Skip the group walk for every node that is a group of one Two thirds of the groups recalcNode builds hold a single node, but the on-degree fast path only catches the half of them nothing is connected to. The rest are alone because their only turned-on transistors lead to vss or vcc, and the walk stops at the rails anyway. Count rail connections apart from ordinary ones, three counters packed into one word so the test stays a single load and mask. A node is alone exactly when the ordinary count is zero, and what remains decides the value in addNodeToGroup's order: vss beats vcc, vcc beats the node's own pulldown or pullup. Fast path coverage goes from a third of recalcNode calls to two thirds. cbmbasic to READY. is 26.72% fewer cycles than the commit before it, minimum of twelve interleaved runs. measure is byte-identical over all 256 opcodes, with and without the on-degree checker. --- netlist_sim.c | 82 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/netlist_sim.c b/netlist_sim.c index 4f5d3ba..9dd51f1 100644 --- a/netlist_sim.c +++ b/netlist_sim.c @@ -37,6 +37,20 @@ typedef uint16_t transnum_t; typedef uint16_t count_t; /* nodenum_t is declared in types.h, because it's API */ +/* + * An on-degree: how many turned-on transistors connect a node to an ordinary + * node, to vss, and to vcc, counted separately but packed into one word so + * that the whole lot is a single load. 10 bits each is far more than the + * netlist needs - the 6502's busiest node reaches 12, 9 and 1 respectively. + */ +typedef unsigned int degree_t; +#define DEGREE_OTHER ((degree_t)1) +#define DEGREE_VSS ((degree_t)1 << 10) +#define DEGREE_VCC ((degree_t)1 << 20) +#define DEGREE_OTHER_MASK (((degree_t)1023)) +#define DEGREE_VSS_MASK (((degree_t)1023) << 10) +#define DEGREE_VCC_MASK (((degree_t)1023) << 20) + /************************************************************ * * Main State Data Structure @@ -97,11 +111,13 @@ typedef struct { nodenum_t *nodes_left_dependant; nodenum_t *dependent_block; - /* number of turned-on transistors that connect this node to something */ - count_t *nodes_on_degree; - /* the nodes touched by the transistors a node gates, NOT deduplicated */ + /* each node's on-degree */ + degree_t *nodes_on_degree; + /* the nodes touched by the transistors a node gates, NOT deduplicated, + with each endpoint's contribution alongside */ count_t *nodes_endpoint_offset; nodenum_t *endpoint_block; + degree_t *endpoint_delta; /* the nodes we are working with */ nodenum_t *list1; @@ -402,11 +418,12 @@ changeNodeValue(state_t *state, nodenum_t nn, BOOL newv) const count_t ep_offset = state->nodes_endpoint_offset[nn]; const count_t ep_end = state->nodes_endpoint_offset[nn+1]; const nodenum_t *endpoint_block = state->endpoint_block; - count_t *on_degree = state->nodes_on_degree; + const degree_t *endpoint_delta = state->endpoint_delta; + degree_t *on_degree = state->nodes_on_degree; if (newv) { for (count_t g = ep_offset; g < ep_end; g++) - on_degree[endpoint_block[g]]++; + on_degree[endpoint_block[g]] += endpoint_delta[g]; /* * the transistors are now on, so the nodes on either side of one @@ -425,7 +442,7 @@ changeNodeValue(state_t *state, nodenum_t nn, BOOL newv) */ for (count_t g = ep_offset; g < ep_end; g++) { const nodenum_t e = endpoint_block[g]; - on_degree[e]--; + on_degree[e] -= endpoint_delta[g]; listout_add(state, e); } } @@ -435,15 +452,21 @@ static inline void recalcNode(state_t *state, nodenum_t node) { /* - * A node that no turned-on transistor connects to anything is a group of - * its own, so the whole group walk collapses to reading its on-degree. - * Its own pulldown or pullup then decides the value, and a node with - * neither of those keeps the value it already has - it cannot change at - * all. + * A node that no turned-on transistor connects to an ordinary node is a + * group of its own, whatever it may be tied to on the power rails, so the + * whole group walk collapses to reading its on-degree. What is left + * decides the value the same way addNodeToGroup would have: vss beats vcc, + * vcc beats the node's own pulldown or pullup, and a node with neither of + * those keeps the value it already has - it cannot change at all. */ - if (state->nodes_on_degree[node] == 0) { + const degree_t deg = state->nodes_on_degree[node]; + if ((deg & DEGREE_OTHER_MASK) == 0) { BOOL newv; - if (get_nodes_pulldown(state, node)) + if (deg & DEGREE_VSS_MASK) + newv = NO; + else if (deg & DEGREE_VCC_MASK) + newv = YES; + else if (get_nodes_pulldown(state, node)) newv = NO; else if (get_nodes_pullup(state, node)) newv = YES; @@ -490,12 +513,15 @@ verify_on_degree(state_t *state) for (nodenum_t n = 0; n < state->nodes; n++) { if (n == state->vss || n == state->vcc) continue; - count_t expected = 0; + degree_t expected = 0; const count_t start = state->nodes_c1c2offset[n]; const count_t end = state->nodes_c1c2offset[n+1]; for (count_t t = start; t < end; t++) - if (get_nodes_value(state, state->nodes_c1c2s[t].gate)) - expected++; + if (get_nodes_value(state, state->nodes_c1c2s[t].gate)) { + const nodenum_t other = state->nodes_c1c2s[t].other_node; + expected += (other == state->vss) ? DEGREE_VSS : + (other == state->vcc) ? DEGREE_VCC : DEGREE_OTHER; + } assert(state->nodes_on_degree[n] == expected); } } @@ -575,8 +601,8 @@ add_nodes_dependant(state_t *state, nodenum_t a, nodenum_t b, nodenum_t *counts, block_dep_size = 3239 endpoint_block_size = 4021 - Working set = 92 KB allocations, 220 KB binary, plus system libs and text buffering - = 607 KB in release build + Working set = 111 KB allocations, 220 KB binary, plus system libs and text buffering + = 626 KB in release build */ state_t * setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nodenum_t nodes, nodenum_t transistors, nodenum_t vss, nodenum_t vcc) @@ -598,11 +624,11 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod /* All node values start out low, so every transistor is off and every on-degree is zero. vss and vcc are deliberately left out of the endpoint list and never have a value of their own assigned, so their counters - would sit at zero forever and wrongly qualify them as inert - park them - on a value that keeps them out of the fast path for good. */ + would sit at zero forever and wrongly qualify them as a group of one - + park them on a value that keeps them out of the fast path for good. */ state->nodes_on_degree = calloc(state->nodes, sizeof(*state->nodes_on_degree)); - state->nodes_on_degree[vss] = 1; - state->nodes_on_degree[vcc] = 1; + state->nodes_on_degree[vss] = DEGREE_OTHER; + state->nodes_on_degree[vcc] = DEGREE_OTHER; /* group content depends on active state, not easy to predict actual size needed */ state->group = calloc(state->nodes, sizeof(*state->group)); @@ -765,8 +791,13 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod } state->nodes_endpoint_offset[state->nodes] = endpoint_index; /* fill the end entry, so we can calculate distances/counts */ state->endpoint_block = calloc(endpoint_index, sizeof(*state->endpoint_block)); + state->endpoint_delta = calloc(endpoint_index, sizeof(*state->endpoint_delta)); endpoint_index = 0; + /* what an endpoint contributes to its own on-degree is decided by what is + on the far side of the transistor, not by the endpoint itself */ +#define ENDPOINT_DELTA(far) ((far) == vss ? DEGREE_VSS : (far) == vcc ? DEGREE_VCC : DEGREE_OTHER) + /* Copy dependencies into smaller data structures */ for (i = 0; i < state->nodes; i++) { nodes_left_dep_count[i] = 0; @@ -774,11 +805,13 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod nodenum_t g_end = g_start + nodes_gatecount[i]; for (nodenum_t t = g_start; t < g_end; t++) { nodenum_t c1 = transistors_c1[t]; + nodenum_t c2 = transistors_c2[t]; if (c1 != vss && c1 != vcc) { + state->endpoint_delta[endpoint_index] = ENDPOINT_DELTA(c2); state->endpoint_block[endpoint_index++] = c1; } - nodenum_t c2 = transistors_c2[t]; if (c2 != vss && c2 != vcc) { + state->endpoint_delta[endpoint_index] = ENDPOINT_DELTA(c1); state->endpoint_block[endpoint_index++] = c2; } if (c1 != vss && c1 != vcc) { @@ -789,6 +822,8 @@ setupNodesAndTransistors(netlist_transdefs *transdefs, BOOL *node_is_pullup, nod } } +#undef ENDPOINT_DELTA + /* these are unused after initialization */ free(nodes_endpoint_count); nodes_endpoint_count = NULL; @@ -828,6 +863,7 @@ destroyNodesAndTransistors(state_t *state) free(state->nodes_on_degree); free(state->nodes_endpoint_offset); free(state->endpoint_block); + free(state->endpoint_delta); free(state->list1); free(state->list2); free(state->listout_bitmap);