GCC优化技巧,它真的有用吗?

时间:2011-10-14 00:03:51

标签: c++ gcc optimization coding-style

在查看有关优化的一些问题时,对于最有效地使用优化程序的编码实践问题,accepted answer引起了我的好奇心。断言是局部变量应该用于函数中的计算,而不是输出参数。有人建议这将允许编译器进行额外的优化,否则是不可能的。

因此,为示例Foo类编写一小段代码并使用g ++ v4.4和-O2编译代码片段会产生一些汇编程序输出(使用-S)。汇编程序列表的部分只包含如下所示的循环部分。在检查输出时,两个循环似乎几乎相同,只有一个地址不同。该地址是第一个示例的输出参数或第二个示例的局部变量的指针。

无论是否使用局部变量,实际效果似乎都没有变化。所以问题分为3部分:

a)是GCC 进行额外的优化,即使提示提示;

b)在两种情况下都是GCC 成功优化,但不应该;

c)在两种情况下都是GCC 成功优化,并且正在生成C ++标准定义的兼容输出?

这是未经优化的功能:

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

相应的组装:

.L3:
    movl    (%esi), %eax
    addl    $1, %ebx
    addl    $4, %esi
    movl    %eax, 8(%esp)
    movl    (%edi), %eax
    movl    %eax, 4(%esp)
    movl    20(%ebp), %eax       ; Note address is that of the output argument
    movl    %eax, (%esp)
    call    _ZN3Foo5mungeES_S_
    cmpl    %ebx, 16(%ebp)
    jg      .L3

这是重写函数:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

以下是使用局部变量的函数的编译器输出:

.L3:
    movl    (%esi), %eax          ; Load foo2[i] pointer into EAX
    addl    $1, %ebx              ; increment i
    addl    $4, %esi              ; increment foo2[i] (32-bit system, 8 on 64-bit systems)
    movl    %eax, 8(%esp)         ; PUSH foo2[i] onto stack (careful! from EAX, not ESI)
    movl    (%edi), %eax          ; Load foo1 pointer into EAX
    movl    %eax, 4(%esp)         ; PUSH foo1
    leal    -28(%ebp), %eax       ; Load barTemp pointer into EAX
    movl    %eax, (%esp)          ; PUSH the this pointer for barTemp
    call    _ZN3Foo5mungeES_S_    ; munge()!
    cmpl    %ebx, 16(%ebp)        ; i < numFoo
    jg      .L3                   ; recall incrementing i by one coming into the loop
                                  ; so test if greater

2 个答案:

答案 0 :(得分:26)

该答案中给出的例子并不是一个非常好的例子,因为调用了一个未知函数,编译器无法理解。这是一个更好的例子:

void FillOneA(int *array, int length, int& startIndex)
{
    for (int i = 0; i < length; i++) array[startIndex + i] = 1;
}

void FillOneB(int *array, int length, int& startIndex)
{
    int localIndex = startIndex;
    for (int i = 0; i < length; i++) array[localIndex + i] = 1;
}

第一个版本的优化效果很差,因为它需要防止某人将其称为

的可能性
int array[10] = { 0 };
FillOneA(array, 5, array[1]);

导致{1, 1, 0, 1, 1, 1, 0, 0, 0, 0 },因为使用i=1的迭代会修改startIndex参数。

第二个不需要担心array[localIndex + i] = 1会修改localIndex的可能性,因为localIndex是一个从未采用过地址的本地变量。

汇编(Intel表示法,因为这就是我使用的):

FillOneA:
    mov     edx, [esp+8]
    xor     eax, eax
    test    edx, edx
    jle     $b
    push    esi
    mov     esi, [esp+16]
    push    edi
    mov     edi, [esp+12]
$a: mov     ecx, [esi]
    add     ecx, eax
    inc     eax
    mov     [edi+ecx*4], 1
    cmp     eax, edx
    jl      $a
    pop     edi
    pop     esi
$b: ret

FillOneB:
    mov     ecx, [esp+8]
    mov     eax, [esp+12]
    mov     edx, [eax]
    test    ecx, ecx
    jle     $a
    mov     eax, [esp+4]
    push    edi
    lea     edi, [eax+edx*4]
    mov     eax, 1
    rep stosd
    pop     edi
$a: ret

ADDED:这是一个例子,编译器的洞察力是Bar,而不是munge:

class Bar
{
public:
    float getValue() const
    {
        return valueBase * boost;
    }

private:
    float valueBase;
    float boost;
};

class Foo
{
public:
    void munge(float adjustment);
};

void Adjust10A(Foo& foo, const Bar& bar)
{
    for (int i = 0; i < 10; i++)
        foo.munge(bar.getValue());
}

void Adjust10B(Foo& foo, const Bar& bar)
{
    Bar localBar = bar;
    for (int i = 0; i < 10; i++)
        foo.munge(localBar.getValue());
}

结果代码是

Adjust10A:
    push    ecx
    push    ebx
    mov     ebx, [esp+12] ;; foo
    push    esi
    mov     esi, [esp+20] ;; bar
    push    edi
    mov     edi, 10
$a: fld     [esi+4] ;; bar.valueBase
    push    ecx
    fmul    [esi] ;; valueBase * boost
    mov     ecx, ebx
    fstp    [esp+16]
    fld     [esp+16]
    fstp    [esp]
    call    Foo::munge
    dec     edi
    jne     $a
    pop     edi
    pop     esi
    pop     ebx
    pop     ecx
    ret     0

Adjust10B:
    sub     esp, 8
    mov     ecx, [esp+16] ;; bar
    mov     eax, [ecx] ;; bar.valueBase
    mov     [esp], eax ;; localBar.valueBase
    fld     [esp] ;; localBar.valueBase
    mov     eax, [ecx+4] ;; bar.boost
    mov     [esp+4], eax ;; localBar.boost
    fmul    [esp+4] ;; localBar.getValue()
    push    esi
    push    edi
    mov     edi, [esp+20] ;; foo
    fstp    [esp+24]
    fld     [esp+24] ;; cache localBar.getValue()
    mov     esi, 10 ;; loop counter
$a: push    ecx
    mov     ecx, edi ;; foo
    fstp    [esp] ;; use cached value
    call    Foo::munge
    fld     [esp]
    dec     esi
    jne     $a ;; loop
    pop     edi
    fstp    ST(0)
    pop     esi
    add     esp, 8
    ret     0

观察Adjust10A中的内部循环必须重新计算该值,因为它必须防止foo.munge更改bar的可能性。

那就是说,这种优化方式不是扣篮。 (例如,我们可以通过手动缓存bar.getValue()localValue来获得相同的效果。)它往往对矢量化操作最有帮助,因为它们可以被平行化。

答案 1 :(得分:2)

首先,我假设munge()无法内联 - 也就是说,它的定义不在同一个翻译单元中;你没有提供完整的来源,所以我不能完全确定,但它会解释这些结果。

由于foo1作为引用传递给munge,因此在实现级别,编译器只传递一个指针。如果我们只是转发我们的论点,这很好又快 - 任何别名问题都是munge()的问题 - 必须是,因为munge()不能假设它的论点,我们可以'假设munge()可能对他们做什么(因为munge()的定义不可用)。

但是,如果我们复制到局部变量,我们必须复制到局部变量并将指针传递给局部变量。这是因为munge()可以观察到行为上的差异 - 如果指针指向其第一个参数,则可以看到它不等于&foo1。由于munge()的实现不在范围内,编译器不能假设它不会这样做。

这里的局部变量复制技巧因此最终导致悲观,而不是优化 - 它试图帮助的优化是不可能的,因为munge()不能内联;出于同样的原因,局部变量会主动损害性能。

再次尝试这一点是有益的,确保munge()是非虚拟的,并且可以作为一个无法使用的函数。