System.Numerics.Vector<T> 在大数据集上
System.Numerics.Vector<T> on large data sets
我正在尝试通过利用 System.Numerics 在 float[]
数组上执行 SIMD 操作来提高 .NET Core 库的性能。 System.Numerics
现在有点时髦,我很难看出它有什么好处。我知道为了看到 SIMD 的性能提升,它必须分摊到大量的计算中,但考虑到它目前的实现方式,我无法弄清楚如何实现这一点。
Vector<float>
需要 8 个 float
值 - 不多也不少。如果我想对一组小于 8 的值执行 SIMD 操作,我必须将这些值复制到一个新数组并用零填充剩余部分。如果这组值大于 8,我需要复制这些值,用零填充以确保其长度与 8 的倍数对齐,然后循环遍历它们。长度要求是有道理的,但适应这一点似乎是抵消任何性能提升的好方法。
我写了一个测试包装器 class 来处理填充和对齐:
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()
可能是降低性能的很大一部分,但似乎没有简单的方法来同时拥有 padding/alignment 和未明确符合的数据集符合 Vector<T>
的规格。
我对 SIMD 比较陌生,我知道 C# 实现仍处于早期阶段。但是,我没有看到真正从中受益的明确方法,尤其是考虑到它在扩展到更大的数据集时最有益。
有没有更好的方法来解决这个问题?
我不确定 "funky" 是什么意思,但它现在完全可用(尽管它可能性能更高)。
使用你的案例(求和浮点数)我得到以下结果超过 10003 个项目与一个老人 Haswell CPU:
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4)
Intel Core i7-4500U CPU 1.80GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
Frequency=1753753 Hz, Resolution=570.2057 ns, Timer=TSC
.NET Core SDK=2.1.602
[Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
DefaultJob : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
| Method | Mean | Error | StdDev |
|--------- |----------:|----------:|----------:|
| ScalarOp | 12.974 us | 0.2579 us | 0.2533 us |
| VectorOp | 3.956 us | 0.0570 us | 0.0505 us |
| CopyData | 1.455 us | 0.0273 us | 0.0228 us |
将数据从向量复制回数组(相对)较慢,因为它几乎用掉了一半的时间。但仍然:向量化运算的总时间不到标量运算的 1/3...
查看反汇编(BenchmarkDotNet 将生成它)似乎内存复制操作使用(较慢的)未对齐操作。 .Net Core 的未来版本可能会对此进行研究。
你可以通过使用Span<T>
和MemoryMarshal.Cast
将生成的向量直接放入Span中来完全避免复制操作。它减少了大约求和的时间。三分之一与复制相比(下面未显示)。
供参考,基准代码是(floatSlots = Vector<float>.Count
;数组在基准运行之前创建并填充数据)并且不一定是最佳解决方案:
[Benchmark]
public void ScalarOp()
{
for (int i = 0; i < data1.Length; i++)
{
sums[i] = data1[i] + data2[i];
}
}
[Benchmark]
public void VectorOp()
{
int ceiling = data1.Length / floatSlots * floatSlots;
int leftOver = data1.Length % floatSlots;
for (int i = 0; i < ceiling; i += floatSlots)
{
Vector<float> v1 = new Vector<float>(data1, i);
Vector<float> v2 = new Vector<float>(data2, i);
(v1 + v2).CopyTo(sums, i);
}
for (int i = ceiling; i < data1.Length; i++)
{
sums[i] = data1[i] + data2[i];
}
}
[Benchmark]
public void CopyData()
{
Vector<float> v1 = new Vector<float>(8);
int ceiling = data1.Length / floatSlots * floatSlots;
int leftOver = data1.Length % floatSlots;
for (int i = 0; i < ceiling; i += floatSlots)
{
(v1).CopyTo(sums, i);
}
for(int i = ceiling; i < data1.Length; i++)
{
sums[i] = 8;
}
}
编辑:更正标量基准,因为与向量相同,添加了 Span
和 MemoryMarshal.Cast
.
我正在尝试通过利用 System.Numerics 在 float[]
数组上执行 SIMD 操作来提高 .NET Core 库的性能。 System.Numerics
现在有点时髦,我很难看出它有什么好处。我知道为了看到 SIMD 的性能提升,它必须分摊到大量的计算中,但考虑到它目前的实现方式,我无法弄清楚如何实现这一点。
Vector<float>
需要 8 个 float
值 - 不多也不少。如果我想对一组小于 8 的值执行 SIMD 操作,我必须将这些值复制到一个新数组并用零填充剩余部分。如果这组值大于 8,我需要复制这些值,用零填充以确保其长度与 8 的倍数对齐,然后循环遍历它们。长度要求是有道理的,但适应这一点似乎是抵消任何性能提升的好方法。
我写了一个测试包装器 class 来处理填充和对齐:
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()
可能是降低性能的很大一部分,但似乎没有简单的方法来同时拥有 padding/alignment 和未明确符合的数据集符合 Vector<T>
的规格。
我对 SIMD 比较陌生,我知道 C# 实现仍处于早期阶段。但是,我没有看到真正从中受益的明确方法,尤其是考虑到它在扩展到更大的数据集时最有益。
有没有更好的方法来解决这个问题?
我不确定 "funky" 是什么意思,但它现在完全可用(尽管它可能性能更高)。 使用你的案例(求和浮点数)我得到以下结果超过 10003 个项目与一个老人 Haswell CPU:
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4)
Intel Core i7-4500U CPU 1.80GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
Frequency=1753753 Hz, Resolution=570.2057 ns, Timer=TSC
.NET Core SDK=2.1.602
[Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
DefaultJob : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
| Method | Mean | Error | StdDev |
|--------- |----------:|----------:|----------:|
| ScalarOp | 12.974 us | 0.2579 us | 0.2533 us |
| VectorOp | 3.956 us | 0.0570 us | 0.0505 us |
| CopyData | 1.455 us | 0.0273 us | 0.0228 us |
将数据从向量复制回数组(相对)较慢,因为它几乎用掉了一半的时间。但仍然:向量化运算的总时间不到标量运算的 1/3...
查看反汇编(BenchmarkDotNet 将生成它)似乎内存复制操作使用(较慢的)未对齐操作。 .Net Core 的未来版本可能会对此进行研究。
你可以通过使用Span<T>
和MemoryMarshal.Cast
将生成的向量直接放入Span中来完全避免复制操作。它减少了大约求和的时间。三分之一与复制相比(下面未显示)。
供参考,基准代码是(floatSlots = Vector<float>.Count
;数组在基准运行之前创建并填充数据)并且不一定是最佳解决方案:
[Benchmark]
public void ScalarOp()
{
for (int i = 0; i < data1.Length; i++)
{
sums[i] = data1[i] + data2[i];
}
}
[Benchmark]
public void VectorOp()
{
int ceiling = data1.Length / floatSlots * floatSlots;
int leftOver = data1.Length % floatSlots;
for (int i = 0; i < ceiling; i += floatSlots)
{
Vector<float> v1 = new Vector<float>(data1, i);
Vector<float> v2 = new Vector<float>(data2, i);
(v1 + v2).CopyTo(sums, i);
}
for (int i = ceiling; i < data1.Length; i++)
{
sums[i] = data1[i] + data2[i];
}
}
[Benchmark]
public void CopyData()
{
Vector<float> v1 = new Vector<float>(8);
int ceiling = data1.Length / floatSlots * floatSlots;
int leftOver = data1.Length % floatSlots;
for (int i = 0; i < ceiling; i += floatSlots)
{
(v1).CopyTo(sums, i);
}
for(int i = ceiling; i < data1.Length; i++)
{
sums[i] = 8;
}
}
编辑:更正标量基准,因为与向量相同,添加了 Span
和 MemoryMarshal.Cast
.