Поделиться через


Используйте числовые типы с ускорением SIMD

SIMD (одна инструкция, несколько данных) обеспечивает поддержку оборудования для выполнения операции с несколькими частями данных параллельно с помощью одной инструкции. В .NET имеется набор типов, ускоренных с помощью SIMD, принадлежащих пространству имен System.Numerics. Операции SIMD можно параллелизировать на уровне оборудования. Это повышает пропускную способность векторных вычислений, которые распространены в математических, научных и графических приложениях.

Типы .NET с ускорением SIMD

Типы .NET с ускорением SIMD включают следующие:

  • Типы Vector2, Vector3 и Vector4, которые представляют собой векторы с 2, 3 и 4 значениями Single.

  • Два типа матриц, Matrix3x2, представляющие матрицу 3x2, и Matrix4x4, представляющие матрицу 4x4 из значений Single.

  • Тип Plane , представляющий плоскость в трехмерном пространстве с помощью Single значений.

  • Тип Quaternion , представляющий вектор, используемый для кодирования трехмерных физических поворотов с помощью Single значений.

  • Тип Vector<T> , представляющий вектор указанного числового типа и предоставляющий широкий набор операторов, которые получают поддержку SIMD. Число экземпляров Vector<T> фиксировано в течение времени существования приложения, но его значение Vector<T>.Count зависит от процессора компьютера, выполняющего код.

    Замечание

    Тип Vector<T> не включен в .NET Framework. Чтобы получить доступ к этому типу, необходимо установить пакет NuGet System.Numerics.Vectors .

Типы с ускорением через SIMD реализованы таким образом, что их можно использовать с аппаратным обеспечением без ускорения через SIMD или компиляторами JIT. Чтобы воспользоваться инструкциями SIMD, 64-разрядные приложения должны запускаться средой выполнения, используюющей компилятор RyuJIT . Компилятор RyuJIT включается в .NET Core и .NET Framework 4.6 и более поздних версий. Поддержка SIMD предоставляется только при выборе 64-разрядных процессоров.

Как использовать SIMD?

Перед выполнением пользовательских алгоритмов SIMD можно проверить, поддерживает ли главный компьютер SIMD с помощью Vector.IsHardwareAccelerated, который возвращает Booleanзначение. Это не гарантирует, что для определенного типа включено ускорение SIMD, но может служить индикатором того, что некоторые типы его поддерживают.

Простые векторы

Наиболее примитивные типы с ускорением SIMD в .NET: Vector2Vector3и Vector4 типы, представляющие векторы с 2, 3 и 4 Single значениями. В приведенном ниже примере используется Vector2 для добавления двух векторов.

var v1 = new Vector2(0.1f, 0.2f);
var v2 = new Vector2(1.1f, 2.2f);
var vResult = v1 + v2;

Кроме того, можно использовать векторы .NET для вычисления других математических свойств векторов, таких как Dot product, TransformClamp и т. д.

var v1 = new Vector2(0.1f, 0.2f);
var v2 = new Vector2(1.1f, 2.2f);
var vResult1 = Vector2.Dot(v1, v2);
var vResult2 = Vector2.Distance(v1, v2);
var vResult3 = Vector2.Clamp(v1, Vector2.Zero, Vector2.One);

«Матрица»

Matrix3x2, представляющий матрицу 3x2 и Matrix4x4, представляющую матрицу 4x4. Можно использовать для вычислений, связанных с матрицами. В приведенном ниже примере показано умножение матрицы на ее корреспондентную транспонированную матрицу с помощью SIMD.

var m1 = new Matrix4x4(
            1.1f, 1.2f, 1.3f, 1.4f,
            2.1f, 2.2f, 3.3f, 4.4f,
            3.1f, 3.2f, 3.3f, 3.4f,
            4.1f, 4.2f, 4.3f, 4.4f);

var m2 = Matrix4x4.Transpose(m1);
var mResult = Matrix4x4.Multiply(m1, m2);

Вектор<T>

Это Vector<T> дает возможность использовать более длинные векторы. Число экземпляров Vector<T> исправлено, но его значение Vector<T>.Count зависит от ЦП компьютера, на котором выполняется код.

В следующем примере показано, как вычислить поэлементную сумму двух массивов с помощью Vector<T>.

double[] Sum(double[] left, double[] right)
{
    if (left is null)
    {
        throw new ArgumentNullException(nameof(left));
    }

    if (right is null)
    {
        throw new ArgumentNullException(nameof(right));
    }

    if (left.Length != right.Length)
    {
        throw new ArgumentException($"{nameof(left)} and {nameof(right)} are not the same length");
    }

    int length = left.Length;
    double[] result = new double[length];

    // Get the number of elements that can't be processed in the vector
    // NOTE: Vector<T>.Count is a JIT time constant and will get optimized accordingly
    int remaining = length % Vector<double>.Count;

    for (int i = 0; i < length - remaining; i += Vector<double>.Count)
    {
        var v1 = new Vector<double>(left, i);
        var v2 = new Vector<double>(right, i);
        (v1 + v2).CopyTo(result, i);
    }

    for (int i = length - remaining; i < length; i++)
    {
        result[i] = left[i] + right[i];
    }

    return result;
}

Замечания

SIMD скорее всего удалит одно узкое место и выявит следующее, например, пропускную способность памяти. В целом преимущество производительности использования SIMD зависит от конкретного сценария, и в некоторых случаях это может даже хуже, чем простой эквивалентный код, отличный от SIMD.