如何在循环中添加代码使其更快?

时间:2009-03-27 02:38:45

标签: c++ optimization visual-c++ vc6

我有一个带内循环的简单函数 - 它缩放输入值,在查找表中查找输出值,并将其复制到目标。 (ftol_ambient是我从网上复制的一种技巧,用于将float快速转换为int)。

for (i = 0;  i < iCount;  ++i)
{
    iScaled = ftol_ambient(*pSource * PRECISION3);
    if (iScaled <= 0)
        *pDestination = 0;
    else if (iScaled >= PRECISION3)
        *pDestination = 255;
    else
    {
        iSRGB = FloatToSRGBTable3[iScaled];
        *pDestination = iSRGB;
    }
    pSource++;
    pDestination++;
}

现在我的查找表是有限的,并且浮点数是无限的,因此有可能出现一个错误。我用一些代码创建了一个函数副本来处理这种情况。请注意,唯一的区别是添加了2行代码 - 请忽略丑陋的指针转换。

for (i = 0;  i < iCount;  ++i)
{
    iScaled = ftol_ambient(*pSource * PRECISION3);
    if (iScaled <= 0)
        *pDestination = 0;
    else if (iScaled >= PRECISION3)
        *pDestination = 255;
    else
    {
        iSRGB = FloatToSRGBTable3[iScaled];
        if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))
            ++iSRGB;
        *pDestination = (unsigned char) iSRGB;
    }
    pSource++;
    pDestination++;
}

这是奇怪的部分。我正在测试两个版本,输入相同的100000个元素,重复100次。在我的Athlon 64 1.8 GHz(32位模式)上,第一个功能需要0.231秒,第二个(更长)功能需要0.185秒。两个函数在相同的源文件中相邻,因此不可能有不同的编译器设置。我已经多次运行测试,扭转它们运行的​​顺序,每次的时间大致相同。

我知道现代处理器中有很多谜,但这怎么可能?

此处用于比较Microsoft VC ++ 6编译器的相关汇编器输出。


; 173  :    for (i = 0;  i < iCount;  ++i)

$L4455:

; 174  :    {
; 175  :        iScaled = ftol_ambient(*pSource * PRECISION3);

    fld DWORD PTR [esi]
    fmul    DWORD PTR __real@4@400b8000000000000000
    fstp    QWORD PTR $T5011[ebp]

; 170  :    int i;
; 171  :    int iScaled;
; 172  :    unsigned int iSRGB;

    fld QWORD PTR $T5011[ebp]

; 173  :    for (i = 0;  i < iCount;  ++i)

    fistp   DWORD PTR _i$5009[ebp]

; 176  :        if (iScaled <= 0)

    mov edx, DWORD PTR _i$5009[ebp]
    test    edx, edx
    jg  SHORT $L4458

; 177  :            *pDestination = 0;

    mov BYTE PTR [ecx], 0

; 178  :        else if (iScaled >= PRECISION3)

    jmp SHORT $L4461
$L4458:
    cmp edx, 4096               ; 00001000H
    jl  SHORT $L4460

; 179  :            *pDestination = 255;

    mov BYTE PTR [ecx], 255         ; 000000ffH

; 180  :        else

    jmp SHORT $L4461
$L4460:

; 181  :        {
; 182  :            iSRGB = FloatToSRGBTable3[iScaled];
; 183  :            *pDestination = (unsigned char) iSRGB;

    mov dl, BYTE PTR _FloatToSRGBTable3[edx]
    mov BYTE PTR [ecx], dl
$L4461:

; 184  :        }
; 185  :        pSource++;

    add esi, 4

; 186  :        pDestination++;

    inc ecx
    dec edi
    jne SHORT $L4455

$L4472:

; 199  :    {
; 200  :        iScaled = ftol_ambient(*pSource * PRECISION3);

    fld DWORD PTR [esi]
    fmul    DWORD PTR __real@4@400b8000000000000000
    fstp    QWORD PTR $T4865[ebp]

; 195  :    int i;
; 196  :    int iScaled;
; 197  :    unsigned int iSRGB;

    fld QWORD PTR $T4865[ebp]

; 198  :    for (i = 0;  i < iCount;  ++i)

    fistp   DWORD PTR _i$4863[ebp]

; 201  :        if (iScaled <= 0)

    mov edx, DWORD PTR _i$4863[ebp]
    test    edx, edx
    jg  SHORT $L4475

; 202  :            *pDestination = 0;

    mov BYTE PTR [edi], 0

; 203  :        else if (iScaled >= PRECISION3)

    jmp SHORT $L4478
$L4475:
    cmp edx, 4096               ; 00001000H
    jl  SHORT $L4477

; 204  :            *pDestination = 255;

    mov BYTE PTR [edi], 255         ; 000000ffH

; 205  :        else

    jmp SHORT $L4478
$L4477:

; 206  :        {
; 207  :            iSRGB = FloatToSRGBTable3[iScaled];

    xor ecx, ecx
    mov cl, BYTE PTR _FloatToSRGBTable3[edx]

; 208  :            if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))

    mov edx, DWORD PTR _SRGBCeiling[ecx*4]
    cmp edx, DWORD PTR [esi]
    jg  SHORT $L4481

; 209  :                ++iSRGB;

    inc ecx
$L4481:

; 210  :            *pDestination = (unsigned char) iSRGB;

    mov BYTE PTR [edi], cl
$L4478:

; 211  :        }
; 212  :        pSource++;

    add esi, 4

; 213  :        pDestination++;

    inc edi
    dec eax
    jne SHORT $L4472

<小时/> 编辑:尝试测试Nils Pipenbrinck's hypothesis,我在第一个函数的循环之前和之内添加了几行:

int one = 1;
int two = 2;

        if (one == two)
            ++iSRGB;

第一个功能的运行时间现在下降到0.152秒。有趣。

<小时/> 编辑2: Nils指出比较将在发布版本中进行优化,实际上是。汇编代码中的更改非常微妙,我将在此处发布,以查看它是否提供了任何线索。在这一点上,我想知道它是否是代码对齐?

; 175  :    for (i = 0;  i < iCount;  ++i)

$L4457:

; 176  :    {
; 177  :        iScaled = ftol_ambient(*pSource * PRECISION3);

    fld DWORD PTR [edi]
    fmul    DWORD PTR __real@4@400b8000000000000000
    fstp    QWORD PTR $T5014[ebp]

; 170  :    int i;
; 171  :    int iScaled;
; 172  :    int one = 1;

    fld QWORD PTR $T5014[ebp]

; 173  :    int two = 2;

    fistp   DWORD PTR _i$5012[ebp]

; 178  :        if (iScaled <= 0)

    mov esi, DWORD PTR _i$5012[ebp]
    test    esi, esi
    jg  SHORT $L4460

; 179  :            *pDestination = 0;

    mov BYTE PTR [edx], 0

; 180  :        else if (iScaled >= PRECISION3)

    jmp SHORT $L4463
$L4460:
    cmp esi, 4096               ; 00001000H
    jl  SHORT $L4462

; 181  :            *pDestination = 255;

    mov BYTE PTR [edx], 255         ; 000000ffH

; 182  :        else

    jmp SHORT $L4463
$L4462:

; 183  :        {
; 184  :            iSRGB = FloatToSRGBTable3[iScaled];

    xor ecx, ecx
    mov cl, BYTE PTR _FloatToSRGBTable3[esi]

; 185  :            if (one == two)
; 186  :                ++iSRGB;
; 187  :            *pDestination = (unsigned char) iSRGB;

    mov BYTE PTR [edx], cl
$L4463:

; 188  :        }
; 189  :        pSource++;

    add edi, 4

; 190  :        pDestination++;

    inc edx
    dec eax
    jne SHORT $L4457

5 个答案:

答案 0 :(得分:11)

我的猜测是,在第一种情况下,两个不同的分支最终在CPU的同一分支预测槽中。如果这两个分支在每次代码减速时预测不同。

在第二个循环中,添加的代码可能足以将其中一个分支移动到不同的分支预测槽。

确保您可以尝试使用英特尔VTune分析器或AMD CodeAnalyst工具。这些工具将向您显示代码中的确切内容。

但是,请记住,进一步优化此代码很可能不值得。如果您在CPU上调整代码速度更快,则可能会在不同品牌上变慢。


修改

如果您想阅读分支预测,请尝试给Agner Fog优秀的网站:http://www.agner.org/optimize/

本pdf详细解释了分支预测时隙分配:http://www.agner.org/optimize/microarchitecture.pdf

答案 1 :(得分:4)

我的第一个猜测是在第二种情况下分支被预测得更好。可能是因为嵌套if提供了处理器使用更多信息来猜测的任何算法。出于好奇,当您删除行

时会发生什么

if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))

答案 2 :(得分:1)

你如何计算这些例程?我想知道分页或缓存是否对时间有影响?调用第一个例程可能会加载到内存中,跨越页面边界或导致堆栈进入无效页面(导致页面调入),但只有第一个例程才会付出代价。

在进行测量以减少虚拟内存和缓存可能产生的影响的调用之前,您可能希望运行两次这两个函数。

答案 3 :(得分:0)

您只是测试这个内循环,还是您正在测试未公开的外循环?如果是这样,请看这三行:

if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))  
    ++iSRGB;
*pDestination = (unsigned char) iSRGB;

现在, *pDestination 似乎是外循环的计数器。因此,通过有时对 iSRGB 值进行额外增量,您可以跳过外部循环中的一些迭代,从而减少代码需要完成的工作量。

答案 4 :(得分:0)

我曾经有过类似的情况。我从循环中提取了一些代码以使其更快,但速度变慢了。混乱。事实证明,循环的平均次数小于1。

这一课(显然你不需要)是改变不会使你的代码更快,除非你测量它实际运行得更快。