【发布时间】:2019-09-19 17:04:41
【问题描述】:
我正在尝试通过利用 System.Numerics 对 float[] 数组执行 SIMD 操作来提高 .NET Core 库的性能。 System.Numerics 现在有点时髦,我很难看到它有什么好处。我知道,为了看到 SIMD 的性能提升,它必须通过大量计算进行分摊,但考虑到它目前的实现方式,我不知道如何实现这一点。
Vector<float> 需要 8 个 float 值 - 不多也不少。如果我想对一组小于 8 的值执行 SIMD 操作,我不得不将这些值复制到一个新数组并用零填充剩余部分。如果这组值大于 8,我需要复制这些值,用零填充以确保其长度与 8 的倍数对齐,然后循环它们。长度要求是有道理的,但适应这一点似乎是抵消任何性能提升的好方法。
我编写了一个测试包装类来处理填充和对齐:
public readonly struct VectorWrapper<T>
where T : unmanaged
{
#region Data Members
public readonly int Length;
private readonly T[] data_;
#endregion
#region Constructor
public VectorWrapper( T[] data )
{
Length = data.Length;
var stepSize = Vector<T>.Count;
var bufferedLength = data.Length - ( data.Length % stepSize ) + stepSize;
data_ = new T[ bufferedLength ];
data.CopyTo( data_, 0 );
}
#endregion
#region Public Methods
public T[] ToArray()
{
var returnData = new T[ Length ];
data_.AsSpan( 0, Length ).CopyTo( returnData );
return returnData;
}
#endregion
#region Operators
public static VectorWrapper<T> operator +( VectorWrapper<T> l, VectorWrapper<T> r )
{
var resultLength = l.Length;
var result = new VectorWrapper<T>( new T[ l.Length ] );
var lSpan = l.data_.AsSpan();
var rSpan = r.data_.AsSpan();
var stepSize = Vector<T>.Count;
for( var i = 0; i < resultLength; i += stepSize )
{
var lVec = new Vector<T>( lSpan.Slice( i ) );
var rVec = new Vector<T>( rSpan.Slice( i ) );
Vector.Add( lVec, rVec ).CopyTo( result.data_, i );
}
return result;
}
#endregion
}
这个包装器可以解决问题。计算似乎是正确的,Vector<T> 不会抱怨元素的输入计数。但是,它的速度是简单的基于范围的 for 循环的两倍。
这是基准:
public class VectorWrapperBenchmarks
{
#region Data Members
private static float[] arrayA;
private static float[] arrayB;
private static VectorWrapper<float> vecA;
private static VectorWrapper<float> vecB;
#endregion
#region Constructor
public VectorWrapperBenchmarks()
{
arrayA = new float[ 1024 ];
arrayB = new float[ 1024 ];
for( var i = 0; i < 1024; i++ )
arrayA[ i ] = arrayB[ i ] = i;
vecA = new VectorWrapper<float>( arrayA );
vecB = new VectorWrapper<float>( arrayB );
}
#endregion
[Benchmark]
public void ForLoopSum()
{
var aA = arrayA;
var aB = arrayB;
var result = new float[ 1024 ];
for( var i = 0; i < 1024; i++ )
result[ i ] = aA[ i ] + aB[ i ];
}
[Benchmark]
public void VectorSum()
{
var vA = vecA;
var vB = vecB;
var result = vA + vB;
}
}
结果:
| Method | Mean | Error | StdDev |
|----------- |-----------:|---------:|---------:|
| ForLoopSum | 757.6 ns | 15.67 ns | 17.41 ns |
| VectorSum | 1,335.7 ns | 17.25 ns | 16.13 ns |
我的处理器 (i7-6700k) 确实支持 SIMD 硬件加速,它在 64 位发布模式下运行,并在 .NET Core 2.2 (Windows 10) 上启用了优化。
我意识到Array.CopyTo() 可能是影响性能的很大一部分,但似乎没有简单的方法来同时拥有不明确符合Vector<T> 的填充/对齐和数据集规格。
我对 SIMD 比较陌生,而且我知道 C# 实现仍处于早期阶段。但是,我看不出有什么明确的方法可以真正从中受益,尤其是考虑到它在扩展到更大的数据集时是最有益的。
有没有更好的方法来解决这个问题?
【问题讨论】:
-
我不明白你为什么要把这变成一个非此即彼的场景。对源数组的向量大小的切片使用向量指令,然后在标准循环中一个一个地处理最后几个剩余的元素。不需要复制或填充。顺便说一句,如果您专门针对 Core,您可能还会对更新、更灵活的
System.Runtime.Intrinsics命名空间感兴趣。 -
我无法给出完整的答案。但是,如果您需要为每个向量创建一个跨度或数据副本以进行处理,那么由于开销(如您所知),这是错误的方法。有些代码模式不需要它。
+的每个循环迭代只是两次内存读取,一次添加,一次存储。就像在本机代码中一样。必须使用第二个循环逐元素处理数组的尾部。没有填充。
标签: c# .net .net-core simd system.numerics