Skip to content

Commit 39e0fd9

Browse files
smolkajclaude
andcommitted
[NetKAT] Port the pointer-keyed unique table to PacketSetManager; polish flat-node ergonomics.
Three follow-ups to the flat-node work: * PacketSetManager's unique table now stores stable pointers into the node arena instead of full DecisionNode copies, with transparent by-value lookup — the same design just applied to the transformer manager, and more impactful here since packet-set nodes (every Match and every And/Or/Not result) far outnumber transformer nodes. * The transformer's NodeHash streams the canonical flat sequence through a single absl hash state (via an AbslHashValue adapter) instead of re-seeding absl::HashOf once per element. * DecisionNode::Matches() iterates the "if" branches as (value, modifies-span) views, replacing the index-loop + MatchModifies(i) ceremony at six call sites. With these, all five transformer benchmarks (including the new Push/Pull read-path benchmark) run ahead of main: ReCompile* -6%, FirstTimeCompile* -1 to -3%, PushAndPull -2% (min-of-4 interleaved optimized runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cca6a1f commit 39e0fd9

6 files changed

Lines changed: 151 additions & 71 deletions

File tree

netkat/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ cc_library(
132132
"@com_google_absl//absl/container:fixed_array",
133133
"@com_google_absl//absl/container:flat_hash_map",
134134
"@com_google_absl//absl/container:flat_hash_set",
135+
"@com_google_absl//absl/hash",
135136
"@com_google_absl//absl/log",
136137
"@com_google_absl//absl/log:check",
137138
"@com_google_absl//absl/status",
@@ -375,6 +376,7 @@ cc_library(
375376
"@com_google_absl//absl/container:fixed_array",
376377
"@com_google_absl//absl/container:flat_hash_map",
377378
"@com_google_absl//absl/container:flat_hash_set",
379+
"@com_google_absl//absl/hash",
378380
"@com_google_absl//absl/functional:any_invocable",
379381
"@com_google_absl//absl/log",
380382
"@com_google_absl//absl/log:check",

netkat/packet_set.cc

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "absl/algorithm/container.h"
2525
#include "absl/container/fixed_array.h"
2626
#include "absl/container/flat_hash_set.h"
27+
#include "absl/hash/hash.h"
2728
#include "absl/log/check.h"
2829
#include "absl/log/log.h"
2930
#include "absl/status/status.h"
@@ -87,6 +88,14 @@ const PacketSetManager::DecisionNode& PacketSetManager::GetNodeOrDie(
8788
return nodes_[packet_set.node_index_];
8889
}
8990

91+
size_t PacketSetManager::NodeHash::operator()(const DecisionNode* node) const {
92+
return absl::HashOf(*node);
93+
}
94+
95+
size_t PacketSetManager::NodeHash::operator()(const DecisionNode& node) const {
96+
return absl::HashOf(node);
97+
}
98+
9099
PacketSetHandle PacketSetManager::NodeToPacket(DecisionNode&& node) {
91100
if (node.branch_by_field_value.empty()) return node.default_branch;
92101

@@ -109,16 +118,19 @@ PacketSetHandle PacketSetManager::NodeToPacket(DecisionNode&& node) {
109118
}
110119
#endif
111120

112-
auto [it, inserted] =
113-
packet_by_node_.try_emplace(node, PacketSetHandle(nodes_.size()));
114-
if (inserted) {
115-
nodes_.push_back(std::move(node));
116-
LOG_IF(DFATAL, nodes_.size() > SentinelNodeIndex::kMinSentinel)
117-
<< "Internal invariant violated: Proper and sentinel node indices must "
118-
"be disjoint. This indicates that we allocated more nodes than are "
119-
"supported (> 2^32 - 2).";
121+
// Look up the node by value via the transparent `NodeHash`/`NodeEq`
122+
// functors; only new nodes get stored (exactly once, in `nodes_`).
123+
if (auto it = packet_by_node_.find(node); it != packet_by_node_.end()) {
124+
return it->second;
120125
}
121-
return it->second;
126+
PacketSetHandle packet(nodes_.size());
127+
nodes_.push_back(std::move(node));
128+
packet_by_node_.insert({&nodes_[packet.node_index_], packet});
129+
LOG_IF(DFATAL, nodes_.size() > SentinelNodeIndex::kMinSentinel)
130+
<< "Internal invariant violated: Proper and sentinel node indices must "
131+
"be disjoint. This indicates that we allocated more nodes than are "
132+
"supported (> 2^32 - 2).";
133+
return packet;
122134
}
123135

124136
bool PacketSetManager::Contains(PacketSetHandle packet_set,
@@ -483,14 +495,19 @@ absl::Status PacketSetManager::CheckInternalInvariants() const {
483495
// Invariant: Proper and sentinel node indices are disjoint.
484496
RET_CHECK(nodes_.size() <= SentinelNodeIndex::kMinSentinel);
485497

486-
// Invariant: `packet_by_node_[n] = s` iff `nodes_[s.node_index_] == n`.
487-
for (const auto& [node, packet] : packet_by_node_) {
498+
// Invariant: `packet_by_node_[p] = s` iff `p == &nodes_[s.node_index_]`.
499+
for (const auto& [node_ptr, packet] : packet_by_node_) {
488500
RET_CHECK(packet.node_index_ < nodes_.size());
489-
RET_CHECK(nodes_[packet.node_index_] == node);
501+
RET_CHECK(node_ptr == &nodes_[packet.node_index_]);
490502
}
491503
for (int i = 0; i < nodes_.size(); ++i) {
492504
const DecisionNode& node = nodes_[i];
493-
auto it = packet_by_node_.find(node);
505+
// Look up both by pointer and by value (exercising the transparent
506+
// functors used by `NodeToPacket`).
507+
auto it = packet_by_node_.find(&node);
508+
RET_CHECK(it != packet_by_node_.end());
509+
RET_CHECK(it->second == PacketSetHandle(i));
510+
it = packet_by_node_.find(node);
494511
RET_CHECK(it != packet_by_node_.end());
495512
RET_CHECK(it->second == PacketSetHandle(i));
496513
}

netkat/packet_set.h

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,38 @@ class PacketSetManager {
356356
// `And`, `Or`, `Not`). The class also avoids expensive relocations.
357357
PagedStableVector<DecisionNode, kPageSize> nodes_;
358358

359+
// Transparent hash and equality functors for the unique table
360+
// (`packet_by_node_`), which is keyed by stable `DecisionNode*` pointers
361+
// into `nodes_` (so each node is stored only once). Lookups work directly
362+
// with a not-yet-interned `DecisionNode` value. Both functors are
363+
// stateless: keys are pointers, and the pages holding the nodes are stable
364+
// across moves of the manager.
365+
struct NodeHash {
366+
using is_transparent = void;
367+
size_t operator()(const DecisionNode* node) const;
368+
size_t operator()(const DecisionNode& node) const;
369+
};
370+
struct NodeEq {
371+
using is_transparent = void;
372+
bool operator()(const DecisionNode* a, const DecisionNode* b) const {
373+
return a == b || *a == *b;
374+
}
375+
bool operator()(const DecisionNode* a, const DecisionNode& b) const {
376+
return *a == b;
377+
}
378+
bool operator()(const DecisionNode& a, const DecisionNode* b) const {
379+
return a == *b;
380+
}
381+
};
382+
359383
// A so called "unique table" to ensure each node is only added to `nodes_`
360384
// once, and thus has a unique `PacketSetHandle::node_index`.
385+
// Keyed by pointers into `nodes_` (stable, see `PagedStableVector`), so
386+
// nodes are not stored twice.
361387
//
362-
// INVARIANT: `packet_by_node_[n] = s` iff `nodes_[s.node_index_] == n`.
363-
absl::flat_hash_map<DecisionNode, PacketSetHandle> packet_by_node_;
388+
// INVARIANT: `packet_by_node_[p] = s` iff `p == &nodes_[s.node_index_]`.
389+
absl::flat_hash_map<const DecisionNode*, PacketSetHandle, NodeHash, NodeEq>
390+
packet_by_node_;
364391

365392
// A map of a given `PredicateProto` to a `PacketSetHandle`.
366393
//

netkat/packet_transformer.cc

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -180,35 +180,14 @@ PacketTransformerManager::DecisionNode PacketTransformerManager::Flatten(
180180
return node;
181181
}
182182

183-
template <typename NodeOrBuilder>
184-
size_t PacketTransformerManager::NodeHash::HashOfFlatSequence(
185-
const NodeOrBuilder& node) {
186-
size_t num_matches = 0;
187-
size_t num_modifies = 0;
188-
size_t hash = absl::HashOf(node.field, node.default_branch);
189-
ForEachFlatEntry(
190-
node,
191-
[&](int match_value, uint32_t end_offset) {
192-
hash = absl::HashOf(hash, match_value, end_offset);
193-
++num_matches;
194-
return true;
195-
},
196-
[&](int modify_value, PacketTransformerHandle branch) {
197-
hash = absl::HashOf(hash, modify_value, branch);
198-
++num_modifies;
199-
return true;
200-
});
201-
return absl::HashOf(hash, num_matches, num_modifies);
202-
}
203-
204183
size_t PacketTransformerManager::NodeHash::operator()(
205184
const DecisionNode* node) const {
206-
return HashOfFlatSequence(*node);
185+
return absl::HashOf(FlatSequenceView<DecisionNode>{*node});
207186
}
208187

209188
size_t PacketTransformerManager::NodeHash::operator()(
210189
const DecisionNodeBuilder& builder) const {
211-
return HashOfFlatSequence(builder);
190+
return absl::HashOf(FlatSequenceView<DecisionNodeBuilder>{builder});
212191
}
213192

214193
bool PacketTransformerManager::NodeEq::operator()(
@@ -240,10 +219,10 @@ PacketTransformerManager::ToBuilder(const DecisionNode& node) {
240219
.field = node.field,
241220
.default_branch = node.default_branch,
242221
};
243-
for (size_t i = 0; i < node.matches.size(); ++i) {
222+
for (const DecisionNode::Match& match : node.Matches()) {
244223
absl::btree_map<int, PacketTransformerHandle>& modify_branch_by_value =
245-
builder.modify_branch_by_field_match[node.matches[i].first];
246-
for (const DecisionNode::ModifyEntry& entry : node.MatchModifies(i)) {
224+
builder.modify_branch_by_field_match[match.value];
225+
for (const DecisionNode::ModifyEntry& entry : match.modifies) {
247226
modify_branch_by_value.insert(modify_branch_by_value.end(), entry);
248227
}
249228
}
@@ -950,8 +929,8 @@ PacketSetHandle PacketTransformerManager::GetAllPossibleOutputPackets(
950929
// Implements the `b_B` in the `fwd` function in section C.3 Push and Pull
951930
// in KATch: A Fast Symbolic Verifier for NetKAT.
952931
absl::flat_hash_set<int> branch_modify_values;
953-
for (size_t i = 0; i < node.matches.size(); ++i) {
954-
for (const auto& [modify_value, branch] : node.MatchModifies(i)) {
932+
for (const DecisionNode::Match& match : node.Matches()) {
933+
for (const auto& [modify_value, branch] : match.modifies) {
955934
branch_modify_values.insert(modify_value);
956935
add_to_output_by_field_value(modify_value,
957936
GetAllPossibleOutputPackets(branch));
@@ -1035,13 +1014,13 @@ PacketTransformerManager::GetAllInputPacketsThatProduceAnyOutput(
10351014
// Implements the `b_A` in the `bwd` function in section C.3 Push and Pull
10361015
// in KATch: A Fast Symbolic Verifier for NetKAT.
10371016
absl::flat_hash_map<int, PacketSetHandle> branch_by_field_value_map;
1038-
for (size_t i = 0; i < node.matches.size(); ++i) {
1017+
for (const DecisionNode::Match& match : node.Matches()) {
10391018
PacketSetHandle union_of_branches;
1040-
for (const auto& [modify_value, branch] : node.MatchModifies(i)) {
1019+
for (const auto& [modify_value, branch] : match.modifies) {
10411020
union_of_branches = packet_set_manager_.Or(
10421021
union_of_branches, GetAllInputPacketsThatProduceAnyOutput(branch));
10431022
}
1044-
branch_by_field_value_map[node.matches[i].first] = union_of_branches;
1023+
branch_by_field_value_map[match.value] = union_of_branches;
10451024
}
10461025

10471026
// Case 3: Input packets that do not get matched on an explicit branch, but
@@ -1110,10 +1089,9 @@ std::string PacketTransformerManager::ToString(const DecisionNode& node) const {
11101089
absl::CEscape(
11111090
packet_set_manager_.field_manager_.GetFieldName(node.field)));
11121091

1113-
for (size_t i = 0; i < node.matches.size(); ++i) {
1114-
absl::StrAppendFormat(&result, " %s == %d:\n", field,
1115-
node.matches[i].first);
1116-
pretty_print_map(field, node.MatchModifies(i));
1092+
for (const DecisionNode::Match& match : node.Matches()) {
1093+
absl::StrAppendFormat(&result, " %s == %d:\n", field, match.value);
1094+
pretty_print_map(field, match.modifies);
11171095
}
11181096
absl::StrAppendFormat(&result, " %s == *:\n", field);
11191097
pretty_print_map(field, node.DefaultModifies());
@@ -1160,10 +1138,9 @@ std::string PacketTransformerManager::ToString(
11601138
"%v:'%s'", node.field,
11611139
absl::CEscape(
11621140
packet_set_manager_.field_manager_.GetFieldName(node.field)));
1163-
for (size_t i = 0; i < node.matches.size(); ++i) {
1164-
absl::StrAppendFormat(&result, " %s == %d:\n", field,
1165-
node.matches[i].first);
1166-
pretty_print_map(field, node.MatchModifies(i));
1141+
for (const DecisionNode::Match& match : node.Matches()) {
1142+
absl::StrAppendFormat(&result, " %s == %d:\n", field, match.value);
1143+
pretty_print_map(field, match.modifies);
11671144
}
11681145
absl::StrAppendFormat(&result, " %s == *:\n", field);
11691146
pretty_print_map(field, node.DefaultModifies());
@@ -1216,10 +1193,9 @@ std::string PacketTransformerManager::ToDot(
12161193
packet_set_manager_.field_manager_.GetFieldName(node.field);
12171194
absl::StrAppendFormat(&result, " %d [label=\"%s\"]\n",
12181195
transformer.node_index_, field);
1219-
for (size_t i = 0; i < node.matches.size(); ++i) {
1220-
int value = node.matches[i].first;
1221-
absl::Span<const DecisionNode::ModifyEntry> modify_map =
1222-
node.MatchModifies(i);
1196+
for (const DecisionNode::Match& match : node.Matches()) {
1197+
int value = match.value;
1198+
absl::Span<const DecisionNode::ModifyEntry> modify_map = match.modifies;
12231199
if (modify_map.empty()) {
12241200
absl::StrAppendFormat(&result, " %d -> %d [label=\"%s==%s\"]\n",
12251201
transformer.node_index_, SentinelNodeIndex::kDeny,
@@ -1309,8 +1285,8 @@ absl::Status PacketTransformerManager::CheckInternalInvariants() const {
13091285
}
13101286
return true;
13111287
};
1312-
for (size_t j = 0; j < node.matches.size(); ++j) {
1313-
RET_CHECK(is_strictly_sorted_by_value(node.MatchModifies(j)))
1288+
for (const DecisionNode::Match& match : node.Matches()) {
1289+
RET_CHECK(is_strictly_sorted_by_value(match.modifies))
13141290
<< ":\n"
13151291
<< ToString(node);
13161292
}
@@ -1324,12 +1300,11 @@ absl::Status PacketTransformerManager::CheckInternalInvariants() const {
13241300
<< ":\n"
13251301
<< ToString(node);
13261302

1327-
for (size_t j = 0; j < node.matches.size(); ++j) {
1328-
int match_value = node.matches[j].first;
1329-
for (const auto& [modify_value, branch] : node.MatchModifies(j)) {
1303+
for (const DecisionNode::Match& match : node.Matches()) {
1304+
for (const auto& [modify_value, branch] : match.modifies) {
13301305
// Invariant: Modify branches are not Deny unless `modify_value ==
1331-
// match_value`.
1332-
RET_CHECK(!IsDeny(branch) || modify_value == match_value)
1306+
// match.value`.
1307+
RET_CHECK(!IsDeny(branch) || modify_value == match.value)
13331308
<< ":\n"
13341309
<< ToString(node);
13351310

netkat/packet_transformer.h

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,41 @@ class PacketTransformerManager {
376376
matches[i].second - begin);
377377
}
378378

379+
// A single "if (field == value)" branch: the match value together with
380+
// its ModifyEntry range.
381+
struct Match {
382+
int value;
383+
absl::Span<const ModifyEntry> modifies;
384+
};
385+
386+
// Iterates the "if" branches as `Match` views, in order of strictly
387+
// increasing match value. Allows range-for loops over the branches
388+
// without manual index bookkeeping.
389+
class MatchIterator {
390+
public:
391+
MatchIterator(const DecisionNode* node, size_t index)
392+
: node_(node), index_(index) {}
393+
Match operator*() const {
394+
return {node_->matches[index_].first, node_->MatchModifies(index_)};
395+
}
396+
MatchIterator& operator++() {
397+
++index_;
398+
return *this;
399+
}
400+
friend bool operator==(const MatchIterator& a,
401+
const MatchIterator& b) = default;
402+
403+
private:
404+
const DecisionNode* node_;
405+
size_t index_;
406+
};
407+
struct MatchRange {
408+
const DecisionNode* node;
409+
MatchIterator begin() const { return {node, 0}; }
410+
MatchIterator end() const { return {node, node->matches.size()}; }
411+
};
412+
MatchRange Matches() const { return {this}; }
413+
379414
// The ModifyEntry range of the "else" branch.
380415
absl::Span<const ModifyEntry> DefaultModifies() const {
381416
uint32_t begin = matches.empty() ? 0 : matches.back().second;
@@ -457,11 +492,35 @@ class PacketTransformerManager {
457492
size_t operator()(const DecisionNodeBuilder& builder) const;
458493

459494
private:
460-
// Shared implementation of both overloads: hashes the canonical flat
461-
// element sequence (via `ForEachFlatEntry`), so flat nodes and builders
462-
// with the same logical content hash identically.
495+
// Adapter implementing both overloads: hashes the canonical flat element
496+
// sequence (via `ForEachFlatEntry`) in a single streaming pass, so flat
497+
// nodes and builders with the same logical content hash identically.
463498
template <typename NodeOrBuilder>
464-
static size_t HashOfFlatSequence(const NodeOrBuilder& node);
499+
struct FlatSequenceView {
500+
const NodeOrBuilder& node;
501+
502+
// Hashing, see https://abseil.io/docs/cpp/guides/hash.
503+
template <typename H>
504+
friend H AbslHashValue(H h, const FlatSequenceView& view) {
505+
size_t num_matches = 0;
506+
size_t num_modifies = 0;
507+
h = H::combine(std::move(h), view.node.field,
508+
view.node.default_branch);
509+
ForEachFlatEntry(
510+
view.node,
511+
[&](int match_value, uint32_t end_offset) {
512+
h = H::combine(std::move(h), match_value, end_offset);
513+
++num_matches;
514+
return true;
515+
},
516+
[&](int modify_value, PacketTransformerHandle branch) {
517+
h = H::combine(std::move(h), modify_value, branch);
518+
++num_modifies;
519+
return true;
520+
});
521+
return H::combine(std::move(h), num_matches, num_modifies);
522+
}
523+
};
465524
};
466525
struct NodeEq {
467526
using is_transparent = void;

netkat/packet_transformer_test.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -968,8 +968,8 @@ class PacketTransformerManagerTestPeer {
968968
value);
969969
};
970970
// Case 1: Output from explicit match+modify branches.
971-
for (size_t i = 0; i < node.matches.size(); ++i) {
972-
for (const auto& [modify_value, branch] : node.MatchModifies(i)) {
971+
for (const auto& match : node.Matches()) {
972+
for (const auto& [modify_value, branch] : match.modifies) {
973973
add_to_output(
974974
and_fn(match_fn(field, modify_value),
975975
GetAllPossibleOutputPacketsReferenceImplementation(branch)));

0 commit comments

Comments
 (0)