【问题标题】:Is this PLINQ bug?这是 PLINQ 错误吗?
【发布时间】:2011-02-18 07:19:52
【问题描述】:

为什么 PLINQ 输出不同于顺序处理和 Parallel.For 循环

我想添加 10,000,000 个数字的平方根之和。这是 3 种情况的代码:

顺序for循环:

double sum = 0.0;
for(int i = 1;i<10000001;i++)
sum += Math.Sqrt(i);

这个输出是:21081852648.717

现在使用 Parallel.For 循环:

object locker = new object();
double total ;

Parallel.For(1,10000001,
()=>0.0,
(i,state,local)=> local+Math.Sqrt(i),
(local)=>
{
  lock(locker){ total += local; }
}
);

这个输出是:21081852648.7199

现在使用 PLINQ

double tot =  ParallelEnumerable.Range(1, 10000000)
                .Sum(i => Math.Sqrt(i)); 

这个输出是:21081852648.72

为什么 PLINQ 输出与 Parallel.For 和 Sequential for 循环之间存在差异?

【问题讨论】:

    标签: parallel-processing task-parallel-library plinq parallel-extensions


    【解决方案1】:

    我强烈怀疑这是因为双打算术并不是真正的关联。在对值求和时可能会丢失信息,而丢失的信息究竟是什么取决于操作的顺序。

    这是一个展示这种效果的例子:

    using System;
    
    class Test
    {
        static void Main()
        {
            double d1 = 0d;
            for (int i = 0; i < 10000; i++)
            {
                d1 += 0.00000000000000001;
            }
            d1 += 1;
            Console.WriteLine(d1);
    
            double d2 = 1d;
            for (int i = 0; i < 10000; i++)
            {
                d2 += 0.00000000000000001;
            }
            Console.WriteLine(d2);
        }
    }
    

    在第一种情况下,我们可以多次添加非常小的数字,直到它们变得足够大以在添加到 1 时仍然相关。

    在第二种情况下,将 0.00000000000000001 加到 1 总是只会得到 1,因为双精度中没有足够的信息来表示 1.00000000000000001 - 所以最终结果仍然只是 1。

    编辑:我想到了另一个可能令人困惑的方面。对于局部变量,JIT 编译器能够(并且允许)使用 80 位 FP 寄存器,这意味着可以在更少的信息丢失的情况下执行算术运算。对于肯定必须是 64 位的实例变量,不是的情况。在您的 Parallel.For 案例中,total 变量实际上是生成类中的实例变量,因为它是由 lambda 表达式捕获的。这可能会改变结果 - 但它很可能取决于计算机架构、CLR 版本等。

    【讨论】:

    • 通过编写程序在对平方根求和之前对列表进行洗牌进行验证。每次运行时,它会在小数点后给出不同的值。我怀疑您的意思是“交换”而不是“关联”。 :)
    猜你喜欢
    • 2012-03-18
    • 2015-03-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多