An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
The algorithm is logically incorrect for a general associative reduction; it only happens to work for some inputs by coincidence.
Key issues:
- Mixing odd-element handling into
array[0]repeatedly- When
countis odd, this line runs:
That folds the last element intoif (r == 1) array[0] = func(array[0], source[count - 1]);array[0]once. - Inside the loop, whenever
lengthis odd, this runs again:
This keeps reusingif (length % 2 == 1) array[0] = func(array[0], array[length - 1]);array[0]as a special accumulator while also treating it as a normal element in the pairwise combine step. That double role breaks the invariant that each element participates exactly once.
- When
- Pairwise combine loop assumes
lengthis even- The loop:
assumes that for every evenfor (int index = 0; index < length; index++) { if (index % 2 == 0) { int setIndex = index / 2; int getIndex = index + 1; int input1 = array[index]; int input2 = array[getIndex]; int result = func(input1, input2); array[setIndex] = result; } }index,index + 1is a valid partner. Whenlengthis odd,index + 1for the last even index is out of the logical range of elements that should be combined in this pass. The code tries to compensate by pre-folding the last element intoarray[0], but that interacts badly with the reuse ofarray[0]in the same pass.
- The loop:
-
lengthis halved blindly- After each pass:
This is correct only if the pass always reduceslength = length / 2;lengthelements to exactlylength / 2results. Because odd lengths are handled by mutatingarray[0]and then still running the same pairwise loop, the actual number of logical results inarray[0..)no longer matcheslength / 2in a consistent way.
- After each pass:
- Parallelization would introduce race conditions
- The commented
Parallel.For:
If re-enabled as-is, multiple threads would write to the shared//Parallel.For(0, length, (int index) =>arraywithout synchronization. Even though eachsetIndexis unique for each evenindex, the combination with the odd-length handling and reuse ofarray[0]makes correctness highly fragile. Parallel writes to shared state without proper design can lead to unpredictable results, as illustrated in the PLINQ race-condition example wheretotal += nis unsafe when run in parallel.
- The commented
- Why it “sometimes” works
- For small, even-sized lists and simple associative
funclike addition, the particular pattern of pairwise combination happens to produce the same result as a straightforward fold. - As soon as the size, parity, or function changes, the flawed odd-element handling and reuse of
array[0]cause elements to be combined in the wrong order or multiple times.
- For small, even-sized lists and simple associative
A correct reduction should:
- Treat
array[0..length-1]as exactlylengthactive elements. - For each pass, combine elements in disjoint pairs into the first
length/2positions. - If
lengthis odd, carry the last element forward separately (e.g., copy it toarray[length/2]) instead of folding it intoarray[0]. - Update
lengthto the exact new count of active elements.
For example, a safer structure (still single-threaded) is:
public static int Reduce(this List<int> source, Func<int, int, int> func)
{
int[] array = source.ToArray();
int length = array.Length;
while (length > 1)
{
int newLength = length / 2;
int i = 0;
// pairwise combine
for (; i + 1 < length; i += 2)
{
array[i / 2] = func(array[i], array[i + 1]);
}
// carry last element if odd
if (i < length)
{
array[newLength] = array[i];
newLength++;
}
length = newLength;
}
return array[0];
}
This ensures each element is used exactly once per level and avoids the special-case accumulation into array[0] that causes the incorrect results.
References: