c# - 在 C# 中按元素相乘数组具有意想不到的性能

标签 c# arrays task-parallel-library simd parallel.for

我想找到按元素相乘两个数组的最佳方法。这是更广泛项目的一部分,其中性能而不是唯一的考虑因素。

我今天开始用 C# (Linqpad) 编写一些函数,因此它还没有以任何方式进行优化。下面代码的输出如下:

Environment.ProcessorCount: 4
Vector<double>.Count: 4

For sequential: 129ms, sum: 2.30619276241231E+25
Plinq: 344ms, sum: 2.30619276241231E+25
Parallel.For: 137ms, 2.30619276241231E+25
Simd sequential: 100ms, sum: 2.30619276241231E+25
Simd parallel: 761ms

这包括乘法的执行时间和作为检查的结果的总和。这里有一些奇怪的结果(我对 C# 有点生疏,所以它很可能是我的代码):

  • 常规 for 比并行 for 更快
  • plinq 相对于其他程序来说非常慢 - 我在这里做了一些愚蠢的事情吗?
  • simd 是最快的,但差距不大
  • 间歇性地,simd 方法需要更长的时间
  • 并行 simd 是否可能(提供实现或解释的加分)?

我的代码如下 - 有对 Nuget System.Numerics.Vector 包的引用。如果有任何意见、建议、更正或替代方案,我将不胜感激...

using System.Threading.Tasks;
using System.Numerics;
using System.Collections.Concurrent;

void Main()
{
    var random = new Random();

    var arraySize = 20_000_001;

    var x = new double[arraySize];
    var y = new double[arraySize];

    for (var i = 0; i < x.Length; ++i)
    {
        x[i] = random.Next();
        y[i] = random.Next();
    }

    Console.WriteLine($"Environment.ProcessorCount: {Environment.ProcessorCount}");
    Console.WriteLine($"Vector<double>.Count: {Vector<double>.Count}\n");

    MultiplyFor(x, y);
    MultiplyPlinq(x, y);
    MultiplyParallelFor(x, y);
    MultiplySIMD(x, y);
    MultiplyParallelSIMD(x, y);

}

void MultiplyPlinq(double[] x, double[] y)
{
    var result = new double[x.Length];

    var sw = new Stopwatch();

    sw.Start();

    result = ParallelEnumerable.Range(0, x.Length).Select(i => x[i] * y[i]).ToArray();

    sw.Stop();

    Console.WriteLine($"Plinq: {sw.ElapsedMilliseconds}ms, sum: {SumCheck(result)}");
}

double SumCheck(double[] x)
{
    return Math.Round(x.Sum() , 4);
}

void MultiplyFor(double[] x, double[] y)
{
    var result = new double[x.Length];

    var sw = new Stopwatch();

    sw.Start();

    for (var i = 0; i < x.Length; ++i)
    {
        result[i] = x[i] * y[i];
    }

    sw.Stop();

    Console.WriteLine($"For sequential: {sw.ElapsedMilliseconds}ms, sum: {SumCheck(result)}");

}

void MultiplyParallelFor(double[] x, double[] y)
{
    var result = new double[x.Length];

    var sw = new Stopwatch();

    sw.Start();

    Parallel.For(0, x.Length, i =>
    {
        result[i] = x[i] * y[i];
    });

    sw.Stop();

    Console.WriteLine($"Parallel.For: {sw.ElapsedMilliseconds}ms, {SumCheck(result)}");

}

void MultiplySIMD(double[] x, double[] y)
{
    var sw = new Stopwatch();

    sw.Start();

    var result = MultiplyByVectors(x, y);

    sw.Stop();

    // 2 cores, 4 logical, 256b register
    Console.WriteLine($"Simd sequential: {sw.ElapsedMilliseconds}ms, sum: {SumCheck(result)}");
}

double[] MultiplyByVectors(double[] x, double[] y)
{
    var result = new double[x.Length];

    var vectorSize = Vector<double>.Count;

    int i;

    for (i = 0; i < x.Length - vectorSize; i += vectorSize)
    {
        var vx = new Vector<double>(x, i);
        var vy = new Vector<double>(y, i);
        (vx * vy).CopyTo(result, i);
    }

    for (; i < x.Length; i++)
    {
        result[i] = x[i] * y[i];
    }

    return result;
}

void MultiplyParallelSIMD(double[] x, double[] y)
{
    var sw = new Stopwatch();

    sw.Start();

    var chunkSize = (int)(x.Length / Environment.ProcessorCount);

    Parallel.For(0, Environment.ProcessorCount, i => {

        var complete = i * chunkSize;
        var take = Math.Min(chunkSize, x.Length - i * chunkSize);
        var xSegment = x.Skip((int)complete).Take((int)take);
        var ySegment = y.Skip((int)complete).Take((int)take);
        var result = MultiplyByVectors(xSegment.ToArray(), ySegment.ToArray());

    });

    sw.Stop();

    Console.WriteLine($"Simd parallel: {sw.ElapsedMilliseconds}ms");

}

最佳答案

Parallel.For最简单的形式不适合非常细粒度的工作负载,因为在每个循环上调用匿名函数的开销抵消了并行性的好处(匿名函数无法内联)。解决方案是对数据进行分区,以便并行处理多个分区,同时使用快速直接循环处理每个分区:

Parallel.ForEach(Partitioner.Create(0, x.Length), range =>
{
    for (int i = range.Item1; i < range.Item2; i++)
    {
        result[i] = x[i] * y[i];
    }
});

内置Partitioner在其 current implementation创建与 CPU 核心数 x 3 一样多的分区。

关于并行化 SIMD 操作,在我自己的实验中,我没有在我的 PC 中观察到令人印象深刻的性能改进。我对此的理论是(这只是一个疯狂的猜测,而不是一个有根据的猜测),SIMD 计算发生得如此之快,以至于 RAM 无法跟上 CPU 处理数据的速度。

关于c# - 在 C# 中按元素相乘数组具有意想不到的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59012299/

相关文章:

c# - 画线和时的奇怪行为。矩形

c# - 从两个排序数组中获取前 K 项而不合并它们

ios - 如何映射 json 响应并创建带索引的简单列表

javascript - 从数组中删除特定元素 - javascript

c# - .Net BlockingCollection.Take(2) : Safely removing two items at a time

javascript - Prestashop 中未添加优惠券(结帐页面)

c# - TPL 队列处理

c# - 命令行解析器库 - bool 参数

c# - MonthCalendar 上的 DateChanged 事件有问题

c# - 不在两位数之间时去除小数点