我想找到按元素相乘两个数组的最佳方法。这是更广泛项目的一部分,其中性能而不是唯一的考虑因素。
我今天开始用 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/