Уловка оптимизации GCC, это действительно работает? - PullRequest
8 голосов
/ 14 октября 2011

Глядя на некоторые вопросы по оптимизации, этот принял ответ на вопрос о методах кодирования для наиболее эффективного использования оптимизатора пробудил мое любопытство. Утверждение состоит в том, что локальные переменные должны использоваться для вычислений в функции, а не для вывода аргументов. Предполагалось, что это позволит компилятору выполнить дополнительные оптимизации, в противном случае это невозможно.

Итак, написание простого фрагмента кода для примера класса 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 ]

27 голосов
/ 14 октября 2011

Пример, приведенный в этом ответе, был не очень хорошим из-за вызова неизвестной функции, о которой компилятор не может много рассуждать.Вот лучший пример:

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

ДОБАВЛЕНО: Вот пример, где понимание компилятором находится в 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.) Это имеет тенденцию быть наиболее полезным для векторизованных операций, поскольку они могут быть парализованы.

2 голосов
/ 14 октября 2011

Во-первых, я предполагаю, что munge() не может быть встроенным, то есть его определение не в той же единице перевода; Вы не предоставили полный источник, поэтому я не могу быть полностью уверен, но это объяснило бы эти результаты.

Поскольку foo1 передается в munge в качестве ссылки, на уровне реализации компилятор просто передает указатель. Если мы просто передадим наш аргумент, это хорошо и быстро - любые проблемы с алиасами являются проблемой munge() - и должны быть, поскольку munge() не может ничего предположить о его аргументах, и мы не можем ничего предположить о что munge() может с ними сделать (поскольку определение munge() недоступно).

Однако, если мы копируем в локальную переменную, мы должны скопировать в локальную переменную и передать указатель на локальную переменную. Это потому, что munge() может наблюдать разницу в поведении - если он берет указатель на свой первый аргумент, он может видеть, что он не равен &foo1. Поскольку реализация munge() не находится в области видимости, компилятор не может предположить, что он этого не сделает.

Таким образом, этот трюк с копированием локальных переменных в итоге приводит к пессимизации, а не к оптимизации. Оптимизации, которые он пытается помочь, невозможны, потому что munge() не может быть встроенным; по той же причине локальная переменная активно снижает производительность.

Было бы полезно повторить попытку, убедившись, что munge() не виртуален и доступен как встроенная функция.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...