Skip to content
10 changes: 10 additions & 0 deletions src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,15 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// the image buffers internally.
/// </remarks>
bool IsValid { get; }

/// <summary>
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
/// instance. The return type shouldn't be used directly: just use a <see langword="foreach"/> block on
/// the <see cref="IMemoryGroup{T}"/> instance in use and the C# compiler will automatically invoke this
/// method behind the scenes. This method takes precedence over the <see cref="IEnumerable{T}.GetEnumerator"/>
/// implementation, which is still available when casting to one of the underlying interfaces.
/// </summary>
/// <returns>A new <see cref="MemoryGroupEnumerator{T}"/> instance mapping the current <see cref="Memory{T}"/> values in use.</returns>
new MemoryGroupEnumerator<T> GetEnumerator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// A value-type enumerator for <see cref="MemoryGroup{T}"/> instances.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public ref struct MemoryGroupEnumerator<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this doesn't require to be a ref struct (all members are heapable). Have you added the constraint to ensure it's only used in method locals?

Copy link
Member Author

@Sergio0694 Sergio0694 Apr 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was mostly a precaution to prevent users from doing stuff like storing the iterator as a field in a class for later use, or other weird things. This type should really only ever be used implicitly by the compiler when using foreach, it should be completely transparent to users. Same reason for that EditorBrowsableState.Never as well 😄

where T : struct
{
private readonly IMemoryGroup<T> memoryGroup;
private readonly int count;
private int index;

[MethodImpl(InliningOptions.ShortMethod)]
internal MemoryGroupEnumerator(MemoryGroup<T>.Owned memoryGroup)
{
this.memoryGroup = memoryGroup;
this.count = memoryGroup.Count;
this.index = -1;
}

[MethodImpl(InliningOptions.ShortMethod)]
internal MemoryGroupEnumerator(MemoryGroup<T>.Consumed memoryGroup)
{
this.memoryGroup = memoryGroup;
this.count = memoryGroup.Count;
this.index = -1;
}

[MethodImpl(InliningOptions.ShortMethod)]
internal MemoryGroupEnumerator(MemoryGroupView<T> memoryGroup)
{
this.memoryGroup = memoryGroup;
this.count = memoryGroup.Count;
this.index = -1;
}

/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
public Memory<T> Current
{
[MethodImpl(InliningOptions.ShortMethod)]
get => this.memoryGroup[this.index];
}

/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool MoveNext()
{
int index = this.index + 1;

if (index < this.count)
{
this.index = index;

return true;
}

return false;
}
}
}
15 changes: 13 additions & 2 deletions src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp.Memory
{
Expand Down Expand Up @@ -37,6 +38,7 @@ public MemoryGroupView(MemoryGroup<T> owner)

public int Count
{
[MethodImpl(InliningOptions.ShortMethod)]
get
{
this.EnsureIsValid();
Expand Down Expand Up @@ -73,7 +75,15 @@ public Memory<T> this[int index]
}
}

public IEnumerator<Memory<T>> GetEnumerator()
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}

/// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
this.EnsureIsValid();
for (int i = 0; i < this.Count; i++)
Expand All @@ -82,7 +92,8 @@ public IEnumerator<Memory<T>> GetEnumerator()
}
}

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();

internal void Invalidate()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp.Memory
{
internal abstract partial class MemoryGroup<T>
{
// Analogous to the "consumed" variant of MemorySource
private sealed class Consumed : MemoryGroup<T>
/// <summary>
/// A <see cref="MemoryGroup{T}"/> implementation that consumes the underlying memory buffers.
/// </summary>
public sealed class Consumed : MemoryGroup<T>, IEnumerable<Memory<T>>
{
private readonly Memory<T>[] source;

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

public override int Count => this.source.Length;
public override int Count
{
[MethodImpl(InliningOptions.ShortMethod)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type is not sealed. Will Count be ever inlined actually?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added that because those methods are being called directly from the enumerator:

In theory (I hope), the JIT should be able to devirtualize and then inline that call, since all APIs are internal, so I assume it could potentially know the type is definitely just the one declared from there as a parameter. Of course, we could also declare those types as sealed, to be honest I'm not even sure why they're not already? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about JIT here, anyways this is a question of low importance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better one AggressiveInlining in excess than lacking one 🤣

get => this.source.Length;
}

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

public override IEnumerator<Memory<T>> GetEnumerator()
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}

/// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
for (int i = 0; i < this.source.Length; i++)
{
yield return this.source[i];
}
/* The runtime sees the Array class as if it implemented the
* type-generic collection interfaces explicitly, so here we
* can just cast the source array to IList<Memory<T>> (or to
* an equivalent type), and invoke the generic GetEnumerator
* method directly from that interface reference. This saves
* having to create our own iterator block here. */
return ((IList<Memory<T>>)this.source).GetEnumerator();
}

public override void Dispose()
Expand Down
29 changes: 24 additions & 5 deletions src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp.Memory
{
// Analogous to the "owned" variant of MemorySource
internal abstract partial class MemoryGroup<T>
{
private sealed class Owned : MemoryGroup<T>
/// <summary>
/// A <see cref="MemoryGroup{T}"/> implementation that owns the underlying memory buffers.
/// </summary>
public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>>
{
private IMemoryOwner<T>[] memoryOwners;

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

public override int Count
{
[MethodImpl(InliningOptions.ShortMethod)]
get
{
this.EnsureNotDisposed();
Expand All @@ -45,7 +49,15 @@ public override Memory<T> this[int index]
}
}

public override IEnumerator<Memory<T>> GetEnumerator()
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}

/// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
this.EnsureNotDisposed();
return this.memoryOwners.Select(mo => mo.Memory).GetEnumerator();
Expand All @@ -69,14 +81,21 @@ public override void Dispose()
this.IsValid = false;
}

[MethodImpl(InliningOptions.ShortMethod)]
private void EnsureNotDisposed()
{
if (this.memoryOwners == null)
if (this.memoryOwners is null)
{
throw new ObjectDisposedException(nameof(MemoryGroup<T>));
ThrowObjectDisposedException();
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(MemoryGroup<T>));
}

internal static void SwapContents(Owned a, Owned b)
{
a.EnsureNotDisposed();
Expand Down
16 changes: 13 additions & 3 deletions src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;

namespace SixLabors.ImageSharp.Memory
{
Expand Down Expand Up @@ -48,10 +47,21 @@ private MemoryGroup(int bufferLength, long totalLength)
public abstract void Dispose();

/// <inheritdoc />
public abstract IEnumerator<Memory<T>> GetEnumerator();
public abstract MemoryGroupEnumerator<T> GetEnumerator();

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

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();

/// <summary>
/// Creates a new memory group, allocating it's buffers with the provided allocator.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
Expand Down Expand Up @@ -165,7 +167,7 @@ public void GetBoundedSlice_WhenOverlapsBuffers_Throws(long totalLength, int buf
}

[Fact]
public void Fill()
public void FillWithFastEnumerator()
{
using MemoryGroup<int> group = this.CreateTestGroup(100, 10, true);
group.Fill(42);
Expand All @@ -177,6 +179,34 @@ public void Fill()
}
}

[Fact]
public void FillWithSlowGenericEnumerator()
{
using MemoryGroup<int> group = this.CreateTestGroup(100, 10, true);
group.Fill(42);

int[] expectedRow = Enumerable.Repeat(42, 10).ToArray();
IReadOnlyList<Memory<int>> groupAsList = group;
foreach (Memory<int> memory in groupAsList)
{
Assert.True(memory.Span.SequenceEqual(expectedRow));
}
}

[Fact]
public void FillWithSlowEnumerator()
{
using MemoryGroup<int> group = this.CreateTestGroup(100, 10, true);
group.Fill(42);

int[] expectedRow = Enumerable.Repeat(42, 10).ToArray();
IEnumerable groupAsList = group;
foreach (Memory<int> memory in groupAsList)
{
Assert.True(memory.Span.SequenceEqual(expectedRow));
}
}

[Fact]
public void Clear()
{
Expand Down