Skip to content

ytypes: optimize single-key list lookup in retrieveNodeList to O(1) - #1111

Closed
alshabib wants to merge 1 commit into
masterfrom
optimize-single-key-list-lookup
Closed

alshabib wants to merge 1 commit into
masterfrom
optimize-single-key-list-lookup

Conversation

@alshabib

Copy link
Copy Markdown

Currently, retrieveNodeList iterates over all map keys via rv.MapKeys() and invokes getKeyValue (which reflects over every struct field and parses YANG path struct tags) for every existing list element on each ytypes.SetNode / retrieveNode call. Inserting N elements into a YANG list therefore performs O(N^2) struct-tag parses and reflection lookups.

For single-keyed lists (where the map key is not a struct) and non-wildcard/non-partial-key queries:

  1. Convert the gNMI path key string to the map key's reflect.Type via StringToType and perform a direct O(1) rv.MapIndex(kfv) lookup.
  2. When the element is absent and modifyRoot is set, insert the new entry directly via insertAndGetKey without scanning existing keys.
  3. For types where StringToType cannot directly construct the map key (e.g., union interface keys), pre-compute the key field struct index once outside the rv.MapKeys() loop instead of calling getKeyValue on every element.

Currently, retrieveNodeList iterates over all map keys via rv.MapKeys()
and invokes getKeyValue (which reflects over every struct field and
parses YANG path struct tags) for every existing list element on each
ytypes.SetNode / retrieveNode call. Inserting N elements into a YANG
list therefore performs O(N^2) struct-tag parses and reflection lookups.

For single-keyed lists (where the map key is not a struct) and
non-wildcard/non-partial-key queries:
1. Convert the gNMI path key string to the map key's reflect.Type via
   StringToType and perform a direct O(1) rv.MapIndex(kfv) lookup.
2. When the element is absent and modifyRoot is set, insert the new
   entry directly via insertAndGetKey without scanning existing keys.
3. For types where StringToType cannot directly construct the map key
   (e.g., union interface keys), pre-compute the key field struct index
   once outside the rv.MapKeys() loop instead of calling getKeyValue on
   every element.
@alshabib
alshabib requested a review from robshakir October 19, 2026 12:54
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 88.444% (-0.3%) from 88.758% — optimize-single-key-list-lookup into master

Comment thread ytypes/node.go
schemaKeyMap[fieldName] = schemaKey
listKeyFieldNameMap[i] = fieldName
}
} else if rv.Len() > 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is a bit of a mess. I'll add comments throughout.

Comment thread ytypes/node.go
schemaKeyMap[fieldName] = schemaKey
listKeyFieldNameMap[i] = fieldName
}
} else if rv.Len() > 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this is an else if -- if the list key type was a struct (as above), then we handle the fields above. I understand they are mutually exclusive, but it seems like we should make this clearer to understand (at least with some comments).

Comment thread ytypes/node.go

// Reduce FieldByName calls in the loop and reduce Field calls and tag lookup calls in the loop.
singleKeyFieldIdx := -1
if util.IsTypeStruct(listKeyT) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest here that we have a check that says:

if rv.Len() == 0 {
  // we've got a 0 length map the root value -- it seems
  // like we should just be able to return nothing since there are
  // no nodes that match the keys that we specified.
}

switch {
case util.IsTypeStruct(listKeyT):
  // handle struct type
default:
  // we know here that we have a length > 0 so we can just go ahead and handle this case.
}

This needs checking though, since there seems to be a case that we're handling transitory cases later down the function -- but still, if there's nothing in the map, we don't have any list elements to traverse.

Comment thread ytypes/node.go
if !ok {
return nil, status.Errorf(codes.NotFound, "schema key %s is not found in gNMI path %v, root %T", schema.Key, path, root)
}
if kfv, err := StringToType(listKeyT, pathKey); err == nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should prefer to handle the err != nil case here, and understand what it is we want to do at that point.

Almost all paths inside this function seem to return just very late in the code -- not close to when we do the check.

Comment thread ytypes/node.go
schemaKeyMap[fieldName] = schemaKey
listKeyFieldNameMap[i] = fieldName
}
} else if rv.Len() > 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function should be refactored out -- since it seems like we're saying that there's a shortcut that we can take when we're not handling wildcard keys.

Can you split the function that we have:

func retrieveNodeList(...) {
  // validity checks

  switch {
  // check whether we are handling wildcards
  case noWildcards:
     return retrieveNodeListConcrete(...)
  case wildCards:
     return retrieveNodeListWildcard(...)
  }
}

Since this doesn't add a bunch of lines into an already very long function.

@robshakir

Copy link
Copy Markdown
Member

@alshabib Can you PTAL at #1112 and #1113 ? I had some strong feelings about code structure, and implemented them here -- PTAL as to whether this meets your requirement. If so, can you review that code?

@robshakir

Copy link
Copy Markdown
Member

TLDR: I created #1112 as an alternative to #1111, and I added benchmarks in #1113. Across all benchmarks, #1113 is ~39% faster than #1111 and uses ~38% fewer allocations.

benchstat Results at N = 1000 (#1111 vs. #1113)

                                           │   pr1111    │               pr1113                │
                                           │   sec/op    │   sec/op     vs base                │
SetNodeSingleKeyList/n=1000-16               35.11m ±22%   34.35m ± 7%        ~ (p=0.937 n=6)
GetNodeSingleKeyList/n=1000-16               22.67µ ± 3%   23.27µ ±13%        ~ (p=0.180 n=6)
GetOrCreateNodeSingleKeyList/n=1000-16       23.91µ ± 4%   25.61µ ±57%        ~ (p=0.093 n=6)
DeleteNodeSingleKeyList/n=1000-16            4.506m ± 4%   4.418m ± 4%        ~ (p=0.310 n=6)
SetNodeMultiKeyList/n=1000-16               233.66m ±17%   51.03m ±13%  -78.16% (p=0.002 n=6)
GetNodeMultiKeyList/n=1000-16               390.61µ ±13%   30.54µ ±32%  -92.18% (p=0.002 n=6)
GetOrCreateNodeMultiKeyList/n=1000-16       392.05µ ±16%   31.59µ ±13%  -91.94% (p=0.002 n=6)
DeleteNodeMultiKeyList/n=1000-16           125.978m ± 5%   7.969m ±12%  -93.67% (p=0.002 n=6)
SetNodeDecimal64SingleKeyList/n=1000-16     259.11m ± 4%   20.55m ± 7%  -92.07% (p=0.002 n=6)
GetNodeDecimal64SingleKeyList/n=1000-16    279.912µ ±68%   9.519µ ± 2%  -96.60% (p=0.002 n=6)
DeleteNodeDecimal64SingleKeyList/n=1000-16 138.037m ± 4%   3.599m ± 2%  -97.39% (p=0.002 n=6)

@alshabib -- please can you review #1112 and #1113 and let me know what you think?

Thanks,
r.

robshakir added a commit that referenced this pull request Oct 23, 2026
…to O(1) (#1112)

* ytypes: refactor retrieveNodeList and optimize concrete list lookups to O(1)

Proposed alternative implementation to #1111 addressing review comments in #1111 (review):

* **`ytypes/node.go`**:
  - Split `retrieveNodeList` into upfront validity checks and a `switch` via `isWildcardListQuery` dispatching to `retrieveNodeListWildcard` vs. `retrieveNodeListConcrete`.
  - Handle `rv.Len() == 0` upfront in both `retrieveNodeListConcrete` (inserting directly via `insertAndRetrieveNodeList` when `args.modifyRoot` is set, and returning `nil, nil` otherwise) and `retrieveNodeListWildcard`.
  - Replace the chained `else if rv.Len() > 0` with an explicit `switch` on `listKeyT` (`util.IsTypeStruct`, `util.IsTypeInterface`, `default`).
  - Handle `StringToType` errors immediately at the call site by returning `codes.InvalidArgument`, rather than ignoring `err != nil` and falling through to full-map iteration.
  - Extend $O(1)$ `rv.MapIndex` lookup to concrete multi-keyed lists (`retrieveNodeListConcreteStructKey`) in addition to single-keyed lists.
  - Deduplicate list-element delete/prune and insert-and-recurse logic into `retrieveNodeListElem`, `insertAndRetrieveNodeList`, and `structKeyFields`.
* **`ytypes/util_types.go`**:
  - Extend `StringToType` to handle `reflect.Float64` (`decimal64` keys, rejecting non-finite `NaN`/`Inf`) and `.Convert(t)` on `reflect.String` and `reflect.Bool` (for defined types such as `YANGEmpty`).
* **`ytypes/node_test.go`, `ytypes/util_test.go`**:
  - Add unit tests covering `StringToType`, concrete/wildcard single-key and multi-key lookups, `decimal64` and wrapper-union keys, immediate `InvalidArgument` errors on malformed keys, `GetOrCreateNode` on union-keyed lists, and batch-delete transitory states.
  - Add `SetNode` and `GetNode` benchmarks for single-keyed and multi-keyed lists (`n = 10, 100, 1000`).

* Consolidate GetNode list test cases into TestGetNode.

* ytypes: align list lookup refactor and tests with robjs coding style

* ytypes: add GetOrCreateNode and DeleteNode single/multi-key benchmarks

Add BenchmarkGetOrCreateNode{Single,Multi}KeyList and
BenchmarkDeleteNode{Single,Multi}KeyList alongside SetNode and GetNode
benchmarks across n = 10, 100, 1000, and cycle GetNode/GetOrCreateNode
queries uniformly across all populated list keys.

* ytypes: add 5% allocation regression test sharing list benchmark workloads

Share the single-keyed and multi-keyed SetNode, GetNode,
GetOrCreateNode, and DeleteNode workloads between Benchmark* and
TestListTraversalPerformance, encoding baseline heap-allocation
constants and failing if any case regresses by more than 5%.

* ytypes: extract wildcardKey constant and move list benchmarks to stacked PR

* ytypes: add list traversal benchmarks and 5% allocation regression tests (#1113)
@robshakir

Copy link
Copy Markdown
Member

Closing as we merged the other implementation.

@robshakir robshakir closed this Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants