Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Last active February 23, 2017 15:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ufcpp/8085250f7e01d1cc0bb655a5eb1ef748 to your computer and use it in GitHub Desktop.
Save ufcpp/8085250f7e01d1cc0bb655a5eb1ef748 to your computer and use it in GitHub Desktop.
Exploration: no-allocation foreach with the Shapes
// Exploration: no-allocation foreach with Shapes
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Xunit;
#if false
// 1. I want to write shapes like as following (but seems like it's imposible)
shape SEnumerable<T>
{
// The SEnumerator<T> shape is not a type. This method is not allowed. right?
SEnumerator<T> GetEnumerator();
}
shape SEnumerator<T>
{
bool MoveNext();
T Current { get; }
void Dispose();
}
#endif
#if false
// 2. This seems to be possible
shape SEnumerable<TEnumerator>
{
TEnumerator GetEnumerator();
}
shape SEnumerator<T>
{
bool MoveNext();
T Current { get; }
void Dispose();
}
#endif
// 3. The shapes above (2) are expected to translate into:
interface SEnumerable<This, TEnumerator>
{
TEnumerator GetEnumerator(This @this);
}
interface SEnumerator<This, TItem>
{
// In this case, the @this parameter must be ref
// otherwise, the MoveNext bellow never end
bool MoveNext(ref This @this);
TItem Current(ref This @this);
void Dispose(ref This @this);
}
// implements the shapes for ImmutableArray<T>
struct ImmutableArrayExtensions<T> : SEnumerable<ImmutableArray<T>, ImmutableArray<T>.Enumerator>
{
public ImmutableArray<T>.Enumerator GetEnumerator(ImmutableArray<T> @this) => @this.GetEnumerator();
}
struct ImmutableArrayEnumeratorExtensions<T> : SEnumerator<ImmutableArray<T>.Enumerator, T>
{
public bool MoveNext(ref ImmutableArray<T>.Enumerator @this) => @this.MoveNext();
public T Current(ref ImmutableArray<T>.Enumerator @this) => @this.Current;
public void Dispose(ref ImmutableArray<T>.Enumerator @this) { }
}
// usecase of the Shapes
class Usecase
{
// Nomal foreach
// two allocations for value types
// one allocation for reference types
public static void Write<T>(IEnumerable<T> source, Action<T> action)
{
// source itself leads to boxing.
// (List<T> →[box]→ IEnumerable<T>)
foreach (var x in source)
{
action(x);
}
}
// To avoid boxing, add an extra type parameter.
// one allocation
public static void Write<T, TEnumerable>(TEnumerable source, Action<T> action)
where TEnumerable : IEnumerable<T>
{
// source itself does not lead to boxing
// but source.GetEnumerator() does
// (List<T>.Enumerator →[box]→ IEnumerator<T>)
foreach (var x in source)
{
action(x);
}
}
// Shape-based foreach
// foreach with the Shapes is expected to translate into:
// zero allocation
public static void Write<T, TCollection, TEnumerator, SEnumerable, SEnumerator>(TCollection source, Action<T> action)
where SEnumerable : struct, SEnumerable<TCollection, TEnumerator>
where SEnumerator : struct, SEnumerator<TEnumerator, T>
{
var enumerable = default(SEnumerable);
var enumerator = default(SEnumerator);
var e = enumerable.GetEnumerator(source);
try
{
while (enumerator.MoveNext(ref e))
{
var x = enumerator.Current(ref e);
action(x);
}
}
finally
{
enumerator.Dispose(ref e);
}
}
}
public class Test
{
static ImmutableArray<int> list = ImmutableArray.Create(1, 2, 3, 4, 5);
const int Sum = 15;
[Fact]
public void Write1()
{
var sum = 0;
Usecase.Write<int>(list, x => sum += x);
Assert.Equal(Sum, sum);
}
[Fact]
public void Write2()
{
var sum = 0;
Usecase.Write<int, ImmutableArray<int>>(list, x => sum += x);
Assert.Equal(Sum, sum);
}
[Fact]
public void Write3()
{
var sum = 0;
// Too ugly to use...
Usecase.Write<int, ImmutableArray<int>, ImmutableArray<int>.Enumerator, ImmutableArrayExtensions<int>, ImmutableArrayEnumeratorExtensions<int>>(list, x => sum += x);
Assert.Equal(Sum, sum);
}
}
public class BenchmarkCode
{
static ImmutableArray<int> list = ImmutableArray.Create(1, 2, 3, 4, 5);
static Action<int> none = _ => { };
// A result in my PC:
// Method | Mean | StdDev | Gen 0 | Allocated |
// Write1 | 59.2566 ns | 0.6774 ns | 0.0099 | 56 B |
[Benchmark]
public static void Write1() => Usecase.Write<int>(list, none);
// Write2 | 56.8654 ns | 0.1158 ns | 0.0043 | 32 B | // half allocation
[Benchmark]
public static void Write2() => Usecase.Write<int, ImmutableArray<int>>(list, none);
// Write3 | 33.5727 ns | 0.4087 ns | - | 0 B | // zero allocation
[Benchmark]
public static void Write3() => Usecase.Write<int, ImmutableArray<int>, ImmutableArray<int>.Enumerator, ImmutableArrayExtensions<int>, ImmutableArrayEnumeratorExtensions<int>>(list, none);
}
class Program
{
static void Main()
{
var t = new Test();
t.Write1();
t.Write2();
t.Write3();
BenchmarkRunner.Run<BenchmarkCode>();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment