Быстрое копирование каждого второго байта в новую область памяти

Мне нужен быстрый способ скопировать каждый второй байт в новую область памяти, выделенную malloc. У меня есть необработанное изображение с данными RGB и 16 битами на канал (48 бит), и я хочу создать изображение RGB с 8 битами на канал (24 бита).

Есть ли более быстрый метод, чем копирование побайтно? Я мало что знаю о SSE2, но я полагаю, что это возможно с SSE/SSE2.


person akw    schedule 28.09.2017    source источник
comment
Похоже, вы хотите сделать не копию, а преобразование. И если вы хотите конвертировать изображение только один раз в секунду, вам не нужно быть очень быстрым (если только изображения не имеют размер несколько тысяч на несколько тысяч пикселей или если вы используете какую-то встроенную систему), современный мульти -GHZ ПК сможет прекрасно справиться с преобразованием за доли секунды для изображений нормального размера. Даже если использовать довольно прямолинейный и простой цикл.   -  person Jean-François Fabre    schedule 28.09.2017
comment
@ Jean-FrançoisFabre, может быть, специальное оборудование? ;)   -  person Some programmer dude    schedule 28.09.2017
comment
@ tilz0R, не заводи меня на это. У меня 2 Амиги :)   -  person tilz0R    schedule 28.09.2017
comment
comment
@PaulR звучит так, будто у тебя есть навыки, чтобы ответить и на этот вопрос. почти дубликаты в коде SSE / SSE2 - это все равно, что сказать, что создание атомной бомбы почти то же самое, что создание водородной бомбы :)   -  person Paul R    schedule 28.09.2017
comment
@Jean-FrançoisFabre: Я бы хотел, но я немного занят, и связанные почти дубликаты должно быть довольно легко адаптировать к варианту использования OP. Если за это время никто не ответит, я, вероятно, опубликую решение позже сегодня.   -  person Jean-François Fabre    schedule 28.09.2017
comment
@PaulR понял. Никто не собирается украсть ваш гром :)   -  person Paul R    schedule 28.09.2017
comment
@PaulR: _1_ подходит для одного регистра, но пропускная способность порта в случайном порядке будет вашим узким местом для зацикливания всего изображения. Таким образом, вы должны И исключить старшую половину или сдвинуть ее вниз, чтобы отбросить младшую половину, а затем _2_ каждую пару входных векторов в один выходной вектор. Вероятно, где-то есть его дубликат...   -  person Jean-François Fabre    schedule 28.09.2017
comment
@Someprogrammerdude: секунда здесь не единица времени, она вторая и описывает, какое преобразование / фильтрацию хочет OP ›. ‹ Я тоже неправильно понял это на полсекунды.   -  person Peter Cordes    schedule 28.09.2017
comment
@AKW: вы хотите сохранить старший или младший байт ваших данных RGB16? то есть _1_ или _2_?   -  person Peter Cordes    schedule 28.09.2017
comment
Вы читаете образ с диска? Это, вероятно, затмит стоимость преобразования ...   -  person Peter Cordes    schedule 28.09.2017
comment
<Сильный> См код + ассемблер для этого и более поздних версий на Godbolt .   -  person Marc Glisse    schedule 28.09.2017


Ответы (1)


Ваши данные RGB упакованы, поэтому нам не нужно заботиться о границах пикселей. Проблема заключается в упаковке каждого второго байта массива. (По крайней мере, в каждой строке вашего изображения; если вы используете шаг строки 16 или 32B, отступ может быть не целым числом пикселей.)

Это можно эффективно сделать с помощью перетасовки SSE2, AVX или AVX2. (Также AVX512BW и, возможно, даже больше с AVX512VBMI, но первые процессоры AVX512VBMI, вероятно, не будут иметь очень эффективной vpermt2b, перетасовка байтов с пересечением дорожек с двумя входами.)


Вы можете использовать SSSE3 pshufb для захвата нужных вам байтов, но это всего лишь перетасовка с 1 входом, которая даст вам 8 байтов на выходе. Сохранение 8 байтов за раз требует больше команд сохранения, чем сохранение 16 байтов за раз. (Вы также были бы узким местом в пропускной способности тасования на процессорах Intel со времен Haswell, которые имеют только один порт тасования и, следовательно, пропускную способность тасования один раз за такт). (Можно также рассмотреть вариант 2xpshufb + por для заполнения хранилища 16 байт, и это может быть хорошо для Ryzen. Используйте 2 разных вектора управления перемешиванием, один из которых помещает результат в младшие 64 байта, а другой — в старшие 64 байта. См. Преобразовать 8 16-битных регистров SSE в 8-битные данные < /а>).

Вместо этого лучше использовать _mm_packus_epi16 (packuswb). Но поскольку он насыщается вместо того, чтобы отбрасывать байты, которые вам не нужны, вам нужно вводить данные, которые вы хотите сохранить в младшем байте каждого 16-битного элемента.

В вашем случае это, вероятно, старший байт каждого компонента RGB16, отбрасывая 8 младших битов из каждого компонента цвета. то есть _mm_srli_epi16(v, 8). Чтобы обнулить старший байт в каждом 16-битном элементе, используйте вместо него _mm_and_si128(v, _mm_set1_epi16(0x00ff)). (В таком случае не обращайте внимания на все эти вещи, связанные с использованием невыровненной нагрузки для замены одной из смен; это простой случай, и вы должны просто использовать два И для питания PACKUS.)

Это примерно так, как gcc и clang автоматически векторизуют это, в -O3. За исключением того, что они и портят, и тратят впустую важные инструкции (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82356, https://bugs.llvm.org/show_bug.cgi?id=34773). Тем не менее, автоматическая векторизация с помощью SSE2 (базовый уровень для x86-64) или NEON для ARM или чего-то еще — это хороший безопасный способ получить некоторую производительность без риска внесения ошибок при ручной векторизации. За исключением ошибок компилятора, все, что они генерируют, будет правильно реализовывать семантику C этого кода, который работает для любого размера и выравнивания:

// gcc and clang both auto-vectorize this sub-optimally with SSE2.
// clang is *really* sub-optimal with AVX2, gcc no worse
void pack_high8_baseline(uint8_t *__restrict__ dst, const uint16_t *__restrict__ src, size_t bytes) {
  uint8_t *end_dst = dst + bytes;
  do{
     *dst++ = *src++ >> 8;
  } while(dst < end_dst);
}

Но пропускная способность векторного сдвига ограничена 1 за такт во многих микроархитектурах (Intel до Skylake, AMD Bulldozer/Ryzen). Кроме того, до AVX512 не существовало ассемблерной инструкции load+shift, поэтому все эти операции трудно выполнить через конвейер. (т. е. мы легко становимся узким местом на внешнем интерфейсе.)

// Compilers auto-vectorize sort of like this, but with different
// silly missed optimizations.
// This is a sort of reasonable SSE2 baseline with no manual unrolling.
void pack_high8(uint8_t *restrict dst, const uint16_t *restrict src, size_t bytes) {
  // TODO: handle non-multiple-of-16 sizes
  uint8_t *end_dst = dst + bytes;
  do{
     __m128i v0 = _mm_loadu_si128((__m128i*)src);
     __m128i v1 = _mm_loadu_si128(((__m128i*)src)+1);
     v0 = _mm_srli_epi16(v0, 8);
     v1 = _mm_srli_epi16(v1, 8);
     __m128i pack = _mm_packus_epi16(v0, v1);
     _mm_storeu_si128((__m128i*)dst, pack);
     dst += 16;
     src += 16;  // 32 bytes, unsigned short
  } while(dst < end_dst);
}

Вместо смещения мы можем загружать с адреса, смещенного на один байт, чтобы нужные нам байты находились в нужном месте. И маскирование нужных нам байтов имеет хорошую пропускную способность, особенно с AVX, где компилятор может сложить загрузку+и в одну инструкцию. Если входные данные выровнены по 32 байтам, и мы проделываем этот трюк со смещением только для нечетных векторов, наша загрузка никогда не пересечет границу строки кэша. С развертыванием цикла это, вероятно, лучший выбор для SSE2 или AVX (без AVX2) на многих процессорах.

Без AVX внутренний цикл занимает 6 инструкций (6 мопов) на 16-битный вектор результатов. (С AVX всего 5, так как нагрузка сворачивается в и). Поскольку это абсолютно узкие места во внешнем интерфейсе, развертывание цикла очень помогает. gcc -O3 -funroll-loops выглядит неплохо для этой векторизованной вручную версии, особенно с gcc -O3 -funroll-loops -march=sandybridge для включения AVX.

// take both args as uint8_t* so we can offset by 1 byte to replace a shift with an AND
// if src is 32B-aligned, we never have cache-line splits
void pack_high8_alignhack(uint8_t *restrict dst, const uint8_t *restrict src, size_t bytes) {
  uint8_t *end_dst = dst + bytes;
  do{
     __m128i v0 = _mm_loadu_si128((__m128i*)src);
     __m128i v1_offset = _mm_loadu_si128(1+(__m128i*)(src-1));
     v0 = _mm_srli_epi16(v0, 8);
     __m128i v1 = _mm_and_si128(v1_offset, _mm_set1_epi16(0x00FF));
     __m128i pack = _mm_packus_epi16(v0, v1);
     _mm_store_si128((__m128i*)dst, pack);
     dst += 16;
     src += 32;  // 32 bytes
  } while(dst < end_dst);
}

С AVX, возможно, стоит выполнить как v0, так и v1 с and, чтобы уменьшить узкое место во внешнем интерфейсе за счет разделения строк кэша. (И случайные разрывы страниц). Но, возможно, нет, в зависимости от uarch, и если ваши данные уже смещены или нет. (Разветвление на это может стоить того, так как вам нужно максимально увеличить пропускную способность кеша, если данные горячие в L1D).

С AVX2 версия 256b с загрузкой 256b должна хорошо работать на Haswell/Skylake. При src выравнивании по 64B загрузка смещения по-прежнему никогда не будет разделять строку кэша. (Он всегда будет загружать байты [62:31] строки кэша, а загрузка v0 всегда будет загружать байты [31:0]). Но пакет работает в пределах 128-битных дорожек, поэтому после пакета вам нужно перетасовать (с vpermq), чтобы расположить 64-битные фрагменты в правильном порядке. Посмотрите, как gcc автоматически векторизует скалярную базовую версию с помощью vpackuswb ymm7, ymm5, ymm6/vpermq ymm8, ymm7, 0xD8.

С AVX512F этот трюк перестает работать, потому что 64-битная загрузка должна быть выровнена, чтобы оставаться в пределах одной 64-битной строки кэша. Но с AVX512 доступны различные перетасовки, а пропускная способность uop ALU более ценна (на Skylake-AVX512, где порт 1 отключается, пока 512b uop находятся в полете). Итак, v = load+shift -> __m256i packed = _mm512_cvtepi16_epi8(v) может работать хорошо, даже если он сохраняет только 256b.

Правильный выбор, вероятно, зависит от того, обычно ли ваши src и dst выровнены по 64 байтам. У KNL нет AVX512BW, так что это, вероятно, в любом случае относится только к Skylake-AVX512.

во всяком случае, ничто не сравнится с хорошим оптимизированным циклом ассемблера.

person Peter Cordes    schedule 28.09.2017