You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Parallel.For循环计算精度丢失问题排查与解决问询

并行求和与串行求和结果不一致问题分析与解决

问题背景

处理含300000+个double类型元素的数组时,串行for循环计算结果正确但速度较慢,改用Parallel.For后结果出现细微偏差且不稳定。复现代码如下:

static void Main(string[] args)
{
    int iterations = 200000;

   // Method 1, Parallel.For-loop. Result is inconsistent and slightly off

    ConcurrentBag<double> sums = new ConcurrentBag<double>();

    Parallel.For<double>(0, iterations, () => 0, (i, state, sum) =>
    {
        sum += i * Math.PI / 1000000;
        return sum;
    },
    (x) => sums.Add(x));                       // <= cause of the problem?

    double parallelsum = sums.Sum();

    // Method 2, regular for-loop. Result is consistent and correct

    double regularsum = 0;

    for (int i = 0; i < iterations; i++)
        regularsum += i * Math.PI / 1000000;

    // parallelsum: 62831.538912530465 (roughly, as it varies a bit)
    // regularsum:  62831.5389125305
}

串行计算结果稳定且正确,并行计算结果在第10位小数后出现偏差,且每次运行结果略有不同。曾怀疑类型被转为float,但调试确认sum和x均为double类型,类型转换尝试无效。


问题原因

核心原因是浮点数加法的非交换性:

  • 串行求和严格按照i=0,1,2,...,199999的顺序累加,所有数值依次加到同一个累加器上,累加顺序固定。
  • 并行求和时,Parallel.For会将迭代任务拆分给多个线程,每个线程维护独立的局部累加器,最后汇总所有局部结果。由于线程处理的迭代区间随机,累加顺序完全打乱,而double这类浮点数的加法不满足交换律——不同的累加顺序会导致微小的精度差异,尤其是累加数值量级差异较大时,偏差会更明显。

ConcurrentBag并非问题根源,它只是收集局部累加结果的容器,真正的问题出在累加顺序的改变。


解决方法

根据业务对精度和性能的要求,可选择以下方案:

1. 接受微小精度差异

如果业务场景允许这种极微小的偏差(比如多数科学计算、工程计算场景),直接保留当前并行方案即可,这种偏差通常远低于业务允许的误差阈值。

2. 使用高精度累加器

改用decimal类型进行累加,它的精度远高于double,累加顺序带来的差异会显著减小。注意decimal计算速度比double慢,需要权衡性能与精度:

static void Main(string[] args)
{
    int iterations = 200000;

    ConcurrentBag<decimal> sums = new ConcurrentBag<decimal>();

    Parallel.For<decimal>(0, iterations, () => 0, (i, state, sum) =>
    {
        sum += (decimal)i * (decimal)Math.PI / 1000000;
        return sum;
    },
    (x) => sums.Add(x));

    decimal parallelsum = sums.Sum();

    decimal regularsum = 0;
    for (int i = 0; i < iterations; i++)
        regularsum += (decimal)i * (decimal)Math.PI / 1000000;

    // 此时parallelsum和regularsum结果高度一致
}

3. 保持累加顺序(兼顾并行与精度)

如果必须和串行结果完全一致,可以将迭代任务分块,每个块内保持串行累加,最后汇总块结果。这种方式既利用并行提升速度,又保证块内累加顺序与串行一致,最终结果完全匹配:

static void Main(string[] args)
{
    int iterations = 200000;
    int chunkSize = 1000; // 可根据实际情况调整块大小
    double[] chunkSums = new double[(iterations + chunkSize - 1) / chunkSize];

    Parallel.For(0, chunkSums.Length, chunkIndex =>
    {
        int start = chunkIndex * chunkSize;
        int end = Math.Min(start + chunkSize, iterations);
        double sum = 0;
        for (int i = start; i < end; i++)
        {
            sum += i * Math.PI / 1000000;
        }
        chunkSums[chunkIndex] = sum;
    });

    double parallelsum = chunkSums.Sum();

    double regularsum = 0;
    for (int i = 0; i < iterations; i++)
        regularsum += i * Math.PI / 1000000;

    // 此时parallelsum和regularsum结果完全一致
}

内容的提问来源于stack exchange,提问作者user30064726

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 15:42:37