Быстрый способ копирования памяти с переводом - ARGB в BGR

Обзор

У меня есть буфер изображений, который мне нужно преобразовать в другой формат. Буфер исходного изображения состоит из четырех каналов, 8 бит на канал: альфа, красный, зеленый и синий. Целевой буфер состоит из трех каналов по 8 бит на канал: синий, зеленый и красный.

Итак, метод грубой силы:

// Assume a 32 x 32 pixel image
#define IMAGESIZE (32*32)

typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

ARGB orig[IMAGESIZE];
BGR  dest[IMAGESIZE];

for(x = 0; x < IMAGESIZE; x++)
{
     dest[x].Red = orig[x].Red;
     dest[x].Green = orig[x].Green;
     dest[x].Blue = orig[x].Blue;
}

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

Дополнительная информация

Каждое изображение кратно как минимум 4 пикселям. Таким образом, мы могли адресовать 16 байтов ARGB и переместить их в 12 байтов RGB на цикл. Возможно, этот факт можно использовать для ускорения работы, тем более, что он хорошо укладывается в 32-битные границы.

У меня есть доступ к OpenCL - и хотя это требует перемещения всего буфера в память графического процессора, а затем перемещения результата обратно, тот факт, что OpenCL может работать со многими частями изображения одновременно, и тот факт, что перемещение больших блоков памяти на самом деле является довольно эффективное может сделать это исследование стоящим.

Хотя я привел пример небольших буферов выше, я действительно перемещаю HD-видео (1920x1080) и иногда большие, в основном меньшие, буферы, поэтому, хотя ситуация 32x32 может быть тривиальной, копирование 8,3 МБ данных изображения побайтно. действительно, очень плохо.

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

Это входит в приложение OS X, и я использую XCode 4. Если сборка безболезненна и очевиден, я могу идти по этому пути, но то, что я не сделал этого в этой настройке раньше, заставляет меня опасаться погружаясь в это слишком много времени.

Псевдокод в порядке - я не ищу полного решения, только алгоритм и объяснение любых уловок, которые могут быть не сразу понятны.


person Adam Davis    schedule 24.07.2011    source источник
comment
@marinara Нет, выровнен по байтам.   -  person marinara    schedule 24.07.2011
comment
Нет смысла использовать для этого GPU, если только данные не поступают в систему оттуда. Вы должны иметь возможность переполнить шину памяти процессором.   -  person Adam Davis    schedule 24.07.2011
comment
Я не играл ни с одним кодом, но AFAICT никто не упомянул о возможности эквивалента _1_. Обгоняют ли более простые петли тиддлинг в этом случае?   -  person Stephan Eggermont    schedule 16.08.2011
comment
Мне было бы интересно узнать, как работают новые версии GCC. Некоторые из оптимизаций, внесенных между 4.2 и 4.6, впечатляют. Кроме того, не могли бы вы рассчитать стандартное отклонение для этого времени?   -  person Mark Hurd    schedule 24.01.2015


Ответы (11)


Я написал 4 разные версии, которые работают с перестановкой байтов. Я скомпилировал их с помощью gcc 4.2.1 с -O3 -mssse3, прогнал их 10 раз над 32 МБ случайных данных и нашел средние значения.


Примечание редактора: исходный встроенный asm использовал небезопасные ограничения, например изменение операндов, предназначенных только для ввода, и отказ от сообщения компилятору о побочный эффект для памяти, на которую указывают входные данные указателя в регистрах. По-видимому, это сработало для теста. Я исправил ограничения, чтобы они были безопасными для всех вызывающих абонентов. Это не должно влиять на результаты тестов, только убедитесь, что окружающий код безопасен для всех вызывающих. Современные процессоры с более высокой пропускной способностью памяти должны увидеть большее ускорение для SIMD по сравнению с 4-байтовым скаляром, но самые большие преимущества проявляются, когда данные в кеше находятся в горячем состоянии (работают с меньшими блоками или с меньшими общими размерами).

В 2020 году лучше всего использовать портативную версию _mm_loadu_si128 встроенных функций, которая будет компилироваться в эквивалентный цикл asm: https://gcc.gnu.org/wiki/DontUseInlineAsm.

Также обратите внимание, что все эти байты перезаписывают 1 (скаляр) или 4 (SIMD) байта после конца вывода, поэтому последние 3 байта следует делать отдельно, если это проблема.

--- @PeterCordes


Первая версия использует цикл C для преобразования каждого пикселя отдельно с помощью функции OSSwapInt32 (которая компилируется в инструкцию bswap с -O3).

void swap1(ARGB *orig, BGR *dest, unsigned imageSize) {
    unsigned x;
    for(x = 0; x < imageSize; x++) {
        *((uint32_t*)(((uint8_t*)dest)+x*3)) = OSSwapInt32(((uint32_t*)orig)[x]);
        // warning: strict-aliasing UB.  Use memcpy for unaligned loads/stores
    }
}

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

void swap2(ARGB *orig, BGR *dest, unsigned imageSize) {
    asm volatile ( // has to be volatile because the output is a side effect on pointed-to memory
        "0:\n\t"                   // do {
        "movl   (%1),%%eax\n\t"
        "bswapl %%eax\n\t"
        "movl   %%eax,(%0)\n\t"    // copy a dword byte-reversed
        "add    $4,%1\n\t"         // orig += 4 bytes
        "add    $3,%0\n\t"         // dest += 3 bytes
        "dec    %2\n\t"
        "jnz    0b"                // }while(--imageSize)
        : "+r" (dest), "+r" (orig), "+r" (imageSize)
        : // no pure inputs; the asm modifies and dereferences the inputs to use them as read/write outputs.
        : "flags", "eax", "memory"
    );
}

Третья версия - это модифицированная версия просто ответ позера. Я преобразовал встроенные функции в эквиваленты GCC и использовал встроенную функцию lddqu, так что входной аргумент не нужно выравнивать. (Примечание редактора: только P4 когда-либо извлекал выгоду из lddqu; movdqu можно использовать, но нет недостатков.)

typedef char v16qi __attribute__ ((vector_size (16)));
void swap3(uint8_t *orig, uint8_t *dest, size_t imagesize) {
    v16qi mask = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};
    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 16, dest += 12) {
        __builtin_ia32_storedqu(dest,__builtin_ia32_pshufb128(__builtin_ia32_lddqu(orig),mask));
    }
}

Наконец, четвертая версия - это встроенный ассемблерный эквивалент третьей.

void swap2_2(uint8_t *orig, uint8_t *dest, size_t imagesize) {
    static const int8_t mask[16] = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};
    asm volatile (
        "lddqu  %3,%%xmm1\n\t"
        "0:\n\t"
        "lddqu  (%1),%%xmm0\n\t"
        "pshufb %%xmm1,%%xmm0\n\t"
        "movdqu %%xmm0,(%0)\n\t"
        "add    $16,%1\n\t"
        "add    $12,%0\n\t"
        "sub    $4,%2\n\t"
        "jnz    0b"
        : "+r" (dest), "+r" (orig), "+r" (imagesize)
        : "m" (mask)  // whole array as a memory operand.  "x" would get the compiler to load it
        : "flags", "xmm0", "xmm1", "memory"
    );
}

На моем MacBook Pro 2010 года, i5 2,4 ГГц (Westmere / Arrandale), 4 ГБ ОЗУ, это было среднее время для каждого:

Как видите, компилятор достаточно хорош в оптимизации, поэтому писать сборку не нужно. Кроме того, векторные функции были всего на 1,5 миллисекунды быстрее на 32 МБ данных, поэтому это не причинит большого вреда, если вы хотите поддерживать самые ранние компьютеры Mac от Intel, которые не поддерживали SSSE3.

Version 1: 10.8630 milliseconds
Version 2: 11.3254 milliseconds
Version 3:  9.3163 milliseconds
Version 4:  9.3584 milliseconds

Изменить: Лиори запросил информацию о стандартном отклонении. К сожалению, я не сохранил точки данных, поэтому я провел еще один тест с 25 итерациями.

Кроме того, вот необработанные данные из новых тестов на случай, если они кому-то понадобятся. Для каждой итерации случайным образом генерировался набор данных размером 32 МБ, который выполнялся четырьмя функциями. Время выполнения каждой функции в микросекундах указано ниже.

              Average    | Standard Deviation
Brute force: 18.01956 ms | 1.22980 ms (6.8%)
Version 1:   11.13120 ms | 0.81076 ms (7.3%)
Version 2:   11.27092 ms | 0.66209 ms (5.9%)
Version 3:    9.29184 ms | 0.27851 ms (3.0%)
Version 4:    9.40948 ms | 0.32702 ms (3.5%)

Очевидное, используя pshufb.

Brute force: 22173 18344 17458 17277 17508 19844 17093 17116 19758 17395 18393 17075 17499 19023 19875 17203 16996 17442 17458 17073 17043 18567 17285 17746 17845
Version 1:   10508 11042 13432 11892 12577 10587 11281 11912 12500 10601 10551 10444 11655 10421 11285 10554 10334 10452 10490 10554 10419 11458 11682 11048 10601
Version 2:   10623 12797 13173 11130 11218 11433 11621 10793 11026 10635 11042 11328 12782 10943 10693 10755 11547 11028 10972 10811 11152 11143 11240 10952 10936
Version 3:    9036  9619  9341  8970  9453  9758  9043 10114  9243  9027  9163  9176  9168  9122  9514  9049  9161  9086  9064  9604  9178  9233  9301  9717  9156
Version 4:    9339 10119  9846  9217  9526  9182  9145 10286  9051  9614  9249  9653  9799  9270  9173  9103  9132  9550  9147  9157  9199  9113  9699  9354  9314
person ughoavgfhw    schedule 24.07.2011
comment
@liori Я добавил еще несколько данных, включая стандартное отклонение. Извините, но сейчас у меня нет версии новее, чем 4.2.1. Я обновлюсь в будущем, когда получу более новую версию. - person liori; 24.07.2011
comment
Возможно, глупый вопрос: для сравнения, насколько быстро / медленно код, указанный в вопросе, на вашем компьютере? - person ughoavgfhw; 24.07.2011
comment
@MrE Не глупый вопрос. Я добавил набор данных для предоставленного кода, и это заняло примерно вдвое больше времени, чем _1_. - person YXD; 28.07.2011
comment
@ughoavgfhw продемонстрировал, что в основном это связано с памятью. Было бы интересно поэкспериментировать с явной предварительной выборкой в ​​одном из этих циклов, чтобы посмотреть, поможет ли это. - person ughoavgfhw; 28.07.2011
comment
Почему третья версия быстрее, если четвертая - аналог сборки? - person Ben Jackson; 30.07.2011
comment
@Camilo Компилятор может оптимизировать код C, но не встроенную сборку, поэтому версия сборки может иметь ненужные дополнения, такие как сохранение неиспользуемых регистров. Разница между ними меньше стандартного отклонения, поэтому она также может быть вызвана внешними воздействиями. - person Camilo Martin; 17.01.2012
comment
Я исправил небезопасные ограничения для встроенного asm. Подобные вещи нарушили бы код, в который встроены эти вспомогательные функции. Просто сообщаю вам, что это была довольно большая правка, и я закончил тем, что написал новый раздел в верхней части, чтобы указать, что оригинал небезопасен. IDK, если это полезно, чтобы заставить любого, кто скопировал / вставил это, вернуться и исправить свою копию, или как отказ от ответственности, что код в ответе не в точности тот, который вы тестировали. Но, возможно, в качестве общего предупреждения, что встроенный asm обычно является плохой идеей, и компиляторы могут сгенерировать для вас тот же asm с встроенными функциями. - person ughoavgfhw; 17.01.2012
comment
+1 это почти наверняка будет оптимальным. Возможно, компилятор сможет сгенерировать такой же или похожий код без использования непереносимых встроенных функций, хотя ... - person Peter Cordes; 19.04.2020

Объединяя только ответы позера и Джитамаро, если вы предполагаете, что входы и выходы выровнены по 16 байт, и если вы обрабатываете пиксели по 4 за раз, вы можете использовать комбинацию тасовок, масок и или или для сохранения с использованием выровненных магазины. Основная идея состоит в том, чтобы сгенерировать четыре промежуточных набора данных, а затем или их вместе с масками для выбора соответствующих значений пикселей и записи 3 16-байтовых наборов данных пикселей. Обратите внимание, что я не компилировал это и не пытался запустить его вообще.

#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>

// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
    assert((uintptr_t)orig % 16 == 0);
    assert(imagesize % 4 == 0);
    __m128i mask = _mm_set_epi8(-128, -128, -128, -128, 13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3);
    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 16, dest += 12) {
        _mm_storeu_si128((__m128i *)dest, _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), mask));
    }
}
person just a poseur    schedule 24.07.2011
comment
Было бы признательно за объяснение магических чисел для _mm_set_epi8. - person R.. GitHub STOP HELPING ICE; 24.07.2011
comment
@ Дэниел, посмотри на мой ответ. - person deceleratedcaviar; 28.07.2011
comment
Есть ли шанс, что вы можете предоставить перетасовки для BGRA в RGB? Я не могу понять, как все это работает. - person MSN; 28.07.2011

EDIT2: Подробнее о базовой структуре кода:

С SSE2 вы получаете лучшую производительность за счет чтения и записи 16 байт с выравниванием по 16 байт. Поскольку ваш 3-байтовый пиксель может быть выровнен только по 16 байтам на каждые 16 пикселей, мы группируем по 16 пикселей за раз, используя комбинацию перетасовок и масок и или 16 входных пикселей за раз.

Входы от LSB до MSB выглядят следующим образом, без учета конкретных компонентов:

и результаты выглядят так:

s[0]: 0000 0000 0000 0000
s[1]: 1111 1111 1111 1111
s[2]: 2222 2222 2222 2222
s[3]: 3333 3333 3333 3333

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

d[0]: 000 000 000 000 111 1
d[1]:  11 111 111 222 222 22
d[2]:   2 222 333 333 333 333

Итак, как должно выглядеть combine_<x>? Если мы предположим, что d просто s уплотнено вместе, мы можем объединить два s с маской и или:

d[0]= combine_0(f_0_low(s[0]), f_0_high(s[1]))
d[1]= combine_1(f_1_low(s[1]), f_1_high(s[2]))
d[2]= combine_2(f_1_low(s[2]), f_1_high(s[3]))

где (1 означает выбор левого пикселя, 0 означает выбор правого пикселя): маска (0) = 111 111 111 111 000 0 маска (1) = 11 111 111 000 000 00 маска (2) = 1 111 000 000 000 000

combine_x(left, right)= (left & mask(x)) | (right & ~mask(x))

Но настоящие преобразования (f_<x>_low, f_<x>_high) на самом деле не так просты. Поскольку мы меняем местами и удаляем байты из исходного пикселя, фактическое преобразование (для краткости в качестве первого пункта назначения):

Если вы переведете приведенное выше в байтовые смещения от источника к месту назначения, вы получите: d [0] = & s [0] +3 & s [0] +2 & s [0] +1
& s [0] +7 & s [ 0] +6 & s [0] +5 & s [0] +11 & s [0] +10 & s [0] +9 & s [0] +15 & s [0] +14 & s [0] +13
& s [1] +3 & s [1] +2 & s [1] +1
& s [1] +7

d[0]= 
    s[0][0].Blue s[0][0].Green s[0][0].Red 
    s[0][1].Blue s[0][1].Green s[0][1].Red 
    s[0][2].Blue s[0][2].Green s[0][2].Red 
    s[0][3].Blue s[0][3].Green s[0][3].Red
    s[1][0].Blue s[1][0].Green s[1][0].Red
    s[1][1].Blue

(Если вы посмотрите на все смещения s [0], они соответствуют только маске перетасовки позера в обратном порядке.)

Теперь мы можем сгенерировать маску перемешивания, чтобы сопоставить каждый исходный байт с целевым байтом (X означает, что нам все равно, что это за значение):

Мы можем дополнительно оптимизировать это, просмотрев маски, которые мы используем для каждого исходного пикселя. Если вы посмотрите на маски перемешивания, которые мы используем для s [1]:

f_0_low=  3 2 1  7 6 5  11 10 9  15 14 13  X X X  X
f_0_high= X X X  X X X   X  X X   X  X  X  3 2 1  7

f_1_low=    6 5  11 10 9  15 14 13  X X X   X X X  X  X
f_1_high=   X X   X  X X   X  X  X  3 2 1   7 6 5  11 10

f_2_low=      9  15 14 13  X  X  X  X X X   X  X  X  X  X  X
f_2_high=     X   X  X  X  3  2  1  7 6 5   11 10 9  15 14 13

Поскольку две маски перемешивания не перекрываются, мы можем объединить их и просто замаскировать нерелевантные пиксели в comb_, что мы уже сделали! Следующий код выполняет все эти оптимизации (плюс он предполагает, что адреса источника и назначения выровнены по 16 байт). Кроме того, маски записываются в коде в порядке MSB-> LSB, на случай, если вы запутаетесь с порядком.

f_0_high=  X  X  X  X  X  X  X  X  X  X  X  X  3  2  1  7
f_1_low=   6  5 11 10  9 15 14 13  X  X  X  X  X  X  X  X

РЕДАКТИРОВАТЬ: изменил хранилище на _mm_stream_si128, поскольку вы, вероятно, делаете много записей, и мы не хотим обязательно очищать кеш. Кроме того, он должен быть выровнен в любом случае, чтобы получить бесплатную производительность!

Я немного опаздываю на вечеринку, кажется, что сообщество уже решило использовать pshufb-answer от Позера, но репутация 2000 года настолько велика, что я должен попробовать.

#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>

// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
    assert((uintptr_t)orig % 16 == 0);
    assert(imagesize % 16 == 0);

    __m128i shuf0 = _mm_set_epi8(
        -128, -128, -128, -128, // top 4 bytes are not used
        13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3); // bottom 12 go to the first pixel

    __m128i shuf1 = _mm_set_epi8(
        7, 1, 2, 3, // top 4 bytes go to the first pixel
    -128, -128, -128, -128, // unused
        13, 14, 15, 9, 10, 11, 5, 6); // bottom 8 go to second pixel

    __m128i shuf2 = _mm_set_epi8(
        10, 11, 5, 6, 7, 1, 2, 3, // top 8 go to second pixel
    -128, -128, -128, -128, // unused
        13, 14, 15, 9); // bottom 4 go to third pixel

    __m128i shuf3 = _mm_set_epi8(
        13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3, // top 12 go to third pixel
        -128, -128, -128, -128); // unused

    __m128i mask0 = _mm_set_epi32(0, -1, -1, -1);
    __m128i mask1 = _mm_set_epi32(0,  0, -1, -1);
    __m128i mask2 = _mm_set_epi32(0,  0,  0, -1);

    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 64, dest += 48) {
        __m128i a= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), shuf0);
        __m128i b= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 1), shuf1);
        __m128i c= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 2), shuf2);
        __m128i d= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 3), shuf3);

        _mm_stream_si128((__m128i *)dest, _mm_or_si128(_mm_and_si128(a, mask0), _mm_andnot_si128(b, mask0));
        _mm_stream_si128((__m128i *)dest + 1, _mm_or_si128(_mm_and_si128(b, mask1), _mm_andnot_si128(c, mask1));
        _mm_stream_si128((__m128i *)dest + 2, _mm_or_si128(_mm_and_si128(c, mask2), _mm_andnot_si128(d, mask2));
    }
}
person MSN    schedule 24.07.2011
comment
кстати, 1200 кадров с изображениями 1920 * 1080 пикселей, конечно - person Geoffrey; 16.11.2017

Вот моя версия без встроенных функций, специфичных для платформы, или специфичного для машины asm, я включил некоторый кроссплатформенный код синхронизации, показывающий 4-кратное ускорение, если вы выполняете и то, и другое, как я, И активировать оптимизацию компилятора (оптимизация регистров, развертывание цикла):

Результаты следующие (на моем субноутбуке Core 2):

#include "stdlib.h"
#include "stdio.h"
#include "time.h"

#define UInt8 unsigned char

#define IMAGESIZE (1920*1080) 
int main() {
    time_t  t0, t1;
    int frames;
    int frame; 
    typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
    typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

    ARGB* orig = malloc(IMAGESIZE*sizeof(ARGB));
    if(!orig) {printf("nomem1");}
    BGR* dest = malloc(IMAGESIZE*sizeof(BGR));
    if(!dest) {printf("nomem2");}

    printf("to start original hit a key\n");
    getch();
    t0 = time(0);
    frames = 1200;
    for(frame = 0; frame<frames; frame++) {
        int x; for(x = 0; x < IMAGESIZE; x++) {
            dest[x].Red = orig[x].Red;
            dest[x].Green = orig[x].Green;
            dest[x].Blue = orig[x].Blue;
            x++;
        }
    }
    t1 = time(0);
    printf("finished original of %u frames in %u seconds\n", frames, t1-t0);

    // on my core 2 subnotebook the original took 16 sec 
    // (8 sec with compiler optimization -O3) so at 60 FPS 
    // (instead of the 1200) this would be faster than realtime 
    // (if you disregard any other rendering you have to do). 
    // However if you either want to do other/more processing 
    // OR want faster than realtime processing for e.g. a video-conversion 
    // program then this would have to be a lot faster still.

    printf("to start alternative hit a key\n");
    getch();
    t0 = time(0);
    frames = 1200;
    unsigned int* reader;
    unsigned int* end = reader+IMAGESIZE;
    unsigned int cur; // your question guarantees 32 bit cpu
    unsigned int next;
    unsigned int temp;
    unsigned int* writer;
    for(frame = 0; frame<frames; frame++) {
        reader = (void*)orig;
        writer = (void*)dest;
        next = *reader;
        reader++;
        while(reader<end) {
            cur = next;
            next = *reader;         
            // in the following the numbers are of course the bitmasks for 
            // 0-7 bits, 8-15 bits and 16-23 bits out of the 32
            temp = (cur&255)<<24 | (cur&65280)<<16|(cur&16711680)<<8|(next&255); 
            *writer = temp;
            reader++;
            writer++;
            cur = next;
            next = *reader;
            temp = (cur&65280)<<24|(cur&16711680)<<16|(next&255)<<8|(next&65280);
            *writer = temp;
            reader++;
            writer++;
            cur = next;
            next = *reader;
            temp = (cur&16711680)<<24|(next&255)<<16|(next&65280)<<8|(next&16711680);
            *writer = temp;
            reader++;
            writer++;
        }
    }
    t1 = time(0);
    printf("finished alternative of %u frames in %u seconds\n", frames, t1-t0);

    // on my core 2 subnotebook this alternative took 10 sec 
    // (4 sec with compiler optimization -O3)

}

Вы хотите использовать устройство Даффа: http://en.wikipedia.org/wiki/Duff%27s_device. Он также работает в JavaScript. Однако этот пост немного забавно читать http://lkml.indiana.edu/hypermail/linux/kernel/0008.2/0171.html. Представьте себе устройство Даффа с 512 Кбайт перемещений.

F:\>gcc b.c -o b.exe

F:\>b
to start original hit a key
finished original of 1200 frames in 16 seconds
to start alternative hit a key
finished alternative of 1200 frames in 10 seconds

F:\>gcc b.c -O3 -o b.exe

F:\>b
to start original hit a key
finished original of 1200 frames in 8 seconds
to start alternative hit a key
finished alternative of 1200 frames in 4 seconds
person Bernd Elkemann    schedule 24.07.2011
comment
Устройство Даффа - это просто странный, специфичный для Си способ разворачивания цикла. Чтобы добиться действительно хорошей производительности, потребуется нечто большее. - person Bernd Elkemann; 24.07.2011

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

person Gigamegs    schedule 24.07.2011
comment
Мои C и сборка немного заржавели, но развернуть цикл лучше, чем ничего, когда вы должны перемещать все вместе с процессором. - person ; 24.07.2011
comment
@R: Я не опытный программист на C. Я занимаюсь дизайном веб-приложений. Не могли бы вы объяснить? Что тут смешного? - person Gigamegs; 24.07.2011
comment
Действительно? Я ожидал, что узким местом здесь является доступ к памяти, а не обработка процессора, поэтому использование дополнительных ядер ничего вам не даст? - person Gigamegs; 28.07.2011

Эта функция сборки должна работать, однако я не знаю, хотите ли вы сохранить старые данные или нет, эта функция переопределяет ее.

void bulk_bgrFromArgb(byte[] dest, byte[] src, int n)
{
       thread threads[] = {
           create_thread(bgrFromArgb, dest, src, n/4),
           create_thread(bgrFromArgb, dest+n/4, src+n/4, n/4),
           create_thread(bgrFromArgb, dest+n/2, src+n/2, n/4),
           create_thread(bgrFromArgb, dest+3*n/4, src+3*n/4, n/4),
       }
       join_threads(threads);
}
person Dave    schedule 24.07.2011
comment
Каждое дополнительное ядро ​​поставляется с сопутствующим кешем L1, поэтому, хотя память является узким местом, использование большего количества ядер может купить вам дополнительный кеш, чтобы облегчить его. - person Thomas Padron-McCarthy; 24.07.2011
comment
Только если ваши потоки работают на ядрах, где части массивов уже горячие в кеше. Например, если бы у вас были рабочие потоки, которые просто записали отдельные фрагменты _1_, или позже собираетесь проделать дополнительную работу с частями _2_, которые они только что написали, с правильным потоком, совпадающим с правильным фрагментом, и, надеюсь, все еще выполняющимся на том же Ядро процессора. В противном случае это помогает только в том случае, если одно ядро ​​процессора не может насыщать пропускную способность памяти (что имеет место на большом Xeon, где пропускная способность на ядро ​​низкая, но не совсем на типичном современном четырехъядерном настольном ПК). - person Dave; 24.07.2011
comment
Я реализовал аналогичный подход в сборке с использованием низкоуровневых инструкций по обработке строк, таких как src и dest. К сожалению, это испытание кажется бесполезным - предлагаемое _3_ решение имеет более высокие оценки. - person Peter Cordes; 19.04.2020

Код предназначен для MinGW GCC с ассемблером Intel, вам придется изменить его в соответствии с вашим компилятором / ассемблером.

Вы должны назвать это так:

extern "C" {
    int convertARGBtoBGR(uint buffer, uint size);
    __asm(
        ".globl _convertARGBtoBGR\n"
        "_convertARGBtoBGR:\n"
        "  push ebp\n"
        "  mov ebp, esp\n"
        "  sub esp, 4\n"
        "  mov esi, [ebp + 8]\n"
        "  mov edi, esi\n"
        "  mov ecx, [ebp + 12]\n"
        "  cld\n"
        "  convertARGBtoBGR_loop:\n"
        "    lodsd          ; load value from [esi] (4byte) to eax, increment esi by 4\n"
        "    bswap eax ; swap eax ( A R G B ) to ( B G R A )\n"
        "    stosd          ; store 4 bytes to [edi], increment  edi by 4\n"
        "    sub edi, 1; move edi 1 back down, next time we will write over A byte\n"
        "    loop convertARGBtoBGR_loop\n"
        "  leave\n"
        "  ret\n"
    );
}

Эта функция обращается к памяти только дважды на пиксель / пакет (1 чтение, 1 запись) по сравнению с вашим методом грубой силы, который имел (по крайней мере / при условии, что он был скомпилирован для регистрации) 3 операции чтения и 3 операции записи. . Метод тот же, но реализация делает его более эффективным.

convertARGBtoBGR( &buffer, IMAGESIZE );

Вы можете делать это кусками по 4 пикселя, перемещая 32 бита с помощью беззнаковых длинных указателей. Просто подумайте, что с 4 32-битными пикселями вы можете построить, сдвигая и ИЛИ / И, 3 слова, представляющие 4 24-битных пикселя, например:

person Sebi    schedule 24.07.2011

Операции сдвига всегда выполняются за 1 цикл команд во всех современных 32/64 битных процессорах (техника сдвига ствола), так что это самый быстрый способ построения этих трех слов для записи, побитовое И и ИЛИ также очень быстро.

//col0 col1 col2 col3
//ARGB ARGB ARGB ARGB 32bits reading (4 pixels)
//BGRB GRBG RBGR  32 bits writing (4 pixels)

Нравится:

и так далее .... со следующими пикселями в цикле.

//assuming we have 4 ARGB1 ... ARGB4 pixels and  3 32 bits words,  W1, W2 and W3 to write
// and *dest  its an unsigned long pointer for destination
W1 = ((ARGB1 & 0x000f) << 24) | ((ARGB1 & 0x00f0) << 8) | ((ARGB1 & 0x0f00) >> 8) | (ARGB2 & 0x000f);
*dest++ = W1;

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

И, кстати, забудьте об использовании структур и индексированного доступа, это МЕДЛЕННЫЕ способы перемещения данных, просто взгляните на листинг дизассемблирования скомпилированной программы на C ++, и вы согласитесь со мной.

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

person Community    schedule 28.07.2011

typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

а затем для ядра вашего кода с любым подходящим развертыванием цикла:

union uARGB
{
   struct ARGB argb;
   UInt32 x;
};
union uBGRA
{
   struct 
   {
     BGR bgr;
     UInt8 Alpha;
   } bgra;
   UInt32 x;
};

где __byte_reverse_32() предполагает наличие встроенной функции компилятора, которая меняет местами байты 32-битного слова.

inline void argb2bgr(BGR* pbgr, ARGB* pargb)
{
    uARGB* puargb = (uARGB*)pargb;
    uBGRA ubgra;
    ubgra.x = __byte_reverse_32(pargb->x);
    *pbgr = ubgra.bgra.bgr;
}

Подводя итог основополагающему подходу:

просматривать структуру ARGB как 32-битное целое число

  • перевернуть 32-битное целое число
  • просматривать перевернутое 32-битное целое число как структуру (BGR) A
  • пусть компилятор скопирует (BGR) часть структуры (BGR) A
  • Хотя вы можете использовать некоторые приемы, основанные на использовании ЦП,
person Jason S    schedule 24.07.2011
comment
Не забывайте, что доступ к видеопамяти GPU из шины CPU происходит медленнее, чем операции перемещения, выполняемые в карте памяти CPU. Чтобы быть быстрее основного процессора, необходимо выполнять все преобразования в видеопамяти. - person eugene_che; 24.07.2011

Похоже, вы используете C / C ++ ... Итак, вашими альтернативами для программирования GPU могут быть (на платформе Windows)

This kind of operations can be done fasted with GPU.

Вскоре используйте GPU для такого рода операций с массивами, чтобы ускорить вычисления. Они созданы для этого.

  • Microsoft Research Project Accelerator Проверьте эту ссылку
  • Cuda
  • "google" программирование GPU ...
  • Я не видел, чтобы кто-нибудь показывал пример того, как это сделать на графическом процессоре.

DirectCompute (DirectX 11) См. Это видео

person Novalis    schedule 28.07.2011
comment
он будет значительно быстрее, чем все подходы к ЦП. Не для небольших буферов, особенно тех, которые уже нагреваются в кэше L2 на ядре ЦП. Но конечно, для большого количества буферов это может быть хорошо, особенно если это происходит, когда у вас есть процессор, работающий над чем-то другим. Если вы можете эффективно вернуть данные в ЦП, но с помощью графического процессора ›передача данных ЦП обычно не очень быстрая. - person ruhalde; 05.08.2011

Некоторое время назад я написал нечто похожее на вашу проблему. Я получил данные с камеры video4linux2 в формате YUV и хотел нарисовать их в виде уровней серого на экране (только компонент Y). Я также хотел нарисовать слишком темные области синим, а перенасыщенные - красным.

Я начал с примера smooth_opengl3.c из freeglut раздача.

Данные копируются как YUV в текстуру, а затем применяются следующие программы шейдеров GLSL. Я уверен, что код GLSL в настоящее время работает на всех Mac, и он будет значительно быстрее, чем все подходы с ЦП.

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

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

введите описание изображения здесь

(defparameter *vertex-shader*
"void main(){
    gl_Position    = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_FrontColor  = gl_Color;
    gl_TexCoord[0] = gl_MultiTexCoord0;
}
")

(progn
 (defparameter *fragment-shader*
   "uniform sampler2D textureImage;
void main()
{
  vec4 q=texture2D( textureImage, gl_TexCoord[0].st);
  float v=q.z;
  if(int(gl_FragCoord.x)%2 == 0)
     v=q.x; 
  float x=0; // 1./255.;
  v-=.278431;
  v*=1.7;
  if(v>=(1.0-x))
    gl_FragColor = vec4(255,0,0,255);
  else if (v<=x)
    gl_FragColor = vec4(0,0,255,255);
  else
    gl_FragColor = vec4(v,v,v,255); 
}
")

компилятор выравнивает BGR с dword?

person whoplisp    schedule 03.08.2011
comment
(Это все отлично компилируется с GCC9. 3, но clang10 не знает _13_; используйте _14_.) - person Peter Cordes; 19.04.2020