Skip to content

Commit 8d404d6

Browse files
Merge pull request #1173 from SixLabors/optimizations/memory-group-enumeration
Allocation-free IMemoryGroup<T> enumeration
2 parents eb312e0 + 9823eed commit 8d404d6

File tree

7 files changed

+187
-22
lines changed

7 files changed

+187
-22
lines changed

src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,15 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
3333
/// the image buffers internally.
3434
/// </remarks>
3535
bool IsValid { get; }
36+
37+
/// <summary>
38+
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
39+
/// instance. The return type shouldn't be used directly: just use a <see langword="foreach"/> block on
40+
/// the <see cref="IMemoryGroup{T}"/> instance in use and the C# compiler will automatically invoke this
41+
/// method behind the scenes. This method takes precedence over the <see cref="IEnumerable{T}.GetEnumerator"/>
42+
/// implementation, which is still available when casting to one of the underlying interfaces.
43+
/// </summary>
44+
/// <returns>A new <see cref="MemoryGroupEnumerator{T}"/> instance mapping the current <see cref="Memory{T}"/> values in use.</returns>
45+
new MemoryGroupEnumerator<T> GetEnumerator();
3646
}
3747
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.ComponentModel;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace SixLabors.ImageSharp.Memory
9+
{
10+
/// <summary>
11+
/// A value-type enumerator for <see cref="MemoryGroup{T}"/> instances.
12+
/// </summary>
13+
/// <typeparam name="T">The element type.</typeparam>
14+
[EditorBrowsable(EditorBrowsableState.Never)]
15+
public ref struct MemoryGroupEnumerator<T>
16+
where T : struct
17+
{
18+
private readonly IMemoryGroup<T> memoryGroup;
19+
private readonly int count;
20+
private int index;
21+
22+
[MethodImpl(InliningOptions.ShortMethod)]
23+
internal MemoryGroupEnumerator(MemoryGroup<T>.Owned memoryGroup)
24+
{
25+
this.memoryGroup = memoryGroup;
26+
this.count = memoryGroup.Count;
27+
this.index = -1;
28+
}
29+
30+
[MethodImpl(InliningOptions.ShortMethod)]
31+
internal MemoryGroupEnumerator(MemoryGroup<T>.Consumed memoryGroup)
32+
{
33+
this.memoryGroup = memoryGroup;
34+
this.count = memoryGroup.Count;
35+
this.index = -1;
36+
}
37+
38+
[MethodImpl(InliningOptions.ShortMethod)]
39+
internal MemoryGroupEnumerator(MemoryGroupView<T> memoryGroup)
40+
{
41+
this.memoryGroup = memoryGroup;
42+
this.count = memoryGroup.Count;
43+
this.index = -1;
44+
}
45+
46+
/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
47+
public Memory<T> Current
48+
{
49+
[MethodImpl(InliningOptions.ShortMethod)]
50+
get => this.memoryGroup[this.index];
51+
}
52+
53+
/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
54+
[MethodImpl(InliningOptions.ShortMethod)]
55+
public bool MoveNext()
56+
{
57+
int index = this.index + 1;
58+
59+
if (index < this.count)
60+
{
61+
this.index = index;
62+
63+
return true;
64+
}
65+
66+
return false;
67+
}
68+
}
69+
}

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Buffers;
66
using System.Collections;
77
using System.Collections.Generic;
8+
using System.Runtime.CompilerServices;
89

910
namespace SixLabors.ImageSharp.Memory
1011
{
@@ -37,6 +38,7 @@ public MemoryGroupView(MemoryGroup<T> owner)
3738

3839
public int Count
3940
{
41+
[MethodImpl(InliningOptions.ShortMethod)]
4042
get
4143
{
4244
this.EnsureIsValid();
@@ -73,7 +75,15 @@ public Memory<T> this[int index]
7375
}
7476
}
7577

76-
public IEnumerator<Memory<T>> GetEnumerator()
78+
/// <inheritdoc/>
79+
[MethodImpl(InliningOptions.ShortMethod)]
80+
public MemoryGroupEnumerator<T> GetEnumerator()
81+
{
82+
return new MemoryGroupEnumerator<T>(this);
83+
}
84+
85+
/// <inheritdoc/>
86+
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
7787
{
7888
this.EnsureIsValid();
7989
for (int i = 0; i < this.Count; i++)
@@ -82,7 +92,8 @@ public IEnumerator<Memory<T>> GetEnumerator()
8292
}
8393
}
8494

85-
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
95+
/// <inheritdoc/>
96+
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();
8697

8798
internal void Invalidate()
8899
{

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5-
using System.Buffers;
65
using System.Collections.Generic;
7-
using System.Linq;
6+
using System.Runtime.CompilerServices;
87

98
namespace SixLabors.ImageSharp.Memory
109
{
1110
internal abstract partial class MemoryGroup<T>
1211
{
13-
// Analogous to the "consumed" variant of MemorySource
14-
private sealed class Consumed : MemoryGroup<T>
12+
/// <summary>
13+
/// A <see cref="MemoryGroup{T}"/> implementation that consumes the underlying memory buffers.
14+
/// </summary>
15+
public sealed class Consumed : MemoryGroup<T>, IEnumerable<Memory<T>>
1516
{
1617
private readonly Memory<T>[] source;
1718

@@ -22,16 +23,31 @@ public Consumed(Memory<T>[] source, int bufferLength, long totalLength)
2223
this.View = new MemoryGroupView<T>(this);
2324
}
2425

25-
public override int Count => this.source.Length;
26+
public override int Count
27+
{
28+
[MethodImpl(InliningOptions.ShortMethod)]
29+
get => this.source.Length;
30+
}
2631

2732
public override Memory<T> this[int index] => this.source[index];
2833

29-
public override IEnumerator<Memory<T>> GetEnumerator()
34+
/// <inheritdoc/>
35+
[MethodImpl(InliningOptions.ShortMethod)]
36+
public override MemoryGroupEnumerator<T> GetEnumerator()
37+
{
38+
return new MemoryGroupEnumerator<T>(this);
39+
}
40+
41+
/// <inheritdoc/>
42+
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
3043
{
31-
for (int i = 0; i < this.source.Length; i++)
32-
{
33-
yield return this.source[i];
34-
}
44+
/* The runtime sees the Array class as if it implemented the
45+
* type-generic collection interfaces explicitly, so here we
46+
* can just cast the source array to IList<Memory<T>> (or to
47+
* an equivalent type), and invoke the generic GetEnumerator
48+
* method directly from that interface reference. This saves
49+
* having to create our own iterator block here. */
50+
return ((IList<Memory<T>>)this.source).GetEnumerator();
3551
}
3652

3753
public override void Dispose()

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
using System.Buffers;
66
using System.Collections.Generic;
77
using System.Linq;
8+
using System.Runtime.CompilerServices;
89

910
namespace SixLabors.ImageSharp.Memory
1011
{
11-
// Analogous to the "owned" variant of MemorySource
1212
internal abstract partial class MemoryGroup<T>
1313
{
14-
private sealed class Owned : MemoryGroup<T>
14+
/// <summary>
15+
/// A <see cref="MemoryGroup{T}"/> implementation that owns the underlying memory buffers.
16+
/// </summary>
17+
public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>>
1518
{
1619
private IMemoryOwner<T>[] memoryOwners;
1720

@@ -29,6 +32,7 @@ public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength,
2932

3033
public override int Count
3134
{
35+
[MethodImpl(InliningOptions.ShortMethod)]
3236
get
3337
{
3438
this.EnsureNotDisposed();
@@ -45,7 +49,15 @@ public override Memory<T> this[int index]
4549
}
4650
}
4751

48-
public override IEnumerator<Memory<T>> GetEnumerator()
52+
/// <inheritdoc/>
53+
[MethodImpl(InliningOptions.ShortMethod)]
54+
public override MemoryGroupEnumerator<T> GetEnumerator()
55+
{
56+
return new MemoryGroupEnumerator<T>(this);
57+
}
58+
59+
/// <inheritdoc/>
60+
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
4961
{
5062
this.EnsureNotDisposed();
5163
return this.memoryOwners.Select(mo => mo.Memory).GetEnumerator();
@@ -69,14 +81,21 @@ public override void Dispose()
6981
this.IsValid = false;
7082
}
7183

84+
[MethodImpl(InliningOptions.ShortMethod)]
7285
private void EnsureNotDisposed()
7386
{
74-
if (this.memoryOwners == null)
87+
if (this.memoryOwners is null)
7588
{
76-
throw new ObjectDisposedException(nameof(MemoryGroup<T>));
89+
ThrowObjectDisposedException();
7790
}
7891
}
7992

93+
[MethodImpl(MethodImplOptions.NoInlining)]
94+
private static void ThrowObjectDisposedException()
95+
{
96+
throw new ObjectDisposedException(nameof(MemoryGroup<T>));
97+
}
98+
8099
internal static void SwapContents(Owned a, Owned b)
81100
{
82101
a.EnsureNotDisposed();

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Collections;
77
using System.Collections.Generic;
88
using System.Runtime.CompilerServices;
9-
using SixLabors.ImageSharp.Memory.Internals;
109

1110
namespace SixLabors.ImageSharp.Memory
1211
{
@@ -48,10 +47,21 @@ private MemoryGroup(int bufferLength, long totalLength)
4847
public abstract void Dispose();
4948

5049
/// <inheritdoc />
51-
public abstract IEnumerator<Memory<T>> GetEnumerator();
50+
public abstract MemoryGroupEnumerator<T> GetEnumerator();
5251

5352
/// <inheritdoc />
54-
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
53+
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
54+
{
55+
/* This method is implemented in each derived class.
56+
* Implementing the method here as non-abstract and throwing,
57+
* then reimplementing it explicitly in each derived class, is
58+
* a workaround for the lack of support for abstract explicit
59+
* interface method implementations in C#. */
60+
throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()");
61+
}
62+
63+
/// <inheritdoc />
64+
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();
5565

5666
/// <summary>
5767
/// Creates a new memory group, allocating it's buffers with the provided allocator.

tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
// Copyright (c) Six Labors and contributors.
1+
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
55
using System.Buffers;
6+
using System.Collections;
7+
using System.Collections.Generic;
68
using System.Linq;
79
using System.Runtime.InteropServices;
810
using SixLabors.ImageSharp.Memory;
@@ -165,7 +167,7 @@ public void GetBoundedSlice_WhenOverlapsBuffers_Throws(long totalLength, int buf
165167
}
166168

167169
[Fact]
168-
public void Fill()
170+
public void FillWithFastEnumerator()
169171
{
170172
using MemoryGroup<int> group = this.CreateTestGroup(100, 10, true);
171173
group.Fill(42);
@@ -177,6 +179,34 @@ public void Fill()
177179
}
178180
}
179181

182+
[Fact]
183+
public void FillWithSlowGenericEnumerator()
184+
{
185+
using MemoryGroup<int> group = this.CreateTestGroup(100, 10, true);
186+
group.Fill(42);
187+
188+
int[] expectedRow = Enumerable.Repeat(42, 10).ToArray();
189+
IReadOnlyList<Memory<int>> groupAsList = group;
190+
foreach (Memory<int> memory in groupAsList)
191+
{
192+
Assert.True(memory.Span.SequenceEqual(expectedRow));
193+
}
194+
}
195+
196+
[Fact]
197+
public void FillWithSlowEnumerator()
198+
{
199+
using MemoryGroup<int> group = this.CreateTestGroup(100, 10, true);
200+
group.Fill(42);
201+
202+
int[] expectedRow = Enumerable.Repeat(42, 10).ToArray();
203+
IEnumerable groupAsList = group;
204+
foreach (Memory<int> memory in groupAsList)
205+
{
206+
Assert.True(memory.Span.SequenceEqual(expectedRow));
207+
}
208+
}
209+
180210
[Fact]
181211
public void Clear()
182212
{

0 commit comments

Comments
 (0)