Эффективно накапливать знаковые биты в ручном неоне

У меня есть цикл, который выполняет некоторые вычисления, а затем сохраняет знаковые биты в вектор:

uint16x8_t rotate(const uint16_t* x);

void compute(const uint16_t* src, uint16_t* dst)
{
    uint16x8_t sign0 = vmovq_n_u16(0);
    uint16x8_t sign1 = vmovq_n_u16(0);
    for (int i=0; i<16; ++i)
    {
        uint16x8_t r0 = rotate(src++);
        uint16x8_t r1 = rotate(src++);
        // pseudo code:
        sign0 |= (r0 >> 15) << i;
        sign1 |= (r1 >> 15) << i;
    }
    vst1q_u16(dst+1, sign0);
    vst1q_u16(dst+8, sign1);
}

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

Вот что у меня получилось:

    r0 = vshrq_n_u16(r0, 15);
    r1 = vshrq_n_u16(r1, 15);
    sign0 = vsraq_n_u16(vshlq_n_u16(r0, 15), sign0, 1);
    sign1 = vsraq_n_u16(vshlq_n_u16(r1, 15), sign1, 1);

Кроме того, обратите внимание, что «псевдокод» на самом деле работает и генерирует почти такой же код с точки зрения производительности. Что здесь можно улучшить? Обратите внимание, что в реальном коде в цикле нет вызовов функций, я урезал фактический код, чтобы упростить его понимание. Еще один момент: в неоне вы не можете использовать переменную для смещения вектора (например, i нельзя использовать для указания количества сдвигов).


person Pavel P    schedule 19.04.2018    source источник
comment
Является ли vsraq арифметическим сдвигом, а не логическим? Зачем это использовать? Кроме того, вы можете использовать меньше сдвигов, если перед сдвигом используете И для обнуления незнаковых битов. как sign0 |= (r0 & 0x8000) >> (15-i); Или с фиксированным количеством смен: sign0 |= (r0 & 0x8000); sign0 >>= 1; Последнее должно быть легко и эффективно реализовано с помощью SIMD, но я плохо знаю ARM.   -  person Peter Cordes    schedule 19.04.2018
comment
VSRA — вектор Сдвиг вправо на непосредственное значение и накопление. _u16 vs _s16 будет определять логический и арифметический сдвиг.   -  person Pavel P    schedule 19.04.2018
comment
О, так вы изолируете бит знака с помощью (x >> 15) << 15 вместо x & 0x8000. Похоже, вы могли бы использовать vsra, чтобы легко реализовать 2-е предложение в моем предыдущем комментарии в 2 инструкциях: tmp = r0 & 0x8000; sign0 = (sign0 >> 1) + tmp;   -  person Peter Cordes    schedule 19.04.2018
comment
для & 0x8000 мне нужно создать q-регистр, и я не уверен, что у меня достаточно в цикле. Я попробую это, чтобы увидеть, получу ли я лучшие результаты. Два сдвига не сложны, за исключением того, что AFAIK в неоне есть только один модуль сдвига, но компилятор распределяет сдвиги в сгенерированном коде.   -  person Pavel P    schedule 19.04.2018
comment
можете ли вы поставить это как ответ, я думаю, что это должно быть лучше, чем мой код   -  person Pavel P    schedule 19.04.2018
comment
Да, вам нужна векторная константа. Бочковой сдвиг SIMD намного сложнее, чем побитовое логическое ALU, например, на x86 Haswell имеет пропускную способность только 1 за такт для целочисленных сдвигов SIMD, но 3 за такт для целочисленных логических операций SIMD. А вам уже нужна как минимум 1 смена. Таким образом, вполне вероятно, что вы столкнетесь с узким местом в пропускной способности сдвига SIMD, если только ваша петля не достаточно велика с множеством других операций, которые могут использовать другие порты для балансировки.   -  person Peter Cordes    schedule 19.04.2018
comment
Удивительно, но сгенерированный код абсолютно идентичен.   -  person Pavel P    schedule 19.04.2018
comment
Ха, я не знал, что ты можешь использовать clang -target arm64 на Godbolt!! Кроме того, разве VSRA не настоящая инструкция? Или clang просто пессимизирует встроенную функцию в отдельные инструкции vshr и vorr?   -  person Peter Cordes    schedule 19.04.2018
comment
* в неоне вы не можете использовать переменную для сдвига вектора * Выходные данные gcc ARM32, которые вы указали в моем ответе, включают vshl.s16 q4, q4, q9, поэтому я думаю, что вы ошибаетесь. Или вы имели в виду, что это не так эффективно?   -  person Peter Cordes    schedule 19.04.2018
comment
Я имею в виду, что вам нужно загрузить счетчик i в q-регистр, чтобы использовать переменный сдвиг   -  person Pavel P    schedule 19.04.2018
comment
О верно. Я надеюсь, что gcc использует vadd для увеличения векторного счетчика отдельно от счетчика циклов регистра GP, а не фактически копирует + транслирует счетчик циклов на каждой итерации.   -  person Peter Cordes    schedule 19.04.2018
comment
Насколько велик блок данных? Оно кратно 64?   -  person Jake 'Alquimista' LEE    schedule 19.04.2018
comment
Который из? src , dst и rotate были добавлены только для примера. Дело в том, что часть цикла при вычислении других вещей мне нужно накапливать знаки в два отдельных q-вектора.   -  person Pavel P    schedule 19.04.2018
comment
Сколько всего знаковых битов нужно собрать? Всего 16?   -  person Jake 'Alquimista' LEE    schedule 20.04.2018
comment
@ Jake'Alquimista'LEE Да, 16 битов знака (умножить на 2, умножить на 8 для переменных sign0 и sign1, и каждый хранит векторы u16x8).   -  person Pavel P    schedule 20.04.2018
comment
Я не понимаю. Два вектора u16x8 имеют общую длину 256 бит. Пожалуйста, будьте более явными.   -  person Jake 'Alquimista' LEE    schedule 20.04.2018
comment
И вам следует пересмотреть vsra, а также vsri. Оба они очень медленные и не позволяют использовать отдельный целевой регистр, поэтому не очень гибкие. Интересно, зачем кому-то нужен либо для сбора битов.   -  person Jake 'Alquimista' LEE    schedule 20.04.2018
comment
@Jake'Alquimista'LEE предоставленная примерная функция в основном делает это. 16 бит, накопленных, как показано, по 8 дорожкам, две переменные. Я согласен насчет vsra, он позволяет сдвигать вправо + накапливать в одной инструкции, но тогда результат vsra нужно переместить в sign0. в любом случае.   -  person Pavel P    schedule 20.04.2018
comment
Я пишу на телефоне, поэтому сейчас не могу предоставить полные коды. Избавьтесь от младших 8 бит с помощью vld2, vzip8, vmovn.16 и т. д., тогда вам придется иметь дело только с половиной данных.   -  person Jake 'Alquimista' LEE    schedule 20.04.2018
comment
Затем примените vclt.s8 с #0, чтобы получить 0xff для отрицательных значений. Затем выполните and с {1, 2, 4, 8, 16, 32, 64, 128}. И, наконец, вы можете сделать горизонтальную сумму. vpadd' or addv` на aarch64   -  person Jake 'Alquimista' LEE    schedule 20.04.2018
comment
В моем случае sign0 является результатом vclt, поэтому это уже 0xffff или 0, но то, что вы предлагаете, работает для одной полосы. Проверьте предоставленный пример кода: я собираю 16 битов знака, но я делаю это по 8 дорожкам в аккумуляторе uint16x8_t sign0. По сути, sign0 хранит в конце 128 битов знака, как и sign1.   -  person Pavel P    schedule 20.04.2018


Ответы (1)


ARM может сделать это за один vsri инструкция (спасибо @Jake'Alquimista'LEE).

Учитывая новый вектор, из которого вы хотите получить биты знака, замените младшие 15 бит каждого элемента на аккумулятор, сдвинутый вправо на 1.

Вы должны развернуться на 2, чтобы компилятору не нужна была инструкция mov для копирования результата обратно в тот же регистр, потому что vsri — это инструкция с двумя операндами, и то, как мы должны использовать ее здесь, дает нам результат в другом регистр, чем старый sign0 аккумулятор.

sign0 =  vsriq_n_u16(r0, sign0, 1);
// insert already-accumulated bits below the new bit we want

После 15 вставок (или 16, если вы начинаете с sign0 = 0 вместо очистки первой итерации и использования sign0=r0) все 16 битов (на элемент) sign0 будут битами знака из значений r0.


Предыдущее предложение: И с константой вектора, чтобы изолировать бит знака. Это более эффективно, чем две смены.

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

tmp = r0 & 0x8000;            // VAND
sign0 = (sign0 >> 1) + tmp;   // VSRA

или используя неоновые свойства:

uint16x8_t mask80 = vmovq_n_u16(0x8000);
r0 = vandq_u16(r0, mask80);        // VAND
sign0 = vsraq_n_u16(r0, sign0, 1); // VSRA

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


Для этого нужна векторная константа в регистре. Если вы очень ограничены в регистрах, то 2 смены могут быть лучше, но всего 3 смены, вероятно, будут узким местом в пропускной способности переключателей, если только чипы ARM обычно не тратят много места на переключатели SIMD.

В этом случае, возможно, используйте эту общую идею SIMD без ARM shift+accumulate или shift+insert.

tmp = r0 >> 15;     // logical right shift
sign0 += sign0;     // add instead of left shifting
sign0 |= tmp;       // or add or xor or whatever.

Это дает вам биты в обратном порядке. Если вы можете произвести их в обратном порядке, то отлично.

В противном случае, есть ли у ARM бит-реверс SIMD или только для скаляра? (Сгенерируйте в обратном порядке и переверните их в конце, с некоторой дополнительной работой для каждого векторного растрового изображения, надеюсь, только с одной инструкцией.)

Обновление: да, AArch64 имеет rbit, поэтому вы можете поменять местами биты внутри байта, а затем перетасовать байты, чтобы расположить их в правильном порядке. x86 может использовать pshufb LUT для реверсирования битов внутри байтов в двух 4-битных фрагментах. Однако это может не произойти раньше, чем дополнительная работа, поскольку вы накапливаете биты на x86.

person Peter Cordes    schedule 19.04.2018
comment
Интересно, что что бы я ни пытался clang генерирует один и тот же код, как будто он понимает мои намерения и делает это по-другому для достижения результата. Однако это работает с gcc и дает гораздо лучший код. - person Pavel P; 19.04.2018
comment
@Pavel: Вероятно, clang просто компилирует встроенный vsraq во внутреннее представление задействованных операций, но затем не может объединить их в ассемблерную инструкцию vsra при генерации кода. При компиляции для x86 он часто использует другую перетасовку, чем встроенные функции исходного кода, потому что он обрабатывает исходный код как исходный код и применяет правило «как если бы» для получения эквивалентных результатов. Часто это хорошо на x86, но это нарушает некоторые оптимизации при выражении с помощью встроенных функций. Кажется, clang не умеет делать такой же хороший код для ARM. - person Peter Cordes; 19.04.2018
comment
О, теперь понятно, почему он все это делает. Кстати, ваше второе предложение может действительно сработать, на arm64 есть RBIT, который переворачивает биты (но работает только с байтами). - person Pavel P; 19.04.2018
comment
vsri лучше, так как он автоматически перезаписывает биты справа, поэтому вам не нужна ни операция and, ни константа 0x8000. Если вы не возражаете, я опубликую коды, которые я считаю наиболее эффективными. - person Jake 'Alquimista' LEE; 19.04.2018
comment
@Jake'Alquimista'LEE Я не понимаю, как можно использовать vsri, это не сдвиг округления, как rotr/rotl - person Pavel P; 20.04.2018
comment
Похоже, бит знака можно изолировать без использования временной маски знака или сдвигов. VAND.i16 r0, #0x7fff следует это, однако, я не вижу соответствия внутреннего для этого. - person Pavel P; 20.04.2018
comment
^ это, однако, перевернуто таким образом. - person Pavel P; 20.04.2018
comment
@Pavel: похоже, vsri работает сам по себе; обновил ответ. Re: vand.i16: в идеале компиляторы должны быть достаточно умны, чтобы использовать это, когда есть постоянная маска. Но, может быть, это медленнее, поэтому они этого не делают? Или компиляторы просто отстой. - person Peter Cordes; 20.04.2018
comment
@Jake'Alquimista'LEE: Спасибо за предложение, vsri выглядит идеально. Обновил мой ответ, но не стесняйтесь публиковать свои собственные с анализом производительности или предложениями по оптимизации или редактировать мой. - person Peter Cordes; 20.04.2018
comment
О, да, vsri это работает очень хорошо, совершенно не думал об этом подходе! Спасибо @Jake'Alquimista'LEE! - person Pavel P; 20.04.2018