Pull to refresh

Comments 23

> В H.264/MPEG-4 AVC цифровой поток должен всегда начинаться с блока адресации IDR,

но это же не так. Intra refresh используется в низколатентном стриминге и на спутнике.

Как видно из таблицы 1, применение инструкций Intel SSE и Intel AVX2 обеспечивает повышение производительности в 100 раз,

Даже если учесть, что работа идет с 8-битными целыми, максимальное теоретическое ускорение, которого можно добиться с SSE — 16 раз. Все остальное — разворачивание циклов, эффективный доступ к памяти, и прочее, напрямую не связанное с SIMD.
Вы совершенно правы, спасибо! Обязательно передам это автору оригинальной статьи для правки.
Вы не совсем правы. Есть такая операция в SSE2 — _mm_sad_epu8 (в AVX2 — _mm256_sad_epu8), которая позволяет за одну операцию найти сумму абсолютных разностей 1-байтных беззнаковых целых чисел (упоминается в статье). Иначе говоря:

__m128i a, b, c;
с = _mm_sad_epu8(a, b); эквивалетна:

unsigned long long c[2];
unsigned char a[16], b[16];

c[0] =
a[0] > b[0]? a[0] — b[0]: b[0] — a[0] +
a[1] > b[1]? a[1] — b[1]: b[1] — a[1] +
a[2] > b[2]? a[2] — b[2]: b[2] — a[2] +
a[3] > b[3]? a[3] — b[3]: b[3] — a[3] +
a[4] > b[4]? a[4] — b[4]: b[4] — a[4] +
a[5] > b[5]? a[5] — b[5]: b[5] — a[5] +
a[6] > b[6]? a[6] — b[6]: b[6] — a[6] +
a[7] > b[7]? a[7] — b[7]: b[7] — a[7];

c[1] =
a[8] > b[8]? a[8] — b[8]: b[8] — a[8] +
a[9] > b[9]? a[9] — b[9]: b[9] — a[9] +
a[10] > b[10]? a[10] — b[10]: b[10] — a[10] +
a[11] > b[11]? a[11] — b[11]: b[11] — a[11] +
a[12] > b[12]? a[12] — b[12]: b[12] — a[12] +
a[13] > b[13]? a[13] — b[13]: b[13] — a[13] +
a[14] > b[14]? a[14] — b[14]: b[14] — a[14] +
a[15] > b[15]? a[15] — b[15]: b[15] — a[15];

Т.е. одна эта операция SSE2 позволяет заменить 62 операции для скалярного кода (для AVX2 — 124 операции соответственно). Отсюда и возможен потенциальный выигрыш до ста раз. Хотя конечно на большинстве других операций он гораздо скромнее — порядка 10 раз для SSE2 кода и 15 раз для AVX2 кода.
Данные взяты из проекта Simd.
И вам спасибо, и вы правы :) но там в таблице результат улучшения аж в 101 раз дан для SSE, причем, не для одной ф-ии, а как я понимаю, для целого блока кода. Так что без дополнительных оптимизаций типа доступа к памяти тут не обошлось.
Использование SIMD инструкций как бы подразумевает, что будут не только, например, выполняться сложение для всех элементов вектора с помощью одной операции, но также векторная загрузка исходных данных и векторное сохранение результатов. Опять же, так как, векторная загрузка/сохранение оптимально работает для выровненных данных, то хороший алгоритм должен по возможности использовать эту возможность.
На практике не удается достичь ускорения в 16 раз даже с выравниванием и разворачиванием.

$ cat /proc/cpuinfo | grep CPU
model name	: Intel(R) Core(TM) i5-4258U CPU @ 2.40GHz

$ cc -O2 -lm ./sad.c -o sad -msse4  && time ./sad
30592637 scalar in 3.350590 sec
30592637 SSE4 in 0.281125 sec
30592637 optim SSE4 in 0.270903 sec

Полный код примера
#include <time.h>
#include <stdint.h>
#include <malloc.h>
#include <smmintrin.h>


int sad(uint8_t *buf1, uint8_t *buf2, int buf_length)
{
  int i;
  int sum = 0;
  for (i = 0; i < buf_length; i++) {
    sum += abs(buf1[i] - buf2[i]);
  }
  return sum;
}


int sad_sse(uint8_t *buf1, uint8_t *buf2, int buf_length)
{
  int i;
  __m128i sum = _mm_setzero_si128();
  int res[4];
  for (i = 0; i < buf_length; i += 16) {
    __m128i b1 = _mm_loadu_si128((__m128i *) &buf1[i]);
    __m128i b2 = _mm_loadu_si128((__m128i *) &buf2[i]);
    __m128i s1 = _mm_sad_epu8(b1, b2);
    sum = _mm_add_epi32(sum, s1);
  }
  sum = _mm_hadd_epi32(sum, sum);
  sum = _mm_hadd_epi32(sum, sum);
  return _mm_cvtsi128_si32(sum);
}


int sad_sse_opt(uint8_t *buf1, uint8_t *buf2, int buf_length)
{
  int i;
  __m128i sum = _mm_setzero_si128();
  int res[4];
  for (i = 0; i < buf_length; i += 64) {
    __m128i b1 = _mm_loadu_si128((__m128i *) &buf1[i]);
    __m128i b2 = _mm_loadu_si128((__m128i *) &buf2[i]);
    __m128i b3 = _mm_loadu_si128((__m128i *) &buf1[i + 16]);
    __m128i b4 = _mm_loadu_si128((__m128i *) &buf2[i + 16]);
    __m128i b5 = _mm_loadu_si128((__m128i *) &buf1[i + 32]);
    __m128i b6 = _mm_loadu_si128((__m128i *) &buf2[i + 32]);
    __m128i b7 = _mm_loadu_si128((__m128i *) &buf1[i + 48]);
    __m128i b8 = _mm_loadu_si128((__m128i *) &buf2[i + 48]);
    __m128i s1 = _mm_sad_epu8(b1, b2);
    __m128i s2 = _mm_sad_epu8(b3, b4);
    __m128i s3 = _mm_sad_epu8(b5, b6);
    __m128i s4 = _mm_sad_epu8(b7, b8);
    sum = _mm_add_epi32(sum, s1);
    sum = _mm_add_epi32(sum, s2);
    sum = _mm_add_epi32(sum, s3);
    sum = _mm_add_epi32(sum, s4);
  }
  sum = _mm_hadd_epi32(sum, sum);
  sum = _mm_hadd_epi32(sum, sum);
  return _mm_cvtsi128_si32(sum);
}


void *malloc16 (size_t s) {
  unsigned char *p;
  unsigned char *porig = malloc (s + 0x10);
  p = (void *)(((int) porig + 16) & (~0xf));
  *(p-1) = p - porig;
  return p;
}

void free16(void *p) {
  unsigned char *porig = p;
  porig = porig - *(porig-1);
  free(porig);
}


int main(int argc, char **argv)
{
  float   time;
  clock_t start;
  int     i;
  uint8_t*buf1, *buf2;
  int     buf_length = 64 * 1024 * 1024;
  int     res;
  int     times = 50;

  buf1 = malloc16(buf_length * sizeof(uint8_t));
  buf2 = malloc16(buf_length * sizeof(uint8_t));

  for (i = 0; i < buf_length; i += 333) {
    buf1[i] = i % 256;
  }
  for (i = 0; i < buf_length; i += 1741) {
    buf1[i] = i % 256;
  }

  start = clock();
  for (i = 0; i < times; ++i) {
    res = sad(buf1, buf2, buf_length);
  }
  printf("%d scalar in %f sec\n", res, (float)(clock() - start) / CLOCKS_PER_SEC);

  start = clock();
  for (i = 0; i < times; ++i) {
    res = sad_sse(buf1, buf2, buf_length);
  }
  printf("%d SSE4 in %f sec\n", res, (float)(clock() - start) / CLOCKS_PER_SEC);

  start = clock();
  for (i = 0; i < times; ++i) {
    res = sad_sse_opt(buf1, buf2, buf_length);
  }
  printf("%d optim SSE4 in %f sec\n", res, (float)(clock() - start) / CLOCKS_PER_SEC);

  free16(buf1);
  free16(buf2);

  return 0;
}

Я не сомневаюсь, что что-то было ускорено в 100 раз, но говорить что это было сделано только за счет SSE или AVX2 — в корне не верно.
По моим данным выигрыш от для данного примера на SSE2 получается порядка 22-25 раз.
К стати, у вас в коде довольно нелогично — вы используете выровненные массивы данных, но при этом для чтения из них используете функцию _mm_loadu_si128, которая вдвое медленнее чем _mm_load_si128. На одном этом теряете порядка 20% производительности.
С разворачиванием цикла (если опустить вопрос о ее целесообразности для кода с SIMD инструкциями — ибо процессор может одновременно исполнить не более двух векторных инструкций за такт) тоже не все гладко — если операции _mm_loadu_si128 и _mm_sad_epu8 у вас потенциально могут исполняться одновременно, то функции _mm_add_epi32 (к стати почему не _mm_add_epi64 ?) работают с oдной переменной sum — что может поставить компилятор в тупик (код с его точки зрения может выглядеть последовательным).
По моим данным выигрыш от для данного примера на SSE2 получается порядка 22-25 раз.
Я не понял, что вы имеете в виду, что у вас за данные. Вы запустили этот же код у себя и получили выигрыш 22-25 раз?

вы используете выровненные массивы данных, но при этом для чтения из них используете функцию _mm_loadu_si128
С этими функциями вообще смешная история. _mm_load_si128 с выравниванием конечно работает быстрее, чем _mm_loadu_si128 без выравнивания. Но еще быстрее работает именно _mm_loadu_si128 c выравниванием. Но здесь это не заметно, потому что на самом деле 64 мб * 50 раз * 2 массива / 0.270903 сек. уже равно скорости чтения 23,6 GB/s, что очень близко к максимальной пропускной способности памяти 25,6 GB/s.
Я использовал тесты производительности встроенные в библиотеку Simd. Хотя вы правы: в качестве тестов там используется серая картинка в разрешении Full HD размеров 2 MB. Так как все данные влазят в процессорный кеш, то получается выигрыш в 20-25 раз. Если размер тестовой картинки увеличить, то выигрыш от оптимизации будет всего 7.5 раз и для SSE2 и для AVX2 кода — все упрется в шину данных. Но честно говоря в моем случае (видеоаналитика), данных обычно не так много — часто все влезает в кеш.
А вы делаете аналитику на процессоре или на Cuda/Quicksync?
рассматривали другие варианты?
Конечно. Однако пока лучшую производительность показывает CPU.
Занятно, что сжатие 4к h265 на 30 ядрах Xeon достигает лишь 7fps, в то время как топовые мобилки начала 2015 на Snapdragon 810 смогут это делать в реальном времени :-)
вы не забывайте, что сжатие того же H264 может занимать разное время из-за разных выбранных оптимизаций.

Можно укладывать YUV в каждый макроблок и это тоже будет валидный H264. С H265 всё то же самое, просто спектр алгоритмов для сжатия ещё шире.

Так что мобилки на snapdragon печатают быстро, но такая фигня получается.
Очевидно, что в потоке, скажем, 4мбит/с простым YUV не отделаешься.
Про «фигню» давайте сравнение что-ли. Особенно в 4к.
60 ядерный комп за десятки тысяч $, который не может жать в fast режиме в реальном времени — вообще не выполняет свою функцию.
А у него функция обязательно в реальном времени кодировать?

Не забывайте, даже для h264 аппаратные решения дают существенно более плохое качество при равном битрейте, чем x264 с хорошим пресетом. Для h265, скорее всего, отрыв намного сильнее.

Мобилки, скорее всего, h265 используют только потому, что он умеет 4К, а не потому, что в 2 раза экономится битрейт при сравнимом качестве. Те алгоритмы, что дают эту экономию, аппаратно просто не реализованы.
Мне вдруг стало интересно, сколько процентов аккумулятора эти мобилки станут расходовать каждую минуту и за сколько минут разогреваться выше 50° по Цельсию во время видеозаписи.
UFO just landed and posted this here
UFO just landed and posted this here
Лучше бы открыли доступ к внутренностям QuickSync, чтобы задействовать эти аппаратные функции из x264/x265 с хорошим качеством на выходе, а не как сейчас — вот вам закрытый QuickSync без настроек, годный максимум для превью файлов или пережатия на мобильные устройства ( compression.ru/video/codec_comparison/h264_2012/ )
Sign up to leave a comment.