System.Numerics.Vector <T>на больших наборах данных - PullRequest
3 голосов
/ 01 мая 2019

Я пытаюсь повысить производительность библиотеки .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 # все еще находится на ранней стадии.Однако я не вижу четкого способа извлечь из этого пользу, особенно учитывая, что он наиболее полезен при масштабировании на большие наборы данных.

Есть ли лучший способ сделать это?

1 Ответ

3 голосов
/ 06 мая 2019

Я не уверен, что вы подразумеваете под "фанки", но сейчас он вполне пригоден для использования (хотя, возможно, он может быть более производительным) Используя ваш случай (суммирующие числа), я получаю следующие результаты для 10003 элементов с устаревшим процессором Haswell:

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.

...