We have something that looks like this:
abstract class TreeNode {
internal abstract Branches GetChildren();
}
class Leaf : TreeNode {
internal override Branches GetChildren() => Branches.Empty();
}
class UnaryTreeNode : TreeNode {
TreeNode child;
internal override Branches GetChildren()
{
}
}
class BinaryTreeNode: TreeNode {
TreeNode left;
TreeNode right;
internal override Branches GetChildren()
{
}
}
class VariadicTreeNode : TreeNode {
TreeNode[] nodes;
internal override Branches GetChildren()
{
}
}
These are still base classes; literally hundreds of classes inherit from the classes shown here.
So the obvious simple implementation is Branches is IReadOnlyList<TreeNode>; but this imposes an allocation penalty. We suppose it would be possible to reduce the number of allocations on descending the tree to 0 by some means. I chased returning a ReadOnlyMemory<TreeNode> but that isn't possible even by non-public reflection because the anchor of a ReadOnlyMemory<T> isn't actually arbitrary.
I turned around and chased a ReadOnlySpan<T> and found a plausible approach: MemoryMarshal.CreateReadOnlySpan<T>. If I understand the comments correctly; the resulting Span from MemoryMarshal doesn't old a reference to its container, so the trivial implementation does not work.
So I'm left wondering: does the following work:
ref struct Branches {
internal object Anchor {get;init;}
internal ReadOnlySpan<TreeNode> Span {get;init;}
}
abstract class TreeNode {
internal abstract Branches GetChildren();
}
class Leaf : TreeNode {
internal override Branches GetChildren() => default;
}
class UnaryTreeNode : TreeNode {
TreeNode child;
internal override Branches GetChildren()
{
var span = System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan(ref child, 1);
return new Branches { Anchor = this, Span = span };
}
}
class BinaryTreeNode: TreeNode {
TreeNode left;
TreeNode right;
internal override Branches GetChildren()
{
var span = System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan(ref left, 2);
return new Branches { Anchor = this, Span = span };
}
}
class VariadicTreeNode : TreeNode {
TreeNode[] nodes;
internal override Branches GetChildren()
{
return new Branches { Anchor = nodes, Span = nodes };
}
}
A simple trial will show it working; however I can't validate if this code is sane directly: what happens when the object is moved by the garbage collector?
I have benchmarked the Span implementation; it's between 10% and 20% faster than the obvious array implementation.
leftandrightin adjacent locations in the memory allocated for theBinaryTreeNodeclass. Assuming that it will do that means you're relying on an implementation detail that could change without notice to you, and accessing index 1 suddenly gives you something other thanright(possibly not even an object reference). Look into[InlineArray].[InlineArray]will yield reliable adjacency because it's specifically intended to be array-like.[InlineArray]of length 2 is probably more sensible than assuming anything about field ordering. See e.g. Workaround on declaring a unsafe fixed custom struct array?.MemoryMarshal.CreateReadOnlySpan()at all. ForUnaryTreeNodeyou can make a 1-element span directly from arefvalue like so:Span = new(ref child). And if you use an inline array you can make a span directly from it, see dotnetfiddle.net/MMAzrp. And now, since you're never using unsafe code, MSFT is guaranteeing that GC references are properly tracked.