我正在优化名为PackJPG
的C ++库的编码步骤我用英特尔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-&gt;比例不一定是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结果: 此新版本包括以下更改:
以下建议不幸不改善了效果:
@example表明,除了分区之一的操作数之外,它不是缓慢的分区。这似乎是正确的。根据VTune的说法,我们经常会遇到缓存丢失问题。关于如何解决这个问题的任何建议?
答案 0 :(得分:4)
根据VTune的说法,我们经常会遇到缓存未命中。任何 关于如何解决这个问题的建议?
我们组织数据的方式直接影响性能data locality,因此缓存机制的行为依赖于此。因此,要实现这一点,我们的程序应尽可能尝试进行线性内存访问,并应避免任何间接内存读/写(基于指针的数据结构)。这确实会受到缓存机制的影响,因为拥有L1缓存的内存概率会显着提高。
在查看代码和VTune报告时,看起来最重要的数据是传递给此特定函数的参数。此对象的各种数据成员在此特定函数中被使用(内存读取)。
void aricoder::encode( symbol* s )
现在,有以下代码,程序正在访问此对象的数据成员:
s->scale
s->high_count
s->low_count
从两个VTune报告中,我们都可以验证所有三个内存访问都有不同的时间。 这表明这些数据与此特定对象的偏移量不同。在访问其中一个( s-&gt; high_count )时,它会从L1缓存中出来,因此需要花费更多时间才能获取数据进入缓存。因此, s-&gt; low_count 正在受益,因为它现在位于L1缓存中。从这些数据我可以想到以下几点:
将访问最多的数据成员放入您的热门区域 宾语。这意味着我们应该将所有这些成员放在第一个/顶部 对象。通过这种方式,我们将有更好的机会成为我们的对象 适合对象的第一个缓存行。所以我们应该尝试 根据其数据成员访问重新组织对象内存布局。 我假设你没有处理虚拟表 对象,因为它们在缓存机制方面不太好。
您的整体计划可能以这种方式组织 在这一点(.i.e执行此函数),L1 缓存已满,因此程序正在尝试从L2和 在这种转变中,会有更多的CPU周期(尖峰)。在这 情景我认为我们不能做太多,因为这是一种限制 机器,在某种意义上我们也在扩展我们的边界 很多,并试图处理太低级别的东西。
您的对象 s 似乎属于POD类型,因此会有 线性访问。这很好,没有改进的余地。但是,我们分配的方式可能会对缓存机制产生影响。如果每次都进行分配,则在当前函数中执行时会产生影响。
除此之外,我认为我们还应该参考以下SO帖子,其中详细讨论了这些概念(数据缓存/指令缓存)。这些帖子也有很好的联系,对此有深入的分析和信息。
What is "cache-friendly" code?
How to write instruction cache friendly program in c++?
我建议你,你应该尝试推荐这些帖子。尽管它可能无法帮助您优化当前的代码,但它们对于理解这些概念的内部实际上非常有帮助。可能你的程序已经过优化,我们可以做的很少:)。
答案 1 :(得分:3)
这不是完整的答案。此代码演示了使用定点运算来执行快速整数除法。广泛用于DSP和信号处理。请注意,只有在&#39; scale&#39;变化很少。此外,如果缩放值较小,则可以重写代码以使用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;
}
答案 2 :(得分:1)
开始CODER_LIMIT050
是一个愚蠢的名字,由于CODER_LIMIT025
和CODER_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->scale
来优化除法,那么问问自己究竟有多少变量?如果只有几个可能的情况,那么将其模板化。一旦它是编译时常量,编译器可以尝试在可能的情况下找到位移,或者如果它有一个则在Galois Field GF(4294967296)中找到它的乘法逆。