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

GCC optimization trick, does it really work?

本文关键字:真的 有用 优化 GCC      更新时间:2023-10-16

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

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

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

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

在那个答案中给出的例子不是一个很好的例子,因为调用了一个编译器无法推断的未知函数。下面是一个更好的例子:

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表示法,因为我用的是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

添加:这里是一个编译器的洞察力是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来获得相同的效果。)它往往对向量化操作最有帮助,因为这些操作可以并行化。

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

由于foo1作为引用传递给munge,在实现层,编译器只传递一个指针。如果我们只是转发我们的参数,这是很好的和快速的-任何混叠问题都是munge()的问题-而且必须是,因为munge()不能假设任何关于它的参数,我们也不能假设munge()可能对它们做什么(因为munge()的定义不可用)。

但是,如果复制到局部变量,必须复制到局部变量,并传递指向该局部变量的指针。这是因为munge()可以观察到行为上的差异——如果它接受指向其第一个参数的指针,它可以看到它不等于&foo1。由于munge()的实现不在作用域中,编译器不能假设它不会这样做。

这种局部变量复制技巧最终会导致悲观,而不是优化——它试图帮助的优化是不可能的,因为munge()不能内联;出于同样的原因,局部变量会对性能产生不利影响。

再试一次,确保munge()不是虚拟的,并且作为一个可内联的函数可用,这将是有指导意义的。