【问题标题】:Optimizing an arithmetic coder优化算术编码器
【发布时间】:2014-05-24 09:09:53
【问题描述】:

我正在优化名为 PackJPG 的 C++ 库的编码步骤

我用 Intel VTune 分析了代码,发现当前的瓶颈是 PackJPG 使用的算术编码器中的以下函数:

void aricoder::encode( symbol* s )
{   
    // update steps, low count, high count
    unsigned int delta_plus_one = ((chigh - clow) + 1);
    cstep = delta_plus_one / s->scale;
    chigh = clow + ( cstep * s->high_count ) - 1;
    clow  = clow + ( cstep * s->low_count );

    // e3 scaling is performed for speed and to avoid underflows
    // if both, low and high are either in the lower half or in the higher half
    // one bit can be safely shifted out
    while ( ( clow >= CODER_LIMIT050 ) || ( chigh < CODER_LIMIT050 ) ) {
        if ( chigh < CODER_LIMIT050 ) { // this means both, high and low are below, and 0 can be safely shifted out
            // write 0 bit
            write_zero();
            // shift out remaing e3 bits
            write_nrbits_as_one();

        }
        else { // if the first wasn't the case, it's clow >= CODER_LIMIT050
            // write 1 bit
            write_one();
            clow  &= CODER_LIMIT050 - 1;
            chigh &= CODER_LIMIT050 - 1;
            // shift out remaing e3 bits

            write_nrbits_as_zeros();
        }
        clow  <<= 1;
        chigh = (chigh << 1) | 1;

    }

    // e3 scaling, to make sure that theres enough space between low and high
    while ( ( clow >= CODER_LIMIT025 ) && ( chigh < CODER_LIMIT075 ) ) {
        ++nrbits;
        clow  &= CODER_LIMIT025 - 1;
        chigh ^= CODER_LIMIT025 + CODER_LIMIT050;
        // clow  -= CODER_LIMIT025;
        // chigh -= CODER_LIMIT025;
        clow  <<= 1;
        chigh = (chigh << 1) | 1;

    }
}

这个函数似乎借鉴了一些想法:http://paginas.fe.up.pt/~vinhoza/itpa/bodden-07-arithmetic-TR.pdf。我已经设法在一定程度上优化了该功能(主要是通过加快位写入),但现在我被卡住了。

目前最大的瓶颈似乎是一开始的分裂。这张来自 VTune 的屏幕截图显示了它所花费的时间以及创建的程序集(右侧的蓝色程序集对应于左侧选择的源代码中的行)。

s->scale 不一定是 2 的偶数次方,因此不能用模运算代替除法。

代码是使用 MSVC(来自 Visual Studio 2013)编译的,具有以下设置:

/GS /Qpar- /GL /analyze- /W3 /Gy- /Zc:wchar_t /Zi /Gm- /Ox /sdl /Fd"Release\vc120.pdb" /fp:precise /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_USRDLL" /D "PACKJPG_EXPORTS" /D "_CRT_SECURE_NO_WARNINGS" /D "BUILD_DLL" /D "_WINDLL" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /arch:IA32 /Gd /Oy- /Oi /MT /Fa"Release\" /EHsc /nologo /Fo"Release\" /Ot /Fp"Release\PackJPG.pch" 

关于如何进一步优化的任何想法?

更新 1 到目前为止,我已经尝试了所有建议,这是目前最快的版本:

void aricoder::encode( symbol* s )
{   
    unsigned int clow_copy = clow;
    unsigned int chigh_copy = chigh;
    // update steps, low count, high count
    unsigned int delta_plus_one = ((chigh_copy - clow_copy) + 1);
    unsigned register int cstep = delta_plus_one / s->scale;

    chigh_copy = clow_copy + (cstep * s->high_count) - 1;
    clow_copy = clow_copy + (cstep * s->low_count);

    // e3 scaling is performed for speed and to avoid underflows
    // if both, low and high are either in the lower half or in the higher half
    // one bit can be safely shifted out
    while ((clow_copy >= CODER_LIMIT050) || (chigh_copy < CODER_LIMIT050)) {
        if (chigh_copy < CODER_LIMIT050) {  // this means both, high and low are below, and 0 can be safely shifted out
            // write 0 bit
            write_zero();
            // shift out remaing e3 bits
            write_nrbits_as_one();

        }
        else { // if the first wasn't the case, it's clow >= CODER_LIMIT050
            // write 1 bit
            write_one();
            clow_copy &= CODER_LIMIT050 - 1;
            chigh_copy &= CODER_LIMIT050 - 1;
            // shift out remaing e3 bits

            write_nrbits_as_zeros();
        }
        clow_copy <<= 1;
        chigh_copy = (chigh_copy << 1) | 1;

    }

    // e3 scaling, to make sure that theres enough space between low and high
    while ((clow_copy >= CODER_LIMIT025) & (chigh_copy < CODER_LIMIT075)){
        ++nrbits;
        clow_copy &= CODER_LIMIT025 - 1;
        chigh_copy ^= CODER_LIMIT025 + CODER_LIMIT050;
        // clow  -= CODER_LIMIT025;
        // chigh -= CODER_LIMIT025;
        clow_copy <<= 1;
        chigh_copy = (chigh_copy << 1) | 1;

    }
    clow = clow_copy;
    chigh = chigh_copy;
}

这是使用此版本更新的 VTune 结果: 此新版本包括以下更改:

  • 在最后一个 while 循环中使用 & 而不是 && 来避免一个分支(该技巧在第一个循环中没有帮助)。
  • 将类字段复制到局部变量。

很遗憾,以下建议没有提高性能:

  • 用带有 goto 语句的 switch 替换第一个 while 循环。
  • 使用定点算法进行除法(它会产生舍入误差)。
  • 在 s->scale 上进行切换并进行位移而不是除以 2 的偶数次幂。

@example 建议不是除法很慢,而是除法操作数之一的内存访问。这似乎是正确的。根据 VTune,我们经常在这里遇到缓存未命中。有关如何解决此问题的任何建议?

【问题讨论】:

  • 这篇文章是关于 lz4 解码而不是算术编码但它可能会给你一些想法,无论如何它是一个很好的阅读:cbloomrants.blogspot.ca/2013/10/10-14-13-oodle-fast-lz4.html
  • 在汇编输出中它说,将结果存储在内存中是该代码行中花费的时间,而不是实际的除法。还是我弄错了?可能是由页面错误引起的。也许你可以改变内存布局来解决这个问题。
  • 您可以尝试在函数开始时将所有必要的类变量读入局部变量,并在最后存储修改后的变量。
  • 那么查找表就这么多了。如果由于对除数的内存访问而不是除法本身,除法很慢,您可以做几件事。 1)您可以尝试将除数移动到一个将存储在寄存器中的值中,以便生成寄存器操作数除法,而不是在内存上操作的除法。然后你也许可以更容易地从 VTune 中看到哪个部分慢,尽管仍然很难说。也许更好的方法是用乘法代替除法,看看它是否仍然很慢,即使结果不正确。
  • 2) 如果因为内存读取而变慢。 s 指向的对象来自哪里? s 指向的所有对象是否都分配在传染性内存中并按照它们在缓冲区中出现的顺序传递给编码?如果不能,你能做到吗?如果在这样的缓冲区上重复调用此函数,这将有助于优化您的内存读取情况,因为大多数时候此值将在缓存中。

标签: c++ performance optimization assembly x86


【解决方案1】:

根据 VTune,我们经常在这里遇到缓存未命中。任何 有关如何解决此问题的建议?

我们组织数据的方式直接影响data locality 的性能,因此缓存机制的行为方式取决于此。所以为了实现这一点,我们的程序应该尽可能地进行线性内存访问,并且应该避免任何间接的内存读/写(基于指针的数据结构)。这确实会受到缓存机制的喜爱,因为内存拥有 L1 缓存的概率会显着提高。

在查看您的代码和 VTune 报告时,看起来最重要的数据是传递给此特定函数的参数。这个对象的各种数据成员在这个特定的函数中被使用(内存读取)。

void aricoder::encode( symbol* s )

现在,程序正在访问该对象的数据成员的代码如下:

s->scale
s->high_count
s->low_count

从两个 VTune 报告中,我们可以验证所有三个内存访问都有不同的时序。 这表明这些数据位于该特定对象的不同偏移量处。并且在访问其中一个时(s->high_count),它正在从 L1 缓存中流出,因此需要更多时间,因为它必须将数据带入缓存。由于这个原因,s->low_count 正在受益,因为它现在处于 L1 缓存中。从这些数据我可以想到以下几点:

  1. 将您最常访问的数据成员放入您的内部的热区 目的。这意味着我们应该将所有这些成员放在第一位/顶部 的对象。通过这种方式,我们更有可能找到我们的对象 适合对象的第一个缓存行。所以我们应该尝试 根据其数据成员访问重新组织我们的对象内存布局。 我假设您没有在此处理虚拟表 对象,因为它们在缓存机制中并不是那么好。

  2. 您的整个程序可能是以这样的方式组织的 围绕这一点(即这个函数的执行),L1 缓存已满,因此程序试图从 L2 访问它 这种转变,会有更多的 CPU 周期(峰值)。在这个 我不认为我们能做太多,因为这是一种限制 机器,从某种意义上说,我们也在扩展我们的边界 太多并且试图处理太低级的东西。

  3. 您的对象 s 似乎属于 POD 类型,因此会有 线性访问。这很好,没有改进的余地。但是我们分配的方式可能会对缓存机制产生影响。如果它每次都被分配,它可能会在当前函数中执行时产生影响。

除此之外,我认为我们还应该参考以下 SO 帖子,其中详细讨论了这些概念(数据缓存/指令缓存)。这些帖子也有很好的链接,其中包含对此的深入分析和信息。

What is "cache-friendly" code?

How to write instruction cache friendly program in c++?

我建议,您应该尝试参考这些帖子。尽管它可能无法帮助您优化当前的代码,但它们对于理解这些概念的内部结构确实很有帮助。可能是您的程序已经优化,我们对此无能为力:)。

【讨论】:

    【解决方案2】:

    这不是完整的答案。此代码演示了如何使用定点算法来执行快速整数除法。广泛用于DSP和信号处理。请注意,仅当“规模”更改不频繁时,代码才对优化有意义。此外,如果 'scale' 的值较小,可以重写代码以使用 uint32_t 作为中间结果。

    #include <stdio.h>
    #include <stdint.h>
    
    int main(int argc, char **argv)
    {
       uint32_t scale;
       uint32_t scale_inv;
       uint32_t delta_plus_one;
       uint32_t val0, val1;
       uint64_t tmp;
    
       scale = 5;
       delta_plus_one = 44533;
    
       /* Place the line in 'scale' setter function */
       scale_inv = 0x80000000 / scale;
    
       /* Original expression */
       val0 = (delta_plus_one / scale);
    
       /* Division using multiplication uint64_t by uint32_t,
          using uint64_t as intermediate result */
       tmp = (uint64_t)(delta_plus_one) * scale_inv;
       /* shift right to produce result */
       val1 = tmp >> 31;
    
       printf("val0 = %u; val1 = %u\n", val0, val1);
       return 0;
    }
    

    【讨论】:

    • 好主意,但我无法让它发挥作用。有些结果与以前相同,但有些结果差了一个。例如。 delta_plus_one = 993602304 和 s->scale = 25
    • 一般在处理定点时,需要做好精度损失和溢出的准备。如果这些误差对算法有显着影响,则不动点不适合算法。
    • 好吧,既然这个算术编码器应该是无损的,所以我想这不是一个选择。
    • 你可以试试 'scale_inv=0xffffffff/scale' 或 'scale_inv=(uint64_t)0x100000000/scale' 和 shift 'val1=tmp>>32;'
    【解决方案3】:

    开头CODER_LIMIT050 是一个愚蠢的名字,由于CODER_LIMIT025CODER_LIMIT075 的共存而变得特别愚蠢。除此之外,如果没有副作用,您可能不想使用短路逻辑,所以第二个 while 语句可以是:

    while ( ( clow >= CODER_LIMIT025 ) & ( chigh < CODER_LIMIT075 ) )
    

    第一个 while 块可以进一步优化,将每次迭代的 3 个可能的分支语句合并为一个:

    start:
    switch ( ( clow >= CODER_LIMIT050 ) | (( chigh < CODER_LIMIT050 )<<1) )
    {
    default: break;
    
    case 1:
        write_zero ( );
        write_nrbits_as_one ( );
        clow <<= 1;
        chigh = ( chigh << 1 ) | 1;
        goto start;
    
    case 3: // think about this case, is this what you want?
    case 2:
        write_one ( );
        clow &= CODER_LIMIT050 - 1;
        chigh &= CODER_LIMIT050 - 1;
        write_nrbits_as_zeros ( );
        clow <<= 1;
        chigh = ( chigh << 1 ) | 1;
        goto start;
    }
    

    如果您想优化除以 s-&gt;scale 的除法,请问问自己它到底有多可变?如果只有少数可能的情况,则将其模板化。一旦它是编译时间常数,编译器可以尝试在可能的情况下找到位移位,或者在伽罗瓦域 GF(4294967296) 中找到其乘法逆元(如果有的话)。

    【讨论】:

    • @amdn 执行比较比分支便宜。如果您要追求性能,请始终尝试进行 0 次副作用比较,这将允许您使用 &amp;| 而不是 &amp;&amp;||
    • 不确定编译器是否会在没有分支的情况下执行比较,但有可能。
    • @amdn 比较函数本身没有分支。可能导致分支的语句包括&amp;&amp;||?:ifelse ifswitchwhiledo while,以及for的中间语句。跨度>
    • extern int foo();外部 int bar();布尔标志 = foo() > bar(); // 编译器要么生成比较和分支,要么为 x86 生成条件移动和减法(可能比比较和分支慢),如果棘手,它可能会减法并提取溢出标志,但我对此表示怀疑。
    • 我刚刚测试过,在x86 gcc上生成cmp后跟setl,忘了setl...我想这取决于目标机器是否需要分支。跨度>
    猜你喜欢
    • 2012-10-05
    • 2017-12-19
    • 2018-08-19
    • 1970-01-01
    • 1970-01-01
    • 2016-09-11
    • 1970-01-01
    • 1970-01-01
    • 2014-11-16
    相关资源
    最近更新 更多