Skip to content

ParallelHelper.For: integer overflow in range length causes single-threaded fallback and OverflowException on valid inputs #1188

@kzorin52

Description

@kzorin52

Describe the bug

ParallelHelper.For<TAction>(int start, int end, in TAction action, int minimumActionsPerThread) silently degrades to a single-threaded execution for very large ranges, and can also throw OverflowException for some valid inputs.

The root cause is this computation inside the method:

int count = Math.Abs(start - end);
int maxBatches = 1 + ((count - 1) / minimumActionsPerThread);

  1. start - end is computed in int and can overflow. For the full range [int.MinValue, int.MaxValue) the subtraction wraps around to 1, so count == 1, maxBatches == 1, numBatches == 1, and the method executes the whole loop on the calling thread instead of scheduling work via Parallel.For as the caller explicitly requested.
  2. For ranges like [int.MinValue, 0) the subtraction produces exactly int.MinValue, and Math.Abs(int.MinValue) throws OverflowException — the caller gets an exception for an otherwise valid input.

The input is already validated with start > end → throw, so end - start is guaranteed to be non-negative as a mathematical value, and it simply needs to be computed as long to avoid overflow.

Regression

Not a regression as far as I can tell; the code has looked like this for a long time.

Steps to reproduce

Tested on .NET 11, CommunityToolkit.HighPerformance 8.4.2.

using CommunityToolkit.HighPerformance.Helpers;

internal readonly struct ThreadRecordingAction(int[] threadIds) : IAction
{
    public void Invoke(int i)
    {
        // Only sample a few iterations to keep the repro fast
        if ((i & 0xFFFFFF) != 0) return;
        
        var slot = i >>> 24;
        Volatile.Write(ref threadIds[slot], Environment.CurrentManagedThreadId);
    }
}

internal static class Program
{
    private static void Main()
    {
        // Bug #1: silently becomes single-threaded
        var ids = new int[256];
        ParallelHelper.For(int.MinValue, int.MaxValue, new ThreadRecordingAction(ids), 1);

        Array.Sort(ids);
        var unique = ids.Where((t, i) => i == 0 || t != ids[i - 1]).Count();

        Console.WriteLine($"Distinct thread ids observed: {unique}"); // prints 1 on any multicore machine

        // Bug #2: throws OverflowException
        try
        {
            ParallelHelper.For(int.MinValue, 0, new ThreadRecordingAction(new int[1]), 1);
        }
        catch (OverflowException ex)
        {
            Console.WriteLine($"Unexpected: {ex}");
        }
    }
}

Expected behavior

  1. ParallelHelper.For(int.MinValue, int.MaxValue, ..., 1) should distribute work across all available cores, just like it does for any smaller range.
  2. ParallelHelper.For(int.MinValue, 0, ..., 1) should execute successfully and not throw OverflowException.

More generally: for any valid pair (start, end) with start <= end, the method must compute the true element count without relying on int arithmetic that overflows.

IDE and version

Rider

IDE version

JetBrains Rider 2026.1 EAP 9 D

Nuget packages

  • CommunityToolkit.Common
  • CommunityToolkit.Diagnostics
  • CommunityToolkit.HighPerformance
  • CommunityToolkit.Mvvm (aka MVVM Toolkit)

Nuget package version(s)

8.4.2

Suggested fix

Compute the range as long:

long count = (long)end - start; // always >= 0 after the start > end check
long maxBatches = 1 + ((count - 1) / minimumActionsPerThread);
int numBatches = (int)Math.Min(maxBatches, (long)Environment.ProcessorCount);
...
int batchSize = (int)(1 + ((count - 1) / (long)numBatches));

(batchSize fits into int because it is divided by numBatches <= ProcessorCount, but for full correctness ActionInvoker<TAction> iteration bounds may need to be reviewed too.)
The bug is especially insidious because it does not throw or log anything — the workload simply runs on the calling thread, which looks like the parallel helper is "slow" rather than broken.

Help us help you

Yes, but only if others can assist

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐛An unexpected issue that highlights incorrect behavior

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions