A Stack data structure implementation using Span

I am back in Microsoft and today we talk about the code below, which is on github here:

  1. public ref struct SpanStack<T>
  2. {
  3.     private Span memory;
  4.     private int index;
  5.     private int size;
  6.     public SpanStack(Span mem) { memory = mem; index = 0; size = mem.Length; }
  7.     public bool IsEmpty() => index < 0;
  8.     public bool IsFull() => index > size – 1;
  9.     public void Push(T item) => memory[index++] = item;
  10.     public T Pop() => memory[–index];
  11. }
  12. public static class SpanExtensions
  13. {
  14.     public static SpanStack AsStack<T>(this Span span) => new SpanStack(span);
  15. }

This Stack data structure can be used over memory that resides on the stack, heap or unmanaged heap. If you know about Span this should immediately make sense to you.

This has to be a ref struct because it contains a Span. It can’t be used on the heap (i.e. in lambdas, async, class field, …). You have to build it on top of Memory if you need that. Also, you can happily blow the stack with this guy …

Let’s micro-benchmark it with BenchmarkDotNet.  For example, a postfix calculator. Let’s first do it naively using inheritance and the generic Stack class in the framework.

This is the naive object model:

  1. abstract class Token {}
  2. sealed class Operand: Token
  3. {
  4.     public int Value { get; }
  5.     public Operand(int v) { Value = v; }
  6. }
  7. abstract class Operator: Token {
  8.     abstract public int Calc(int a, int b);
  9. }
  10. sealed class Add: Operator
  11. {
  12.     public override int Calc(int a, int b) => a + b;
  13. }
  14. sealed class Mult : Operator
  15. {
  16.     public override int Calc(int a, int b) => a * b;
  17. }
  18. sealed class Minus : Operator
  19. {
  20.     public override int Calc(int a, int b) => a – b;
  21. }

Let’s then do it trying to be a bit more performance aware using a stack friendly representation:

  1. public enum TokenType { Operand, Sum, Mult, Minus}
  2. readonly struct SToken
  3. {
  4.     public TokenType Type { get; }
  5.     public int Value { get; }
  6.     public SToken(TokenType t, int v) { Type = t; Value = v; }
  7.     public SToken(TokenType t) { Type = t; Value = 0; }
  8.     public int Calc(int a, int b) =>
  9.                Type == TokenType.Sum   ? a + b :
  10.                Type == TokenType.Minus ? a – b :
  11.                Type == TokenType.Minus ? a * b :
  12.                throw new Exception(“I don’t know that one”);
  13. }

Perhaps not overtly elegant, but not that terrible either. You got to love those expression bodied methods and throw-expression.

We then setup things (I know I could/should parse a string here):

  1. static Token[] tokens;
  2. static SToken[] stokens;
  3. [GlobalSetup]
  4. public void Setup()
  5. {
  6.     tokens = new Token[] { new Operand(2), new Operand(3), new Operand(4), new Add(),
  7.                            new Mult(), new Operand(5), new Minus() };
  8.     stokens = new SToken[] { new SToken(TokenType.Operand, 2),
  9.                              new SToken(TokenType.Operand, 3), new SToken(TokenType.Operand, 4),
  10.                              new SToken(TokenType.Sum),  new SToken(TokenType.Mult),
  11.                              new SToken(TokenType.Operand, 5), new SToken(TokenType.Minus)};
  12. }

And first test the naive object model with the standard Stack from System.Collections.Generic.

  1. [Benchmark]
  2. public int PostfixEvalStack()
  3. {
  4.     var stack = new Stack(100);
  5.     foreach (var token in tokens)
  6.     {
  7.         switch (token)
  8.         {
  9.             case Operand t:
  10.                 stack.Push(t);
  11.                 break;
  12.             case Operator o:
  13.                 var a = stack.Pop() as Operand;
  14.                 var b = stack.Pop() as Operand;
  15.                 var result = o.Calc(a.Value, b.Value);
  16.                 stack.Push(new Operand(result));
  17.                 break;
  18.         }
  19.     }
  20.     return (stack.Pop() as Operand).Value;
  21. }

Then let’s just swap out our own lean-and-mean stack:

  1. [Benchmark]
  2. public int PostfixEvalSpanStack()
  3. {
  4.     Span span = new Token[100];
  5.     var stack = span.AsStack();
  6.     foreach (var token in tokens)
  7.     {
  8.         switch (token)
  9.         {
  10.             case Operand t:
  11.                 stack.Push(t);
  12.                 break;
  13.             case Operator o:
  14.                 var a = stack.Pop() as Operand;
  15.                 var b = stack.Pop() as Operand;
  16.                 var result = o.Calc(a.Value, b.Value);
  17.                 stack.Push(new Operand(result));
  18.                 break;
  19.         }
  20.     }
  21.     return (stack.Pop() as Operand).Value;
  22. }

And finally let’s go the whole way, lean object model and lean data structure, everything on the stack:

  1. [Benchmark(Baseline = true)]
  2. public int PostfixEvalSpanStackStructTypes()
  3. {
  4.     Span span = stackalloc SToken[100];
  5.     var stack = span.AsStack();
  6.     foreach (var token in stokens)
  7.     {
  8.         if (token.Type == TokenType.Operand)
  9.         {
  10.             stack.Push(token);
  11.         } else {
  12.             var a = stack.Pop();
  13.             var b = stack.Pop();
  14.             var result = token.Calc(a.Value, b.Value);
  15.             stack.Push(new SToken(TokenType.Operand, result));
  16.             break;
  17.         }
  18.     }
  19.     return stack.Pop().Value;
  20. }

We also want to check that we didn’t code anything stupid and finally run the benchmark.

  1. static void Test()
  2. {
  3.     var p = new Program();
  4.     p.Setup();
  5.     Trace.Assert(p.PostfixEvalStack() == p.PostfixEvalSpanStack() &&
  6.                  p.PostfixEvalSpanStack() == p.PostfixEvalSpanStackStructTypes());
  7. }
  8. static void Main(string[] args)
  9. {
  10.     Test();
  11.     var summary = BenchmarkRunner.Run();
  12. }

On my machine I get these results:


BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.431 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-6600U CPU 2.60GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
Frequency=2742185 Hz, Resolution=364.6727 ns, Timer=TSC
.NET Core SDK=2.1.300-rc1-008673
  [Host]     : .NET Core 2.1.0-rc1 (CoreCLR 4.6.26426.02, CoreFX 4.6.26426.04), 64bit RyuJIT
  DefaultJob : .NET Core 2.1.0-rc1 (CoreCLR 4.6.26426.02, CoreFX 4.6.26426.04), 64bit RyuJIT
Method Mean Error StdDev Scaled ScaledSD
PostfixEvalSpanStackStructTypes 76.24 ns 1.563 ns 2.857 ns 1.00 0.00
PostfixEvalSpanStack 168.65 ns 5.280 ns 15.319 ns 2.22 0.22
PostfixEvalStack 334.56 ns 7.387 ns 20.593 ns 4.39 0.31

Your mileage might vary. I want to emphasize that I am just playing with things. I haven’t done any deep analysis of this benchmark. There can be flaws, etc… etc…

Still, I find the idea of data structures which are memory-location-independent rather fascinating.

Advertisements