Skip to content

Commit 04f64b6

Browse files
authored
Merge pull request #1901 from SixLabors/af/faster-getrowspan
Speed up DangerousGetRowSpan(y)
2 parents 39f37d0 + 0d3bd57 commit 04f64b6

File tree

18 files changed

+462
-283
lines changed

18 files changed

+462
-283
lines changed

src/ImageSharp/Common/Helpers/Numerics.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ public static uint RotateRightSoftwareFallback(uint value, int offset)
970970
/// <param name="value">Value.</param>
971971
/// <param name="min">Mininum value, inclusive.</param>
972972
/// <param name="max">Maximum value, inclusive.</param>
973+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
973974
public static bool IsOutOfRange(int value, int min, int max)
974975
=> (uint)(value - min) > (uint)(max - min);
975976
}

src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private void ConvertStride(int spectralStep)
179179
// PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row.
180180
// If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row,
181181
// pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row.
182-
if (this.pixelBuffer.TryGetPaddedRowSpan(yy, 3, out Span<TPixel> destRow))
182+
if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span<TPixel> destRow))
183183
{
184184
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(this.configuration, r, g, b, destRow);
185185
}

src/ImageSharp/ImageFrame{TPixel}.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,12 +443,12 @@ internal void Clear(TPixel value)
443443
[MethodImpl(InliningOptions.ShortMethod)]
444444
private void VerifyCoords(int x, int y)
445445
{
446-
if (x < 0 || x >= this.Width)
446+
if ((uint)x >= (uint)this.Width)
447447
{
448448
ThrowArgumentOutOfRangeException(nameof(x));
449449
}
450450

451-
if (y < 0 || y >= this.Height)
451+
if ((uint)y >= (uint)this.Height)
452452
{
453453
ThrowArgumentOutOfRangeException(nameof(y));
454454
}

src/ImageSharp/Image{TPixel}.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,12 +452,12 @@ private static Size ValidateFramesAndGetSize(IEnumerable<ImageFrame<TPixel>> fra
452452
[MethodImpl(InliningOptions.ShortMethod)]
453453
private void VerifyCoords(int x, int y)
454454
{
455-
if (x < 0 || x >= this.Width)
455+
if ((uint)x >= (uint)this.Width)
456456
{
457457
ThrowArgumentOutOfRangeException(nameof(x));
458458
}
459459

460-
if (y < 0 || y >= this.Height)
460+
if ((uint)y >= (uint)this.Height)
461461
{
462462
ThrowArgumentOutOfRangeException(nameof(y));
463463
}

src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,35 @@ internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>, IRefCounted
1313
where T : struct
1414
{
1515
private readonly int lengthInBytes;
16-
private byte[] array;
1716
private LifetimeGuard lifetimeGuard;
1817

1918
public SharedArrayPoolBuffer(int lengthInElements)
2019
{
2120
this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>();
22-
this.array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes);
23-
this.lifetimeGuard = new LifetimeGuard(this.array);
21+
this.Array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes);
22+
this.lifetimeGuard = new LifetimeGuard(this.Array);
2423
}
2524

25+
public byte[] Array { get; private set; }
26+
2627
protected override void Dispose(bool disposing)
2728
{
28-
if (this.array == null)
29+
if (this.Array == null)
2930
{
3031
return;
3132
}
3233

3334
this.lifetimeGuard.Dispose();
34-
this.array = null;
35+
this.Array = null;
3536
}
3637

3738
public override Span<T> GetSpan()
3839
{
3940
this.CheckDisposed();
40-
return MemoryMarshal.Cast<byte, T>(this.array.AsSpan(0, this.lengthInBytes));
41+
return MemoryMarshal.Cast<byte, T>(this.Array.AsSpan(0, this.lengthInBytes));
4142
}
4243

43-
protected override object GetPinnableObject() => this.array;
44+
protected override object GetPinnableObject() => this.Array;
4445

4546
public void AddRef()
4647
{
@@ -53,7 +54,7 @@ public void AddRef()
5354
[Conditional("DEBUG")]
5455
private void CheckDisposed()
5556
{
56-
if (this.array == null)
57+
if (this.Array == null)
5758
{
5859
throw new ObjectDisposedException("SharedArrayPoolBuffer");
5960
}

src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88

99
namespace SixLabors.ImageSharp.Memory.Internals
1010
{
11-
internal partial class UniformUnmanagedMemoryPool
12-
#if !NETSTANDARD1_3
13-
// In case UniformUnmanagedMemoryPool is finalized, we prefer to run its finalizer after the guard finalizers,
14-
// but we should not rely on this.
15-
: System.Runtime.ConstrainedExecution.CriticalFinalizerObject
16-
#endif
11+
// CriticalFinalizerObject:
12+
// In case UniformUnmanagedMemoryPool is finalized, we prefer to run its finalizer after the guard finalizers,
13+
// but we should not rely on this.
14+
internal partial class UniformUnmanagedMemoryPool : System.Runtime.ConstrainedExecution.CriticalFinalizerObject
1715
{
1816
private static int minTrimPeriodMilliseconds = int.MaxValue;
1917
private static readonly List<WeakReference<UniformUnmanagedMemoryPool>> AllPools = new();

src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifeti
3131
this.lifetimeGuard = lifetimeGuard;
3232
}
3333

34-
private void* Pointer => this.lifetimeGuard.Handle.Pointer;
34+
public void* Pointer => this.lifetimeGuard.Handle.Pointer;
3535

3636
public override Span<T> GetSpan()
3737
{

src/ImageSharp/Memory/Buffer2D{T}.cs

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ internal Buffer2D(MemoryGroup<T> memoryGroup, int width, int height)
5656
/// It's public counterpart is <see cref="MemoryGroup"/>,
5757
/// which only exposes the view of the MemoryGroup.
5858
/// </remarks>
59-
internal MemoryGroup<T> FastMemoryGroup { get; }
59+
internal MemoryGroup<T> FastMemoryGroup { get; private set; }
6060

6161
/// <summary>
6262
/// Gets a reference to the element at the specified position.
@@ -97,35 +97,37 @@ internal Buffer2D(MemoryGroup<T> memoryGroup, int width, int height)
9797
[MethodImpl(InliningOptions.ShortMethod)]
9898
public Span<T> DangerousGetRowSpan(int y)
9999
{
100-
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
101-
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
100+
if ((uint)y >= (uint)this.Height)
101+
{
102+
this.ThrowYOutOfRangeException(y);
103+
}
102104

103-
return this.GetRowMemoryCore(y).Span;
105+
return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
104106
}
105107

106-
internal bool TryGetPaddedRowSpan(int y, int padding, out Span<T> paddedSpan)
108+
internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span<T> paddedSpan)
107109
{
108110
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
109111
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
110112

111113
int stride = this.Width + padding;
112114

113-
Memory<T> memory = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width);
115+
Span<T> slice = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width);
114116

115-
if (memory.Length < stride)
117+
if (slice.Length < stride)
116118
{
117119
paddedSpan = default;
118120
return false;
119121
}
120122

121-
paddedSpan = memory.Span.Slice(0, stride);
123+
paddedSpan = slice.Slice(0, stride);
122124
return true;
123125
}
124126

125127
[MethodImpl(InliningOptions.ShortMethod)]
126128
internal ref T GetElementUnsafe(int x, int y)
127129
{
128-
Span<T> span = this.GetRowMemoryCore(y).Span;
130+
Span<T> span = this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
129131
return ref span[x];
130132
}
131133

@@ -139,7 +141,7 @@ internal Memory<T> GetSafeRowMemory(int y)
139141
{
140142
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
141143
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
142-
return this.FastMemoryGroup.View.GetBoundedSlice(y * (long)this.Width, this.Width);
144+
return this.FastMemoryGroup.View.GetBoundedMemorySlice(y * (long)this.Width, this.Width);
143145
}
144146

145147
/// <summary>
@@ -168,25 +170,36 @@ internal Memory<T> GetSafeRowMemory(int y)
168170
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
169171
/// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
170172
/// </summary>
171-
internal static void SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
172-
{
173-
MemoryGroup<T>.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup);
174-
SwapOwnData(destination, source);
175-
}
176-
177-
[MethodImpl(InliningOptions.ShortMethod)]
178-
private Memory<T> GetRowMemoryCore(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width);
179-
180-
private static void SwapOwnData(Buffer2D<T> a, Buffer2D<T> b)
173+
internal static bool SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
181174
{
182-
Size aSize = a.Size();
183-
Size bSize = b.Size();
175+
bool swapped = false;
176+
if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
177+
{
178+
(destination.FastMemoryGroup, source.FastMemoryGroup) =
179+
(source.FastMemoryGroup, destination.FastMemoryGroup);
180+
destination.FastMemoryGroup.RecreateViewAfterSwap();
181+
source.FastMemoryGroup.RecreateViewAfterSwap();
182+
swapped = true;
183+
}
184+
else
185+
{
186+
if (destination.FastMemoryGroup.TotalLength != source.FastMemoryGroup.TotalLength)
187+
{
188+
throw new InvalidMemoryOperationException(
189+
"Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images.");
190+
}
184191

185-
b.Width = aSize.Width;
186-
b.Height = aSize.Height;
192+
source.FastMemoryGroup.CopyTo(destination.MemoryGroup);
193+
}
187194

188-
a.Width = bSize.Width;
189-
a.Height = bSize.Height;
195+
(destination.Width, source.Width) = (source.Width, destination.Width);
196+
(destination.Height, source.Height) = (source.Height, destination.Height);
197+
return swapped;
190198
}
199+
200+
[MethodImpl(InliningOptions.ColdPath)]
201+
private void ThrowYOutOfRangeException(int y) =>
202+
throw new ArgumentOutOfRangeException(
203+
$"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
191204
}
192205
}

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,23 @@ internal static void Clear<T>(this IMemoryGroup<T> group)
2929
/// Returns a slice that is expected to be within the bounds of a single buffer.
3030
/// Otherwise <see cref="ArgumentOutOfRangeException"/> is thrown.
3131
/// </summary>
32-
internal static Memory<T> GetBoundedSlice<T>(this IMemoryGroup<T> group, long start, int length)
32+
internal static Memory<T> GetBoundedMemorySlice<T>(this IMemoryGroup<T> group, long start, int length)
3333
where T : struct
3434
{
3535
Guard.NotNull(group, nameof(group));
3636
Guard.IsTrue(group.IsValid, nameof(group), "Group must be valid!");
3737
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
3838
Guard.MustBeLessThan(start, group.TotalLength, nameof(start));
3939

40-
int bufferIdx = (int)(start / group.BufferLength);
40+
int bufferIdx = (int)Math.DivRem(start, group.BufferLength, out long bufferStartLong);
41+
int bufferStart = (int)bufferStartLong;
4142

4243
// if (bufferIdx < 0 || bufferIdx >= group.Count)
4344
if ((uint)bufferIdx >= group.Count)
4445
{
4546
throw new ArgumentOutOfRangeException(nameof(start));
4647
}
4748

48-
int bufferStart = (int)(start % group.BufferLength);
4949
int bufferEnd = bufferStart + length;
5050
Memory<T> memory = group[bufferIdx];
5151

@@ -57,31 +57,6 @@ internal static Memory<T> GetBoundedSlice<T>(this IMemoryGroup<T> group, long st
5757
return memory.Slice(bufferStart, length);
5858
}
5959

60-
/// <summary>
61-
/// Returns the slice of the buffer starting at global index <paramref name="start"/> that goes until the end of the buffer.
62-
/// </summary>
63-
internal static Memory<T> GetRemainingSliceOfBuffer<T>(this IMemoryGroup<T> group, long start)
64-
where T : struct
65-
{
66-
Guard.NotNull(group, nameof(group));
67-
Guard.IsTrue(group.IsValid, nameof(group), "Group must be valid!");
68-
Guard.MustBeLessThan(start, group.TotalLength, nameof(start));
69-
70-
int bufferIdx = (int)(start / group.BufferLength);
71-
72-
// if (bufferIdx < 0 || bufferIdx >= group.Count)
73-
if ((uint)bufferIdx >= group.Count)
74-
{
75-
throw new ArgumentOutOfRangeException(nameof(start));
76-
}
77-
78-
int bufferStart = (int)(start % group.BufferLength);
79-
80-
Memory<T> memory = group[bufferIdx];
81-
82-
return memory.Slice(bufferStart);
83-
}
84-
8560
internal static void CopyTo<T>(this IMemoryGroup<T> source, Span<T> target)
8661
where T : struct
8762
{
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Buffers;
6+
using SixLabors.ImageSharp.Memory.Internals;
7+
8+
namespace SixLabors.ImageSharp.Memory
9+
{
10+
/// <summary>
11+
/// Cached pointer or array data enabling fast <see cref="Span{T}"/> access from
12+
/// known <see cref="IMemoryOwner{T}"/> implementations.
13+
/// </summary>
14+
internal unsafe struct MemoryGroupSpanCache
15+
{
16+
public SpanCacheMode Mode;
17+
public byte[] SingleArray;
18+
public void* SinglePointer;
19+
public void*[] MultiPointer;
20+
21+
public static MemoryGroupSpanCache Create<T>(IMemoryOwner<T>[] memoryOwners)
22+
where T : struct
23+
{
24+
IMemoryOwner<T> owner0 = memoryOwners[0];
25+
MemoryGroupSpanCache memoryGroupSpanCache = default;
26+
if (memoryOwners.Length == 1)
27+
{
28+
if (owner0 is SharedArrayPoolBuffer<T> sharedPoolBuffer)
29+
{
30+
memoryGroupSpanCache.Mode = SpanCacheMode.SingleArray;
31+
memoryGroupSpanCache.SingleArray = sharedPoolBuffer.Array;
32+
}
33+
else if (owner0 is UnmanagedBuffer<T> unmanagedBuffer)
34+
{
35+
memoryGroupSpanCache.Mode = SpanCacheMode.SinglePointer;
36+
memoryGroupSpanCache.SinglePointer = unmanagedBuffer.Pointer;
37+
}
38+
}
39+
else if (owner0 is UnmanagedBuffer<T>)
40+
{
41+
memoryGroupSpanCache.Mode = SpanCacheMode.MultiPointer;
42+
memoryGroupSpanCache.MultiPointer = new void*[memoryOwners.Length];
43+
for (int i = 0; i < memoryOwners.Length; i++)
44+
{
45+
memoryGroupSpanCache.MultiPointer[i] = ((UnmanagedBuffer<T>)memoryOwners[i]).Pointer;
46+
}
47+
}
48+
49+
return memoryGroupSpanCache;
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)