Эффективно расширить 8-разрядные числа до 12-разрядных в неоновом регистре с одной рукой - PullRequest
0 голосов
/ 26 апреля 2018

У меня 4 байта загружены в неоновый регистр.Как я могу эффективно преобразовать это в 12 бит, например, мне нужно вставить 4 нулевых бита после первого байта, 8 нулевых битов после второго и так далее.Например, если бы у меня были эти 4 байта в шестнадцатеричном виде:

01 02 03 04

Это закончилось бы этим в шестнадцатеричном :

01 20 00 03 40

Та же операция, выраженная в виде простой c-функции, которая работает с 32-битной переменной, которая представляет 4 входных байта:

uint64_t expand12(uint32_t i)
{
    uint64_t r = (i & 0xFF);
    r |= ((i & 0x0000ff00) << 4); // shift second byte by 4 bits
    r |= ((i & 0x00ff0000) << 8); // shift third byte by 8 bits
    r |= (((uint64_t)(i & 0xff000000)) << 12); // 4th by 12
    return r;
}

Итак, если бы у меня были эти байты в неоновом регистре uint8x8_t, что было бы хорошим способом реализовать ту же самую операцию в неоне, чтобы этот же регистр заканчивался этими сдвинутыми значениями?

Обратите внимание, что все четыре байта имеют нули в старших 4 битах, если это помогает каким-либо образом.

Обновление: В моем случае у меня есть 4 регистра uint16x8_t и для каждого из них мне нужно вычислитьсумма всех дорожек (vaddv_u16), затем выполните vclz_u16 для этой суммы и затем объедините эти четыре суммы в неоновом регистре, поместив их на 12 бит:

uint64_t compute(uint16x8_t a, uint16x8_t b, uint16x8_t c, uint16x8_t d)
{
    u16 a0 = clz(vaddv(a));
    u16 b0 = clz(vaddv(b));
    u16 c0 = clz(vaddv(c));
    u16 d0 = clz(vaddv(d));
    return (a0 << 36) | (b0 << 24) | (c0 << 12) | (d0);
}

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

В случае, если это имеет значение, в моем коде у меня есть функциянахождение индексов элементов max в 4 регистрах uint16x8_t.В этой функции эти четыре регистра vand редактируются с максимальным дублированием элемента по всем дорожкам, а затем результат vorr редактируется с битовой маской {1<<15, 1<<14, ... 1<<0};Затем я делаю попарное добавление всех дорожек и clz, которые дают мне индекс элемента max каждого регистра.Все это мне нужно чередовать с дополнительными 4 нулевыми битами, вставленными между элементами и сохраненными в неоновый регистр.Пример на C:

void compute(uint16_t *src, uint64_t* dst)
{
    uint64_t x[4];
    for (int i = 0; i < 4; ++i, src+=16)
    {
        int max = 0;
        for (int j = 0; j < 16; ++j)
        {
            if (src[j] > src[max])
                max = j;
        }
        x[i] = max;
    }
    *dst = (x[0] << 36) | (x[1] << 24) | (x[2] << 12) | (x[3]);
}

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

1 Ответ

0 голосов
/ 27 апреля 2018

Вы должны думать из коробки.Не придерживайтесь типа данных и ширины в битах.

uint32_t - это не что иное, как массив 4 uint8_t, который вы можете легко распределить по vld4 на лету во время загрузки.

Таким образом, проблема становится намного более управляемой.


void foo(uint32_t *pDst, uint32_t *pSrc, uint32_t length)
{
    length >>= 4;
    int i;
    uint8x16x4_t in, out;
    uint8x16_t temp0, temp1, temp2;

    for (i = 0; i < length; ++i)
    {
        in = vld4q_u8(pSrc);
        pSrc += 16;

        temp0 = in.val[1] << 4;
        temp1 = in.val[3] << 4;
        temp1 += in.val[1] >> 4;

        out.val[0] = in.val[0] | temp0;
        out.val[1] = in.val[2] | temp1;
        out.val[2] = in.val[3] >> 4;
        out.val[3] = vdupq_n_u8(0);

        vst4q_u8(pDst, out);
        pDst += 16;
    }
}

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

Что еще более важно, я бы написал эту функцию в ассемблере, не задумываясь, потому что я не думаю, что компилятор так умно управлял бы регистрами, что out.val[3] инициализируется нулем только один раз вне цикла.

И я также сомневаюсь, что temp1 += in.val[1] >> 4; будет переводиться в vsra из-за природы инструкции нераздельного целевого операнда.Кто знает?

Компиляторы отстой.


Обновление: Хорошо, вот коды, которые будут соответствовать вашим потребностям, написанные на ассемблере, для обеих архитектур.


aarch32

vtrn.16     q0, q1
vtrn.16     q2, q3
vtrn.32     q0, q2
vtrn.32     q1, q3

vadd.u16    q0, q1, q0
vadd.u16    q2, q3, q2

adr     r12, shift_table

vadd.u16    q0, q2, q0

vld1.64     {q3}, [r12]


vadd.u16    d0, d1, d0
vclz.u16    d0, d0          // d0 contains the leading zeros

vmovl.u16   q0, d0

vshl.u32    q1, q0, q3

vpadal.u32  d3, d2          // d3 contains the final result


.balign 8
shift_table:
    .dc.b   0x00, 0x00, 0x00, 0x00,     0x0c, 0x00, 0x00, 0x00,     0x18, 0x00, 0x00, 0x00,     0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4

aarch64

trn1        v16.8h, v0.8h, v1.8h
trn1        v18.8h, v2.8h, v3.8h
trn2        v17.8h, v0.8h, v1.8h
trn2        v19.8h, v2.8h, v3.8h

trn2        v0.4s, v18.4s, v16.4s
trn1        v1.4s, v18.4s, v16.4s
trn2        v2.4s, v19.4s, v17.4s
trn1        v3.4s, v19.4s, v17.4s

add         v0.8h, v1.8h, v0.8h
add         v2.8h, v3.8h, v2.8h

adr     x16, shift_table

add         v0.8h, v2.8h, v0.8h

ld1         {v3.2d}, [x16]

mov         v1.d[0], v0.d[1]

add         v0.4h, v1.4h, v0.4h

clz         v0.4h, v0.4h                // v0 contains the leading zeros

uxtl        v0.4s, v0.4h

ushl        v0.4s, v0.4s, v3.4s

mov         v1.d[0], v0.d[1]

uadalp      v1.1d, v0.2s                // v1 contains the final result


.balign 8
shift_table:
.dc.b   0x00, 0x00, 0x00, 0x00,     0x0c, 0x00, 0x00, 0x00,     0x18, 0x00, 0x00, 0x00,     0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4

** Возможно, вам придется изменить .dc.b на .byte в Clang

...