Я пытаюсь повысить производительность библиотеки .NET Core, используя System.Numerics для выполнения SIMD-операций с float[]
массивами. System.Numerics
сейчас немного странно, и мне трудно понять, как это может быть полезно. Я понимаю, что для того, чтобы увидеть прирост производительности с помощью SIMD, он должен амортизироваться за счет большого количества вычислений, но, учитывая то, как это реализовано в настоящее время, я не могу понять, как этого добиться.
Vector<float>
требует 8 значений float
— ни больше, ни меньше. Если я хочу выполнить SIMD-операции с группой значений меньше 8, я вынужден скопировать значения в новый массив и дополнить оставшуюся часть нулями. Если группа значений больше 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# все еще находится на ранней стадии. Однако я не вижу явного способа извлечь из этого пользу, особенно если учесть, что это наиболее полезно при масштабировании на большие наборы данных.
Есть ли лучший способ сделать это?
System.Runtime.Intrinsics
. - person kalimag   schedule 01.05.2019+
— это всего лишь два чтения памяти, одно добавление и одно сохранение. Как и в нативном коде. Хвост массива должен быть обработан поэлементно с использованием второго цикла. Без прокладки. - person usr   schedule 05.05.2019