H264/H265 NALU 起始码搜索性能优化(1)

    在ffmpeg中,在进行h264 rbsp流demux的时候,需要进行starting code的搜索,其采用的方法比较简单,就是不断比较字节流中连续的三个字节,是不是 0x00, 0x00, 0x01,ffmpeg采用如下代码用来找到各个NALU的分界点:

static int find_next_start_code(const uint8_t *buf, const uint8_t *next_avc){
int i = 0;
if (buf + 3 >= next_avc)
return next_avc - buf;
while (buf + i + 3 < next_avc) {
if (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1) break;
i++;
} return i + 3;}

    这种方法比较传统,一般来说如果对于性能没有特别的要求,是能够很好满足要求了。

    按照ffmpeg的算法,搜索一个内存块中的所有H264 NALU的起始码,并将起始码的位置写入nalu_borders数组中,写出来的代码如下,整个逻辑简单明了:


vector<int> split_simd_ffmpeg(const uint8_t *data, size_t cb){    const uint8_t *p = data;
const uint8_t *e = data + cb;
vector<int> list_index;
while (p < e -3){ if (p[0] == 0 && p[1] == 0 && p[2] == 1){ list_index.emplace_back(p - data); } p++; }
return list_index;}

    本文提出一种更高效的starting code的搜索算法:

void split_nalu(char *video_data, int size,  std::vector<int> &nalu_borders){
while(index + 8 <= (size_t)size){
uint64_t code = *(uint64_t*)(video_data+index);
if ((code & 0xFFFFFFull) == 0x010000ull){ nalu_borders.emplace_back(index); index += 3; continue; } if ((code & 0xFFFFFF00ull) == 0x01000000ull){ nalu_borders.emplace_back(index+1); index += 4; continue; }
if ((code & 0xFFFFFF0000ull) == 0x0100000000ull){ nalu_borders.emplace_back(index+2); index += 5; continue; } if (((code & 0xFFFFFF000000ull) == 0x010000000000ull){ nalu_borders.emplace_back(index+3); index += 6; continue; }
if ((code & 0xFFFFFF00000000ull) == 0x01000000000000ull){ nalu_borders.emplace_back(index+4); index += 7; continue; }
if ((code & 0xFFFFFF0000000000ull) == 0x0100000000000000ull){ nalu_borders.emplace_back(index+5); index += 8; continue; }
if ((code & 0xFFFF000000000000ull) == 0x0ull){ index += 6; }else if ((code & 0xFF00000000000000ull) == 0x0ull){ index += 7; } else{ index += 8; } }
while(index + 4 < (size_t)size){
uint32_t code = *(uint64_t*)(video_data+index);
if ((code & 0x00FFFFFFu) == 0x00010000u){ nalu_borders.emplace_back(index); index += 3; continue; }
index++; }}

     看上去代码复杂了很多,但是实际测试结果,对比ffmpeg的搜索算法,在x64的CPU的机器上面,性能至少提升了30%。

     本算法利用了64位的寄存器预先读取8个字节,然后在循环提内通过掩码与操作进行比对,一个循环中对8个字节进行264起始码的搜索,相比ffmpeg需要不断从内存中load,然后逐字节比对来说,使得搜索性能得到了显著提升。毕竟寄存器的性能远比内存的读写性能要强多了!而且在一个循环里面可以通过编译器和CPU的优化进行指令的多路并发执行。

     然后最近几天拼命想利用SIMD优化指令进行搜索的算法,好不容易熟悉了simd的几个指令,调试了老半天,终于写出来如下代码:

    const uint8_t mask_1[32] = { 0xFF, 0xFF, 0xFF, 0x00,                                 0xFF, 0xFF, 0xFF, 0x00,                                   0xFF, 0xFF, 0xFF, 0x00,                                   0xFF, 0xFF, 0xFF, 0x00  };
const uint8_t dest_1[32] = { 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0 };

const uint8_t mask_2[32] = { 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF,};
const uint8_t dest_2[32] = { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1};
vector<int> split_nalu_simd(const uint8_t *video_data, size_t size){ size_t index = 0; vector<int> list_index; op_data od;
size_t next_block = 31;
while(index + 16 <= (size_t)size){ __m128i src = _mm_load_si128((__m128i*)(video_data+index)); __m128i mask = _mm_load_si128((__m128i*)od.mask_1); __m128i dest = _mm_load_si128((__m128i*)od.dest_1); __m128i tmp = _mm_and_si128(src, mask); __m128i tmp2 = _mm_cmpeq_epi32(tmp, dest);
if (_mm_extract_epi32(tmp2, 0) == 0xFFFFFFFFu){ list_index.emplace_back(index); } if (_mm_extract_epi32(tmp2, 1) == 0xFFFFFFFFu){ list_index.emplace_back(index + 4); } if (_mm_extract_epi32(tmp2, 2) == 0xFFFFFFFFu){ list_index.emplace_back(index + 8); } if (_mm_extract_epi32(tmp2, 3) == 0xFFFFFFFFu){ list_index.emplace_back(index + 12); }


mask = _mm_load_si128((__m128i*)od.mask_2); dest = _mm_load_si128((__m128i*)od.dest_2); tmp = _mm_and_si128(src, mask); tmp2 = _mm_cmpeq_epi32(tmp, dest);
if (_mm_extract_epi32(tmp2, 0) == 0xFFFFFFFFu){ list_index.emplace_back(index + 1); } if (_mm_extract_epi32(tmp2, 1) == 0xFFFFFFFFu){ list_index.emplace_back(index + 5); } if (_mm_extract_epi32(tmp2, 2) == 0xFFFFFFFFu){ list_index.emplace_back(index + 9); } if (_mm_extract_epi32(tmp2, 3) == 0xFFFFFFFFu){ list_index.emplace_back(index + 13); }

src = _mm_srli_si128(src, 2);
mask = _mm_load_si128((__m128i*)od.mask_1); dest = _mm_load_si128((__m128i*)od.dest_1); tmp = _mm_and_si128(src, mask); tmp2 = _mm_cmpeq_epi32(tmp, dest);
if (_mm_extract_epi32(tmp2, 0) == 0xFFFFFFFFu){ list_index.emplace_back(index + 2); } if (_mm_extract_epi32(tmp2, 1) == 0xFFFFFFFFu){ list_index.emplace_back(index + 6); } if (_mm_extract_epi32(tmp2, 2) == 0xFFFFFFFFu){ list_index.emplace_back(index + 10); }

mask = _mm_load_si128((__m128i*)od.mask_2); dest = _mm_load_si128((__m128i*)od.dest_2); tmp = _mm_and_si128(src, mask); tmp2 = _mm_cmpeq_epi32(tmp, dest);

if (_mm_extract_epi32(tmp2, 0) == 0xFFFFFFFFu){ list_index.emplace_back(index + 3); } if (_mm_extract_epi32(tmp2, 1) == 0xFFFFFFFFu){ list_index.emplace_back(index + 7); } if (_mm_extract_epi32(tmp2, 2) == 0xFFFFFFFFu){ list_index.emplace_back(index + 11); }

index += 16; uint32_t tail = _mm_extract_epi32(src, 3);


if (tail == 0x0u){ if (index + 1 <= size && video_data[index] == 0x01){ list_index.emplace_back(index-2); continue; } }
if ((tail & 0x0000FF00u) == 0x0u){ if (index + 2 <= size && video_data[index] == 0x00 && video_data[index+1] == 0x01){ list_index.emplace_back(index-1); } } }
while(index + 4 < (size_t)size){
uint32_t code = *(uint64_t*)(video_data+index);
if ((code & 0x00FFFFFFu) == 0x00010000u){ list_index.emplace_back(index); index += 3; continue; }
index++; } std::sort(list_index.begin(), list_index.end());
return list_index;}

        但是测试下来的效果不太理想,大失所望,基本上和上面的split_nalu不相上下,应该是中间用了太多条件判断的分支,抵消了simd的优势,加上插入起始码位置的时候因为并不是按顺序插入的,需要在结尾处进行一次sort操作一定程度上会引起性能的降低,得不偿失。

      感觉能够想到的方法还是split_nalu比较靠谱,也不是太复杂。

今天下午,突然来了灵感,修改了上面的simd的代码,减少了if条件判断分支,尽然性能提升1倍,开心!!!!

    const uint8_t mask_1[32] = { 0xFF, 0xFF, 0xFF, 0x00,                                 0xFF, 0xFF, 0xFF, 0x00,                                   0xFF, 0xFF, 0xFF, 0x00,                                   0xFF, 0xFF, 0xFF, 0x00  };
const uint8_t dest_1[32] = { 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0 };

const uint8_t mask_2[32] = { 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF,};
const uint8_t dest_2[32] = { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1};
vector<int> split_nalu_simd2(const uint8_t *video_data, size_t size){ size_t index = 0; vector<int> list_index; op_data od;
size_t next_block = 31;
while(index + 16 <= (size_t)size){ __m128i src = _mm_load_si128((__m128i*)(video_data+index)); __m128i mask = _mm_load_si128((__m128i*)od.mask_1); __m128i dest = _mm_load_si128((__m128i*)od.dest_1); __m128i tmp = _mm_and_si128(src, mask); __m128i tmp2 = _mm_cmpeq_epi32(tmp, dest); int m = _mm_movemask_epi8(tmp2); if (m != 0){ if (m & 0x0F){ list_index.emplace_back(index); } if (m & 0xF0){ list_index.emplace_back(index + 4); } if (m & 0xF00){ list_index.emplace_back(index + 8); } if (m & 0xF000){ list_index.emplace_back(index + 12); } }
mask = _mm_load_si128((__m128i*)od.mask_2); dest = _mm_load_si128((__m128i*)od.dest_2); tmp = _mm_and_si128(src, mask); tmp2 = _mm_cmpeq_epi32(tmp, dest);
m = _mm_movemask_epi8(tmp2); if (m != 0){ if (m & 0x0F){ list_index.emplace_back(index + 1); } if (m & 0xF0){ list_index.emplace_back(index + 5); } if (m & 0xF00){ list_index.emplace_back(index + 9); } if (m & 0xF000){ list_index.emplace_back(index + 13); } }
src = _mm_srli_si128(src, 2);
mask = _mm_load_si128((__m128i*)od.mask_1); dest = _mm_load_si128((__m128i*)od.dest_1); tmp = _mm_and_si128(src, mask); tmp2 = _mm_cmpeq_epi32(tmp, dest);
m = _mm_movemask_epi8(tmp2); if (m != 0){ if (m & 0x0F){ list_index.emplace_back(index + 2); } if (m & 0xF0){ list_index.emplace_back(index + 6); } if (m & 0xF00){ list_index.emplace_back(index + 10); } }
mask = _mm_load_si128((__m128i*)od.mask_2); dest = _mm_load_si128((__m128i*)od.dest_2); tmp = _mm_and_si128(src, mask); tmp2 = _mm_cmpeq_epi32(tmp, dest);
m = _mm_movemask_epi8(tmp2); if (m != 0){ if (m & 0x0F){ list_index.emplace_back(index + 3); } if (m & 0xF0){ list_index.emplace_back(index + 7); } if (m & 0xF00){ list_index.emplace_back(index + 11); } }

index += 16; uint32_t tail = _mm_extract_epi32(src, 3);


if (tail == 0x0u){ if (index + 1 <= size && video_data[index] == 0x01){ list_index.emplace_back(index-2); continue; } }
if ((tail & 0x0000FF00u) == 0x0u){ if (index + 2 <= size && video_data[index] == 0x00 && video_data[index+1] == 0x01){ list_index.emplace_back(index-1); } } }
while(index + 4 < (size_t)size){
uint32_t code = *(uint64_t*)(video_data+index);
if ((code & 0x00FFFFFFu) == 0x00010000u){ list_index.emplace_back(index); index += 3; continue; }
index++; } std::sort(list_index.begin(), list_index.end());
return list_index;}

       关键是是通过_mm_movemask_epi8将前面比较的结果合并到一个int型的字段m中,然后直接判断m是否都为0,如果为0就可以直接掉过里面的分支逻辑了,大大减少了条件分支,从而提升了性能。

ps:

      需要提一下,如果没有开启-O3编译器优化,split_nalu_simd2比split_nalu版本性能差了一倍,开启以后则反回过了,虽然split_nalu性能也提升了,但是split_nalu_simd2提升非常明显。

     假设待编译的代码为test.cpp,那么用如下编译指令:

      g++ test.cpp -mavx2 -o test -g -O3

     -mavx2是必须要加的,否则编译器不能支持simd相关指令。

原创文章,作者:网络技术联盟站,如若转载,请注明出处:https://www.sudun.com/ask/49816.html

Like (0)
网络技术联盟站的头像网络技术联盟站
Previous 2024年5月10日
Next 2024年5月10日

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注