Skip to content

Commit d3ca6eb

Browse files
Merge pull request #3110 from SixLabors/js/parallel-settings
Allow -1 (unbounded) parallelism; validate settings
2 parents cc9a514 + ad58e74 commit d3ca6eb

File tree

4 files changed

+199
-14
lines changed

4 files changed

+199
-14
lines changed

src/ImageSharp/Advanced/ParallelExecutionSettings.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings
1818
/// <summary>
1919
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
2020
/// </summary>
21-
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
21+
/// <param name="maxDegreeOfParallelism">
22+
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
23+
/// Set to <c>-1</c> to leave the degree of parallelism unbounded.
24+
/// </param>
2225
/// <param name="minimumPixelsProcessedPerTask">The value for <see cref="MinimumPixelsProcessedPerTask"/>.</param>
2326
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
2427
public ParallelExecutionSettings(
@@ -44,7 +47,10 @@ public ParallelExecutionSettings(
4447
/// <summary>
4548
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
4649
/// </summary>
47-
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
50+
/// <param name="maxDegreeOfParallelism">
51+
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
52+
/// Set to <c>-1</c> to leave the degree of parallelism unbounded.
53+
/// </param>
4854
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
4955
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
5056
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
@@ -58,6 +64,7 @@ public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator mem
5864

5965
/// <summary>
6066
/// Gets the value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
67+
/// A value of <c>-1</c> leaves the degree of parallelism unbounded.
6168
/// </summary>
6269
public int MaxDegreeOfParallelism { get; }
6370

src/ImageSharp/Advanced/ParallelRowIterator.cs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ public static void IterateRows<T>(
4444
where T : struct, IRowOperation
4545
{
4646
ValidateRectangle(rectangle);
47+
ValidateSettings(parallelSettings);
4748

4849
int top = rectangle.Top;
4950
int bottom = rectangle.Bottom;
5051
int width = rectangle.Width;
5152
int height = rectangle.Height;
5253

53-
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
54-
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
54+
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
5555

5656
// Avoid TPL overhead in this trivial case:
5757
if (numOfSteps == 1)
@@ -65,7 +65,7 @@ public static void IterateRows<T>(
6565
}
6666

6767
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
68-
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
68+
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
6969
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
7070

7171
_ = Parallel.For(
@@ -109,14 +109,14 @@ public static void IterateRows<T, TBuffer>(
109109
where TBuffer : unmanaged
110110
{
111111
ValidateRectangle(rectangle);
112+
ValidateSettings(parallelSettings);
112113

113114
int top = rectangle.Top;
114115
int bottom = rectangle.Bottom;
115116
int width = rectangle.Width;
116117
int height = rectangle.Height;
117118

118-
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
119-
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
119+
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
120120
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
121121
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
122122

@@ -135,7 +135,7 @@ public static void IterateRows<T, TBuffer>(
135135
}
136136

137137
int verticalStep = DivideCeil(height, numOfSteps);
138-
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
138+
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
139139
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
140140

141141
_ = Parallel.For(
@@ -174,14 +174,14 @@ public static void IterateRowIntervals<T>(
174174
where T : struct, IRowIntervalOperation
175175
{
176176
ValidateRectangle(rectangle);
177+
ValidateSettings(parallelSettings);
177178

178179
int top = rectangle.Top;
179180
int bottom = rectangle.Bottom;
180181
int width = rectangle.Width;
181182
int height = rectangle.Height;
182183

183-
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
184-
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
184+
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
185185

186186
// Avoid TPL overhead in this trivial case:
187187
if (numOfSteps == 1)
@@ -192,7 +192,7 @@ public static void IterateRowIntervals<T>(
192192
}
193193

194194
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
195-
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
195+
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
196196
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
197197

198198
_ = Parallel.For(
@@ -236,14 +236,14 @@ public static void IterateRowIntervals<T, TBuffer>(
236236
where TBuffer : unmanaged
237237
{
238238
ValidateRectangle(rectangle);
239+
ValidateSettings(parallelSettings);
239240

240241
int top = rectangle.Top;
241242
int bottom = rectangle.Bottom;
242243
int width = rectangle.Width;
243244
int height = rectangle.Height;
244245

245-
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
246-
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
246+
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
247247
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
248248
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
249249

@@ -259,7 +259,7 @@ public static void IterateRowIntervals<T, TBuffer>(
259259
}
260260

261261
int verticalStep = DivideCeil(height, numOfSteps);
262-
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
262+
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
263263
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
264264

265265
_ = Parallel.For(
@@ -272,6 +272,37 @@ public static void IterateRowIntervals<T, TBuffer>(
272272
[MethodImpl(InliningOptions.ShortMethod)]
273273
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
274274

275+
/// <summary>
276+
/// Creates the <see cref="ParallelOptions"/> for the current iteration.
277+
/// </summary>
278+
/// <param name="parallelSettings">The execution settings.</param>
279+
/// <param name="numOfSteps">The number of row partitions to execute.</param>
280+
/// <returns>The <see cref="ParallelOptions"/> instance.</returns>
281+
[MethodImpl(InliningOptions.ShortMethod)]
282+
private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps)
283+
=> new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps };
284+
285+
/// <summary>
286+
/// Calculates the number of row partitions to execute for the given region.
287+
/// </summary>
288+
/// <param name="width">The width of the region.</param>
289+
/// <param name="height">The height of the region.</param>
290+
/// <param name="parallelSettings">The execution settings.</param>
291+
/// <returns>The number of row partitions to execute.</returns>
292+
[MethodImpl(InliningOptions.ShortMethod)]
293+
private static int GetNumberOfSteps(int width, int height, in ParallelExecutionSettings parallelSettings)
294+
{
295+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
296+
297+
if (parallelSettings.MaxDegreeOfParallelism == -1)
298+
{
299+
// Row batching cannot produce more useful partitions than the number of rows available.
300+
return Math.Min(height, maxSteps);
301+
}
302+
303+
return Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
304+
}
305+
275306
private static void ValidateRectangle(Rectangle rectangle)
276307
{
277308
Guard.MustBeGreaterThan(
@@ -284,4 +315,35 @@ private static void ValidateRectangle(Rectangle rectangle)
284315
0,
285316
$"{nameof(rectangle)}.{nameof(rectangle.Height)}");
286317
}
318+
319+
/// <summary>
320+
/// Validates the supplied <see cref="ParallelExecutionSettings"/>.
321+
/// </summary>
322+
/// <param name="parallelSettings">The execution settings.</param>
323+
/// <exception cref="ArgumentOutOfRangeException">
324+
/// Thrown when <see cref="ParallelExecutionSettings.MaxDegreeOfParallelism"/> or
325+
/// <see cref="ParallelExecutionSettings.MinimumPixelsProcessedPerTask"/> is invalid.
326+
/// </exception>
327+
/// <exception cref="ArgumentNullException">
328+
/// Thrown when <see cref="ParallelExecutionSettings.MemoryAllocator"/> is null.
329+
/// This also guards the public <see cref="ParallelExecutionSettings"/> default value, which bypasses constructor validation.
330+
/// </exception>
331+
private static void ValidateSettings(in ParallelExecutionSettings parallelSettings)
332+
{
333+
// ParallelExecutionSettings is a public struct, so callers can pass default and bypass constructor validation.
334+
if (parallelSettings.MaxDegreeOfParallelism is 0 or < -1)
335+
{
336+
throw new ArgumentOutOfRangeException(
337+
$"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MaxDegreeOfParallelism)}");
338+
}
339+
340+
Guard.MustBeGreaterThan(
341+
parallelSettings.MinimumPixelsProcessedPerTask,
342+
0,
343+
$"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MinimumPixelsProcessedPerTask)}");
344+
345+
Guard.NotNull(
346+
parallelSettings.MemoryAllocator,
347+
$"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MemoryAllocator)}");
348+
}
287349
}

src/ImageSharp/Configuration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public Configuration(params IImageFormatConfigurationModule[] configurationModul
6464
/// <summary>
6565
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
6666
/// configured with this <see cref="Configuration"/> instance.
67+
/// Set to <c>-1</c> to leave the degree of parallelism unbounded.
6768
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
6869
/// </summary>
6970
public int MaxDegreeOfParallelism

tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers;
1313

1414
public class ParallelRowIteratorTests
1515
{
16+
public delegate void BufferedRowAction<T>(int y, Span<T> span);
1617
public delegate void RowIntervalAction<T>(RowInterval rows, Span<T> span);
1718

1819
private readonly ITestOutputHelper output;
@@ -200,6 +201,47 @@ void RowAction(RowInterval rows, Span<Vector4> buffer)
200201
Assert.Equal(expectedData, actualData);
201202
}
202203

204+
[Fact]
205+
public void IterateRows_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows()
206+
{
207+
ParallelExecutionSettings parallelSettings = new(
208+
-1,
209+
10,
210+
Configuration.Default.MemoryAllocator);
211+
212+
Rectangle rectangle = new(0, 0, 10, 10);
213+
int[] actualData = new int[rectangle.Height];
214+
215+
void RowAction(int y) => actualData[y]++;
216+
217+
TestRowActionOperation operation = new(RowAction);
218+
219+
ParallelRowIterator.IterateRows(
220+
rectangle,
221+
in parallelSettings,
222+
in operation);
223+
224+
Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData);
225+
}
226+
227+
[Fact]
228+
public void IterateRowsWithTempBuffer_DefaultSettingsRequireInitialization()
229+
{
230+
ParallelExecutionSettings parallelSettings = default;
231+
Rectangle rect = new(0, 0, 10, 10);
232+
233+
void RowAction(int y, Span<Rgba32> memory)
234+
{
235+
}
236+
237+
TestRowOperation<Rgba32> operation = new(RowAction);
238+
239+
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
240+
() => ParallelRowIterator.IterateRows<TestRowOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in operation));
241+
242+
Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message);
243+
}
244+
203245
public static TheoryData<int, int, int, int, int, int, int> IterateRows_WithEffectiveMinimumPixelsLimit_Data =
204246
new()
205247
{
@@ -296,6 +338,53 @@ void RowAction(RowInterval rows, Span<Vector4> buffer)
296338
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
297339
}
298340

341+
[Fact]
342+
public void IterateRowIntervalsWithTempBuffer_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows()
343+
{
344+
ParallelExecutionSettings parallelSettings = new(
345+
-1,
346+
10,
347+
Configuration.Default.MemoryAllocator);
348+
349+
Rectangle rectangle = new(0, 0, 10, 10);
350+
int[] actualData = new int[rectangle.Height];
351+
352+
void RowAction(RowInterval rows, Span<Vector4> buffer)
353+
{
354+
for (int y = rows.Min; y < rows.Max; y++)
355+
{
356+
actualData[y]++;
357+
}
358+
}
359+
360+
TestRowIntervalOperation<Vector4> operation = new(RowAction);
361+
362+
ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Vector4>, Vector4>(
363+
rectangle,
364+
in parallelSettings,
365+
in operation);
366+
367+
Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData);
368+
}
369+
370+
[Fact]
371+
public void IterateRows_DefaultSettingsRequireInitialization()
372+
{
373+
ParallelExecutionSettings parallelSettings = default;
374+
Rectangle rect = new(0, 0, 10, 10);
375+
376+
void RowAction(int y)
377+
{
378+
}
379+
380+
TestRowActionOperation operation = new(RowAction);
381+
382+
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
383+
() => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation));
384+
385+
Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message);
386+
}
387+
299388
public static readonly TheoryData<int, int, int, int, int, int, int> IterateRectangularBuffer_Data =
300389
new()
301390
{
@@ -445,6 +534,32 @@ public void Invoke(int y)
445534
}
446535
}
447536

537+
private readonly struct TestRowActionOperation : IRowOperation
538+
{
539+
private readonly Action<int> action;
540+
541+
public TestRowActionOperation(Action<int> action)
542+
=> this.action = action;
543+
544+
public void Invoke(int y)
545+
=> this.action(y);
546+
}
547+
548+
private readonly struct TestRowOperation<TBuffer> : IRowOperation<TBuffer>
549+
where TBuffer : unmanaged
550+
{
551+
private readonly BufferedRowAction<TBuffer> action;
552+
553+
public TestRowOperation(BufferedRowAction<TBuffer> action)
554+
=> this.action = action;
555+
556+
public int GetRequiredBufferLength(Rectangle bounds)
557+
=> bounds.Width;
558+
559+
public void Invoke(int y, Span<TBuffer> span)
560+
=> this.action(y, span);
561+
}
562+
448563
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
449564
{
450565
private readonly Action<RowInterval> action;

0 commit comments

Comments
 (0)