Как перемешать Vector128 ‹T› и добавить элементы, а затем правильно извлечь скалярное значение?

Я использую Vector128<byte> в C # для подсчета совпадений из массива байтов с индексом 16.

Это часть реализации байтовой версии Микрооптимизация гистограммы с 4 сегментами большого массива или списка с использованием метода из Как подсчитать количество символов с помощью SIMD расширения 8-битных счетчиков до 64 во внешнем цикле (hsum_epu8_epu64 вспомогательная функция), а затем после всех циклов суммирования этого вектора счетчиков вниз до одного скаляра (hsum_epu64_scalar).

Так что C ++ с встроенными функциями Intel необходимо перенести на C #. И без AVX2, поэтому мы используем 128-битные целочисленные векторы, а не 256.


Массив байтов состоит из чисел 0 и 1, где встречается 5 0.

Теперь задача состоит в том, чтобы подсчитать те 5 0, где мы видим, что 2 из 0 находятся в верхней полосе Vector128<byte>, а 3 из 0 находятся в нижней полосе Vector128<byte>.

Мне удалось добиться успеха с кодом до тех пор, пока я Sse2.SumAbsoluteDifferences, и могу извлечь число 0 для sumHigh и sumLow, показывающих 3 и 2 соответственно.

Проблема начинается сейчас, когда мне нужно перетасовать, чтобы верхняя и нижняя полосы поменялись местами, чтобы позже я мог извлечь противоположности в: sumHigh и sumLow для sum64b

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

(Код также показывает, что мой процессор AMD K10 поддерживает: Sse, Sse2, Sse3)

    using System.Runtime.Intrinsics;
    using System.Runtime.Intrinsics.X86;

    private void button2_Click(object sender, EventArgs e)
    {
        //This shows what is supported on my processor. However it seems that I could use something from "Avx" anyway
        bool avx = Avx.IsSupported; //false
        bool avx2 = Avx2.IsSupported; //false

        bool sse = Sse.IsSupported; //true
        bool sse2 = Sse2.IsSupported; //true
        bool sse3 = Sse3.IsSupported; //true
        bool ssse3 = Ssse3.IsSupported; //false
        bool sse41 = Sse41.IsSupported; //false
        bool sse42 = Sse42.IsSupported; //false

        //Create a bytearray of 16 indexes. As seen: '0' occur 2 times in the upper band and 3 times in the lower band
        //We want to count those "0" in the below code
        byte[] v1 = new byte[16];
        v1[0] = 0; v1[1] = 0; v1[2] = 1; v1[3] = 1; v1[4] = 1; v1[5] = 1; v1[6] = 1; v1[7] = 1;
        v1[8] = 1; v1[9] = 0; v1[10] = 0; v1[11] = 0; v1[12] = 1; v1[13] = 1; v1[14] = 1; v1[15] = 1;

        Vector128<byte> counts = Vector128<byte>.Zero;
        unsafe
        {
            fixed (byte* fixedInput = v1)
            {
                //Load byte Vector with 16 indexes
                var v = Avx.LoadVector128(&fixedInput[0]);

                //Now match how many "0" we can find in "Vector128: v". 'counts' show the result string where: '1' tells where we found: "0".
                //As seen it happened as expected total times: 5 (2 times in the upper band and 3 times in the lower band of the Vector)
                byte val = 0;
                var match = Avx.CompareEqual(v, Vector128.Create(val));
                counts = Avx.Subtract(counts, match); //counts: <1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0>



                //Extract high/low bands
                //So we use "SumAbsoluteDifferences" to "Separately sum the 8 low differences and 8 high differences to produce two unsigned word integer results."
                //We can see on index 0: 2 and on index 4: 3
                Vector128<ushort> sum64 = Vector128<ushort>.Zero;
                sum64 = Sse2.Add(sum64, Sse2.SumAbsoluteDifferences(counts, Vector128<byte>.Zero)); //sum64: <2,0,0,0,3,0,0,0>

                //I AM NOT SURE OF THE CODE BELOW HOW TO DO IT PROPERLY!

                //Now I need to shuffle the above: "<2,0,0,0,3,0,0,0>" but are not sure of how the complete process is to do this correctly?
                //Below is a start of an "attempt" but are not sure how to do this all the way correctly?

                Vector128<uint> result = Sse2.Shuffle(sum64.AsUInt32(), 0xB1); 

                //Extract high/low bands from ther shuffle above?
                //Vector128<uint> sum64b = Vector128<uint>.Zero;
                //sum64b = Sse2.Add(sum64b, result);
                //sumHigh = Sse2.Extract(sum64b, 1); //0
                //sumLow = Sse2.Extract(sum64b, 0); //                                                                    
            }
        }
    }

Использование 16-битных экстрактов возможно, но не подходит для больших подсчетов.

    var sumHigh = Sse2.Extract(sum64, 4);    // pextrw
    var sumLow = Sse2.Extract(sum64, 0); //sumHigh == 3 and sumLow == 2
    var sumScalar = SumLow + sumHigh;

Примечание от @PeterCordes: реальный вариант использования будет включать в цикл до 255 векторов в counts, затем во внешнем цикле накапливаться в широкие элементы в sum64 с Sse2.SumAbsoluteDifferences и Sse2.Add и сбрасывать counts. Эта часть выглядит правильно в этом порте C #, за исключением того, что sum64 не должен использовать ushort элементы.

Часть, о которой задается этот вопрос, - это горизонтальная сумма двух 64-битных векторных элементов до одного скалярного целого числа. (В реальном варианте использования есть три вектора счетчиков из трех сегментов гистограммы; транспонирование и сумма может работать, но просто выполнение отдельных горизонтальных сумм для каждого вектора нормально.)


person Andreas    schedule 13.04.2020    source источник
comment
sum64 должен быть Vector128<uint> или Vector128<Uint64>. Использование Sse2.Extract(sum64, 4); (pextrw) для добавления 16-битного фрагмента не сработает для вашего реального варианта использования, потому что количество может быть больше 65535.   -  person Peter Cordes    schedule 13.04.2020
comment
Vector128<uint> srcAsUInt32 = counts.AsUInt32(); - это должно быть sum64.AsUInt32(); Вы использовали 8-битные сегменты счетчика, а не результат psadbw.   -  person Peter Cordes    schedule 13.04.2020
comment
@Peter Cordes Еще раз спасибо за помощь с вопросом! Да, вы правы, это то, что я тоже вижу. Проблема, в которой я застрял, заключается в том, что Sse2.Extract( может возвращать только ushort, а эта функция также может принимать только Vector128<ushort> в качестве параметра. Я не уверен, но кажется, что я застрял там, потому что мне нравится использовать, например, <uint>, поэтому переполнение не может произойти. Из кода: от Sse2.Shuffle( до Sse2.Extract( Я даже не могу найти комбинацию типов, которая будет работать, потому что они принимают разные типы.   -  person Andreas    schedule 13.04.2020
comment
sum64.AsUInt32(); Да, конечно, это правда. Это должно быть то, что нужно пройти туда. Я тоже сделал ошибку.   -  person Andreas    schedule 13.04.2020
comment
Возможно, вы захотите исправить эту ошибку в своем вопросе, поскольку она отвлекает от того, о чем вы спрашиваете. Но в любом случае, очевидно, не используйте Extract. Используйте вектор, чтобы перемешать вектор и добавить. Тогда, конечно, есть какой-нибудь способ получить низкий элемент, кроме Extract? В C ++ есть специальные встроенные функции для получения нижнего элемента в виде скаляра, кроме извлечения. В противном случае, в худшем случае, вы могли бы транспонировать и добавить два sum64 вектора (из двух корзин) с помощью unpacklo / unpackhi (punpcklqdq) для хранилища векторов в массив Uint64. Или, что еще хуже, просто сохраните в массиве tmp и перезагрузите как скаляр.   -  person Peter Cordes    schedule 13.04.2020
comment
Да, возможно, это действительно сработало. Посмотрим, что ты думаешь. Я застрял в том, что Sse2.SumAbsoluteDifferences должен возвращать ushort, поэтому sum64 - это ushort. Но затем я использовал Unpack вот так, и в messageBox я мог преобразовать в Scalar и получить числа 3 и 2: .... Vector128<ushort> upper = Sse2.UnpackHigh(sum64, sum64); Vector128<ushort> lower = Sse2.UnpackLow(sum64, sum64); MessageBox.Show(upper.ToScalar() + ":" + lower.ToScalar()); Может ли это быть допустимое решение. Затем я передам 3 и 2 в Int64 счетчики, чтобы они не переполнялись, а затем сбрасывать их после каждой 255 итерации?   -  person Andreas    schedule 13.04.2020
comment
Да, UnpackHigh выглядит полезным, но вы определенно хотите использовать его для Vector128<Uint64>, а не для чередования 16-битных ushort элементов. А можно сделать SIMD Sse2.Add вместо 2-х отдельных ToScalar(). Но да, .ToScalar() предположительно является внутренним элементом для movd / movq, так что да, это правильный строительный блок вместо .Extract (pextrw).   -  person Peter Cordes    schedule 13.04.2020
comment
Кроме того, Sse2.UnpackLow(sum64, sum64) бессмысленно. Низкий элемент уже находится внизу, где вы хотите. И ushort распаковка просто создаст (2<<16) | 2 в младших 32 битах, когда она чередует ushort элементы из младших половин двух векторов. Чтобы получить правильное перемешивание, вы должны использовать правильный метод для правильного вектора типа элемента с C #, в отличие от C ++, где все равно __m128i, а размер элемента перемешивания является частью внутреннего имени.   -  person Peter Cordes    schedule 13.04.2020
comment
Я привел функцию SumAbsoluteDifferences к .AsUInt64(), чтобы я мог полностью отказаться от нее и изменить все типы на UInt64   -  person Andreas    schedule 13.04.2020
comment
Я не уверен, почему Sse2.UnpackLow(sum64, sum64) бессмысленно. Единственное, что я могу понять, это то, что я использую этот метод для получения значения из нижней полосы? (Я не уверен, изменился ли сценарий при использовании только UInt64 сейчас)   -  person Andreas    schedule 13.04.2020
comment
Посмотрите на пример диаграммы в руководстве по asm для punpcklbw - обратите внимание, что нижний элемент остается нижним элементом. Для 64-битных элементов вы просто дублируете нижний элемент в верхний при распаковке, но вы никогда не читаете этот высокий элемент SIMD. officedaytime.com/simd512e также содержит несколько диаграмм и категоризацию инструкций по их назначению.   -  person Peter Cordes    schedule 13.04.2020
comment
так что вместо этого я могу пойти полностью вниз и изменить все типы на UInt64 - да, после SAD вы должны рассматривать этот вектор как имеющий элементы Uint64 с этого момента. Возиться с 16-битными фрагментами этих 64-битных чисел - это ошибка, которую нужно избегать.   -  person Peter Cordes    schedule 13.04.2020
comment
Да, я посмотрел на unpack в ссылке там, и красный / низкий цвет элемента с самого начала остается низким. Но я не уверен, как вернуть меньшее значение без использования: Sse2.UnpackLow. Должен ли я использовать какой-либо другой синтаксис для sum64, чтобы вернуть это значение? Я думаю, что здесь что-то не хватает, как это сделать. Возможно, мне следует использовать только: sum64.ToScalar(), поскольку он возвращает 2, какой именно элемент является нижним?   -  person Andreas    schedule 13.04.2020
comment
Это великолепно. Так что я всегда буду думать об использовании Uint64, чтобы не путаться с разными типами.   -  person Andreas    schedule 13.04.2020
comment
Да, vec.ToScalar() возвращает младший элемент вектора. docs.microsoft. com / en-us / dotnet / api / Для вектора Uint64 это младшие 64 бита. IDK, почему вы думали, что UnpackLow потребуется для использования ToScalar или получения желаемого значения, но это не так.   -  person Peter Cordes    schedule 13.04.2020
comment
Да, вернул нижний элемент. Я не был уверен, что это действительно так, но мог видеть это и в комментарии к самому синтаксису. Теперь я это тоже знаю. Это великолепно.   -  person Andreas    schedule 13.04.2020


Ответы (1)


Это должно быть ответом на то, как подсчитать, сколько 0 в верхних и нижних элементах массива v1 байтов.

Ответ будет:
нижних элементов: 2
высших элементов: 3

  1. Итак, первый Sse2.SumAbsoluteDifferences используется для:
    Суммирования 8 малых разностей и 8 высоких разностей для получения двух целочисленных результатов без знака

  2. Затем мы можем Sse2.UnpackHigh верхние элементы

  3. Используйте sum64.ToScalar(), чтобы получить нижние элементы, потому что scalar совпадает со значением первого элемента.

    private void button2_Click(object sender, EventArgs e)
    {
        //Create a bytearray of 16 indexes. As seen: '0' occur 2 times in the upper band and 3 times in the lower band
        //We want to count those "0" in the below code
        byte[] v1 = new byte[16];
        v1[0] = 0; v1[1] = 0; v1[2] = 1; v1[3] = 1; v1[4] = 1; v1[5] = 1; v1[6] = 1; v1[7] = 1;
        v1[8] = 1; v1[9] = 0; v1[10] = 0; v1[11] = 0; v1[12] = 1; v1[13] = 1; v1[14] = 1; v1[15] = 1;
    
        Vector128<byte> counts = Vector128<byte>.Zero;
        unsafe
        {
            fixed (byte* fixedInput = v1)
            {
                //Load byte Vector with 16 indexes
                var v = Avx.LoadVector128(&fixedInput[0]);
    
                //Now match how many "0" we can find in "Vector128: v". 'counts' show the result string where: '1' tells where we found: "0".
                //As seen it happened as expected total times: 5 (2 times in the upper band and 3 times in the lower band of the Vector)
                byte val = 0;
                var match = Avx.CompareEqual(v, Vector128.Create(val));
                counts = Avx.Subtract(counts, match); //counts: <1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0>
    
                //SumAbsoluteDifferences
                Vector128<UInt64> sum64 = Vector128<UInt64>.Zero;
                sum64 = Sse2.Add(sum64, Sse2.SumAbsoluteDifferences(counts, Vector128<byte>.Zero).AsUInt64()); //sum64: <2,0,0,0,3,0,0,0>
    
                //UnpackHigh and add the lower,upper element from the Vector128<UInt64>
                //var lower = sum64; // low element already where we want it
                UInt64 upper = Sse2.UnpackHigh(sum64, sum64).ToScalar(); //3
                Uint64 total_matches_of_0 = Sse2.Add(sum64, upper).ToScalar(); //2 + 3
            }
        }
    }
    
person Andreas    schedule 13.04.2020
comment
На самом деле вам не нужны отдельные верхние и нижние результаты, вам просто нужна их сумма. Таким образом, вы могли сделать это до ToScalar, например Uint64 total_matches_of_0 = Sse2.Add(sum64, upper).ToScalar();, чтобы получить 5. Подобно тому, как hsum_epu64_scalar делает с SIMD, добавьте перед одним финальным _mm_cvtsi128_si64 (он же ToScalar). - person Peter Cordes; 13.04.2020
comment
Так было даже лучше. Я перешел на эту строчку, как вы там упомянули. Возможно, это все еще может отображаться в коде, откуда взято нижнее значение, просто для демонстрационных целей. Теперь я думаю, что могу начать экспериментировать с этим кодом и посмотреть, как я могу создать 2 внешних цикла и позже вернуться к другому потоку с неполным вопросом. Было здорово решить эту проблему. Спасибо! - person Andreas; 13.04.2020
comment
Конечно var lower = sum64; // low element already where we want it - person Peter Cordes; 13.04.2020
comment
Да, можно так написать. Отлично. Я также перешел на эту строку в приведенном выше коде. Спасибо еще раз! Начну немного поэкспериментировать, чтобы посмотреть, что у меня получится :) - person Andreas; 13.04.2020
comment
/ facepalm. Это имеет смысл только в качестве входных данных для Sse2.Add(lower, upper).ToScalar();. Но вы не учли Sse2.Add, поэтому теперь этот ответ дает скаляр Uint64 из старшей половины и Vector<Uint64> lower. - person Peter Cordes; 13.04.2020
comment
Это было правдой, сначала я подумал, что это упрощение, но да, это было = то же самое, конечно. - person Andreas; 13.04.2020
comment
Хорошо, теперь ваш ответ - не беспокоиться о том, чтобы дать полный ответ, а всего два частичных результата. Использование Sse2.Add() более эффективно, чем два .ToScalar(). Конечно, это в основном не имеет значения для этой очистки после внешнего цикла, но если вы думаете в терминах SIMD, то это естественный способ сделать последний шаг. - person Peter Cordes; 13.04.2020
comment
Да, давайте все-таки добавим эту строку. Я просто пропустил второй, чтобы четко увидеть, откуда взялось низкое значение, но тогда это лучше в качестве полного подхода. - person Andreas; 13.04.2020
comment
Как я уже сказал, var lower = sum64; // low element already where we want it мог четко задокументировать это до Sse2.Add(lower, upper).ToScalar(); - person Peter Cordes; 13.04.2020
comment
Да, я добавил это в качестве комментария перед Sse2.Add(, поэтому на самом деле это не будет объявлено, поскольку тогда это будет ненужная дополнительная строка. - person Andreas; 13.04.2020