Оптимизация преобразования RGBA8888 в RGB565 с NEON

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

Однако мои попытки не увенчались успехом, достигнув лишь незначительного ускорения по сравнению с наивной реализацией c:

for(int i = 0; i < pixelCount; ++i, ++inPixel32) {
    const unsigned int r = ((*inPixel32 >> 0 ) & 0xFF);
    const unsigned int g = ((*inPixel32 >> 8 ) & 0xFF);
    const unsigned int b = ((*inPixel32 >> 16) & 0xFF);
    *outPixel16++ = ((r >> 3) << 11) | ((g >> 2) << 5) | ((b >> 3) << 0);
}

Массив изображений размером 1 мегапиксель на iPad 2:

формат [min avg max n = количество отсчетов таймера] в миллисекундах.

C: [14.446 14.632 18.405 n=1000]ms

НЕОН: [11,920 12,032 15,336 n = 1000] мс

Моя попытка реализации NEON приведена ниже:

    int i;
const int pixelsPerLoop = 8;
for(i = 0; i < pixelCount; i += pixelsPerLoop, inPixel32 += pixelsPerLoop, outPixel16 += pixelsPerLoop) {
    //Read all r,g,b pixels into 3 registers
    uint8x8x4_t rgba  = vld4_u8(inPixel32);
    //Right-shift r,g,b as appropriate
    uint8x8_t r = vshr_n_u8(rgba.val[0], 3);
    uint8x8_t g = vshr_n_u8(rgba.val[1], 2);
    uint8x8_t b = vshr_n_u8(rgba.val[2], 3);

    //Widen b
    uint16x8_t r5_g6_b5 = vmovl_u8(b);
    //Widen r
    uint16x8_t r16 = vmovl_u8(r);
    //Left shift into position within 16-bit int
    r16 = vshlq_n_u16(r16, 11);
    r5_g6_b5 |= r16;

    //Widen g
    uint16x8_t g16 = vmovl_u8(g);
    //Left shift into position within 16-bit int
    g16 = vshlq_n_u16(g16, 5);

    r5_g6_b5 |= g16;

    //Now write back to memory
    vst1q_u16(outPixel16, r5_g6_b5);        
}
//Do the remainder on normal flt hardware

Код был скомпилирован с помощью LLVM 3.0 следующим образом (.loc и лишние метки удалены):

_DNConvert_ARGB8888toRGB565:
    push    {r4, r5, r7, lr}
    mov r9, r1
    mov.w   r12, #0
    add r7, sp, #8
    cmp r2, #0
    mov.w   r1, #0
    it  ne
    movne   r1, #1
    cmp r0, #0
    mov.w   r3, #0
    it  ne
    movne   r3, #1
    cmp.w   r9, #0
    mov.w   r4, #0
    it  ne
    movne   r4, #1
    tst.w   r9, #3
    bne LBB0_8
    ands    r1, r3
    ands    r1, r4
    cmp r1, #1
    bne LBB0_8
    movs    r1, #0
    lsr.w   lr, r9, #2
    cmp.w   r1, r9, lsr #2
    bne LBB0_9
    mov r3, r2
    mov r5, r0
    b   LBB0_5
LBB0_4:
    movw    r1, #65528
    add.w   r0, lr, #7
    movt    r1, #32767
    ands    r1, r0
LBB0_5:
    mov.w   r12, #1
    cmp r1, lr
    bhs LBB0_8
    rsb r0, r1, r9, lsr #2
    mov.w   r9, #63488
    mov.w   lr, #2016
    mov.w   r12, #1
LBB0_7:
    ldr r2, [r5], #4
    subs    r0, #1
    and.w   r1, r9, r2, lsl #8
    and.w   r4, lr, r2, lsr #5
    ubfx    r2, r2, #19, #5
    orr.w   r2, r2, r4
    orr.w   r1, r1, r2
    strh    r1, [r3], #2
    bne LBB0_7
LBB0_8:
    mov r0, r12
    pop {r4, r5, r7, pc}
LBB0_9:
    sub.w   r1, lr, #1
    movs    r3, #32
    add.w   r3, r3, r1, lsl #2
    bic r3, r3, #31
    adds    r5, r0, r3
    movs    r3, #16
    add.w   r1, r3, r1, lsl #1
    bic r1, r1, #15
    adds    r3, r2, r1
    movs    r1, #0
LBB0_10:
    vld4.8  {d16, d17, d18, d19}, [r0]!
    adds    r1, #8
    cmp r1, lr
    vshr.u8 d20, d16, #3
    vshr.u8 d21, d17, #2
    vshr.u8 d16, d18, #3
    vmovl.u8    q11, d20
    vmovl.u8    q9, d21
    vmovl.u8    q8, d16
    vshl.i16    q10, q11, #11
    vshl.i16    q9, q9, #5
    vorr    q8, q8, q10
    vorr    q8, q8, q9
    vst1.16 {d16, d17}, [r2]!
Ltmp28:
    blo LBB0_10
    b   LBB0_4

Полный код доступен по адресу https://github.com/darknoon/DNImageConvert Буду признателен за любую помощь, спасибо !


person Andrew Pouliot    schedule 10.10.2011    source источник


Ответы (5)


Вот вам оптимизированная вручную реализация NEON, готовая для XCode:

/* IT DOESN'T WORK!!! USE THE NEXT VERSION BELOW.
 * BGRA2RGB565.s
 *
 * Created by Jake "Alquimista" Lee on 11. 11. 1..
 * Copyright 2011 Jake Lee. All rights reserved.
 */


    .align 2
    .globl _bgra2rgb565_neon
    .private_extern _bgra2rgb565_neon

// unsigned int * bgra2rgb565_neon(unsigned int * pDst, unsigned int * pSrc, unsigned int count);


//ARM
pDst        .req    r0
pSrc        .req    r1
count       .req    r2

//NEON
blu         .req    d16
grn         .req    d17
red         .req    d18
alp         .req    d19
rg          .req    red
gb          .req    blu

_bgra2rgb565_neon:
    pld     [pSrc]
    tst     count, #0x7
    movne   r0, #0
    bxne    lr

loop:
    pld     [pSrc, #32]
    vld4.8  {blu, grn, red, alp}, [pSrc]!
    subs    count, count, #8
    vshr.u8 red, red, #3
    vext.8  rg, grn, red, #5
    vshr.u8 grn, grn, #2
    vext.8  gb, blu, grn, #3
    vst2.8  {gb, rg}, [pDst]!
    bgt     loop

    bx      lr

Эта версия будет во много раз быстрее, чем вы предложили:

  • увеличенная частота попаданий в кеш через PLD

  • преобразование в "длинный" не требуется

  • меньше инструкций в цикле

Тем не менее, еще есть место для оптимизации, вы можете изменить цикл так, чтобы он преобразовывал 16 пикселей на итерацию вместо 8. Затем вы можете запланировать инструкции, чтобы полностью избежать двух остановок (что просто невозможно в этой версии с 8 / итерацией. выше) и, кроме того, воспользоваться возможностью двойной выдачи NEON.

Я не делал этого, потому что это затрудняло понимание кода.

Важно знать, что должен делать VEXT.

Теперь дело за вами. :)

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

Cya

==============================================================================

Ну вот и улучшенная версия.

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

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

Если это не B, G, R, A, который является стандартным и встроенным в iOS, ваше приложение сильно пострадает от внутренних преобразований iOS.

Если по какой-либо причине это невозможно изменить, дайте мне знать. Напишу соответствующую ему новую версию.

PS: Я забыл убрать подчеркивание в начале прототипа функции. Теперь его нет.

/*
 * BGRA2RGB565.s
 *
 * Created by Jake "Alquimista" Lee on 11. 11. 1..
 * Copyright 2011 Jake Lee. All rights reserved.
 *
 * Version 1.1
 * - bug fix
 *
 * Version 1.0
 * - initial release
 */


    .align 2
    .globl _bgra2rgb565_neon
    .private_extern _bgra2rgb565_neon

// unsigned int * bgra2rgb565_neon(unsigned int * pDst, unsigned int * pSrc, unsigned int count);


//ARM
pDst        .req    r0
pSrc        .req    r1
count       .req    r2

//NEON
blu         .req    d16
grn         .req    d17
red         .req    d18
alp         .req    d19

gb          .req    grn
rg          .req    red

_bgra2rgb565_neon:
    pld     [pSrc]
    tst     count, #0x7
    movne   r0, #0
    bxne    lr

.loop:
    pld     [pSrc, #32]
    vld4.8  {blu, grn, red, alp}, [pSrc]!
    subs    count, count, #8

    vsri.8  red, grn, #5
    vshl.u8 gb, grn, #3
    vsri.8  gb, blu, #3

    vst2.8  {gb, rg}, [pDst]!
    bgt     .loop

    bx      lr
person Jake 'Alquimista' LEE    schedule 01.11.2011
comment
ой, теперь я вижу, что использование VSRI приведет к повышению производительности: - person Jake 'Alquimista' LEE; 02.11.2011
comment
Большое спасибо за вашу реализацию! Он определенно выглядит быстрее моего, но я получаю сбои при тестировании, когда помещаю его в проект Xcode. - person Andrew Pouliot; 03.11.2011
comment
Этот код теперь зарегистрирован в ветке по адресу github.com/darknoon/DNImageConvert/tree / jake-alquimista-lee, но похоже, что он не возвращает правильные результаты. Я посмотрю ... - person Andrew Pouliot; 03.11.2011
comment
Проблема, вероятно, вызвана порядком байтов. Порядок байтов iOS по умолчанию - B, G, R, A, если вы читаете пиксели байт за байтом. Все остальное вызовет внутренние преобразования, которые съедят ценные циклы. Пожалуйста, проверьте формат изображения, который вы используете. Тем временем я создам новую версию с VSRI вместо VEXT и более надежную в отношении порядка байтов. До скорого. - person Jake 'Alquimista' LEE; 03.11.2011
comment
Я ошибся в прототипе функции. Подчеркивание теперь удалено. - person Jake 'Alquimista' LEE; 03.11.2011
comment
Я внимательно изучил ваш код, и я уверен, что ваше исходное изображение упаковано в байтовом порядке R, G, B, A, который будет потреблять безумные циклы каждый раз, когда это имеет какое-то отношение к графическому процессору. Измените это на B, G, R, A, и мой код будет работать нормально. - person Jake 'Alquimista' LEE; 03.11.2011
comment
Если это исходное изображение не имеет ничего общего с графическим процессором и API iOS, и вы просто хотите преобразовать его в RGB565, вставьте одну строку в мой код после vld. vswp blu, красный - person Jake 'Alquimista' LEE; 03.11.2011
comment
vext сдвигает побайтово по всему вектору, а не побитово по дорожкам, поэтому первая версия не сделала бы то, что вы думали. Точно так же vsri и vsli не похожи на побитовое вращение двух регистров. Исходный операнд сдвигается, а операнд-адресат - нет: биты, которые сдвигаются внутрь / наружу, остаются такими же, как и были. Если у вас его нет, я рекомендую взять DDI0406B (Справочное руководство по архитектуре ARM v7a / v7r) для получения информации о том, как работают эти инструкции. - person Exophase; 03.11.2011
comment
@Exophase: ты прав. Здесь я совершил ту же ошибку, что и когда впервые изучал НЕОН :). Что касается VSRI (и VSLI), он работает именно так, как я хочу. Обратите внимание, что мой алгоритм использует другой подход, чем OP, для повышения производительности. Взгляните на назначения регистров и VST2.8 в конце. (rg .req красный, gb .req grn) - person Jake 'Alquimista' LEE; 04.11.2011

Если вы используете iOS или OS X, возможно, вам будет приятно обнаружить vImageConvert_RGBA8888toRGB565 () и друзей в Accelerate.framework. Эта функция округляет 8-битные значения до ближайшего значения 565.

Для еще лучшего дизеринга, качество которого почти неотличимо от 8-битного цвета, попробуйте vImageConvert_AnyToAny ():

vImage_CGImageFormat RGBA8888Format = 
{
    .bitsPerComponent = 8,
    .bitsPerPixel = 32,
    .bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast,
    .colorSpace = NULL,  // sRGB or substitute your own in
};

vImage_CGImageFormat RGB565Format = 
{
    .bitsPerComponent = 5,
    .bitsPerPixel = 16,
    .bitmapInfo = kCGBitmapByteOrder16Little | kCGImageAlphaNone,
    .colorSpace = RGBA8888Format.colorSpace,  
};


err = vImageConverterRef converter = vImageConverter_CreateWithCGImageFormat(
         &RGBA8888Format, &RGB565Format, NULL, kvImageNoFlags, &err );

err = vImageConvert_AnyToAny( converter, &src, &dest, NULL, kvImageNoFlags );

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

person Ian Ollmann    schedule 21.01.2014

Вы можете использовать vld4q_u8 () вместо vld4_u8 () и соответствующим образом скорректировать остальную часть вашего кода. Трудно сказать, в чем может быть проблема, но в остальном ассемблер выглядит не так уж плохо.

person onemasse    schedule 13.10.2011

(Я не знаком ни с NEON, ни с системой памяти Ipad2, но это то, что мы использовали с 88110 pixel-ops, которые были ранним предшественником сегодняшних расширений SIMD)

Насколько велика задержка памяти?

Не могли бы вы скрыть это, развернув внутренний цикл и запустив инструкции NEON для «предыдущих» значений, в то время как ARM извлекает «следующие» значения из памяти? Краткий просмотр руководства NEON подразумевает, что вы можете запускать инструкции ARM и NEON параллельно.

person Martin Thompson    schedule 14.10.2011
comment
Может показаться заманчивым позволить ARM работать параллельно с NEON, но это не очень практично - если не невозможно. В то время как ARM2NEON передает быстро, NEON2ARM ОЧЕНЬ медленный. Теоретически можно позволить им работать каждый с независимыми блоками данных параллельно, но NEON может делать гораздо больше, чем ARM с каждой отдельной инструкцией, что ARM вряд ли может что-либо выполнить за эти несколько циклов. Это очень приятно, хотя элементы управления циклами, условные переходы и т. Д. Бесплатны. - person Jake 'Alquimista' LEE; 05.11.2011
comment
Спасибо, Джейк, это будет полезно узнать, когда я начну заниматься NEONing по-настоящему в ближайшем будущем. - person Martin Thompson; 06.11.2011

Я не думаю, что преобразование vld4_u8 в vld4q_u8 приведет к какому-либо улучшению производительности.

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

Неон кажется достаточно простым. Но я не уверен, что r5_g6_b5 | = g16 используется вместо vorrq_u16.

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

Я не нашел в неоне какой-либо области, которая могла бы улучшить текущий код.

person Anoop K. Prabhu    schedule 18.10.2011