Last active
February 23, 2017 15:48
-
-
Save ufcpp/8085250f7e01d1cc0bb655a5eb1ef748 to your computer and use it in GitHub Desktop.
Exploration: no-allocation foreach with the Shapes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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