From: Craig Gallek <redacted>
Before the delete operator was added, this datastructure maintained
an invariant that intermediate nodes were only present when necessary
to build the tree. This patch updates the delete operation to reinstate
that invariant by removing unnecessary intermediate nodes after a node is
removed and thus keeping the tree structure at a minimal size.
Suggested-by: Daniel Mack <daniel@zonque.org>
Signed-off-by: Craig Gallek <redacted>
---
kernel/bpf/lpm_trie.c | 55 +++++++++++++++++++++++++++------------------------
1 file changed, 29 insertions(+), 26 deletions(-)
@@ -408,14 +408,12 @@ static int trie_delete_elem(struct bpf_map *map, void *_key)/* Walk the tree looking for an exact key/length match and keeping*trackofwherewecouldbegintrimmingthetree.Thetrim-point-*isthesub-treealongthewalkconsistingofonlysingle-child-*intermediatenodesandendingataleafnodethatwewantto-*remove.+*isthelocationofthepointerwherewewillremoveanodefromthe+*tree.*/trim=&trie->root;-node=rcu_dereference_protected(-trie->root,lockdep_is_held(&trie->lock));-while(node){+while((node=rcu_dereference_protected(+*trim,lockdep_is_held(&trie->lock)))){matchlen=longest_prefix_match(trie,node,key);if(node->prefixlen!=matchlen||
@@ -423,15 +421,7 @@ static int trie_delete_elem(struct bpf_map *map, void *_key)break;next_bit=extract_bit(key->data,node->prefixlen);-/* If we hit a node that has more than one child or is a valid-*prefixitself,donotremoveit.Resettherootofthetrim-*pathtoitsdescendantonourpath.-*/-if(!(node->flags&LPM_TREE_NODE_FLAG_IM)||-(node->child[0]&&node->child[1]))-trim=&node->child[next_bit];-node=rcu_dereference_protected(-node->child[next_bit],lockdep_is_held(&trie->lock));+trim=&node->child[next_bit];}if(!node||node->prefixlen!=key->prefixlen||
@@ -442,25 +432,38 @@ static int trie_delete_elem(struct bpf_map *map, void *_key)trie->n_entries--;-/* If the node we are removing is not a leaf node, simply mark it+/* If the node we are removing has two children, simply mark it*asintermediateandwearedone.*/-if(rcu_access_pointer(node->child[0])||+if(rcu_access_pointer(node->child[0])&&rcu_access_pointer(node->child[1])){node->flags|=LPM_TREE_NODE_FLAG_IM;gotoout;}-/* trim should now point to the slot holding the start of a path from-*zeroormoreintermediatenodestoourleafnodefordeletion.-*/-while((node=rcu_dereference_protected(-*trim,lockdep_is_held(&trie->lock)))){+/* If the node has no children, it can be completely removed */+if(!rcu_access_pointer(node->child[0])&&+!rcu_access_pointer(node->child[1])){RCU_INIT_POINTER(*trim,NULL);-trim=rcu_access_pointer(node->child[0])?-&node->child[0]:-&node->child[1];kfree_rcu(node,rcu);+gotoout;+}++/* If the node has one child, we may be able to collapse the tree+*whileremovingthisnodeifthenode'schildisinthesame+*'nextbit'slotasthisnodewasinitsparentorifthenode+*itselfistheroot.+*/+if(trim==&trie->root){+next_bit=node->child[0]?0:1;+rcu_assign_pointer(trie->root,node->child[next_bit]);+kfree_rcu(node,rcu);+}elseif(rcu_access_pointer(node->child[next_bit])){+rcu_assign_pointer(*trim,node->child[next_bit]);+kfree_rcu(node,rcu);+}else{+/* If we can't collapse, just mark this node as intermediate */+node->flags|=LPM_TREE_NODE_FLAG_IM;}out:
This default assignment seems wrong, and I guess you only added it to
squelch a compiler warning?
[...]
+ /* If the node has one child, we may be able to collapse the tree
+ * while removing this node if the node's child is in the same
+ * 'next bit' slot as this node was in its parent or if the node
+ * itself is the root.
+ */
+ if (trim == &trie->root) {
+ next_bit = node->child[0] ? 0 : 1;
+ rcu_assign_pointer(trie->root, node->child[next_bit]);
+ kfree_rcu(node, rcu);
I don't think you should treat this 'root' case special.
Instead, move the 'next_bit' assignment outside of the condition ...
This default assignment seems wrong, and I guess you only added it to
squelch a compiler warning?
Yes, this variable is only initialized after the lookup iterations
below (meaning it will never be initialized the the root-removal
case).
[...]
quoted
+ /* If the node has one child, we may be able to collapse the tree
+ * while removing this node if the node's child is in the same
+ * 'next bit' slot as this node was in its parent or if the node
+ * itself is the root.
+ */
+ if (trim == &trie->root) {
+ next_bit = node->child[0] ? 0 : 1;
+ rcu_assign_pointer(trie->root, node->child[next_bit]);
+ kfree_rcu(node, rcu);
I don't think you should treat this 'root' case special.
Instead, move the 'next_bit' assignment outside of the condition ...
I'm not quite sure I follow. Are you saying do something like this:
if (trim == &trie->root) {
next_bit = node->child[0] ? 0 : 1;
}
if (rcu_access_pointer(node->child[next_bit])) {
...
This would save a couple lines of code, but I think the as-is
implementation is slightly easier to understand. I don't have a
strong opinion either way, though.
Thanks for the pointers,
Craig
This default assignment seems wrong, and I guess you only added it to
squelch a compiler warning?
Yes, this variable is only initialized after the lookup iterations
below (meaning it will never be initialized the the root-removal
case).
Right, and once set, it's only updated in case we don't have an exact
match and try to drill down further.
quoted
[...]
quoted
+ /* If the node has one child, we may be able to collapse the tree
+ * while removing this node if the node's child is in the same
+ * 'next bit' slot as this node was in its parent or if the node
+ * itself is the root.
+ */
+ if (trim == &trie->root) {
+ next_bit = node->child[0] ? 0 : 1;
+ rcu_assign_pointer(trie->root, node->child[next_bit]);
+ kfree_rcu(node, rcu);
I don't think you should treat this 'root' case special.
Instead, move the 'next_bit' assignment outside of the condition ...
I'm not quite sure I follow. Are you saying do something like this:
if (trim == &trie->root) {
next_bit = node->child[0] ? 0 : 1;
}
if (rcu_access_pointer(node->child[next_bit])) {
...
This would save a couple lines of code, but I think the as-is
implementation is slightly easier to understand. I don't have a
strong opinion either way, though.
Me neither :)
My idea was to set
next_bit = node->child[0] ? 0 : 1;
unconditionally, because it should result in the same in both cases.
It might be a bit of bike shedding, but I dislike this default
assignment, and I believe that not relying on next_bit to be set as a
side effect of the lookup loop makes the code a bit more readable.
WDYT?
Thanks,
Daniel
This default assignment seems wrong, and I guess you only added it to
squelch a compiler warning?
Yes, this variable is only initialized after the lookup iterations
below (meaning it will never be initialized the the root-removal
case).
Right, and once set, it's only updated in case we don't have an exact
match and try to drill down further.
quoted
quoted
[...]
quoted
+ /* If the node has one child, we may be able to collapse the tree
+ * while removing this node if the node's child is in the same
+ * 'next bit' slot as this node was in its parent or if the node
+ * itself is the root.
+ */
+ if (trim == &trie->root) {
+ next_bit = node->child[0] ? 0 : 1;
+ rcu_assign_pointer(trie->root, node->child[next_bit]);
+ kfree_rcu(node, rcu);
I don't think you should treat this 'root' case special.
Instead, move the 'next_bit' assignment outside of the condition ...
I'm not quite sure I follow. Are you saying do something like this:
if (trim == &trie->root) {
next_bit = node->child[0] ? 0 : 1;
}
if (rcu_access_pointer(node->child[next_bit])) {
...
This would save a couple lines of code, but I think the as-is
implementation is slightly easier to understand. I don't have a
strong opinion either way, though.
Me neither :)
My idea was to set
next_bit = node->child[0] ? 0 : 1;
unconditionally, because it should result in the same in both cases.
It might be a bit of bike shedding, but I dislike this default
assignment, and I believe that not relying on next_bit to be set as a
side effect of the lookup loop makes the code a bit more readable.
WDYT?
That sounds reasonable. I'll spin a v2 today if no one else has any comments.
Thanks again,
Craig