Как избежать ошибки параметра `out` при использовании встроенных функций? - PullRequest
2 голосов
/ 26 сентября 2019

Я опробую новые аппаратные функции, добавленные в .NET Core 3.0, специально для ускорения операций с матрицами.Для добавления матрицы у меня есть функция, которая принимает две матрицы 4x4 float в качестве параметров in и третью матрицу out для хранения результатов. Она использует 128-битные векторные SSE-свойства для добавления и сохраненияприводит к выводу:

public unsafe static void Add(in Matrix l, in Matrix r, out Matrix o)
{
    fixed (float* lp = &l.m00, rp = &r.m00, op = &o.m00)
    {
        var c1 = Sse.Add(Sse.LoadVector128(lp + 0),  Sse.LoadVector128(rp + 0));
        var c2 = Sse.Add(Sse.LoadVector128(lp + 4),  Sse.LoadVector128(rp + 4));
        var c3 = Sse.Add(Sse.LoadVector128(lp + 8),  Sse.LoadVector128(rp + 8));
        var c4 = Sse.Add(Sse.LoadVector128(lp + 12), Sse.LoadVector128(rp + 12));
        Sse.Store(op + 0,  c1);
        Sse.Store(op + 4,  c2);
        Sse.Store(op + 8,  c3);
        Sse.Store(op + 12, c4);
    }
}

Теперь, очевидно, проблема заключается в компиляторе C #, потому что он не может сказать, что в выходную матрицу когда-либо записывается, поэтому он генерирует ошибку, которую функция не может вернутьпока переменная o не будет присвоена. У меня вопрос, есть ли способ обойти это , не прибегая к присвоению переменной перед выполнением внутренних операций, таких как o = default; в качестве первой строки в функции.

Первоначально я рассматривал что-то вроде:

var op = stackalloc float[16];
fixed (float* lp = &l.m00, rp = &r.m00)
{
...
}
o = *(Matrix*)op;

, но понял, что это не исключает копирования структуры, которая удаляет всю точку прохождения матрицы как out.

Я понимаю, что это работало бы, если бы вместо этого я передавал выходную Матрицу как ref, или если я просто возвратил экземпляр матрицы из функции, но было бы неплохо сохранить полезный встроенный синтаксис (Matrix.Add(l, r, out Matrix o)) и преимущества в производительности от передачи больших типов значений по ссылке.

1 Ответ

2 голосов
/ 26 сентября 2019

Я предполагаю, что вы используете тип Matrix, который является struct.Очевидно, что если бы это был ссылочный тип, то ваш метод фактически должен был бы инициализировать значение параметра, прежде чем вы могли бы его использовать, поэтому тот факт, что ваш код не указывает мне, что это тип значения.

Компилятор C # нельзя заставить игнорировать ошибки времени компиляции.И это ошибка времени компиляции, чтобы не инициализировать параметр out до возврата метода.Итак, вы застряли.

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

public unsafe static void Add(in Matrix l, in Matrix r, out Matrix o)
{
    o = default(Matrix);

    fixed (float* lp = &l.m00, rp = &r.m00, op = &o.m00)
    {
        var c1 = Sse.Add(Sse.LoadVector128(lp + 0),  Sse.LoadVector128(rp + 0));
        var c2 = Sse.Add(Sse.LoadVector128(lp + 4),  Sse.LoadVector128(rp + 4));
        var c3 = Sse.Add(Sse.LoadVector128(lp + 8),  Sse.LoadVector128(rp + 8));
        var c4 = Sse.Add(Sse.LoadVector128(lp + 12), Sse.LoadVector128(rp + 12));
        Sse.Store(op + 0,  c1);
        Sse.Store(op + 4,  c2);
        Sse.Store(op + 8,  c3);
        Sse.Store(op + 12, c4);
    }
}

Это скомпилируется примерно так (я выбрал произвольный тип Matrix для примера ... это, очевидно, не тот, который вы используете,но основная предпосылка та же):

IL_0000:  ldarg.0
IL_0001:  initobj    System.Windows.Media.Matrix

Что, в свою очередь, просто инициализирует блок памяти с 0 значениями :

Инструкция initobj инициализирует каждое поле типа значения, указанного с помощью отправляемого адреса (типа native int, & или *), с нулевой ссылкой или 0 соответствующего примитивного типа.После вызова этого метода экземпляр готов к вызову метода конструктора.Если typeTok является ссылочным типом, эта инструкция имеет тот же эффект, что и ldnull, за которым следует stind.ref.

В отличие от Newobj, initobj не вызывает метод конструктора.Initobj предназначен для инициализации типов значений, в то время как newobj используется для выделения и инициализации объектов.

Другими словами, initobj, что вы получаете, когда используете default(Matrix), это очень простая инициализация, просто обнуление области памяти.Это должно быть достаточно быстро, и в любом случае это явно меньше затрат, чем выделение новой копии объекта и последующее копирование результата обратно в исходную переменную, независимо от того, выполняется ли это локально или с помощью возвращаемого значения.

ВсеТем не менее, это во многом зависит от контекста того, как вы собираетесь вызывать метод.В то время как вы говорите, что хотите сохранить удобство встроенного объявления, мне не ясно, зачем вам это нужно для метода, который, по-видимому, достаточно критичен по производительности, чтобы использовать функции SSE и небезопасный код.Со встроенным объявлением вам обязательно придется повторно инициализировать переменную при каждом вызове.

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

...