【问题标题】:Performance challenge: NAL Unit Wrapping性能挑战:NAL 单元包装
【发布时间】:2008-09-29 03:30:14
【问题描述】:

从我过去看到的情况来看,StackOverflow 似乎喜欢编程挑战,例如收到数十条回复的fast char to string exercise problem。这是一个优化挑战:采用一个非常简单的函数,看看你是否能想出一个更聪明的方法。

很长一段时间以来,我一直想进一步优化一个函数,但我总是发现我的优化存在一些漏洞,导致输出不正确——在一些罕见的特殊情况下它们会失败。但是,鉴于功能,我一直认为应该能够做得比这更好。

该函数采用输入数据流(从熵的角度来看,实际上是随机位)并将其包装到 NAL 单元中。这涉及放置转义码:00 00 00、00 00 01、00 00 02 或 00 00 03 的任何字节序列都将替换为 00 00 03 XX,其中 XX 是原始序列的最后一个字节。正如人们可以猜到的那样,考虑到这种序列的可能性,这些仅在每 400 万字节的输入中放置大约 1 个 - 所以这是一个挑战,一个人搜索大量数据并且几乎什么都不做它除非在极少数情况下。然而,因为“做某事”涉及插入字节,它使事情变得有点棘手。当前未优化的代码如下C:

src 和 dst 是指向字节数组的指针,end 是指向输入数据末尾的指针。

int i_count = 0;
while( src < end )
{
    if( i_count == 2 && *src <= 0x03 )
    {
        *dst++ = 0x03;
        i_count = 0;
    }
    if( *src == 0 )
        i_count++;
    else
        i_count = 0;
    *dst++ = *src++;
}

此函数的常见输入大小范围约为 1000 到 1000000 字节的数据。

我最初的想法包括一个函数,它(以某种方式)快速搜索输入以查找需要转义码的情况,以避免在不需要放置转义码的绝大多数输入中出现更复杂的逻辑。


【问题讨论】:

    标签: c performance optimization


    【解决方案1】:

    嗯……这样的事情怎么样?

    #define likely(x) __builtin_expect((x),1)
    #define unlikely(x) __builtin_expect((x),0)
    
    while( likely(src < end) )
    {
        //Copy non-zero run
        int runlen = strlen( src );
        if( unlikely(src+runlen >= end) )
        {
            memcpy( dest, src, end-src );
            dest += end-src;
            src = end;
            break;
        }
    
        memcpy( dest, src, runlen );
        src += runlen;
        dest += runlen;
    
        //Deal with 0 byte
        if( unlikely(src[1]==0 && src[2]<=3 && src<=end-3) )
        {
            *dest++ = 0;
            *dest++ = 0;
            *dest++ = 3;
            *dest++ = *src++;
        }
        else
        {
            *dest++ = 0;
            src++;
        }
    }
    

    strcpy 和 memcpy 之间有一些重复的工作,但最好摆脱它。

    【讨论】:

    • 令人印象深刻,比我上面的和 Mark Ransom 的快 40% 以上。我不能保证它会给出正确的结果,因为我需要在一个非常大的数据集上进行测试才能确定,但​​初步测试表明它可能是正确的。
    • 不,你还没有完成; src
    • 是的,Mark 在主循环外处理最后 3 个字节的方法可能更好。
    • 是的,合并 strlen 和 memcpy 会很好。 memccpy 非常适合这一点,但正如我的一位开发人员所说,你不能依赖 libc 实现来获得真正晦涩的函数来快速(而且,不出所料,它比你的方法慢)。
    • 是的,strlcpy 也可以完成这项工作,同样的反对意见(我认为它甚至在 glibc 中都没有)。
    【解决方案2】:

    对您的代码应用明显的优化:

    #define unlikely(x) __builtin_expect((x),0)
    
    while( src < end )
    {
        const char s = *src++;
        if( unlikely(i_count==2 && s<=0x03) )
        {
            *dst++ = 0x03;
            i_count = 0;
        }
        if( unlikely(s==0) )
            i_count++;
        else
            i_count = 0;
        *dst++ = s;
    }
    

    【讨论】:

    • 这实际上是一个很好的观点——因为它们是字节指针,C 编译器必须假设它们可以相互别名,因为 char* 没有严格的别名规则。我得试试这个,看看是否有帮助。
    • 不幸的是,您的更改实际上使性能降低了 10% 左右(!?)。当然,我必须查看反汇编才能确切了解原因。我猜它是因为第一个 if 语句提前终止,所以“s”几乎不需要加载,直到循环的最后一行。
    • 那么,有什么方法可以让 C 编译器知道 src 和 dst 不能相互别名,即使它们是 char* 指针?
    • 你是对的—— else 会改变函数的输出;它不正确。未签名与在字符上签名无关紧要。
    • 我认为 restrict 在 GCC 中做到了这一点。另外,性能下降是因为 's' 被(错误地)签名了吗?
    【解决方案3】:

    Mike F,非常感谢“不太可能”的建议:以下是比原来快 10% 左右:

    #define unlikely(x) __builtin_expect((x),0)
    while( src < end )
    {
        if( unlikely(i_count == 2) && unlikely(*src <= 0x03) )
        {
            *dst++ = 0x03;
            i_count = 0;
        }
        if( unlikely(*src == 0) )
            i_count++;
        else
            i_count = 0;
        *dst++ = *src++;
    }
    

    而且,更好的是,以下是 50% 比原来快 (!!!) 我仍然无法弄清楚为什么循环条件中的可能 () 有帮助.. . 一定是 gcc 又奇怪了。

    #define unlikely(x) __builtin_expect((x),0)
    #define likely(x) __builtin_expect((x),1)
    while( likely(src < end) )
    {
        if( unlikely(i_count == 2) && unlikely(*src <= 0x03) )
        {
            *dst++ = 0x03;
            i_count = 0;
        }
        if( unlikely(*src == 0) )
            i_count++;
        else
            i_count = 0;
        *dst++ = *src++;
    }
    

    但是,我希望的不仅仅是优化当前的幼稚方法;我怀疑除了单独处理每个字节之外,肯定有更好的方法。

    【讨论】:

      【解决方案4】:

      我之前回答的更快版本:

      while (src < end-2)
      {
          if (src[0] == 0)
          {
              if (src[1] == 0)
              {
                  if (src[2] <= 3)
                  {
                      dst[0] = 0;
                      dst[1] = 0;
                      dst[2] = 3;
                      dst[3] = src[2];
                      src += 3;
                      dst += 4;
                  }
                  else
                  {
                      dst[0] = 0;
                      dst[1] = 0;
                      dst[2] = src[2];
                      src += 3;
                      dst += 3;
                  }
              }
              else
              {
                  dst[0] = 0;
                  dst[1] = src[1];
                  src += 2;
                  dst += 2;
              }
          }
          else
              *dst++ = *src++;
      }
      while (src < end)
          *dst++ = *src++;
      

      【讨论】:

      • 这里没有更快,可能只是因为 if 的初始条件仅在 256 个案例中发生 1 个,因此额外分支的好处几乎为零。
      • 用 MS VC6 运行,100 次传递相同的 1000000 个随机字节,耗时 0.287 秒。我的原件需要 0.345 秒。你的需要 0.442 秒。
      • 应该提到我有一个 AMD x64 处理器,这可能也会有所作为。
      • MSVC++ 编译器的性能特征也与 GCC 大相径庭,这可能是造成差异的主要原因。
      【解决方案5】:
      while (src < end-2)
      {
          if ((src[0] == 0) && (src[1] == 0) && (src[2] <= 3))
          {
              dst[0] = 0;
              dst[1] = 0;
              dst[2] = 3;
              dst[3] = src[2];
              src += 3;
              dst += 4;
          }
          else
              *dst++ = *src++;
      }
      while (src < end)
          *dst++ = *src++;
      

      【讨论】:

      • 你确定你的意思不是 dst[3] = src[2];?
      【解决方案6】:

      这样的测试非常取决于您的编译器和处理器/内存设置。在我的系统上,我改进的版本与 Mike F 的 strlen/memcpy 版本的速度完全相同。

      【讨论】:

      • 哎呀,刚刚评论了同样的事情。
      • 对于 strlen/memcpy 版本,我想说它更多地依赖于您的 libc。一个好的 libc 会比这里发布的任何天真的基于字节的版本提供更好的结果,但一个糟糕的 libc 可能会更糟。
      【解决方案7】:

      在我看来,只要您逐字节复制,在访问内存时就会遇到字对齐问题,这些都会拖慢您的速度。

      所以,我建议如下(抱歉伪代码,我的 C/C++ 基本上被遗忘了):

      • 搜索以查找下一个插入点
        [Boyer-Moore 搜索算法是一个不错的选择,因为它是次线性的(不需要检查每个字节)]
      • 块复制未更改的块
        [我的理解是 GCC 和其他优秀的 C++ 编译器可以将 memcpy() 调用直接转换为正确的处理器指令,从而提供接近最佳的性能]
      • 插入更改后的代码
      • 重复直到完成

      【讨论】:

        猜你喜欢
        • 2021-02-10
        • 1970-01-01
        • 2015-05-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-10-04
        • 2020-10-16
        • 1970-01-01
        相关资源
        最近更新 更多