diff --git a/src/ImageSharp/Advanced/ParallelExecutionSettings.cs b/src/ImageSharp/Advanced/ParallelExecutionSettings.cs index fd9692f9ae..ad0318297a 100644 --- a/src/ImageSharp/Advanced/ParallelExecutionSettings.cs +++ b/src/ImageSharp/Advanced/ParallelExecutionSettings.cs @@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings /// /// Initializes a new instance of the struct. /// - /// The value used for initializing when using TPL. + /// + /// The value used for initializing when using TPL. + /// Set to -1 to leave the degree of parallelism unbounded. + /// /// The value for . /// The . public ParallelExecutionSettings( @@ -44,7 +47,10 @@ public ParallelExecutionSettings( /// /// Initializes a new instance of the struct. /// - /// The value used for initializing when using TPL. + /// + /// The value used for initializing when using TPL. + /// Set to -1 to leave the degree of parallelism unbounded. + /// /// The . public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator) : this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator) @@ -58,6 +64,7 @@ public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator mem /// /// Gets the value used for initializing when using TPL. + /// A value of -1 leaves the degree of parallelism unbounded. /// public int MaxDegreeOfParallelism { get; } diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs index d170631a29..98c2656d11 100644 --- a/src/ImageSharp/Advanced/ParallelRowIterator.cs +++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs @@ -44,14 +44,14 @@ public static void IterateRows( 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 void IterateRows( } int verticalStep = DivideCeil(rectangle.Height, numOfSteps); - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); _ = Parallel.For( @@ -109,14 +109,14 @@ public static void IterateRows( 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 void IterateRows( } int verticalStep = DivideCeil(height, numOfSteps); - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); _ = Parallel.For( @@ -174,14 +174,14 @@ public static void IterateRowIntervals( 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 void IterateRowIntervals( } int verticalStep = DivideCeil(rectangle.Height, numOfSteps); - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); _ = Parallel.For( @@ -236,14 +236,14 @@ public static void IterateRowIntervals( 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 void IterateRowIntervals( } int verticalStep = DivideCeil(height, numOfSteps); - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); _ = Parallel.For( @@ -272,6 +272,37 @@ public static void IterateRowIntervals( [MethodImpl(InliningOptions.ShortMethod)] private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue); + /// + /// Creates the for the current iteration. + /// + /// The execution settings. + /// The number of row partitions to execute. + /// The instance. + [MethodImpl(InliningOptions.ShortMethod)] + private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps) + => new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps }; + + /// + /// Calculates the number of row partitions to execute for the given region. + /// + /// The width of the region. + /// The height of the region. + /// The execution settings. + /// The number of row partitions to execute. + [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 @@ private static void ValidateRectangle(Rectangle rectangle) 0, $"{nameof(rectangle)}.{nameof(rectangle.Height)}"); } + + /// + /// Validates the supplied . + /// + /// The execution settings. + /// + /// Thrown when or + /// is invalid. + /// + /// + /// Thrown when is null. + /// This also guards the public default value, which bypasses constructor validation. + /// + 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)}"); + } } diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index c2b02dedd9..2673927231 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -64,6 +64,7 @@ public Configuration(params IImageFormatConfigurationModule[] configurationModul /// /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms /// configured with this instance. + /// Set to -1 to leave the degree of parallelism unbounded. /// Initialized with by default. /// public int MaxDegreeOfParallelism diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs index 4b06f877fc..cf68f702ac 100644 --- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs @@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers; public class ParallelRowIteratorTests { + public delegate void BufferedRowAction(int y, Span span); public delegate void RowIntervalAction(RowInterval rows, Span span); private readonly ITestOutputHelper output; @@ -200,6 +201,47 @@ void RowAction(RowInterval rows, Span buffer) 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 memory) + { + } + + TestRowOperation operation = new(RowAction); + + ArgumentOutOfRangeException ex = Assert.Throws( + () => ParallelRowIterator.IterateRows, Rgba32>(rect, in parallelSettings, in operation)); + + Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message); + } + public static TheoryData IterateRows_WithEffectiveMinimumPixelsLimit_Data = new() { @@ -296,6 +338,53 @@ void RowAction(RowInterval rows, Span buffer) 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 buffer) + { + for (int y = rows.Min; y < rows.Max; y++) + { + actualData[y]++; + } + } + + TestRowIntervalOperation operation = new(RowAction); + + ParallelRowIterator.IterateRowIntervals, 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( + () => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation)); + + Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message); + } + public static readonly TheoryData IterateRectangularBuffer_Data = new() { @@ -445,6 +534,32 @@ public void Invoke(int y) } } + private readonly struct TestRowActionOperation : IRowOperation + { + private readonly Action action; + + public TestRowActionOperation(Action action) + => this.action = action; + + public void Invoke(int y) + => this.action(y); + } + + private readonly struct TestRowOperation : IRowOperation + where TBuffer : unmanaged + { + private readonly BufferedRowAction action; + + public TestRowOperation(BufferedRowAction action) + => this.action = action; + + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + public void Invoke(int y, Span span) + => this.action(y, span); + } + private readonly struct TestRowIntervalOperation : IRowIntervalOperation { private readonly Action action;