Browse Source

Merge pull request #3110 from SixLabors/js/parallel-settings

Allow -1 (unbounded) parallelism; validate settings
pull/3114/head
James Jackson-South 1 month ago
committed by GitHub
parent
commit
d3ca6ebd33
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      src/ImageSharp/Advanced/ParallelExecutionSettings.cs
  2. 86
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  3. 1
      src/ImageSharp/Configuration.cs
  4. 115
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

11
src/ImageSharp/Advanced/ParallelExecutionSettings.cs

@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
/// <param name="maxDegreeOfParallelism">
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// Set to <c>-1</c> to leave the degree of parallelism unbounded.
/// </param>
/// <param name="minimumPixelsProcessedPerTask">The value for <see cref="MinimumPixelsProcessedPerTask"/>.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
public ParallelExecutionSettings(
@ -44,7 +47,10 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
/// <param name="maxDegreeOfParallelism">
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// Set to <c>-1</c> to leave the degree of parallelism unbounded.
/// </param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
@ -58,6 +64,7 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Gets the value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// A value of <c>-1</c> leaves the degree of parallelism unbounded.
/// </summary>
public int MaxDegreeOfParallelism { get; }

86
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -44,14 +44,14 @@ public static partial class ParallelRowIterator
where T : struct, IRowOperation
{
ValidateRectangle(rectangle);
ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -65,7 +65,7 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
_ = Parallel.For(
@ -109,14 +109,14 @@ public static partial class ParallelRowIterator
where TBuffer : unmanaged
{
ValidateRectangle(rectangle);
ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@ -135,7 +135,7 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
_ = Parallel.For(
@ -174,14 +174,14 @@ public static partial class ParallelRowIterator
where T : struct, IRowIntervalOperation
{
ValidateRectangle(rectangle);
ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -192,7 +192,7 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
_ = Parallel.For(
@ -236,14 +236,14 @@ public static partial class ParallelRowIterator
where TBuffer : unmanaged
{
ValidateRectangle(rectangle);
ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@ -259,7 +259,7 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
_ = Parallel.For(
@ -272,6 +272,37 @@ public static partial class ParallelRowIterator
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
/// <summary>
/// Creates the <see cref="ParallelOptions"/> for the current iteration.
/// </summary>
/// <param name="parallelSettings">The execution settings.</param>
/// <param name="numOfSteps">The number of row partitions to execute.</param>
/// <returns>The <see cref="ParallelOptions"/> instance.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps)
=> new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps };
/// <summary>
/// Calculates the number of row partitions to execute for the given region.
/// </summary>
/// <param name="width">The width of the region.</param>
/// <param name="height">The height of the region.</param>
/// <param name="parallelSettings">The execution settings.</param>
/// <returns>The number of row partitions to execute.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetNumberOfSteps(int width, int height, in ParallelExecutionSettings parallelSettings)
{
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
if (parallelSettings.MaxDegreeOfParallelism == -1)
{
// Row batching cannot produce more useful partitions than the number of rows available.
return Math.Min(height, maxSteps);
}
return Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
}
private static void ValidateRectangle(Rectangle rectangle)
{
Guard.MustBeGreaterThan(
@ -284,4 +315,35 @@ public static partial class ParallelRowIterator
0,
$"{nameof(rectangle)}.{nameof(rectangle.Height)}");
}
/// <summary>
/// Validates the supplied <see cref="ParallelExecutionSettings"/>.
/// </summary>
/// <param name="parallelSettings">The execution settings.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <see cref="ParallelExecutionSettings.MaxDegreeOfParallelism"/> or
/// <see cref="ParallelExecutionSettings.MinimumPixelsProcessedPerTask"/> is invalid.
/// </exception>
/// <exception cref="ArgumentNullException">
/// Thrown when <see cref="ParallelExecutionSettings.MemoryAllocator"/> is null.
/// This also guards the public <see cref="ParallelExecutionSettings"/> default value, which bypasses constructor validation.
/// </exception>
private static void ValidateSettings(in ParallelExecutionSettings parallelSettings)
{
// ParallelExecutionSettings is a public struct, so callers can pass default and bypass constructor validation.
if (parallelSettings.MaxDegreeOfParallelism is 0 or < -1)
{
throw new ArgumentOutOfRangeException(
$"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MaxDegreeOfParallelism)}");
}
Guard.MustBeGreaterThan(
parallelSettings.MinimumPixelsProcessedPerTask,
0,
$"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MinimumPixelsProcessedPerTask)}");
Guard.NotNull(
parallelSettings.MemoryAllocator,
$"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MemoryAllocator)}");
}
}

1
src/ImageSharp/Configuration.cs

@ -64,6 +64,7 @@ public sealed class Configuration
/// <summary>
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this <see cref="Configuration"/> instance.
/// Set to <c>-1</c> to leave the degree of parallelism unbounded.
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
/// </summary>
public int MaxDegreeOfParallelism

115
tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers;
public class ParallelRowIteratorTests
{
public delegate void BufferedRowAction<T>(int y, Span<T> span);
public delegate void RowIntervalAction<T>(RowInterval rows, Span<T> span);
private readonly ITestOutputHelper output;
@ -200,6 +201,47 @@ public class ParallelRowIteratorTests
Assert.Equal(expectedData, actualData);
}
[Fact]
public void IterateRows_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows()
{
ParallelExecutionSettings parallelSettings = new(
-1,
10,
Configuration.Default.MemoryAllocator);
Rectangle rectangle = new(0, 0, 10, 10);
int[] actualData = new int[rectangle.Height];
void RowAction(int y) => actualData[y]++;
TestRowActionOperation operation = new(RowAction);
ParallelRowIterator.IterateRows(
rectangle,
in parallelSettings,
in operation);
Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData);
}
[Fact]
public void IterateRowsWithTempBuffer_DefaultSettingsRequireInitialization()
{
ParallelExecutionSettings parallelSettings = default;
Rectangle rect = new(0, 0, 10, 10);
void RowAction(int y, Span<Rgba32> memory)
{
}
TestRowOperation<Rgba32> operation = new(RowAction);
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ParallelRowIterator.IterateRows<TestRowOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in operation));
Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message);
}
public static TheoryData<int, int, int, int, int, int, int> IterateRows_WithEffectiveMinimumPixelsLimit_Data =
new()
{
@ -296,6 +338,53 @@ public class ParallelRowIteratorTests
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
}
[Fact]
public void IterateRowIntervalsWithTempBuffer_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows()
{
ParallelExecutionSettings parallelSettings = new(
-1,
10,
Configuration.Default.MemoryAllocator);
Rectangle rectangle = new(0, 0, 10, 10);
int[] actualData = new int[rectangle.Height];
void RowAction(RowInterval rows, Span<Vector4> buffer)
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y]++;
}
}
TestRowIntervalOperation<Vector4> operation = new(RowAction);
ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle,
in parallelSettings,
in operation);
Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData);
}
[Fact]
public void IterateRows_DefaultSettingsRequireInitialization()
{
ParallelExecutionSettings parallelSettings = default;
Rectangle rect = new(0, 0, 10, 10);
void RowAction(int y)
{
}
TestRowActionOperation operation = new(RowAction);
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation));
Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message);
}
public static readonly TheoryData<int, int, int, int, int, int, int> IterateRectangularBuffer_Data =
new()
{
@ -445,6 +534,32 @@ public class ParallelRowIteratorTests
}
}
private readonly struct TestRowActionOperation : IRowOperation
{
private readonly Action<int> action;
public TestRowActionOperation(Action<int> action)
=> this.action = action;
public void Invoke(int y)
=> this.action(y);
}
private readonly struct TestRowOperation<TBuffer> : IRowOperation<TBuffer>
where TBuffer : unmanaged
{
private readonly BufferedRowAction<TBuffer> action;
public TestRowOperation(BufferedRowAction<TBuffer> action)
=> this.action = action;
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
public void Invoke(int y, Span<TBuffer> span)
=> this.action(y, span);
}
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action<RowInterval> action;

Loading…
Cancel
Save