Почему GCC не удается оптимизировать, если возвращаемое значение не имеет имени? - PullRequest
8 голосов
/ 13 июня 2019

Рассмотрим этот код:

#include <array>

class C
{
    std::array<char, 7> a{};
    int b{};
};

C slow()
{
    return {};
}

C fast()
{
    C c;
    return c;
}

GCC с 6 по 9 создает очень раздутый код для slow():

slow():
        xor     eax, eax
        mov     DWORD PTR [rsp-25], 0
        mov     BYTE PTR [rsp-21], 0
        mov     edx, DWORD PTR [rsp-24]
        mov     DWORD PTR [rsp-32], 0
        mov     WORD PTR [rsp-28], ax
        mov     BYTE PTR [rsp-26], 0
        mov     rax, QWORD PTR [rsp-32]
        ret
fast():
        xor     eax, eax
        xor     edx, edx
        ret

Есть ли разница в значении между этими двумя функциями? Clang испускает код, подобный fast() для обоих, в то время как GCC 4-5 справляется лучше, чем 6-9, но тоже не совсем оптимален.

Флаги сборки: -std=c++11 -O3

Демо: https://godbolt.org/z/rPNG9o


Представлено как ошибка GCC на основе обратной связи здесь: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90883

Ответы [ 3 ]

1 голос
/ 11 июля 2019

Сопровождающие GCC согласились, что это ошибка (пропущенная оптимизация), и она исправлена ​​в транке для x86_64 (ARM может быть исправлено позже): https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90883

0 голосов
/ 13 июня 2019

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

Для slow:

  • Результат обращения к медленному pr-значению инициализируется копией с {} в качестве инициализатора ( stmt.return )
  • , поэтому результирующий объект инициализируется списком ( [dcl.init] /17.1);
  • , что приводит нас к агрегатной инициализации ( [dcl.init.list] /3.4)

=> поэтому все члены объекта результата вызова slow инициализируются инициализатором их члена по умолчанию dcl.init.aggr] /5.4.

Для fast:

=> поэтому все члены объекта результата вызова slow инициализируются инициализатором их члена по умолчанию [class.base.init] /9.1

Результирующие сборки двух функций функционально эквивалентны . Таким образом, сборка, произведенная Gcc, соответствует стандарту.

В случае медленного, сборка просто неоптимальная. Объект возвращается соответственно в SystemV x86 abi по двум регистрам: rax и rdx (edx). Сначала он обнуляет объектный класс C в стеке по адресам [rsp-32]. Он обнуляет a байт заполнения между a и b и b. Затем он копирует эту инициализированную часть стека в регистры. Способ обнуления стека является просто неоптимальным, и все эти операции эквивалентны операциям 2 xor сборки fast. Так что это просто очевидная ошибка.

0 голосов
/ 13 июня 2019

Это не совсем полный ответ, но он может дать подсказку.Как я и подозревал, в значениях fast и slow есть небольшая разница, которая, вероятно, отправляет компилятор по разным путям.Это можно увидеть, если вы сделаете конструктор копирования закрытым.

https://godbolt.org/z/FMIRe3

#include <array>

class C
{
    std::array<char, 7> a{};

    public:
    C(){}

    private:
    C(const C & c){}
};

// Compiles
C slow()
{
    return {};
}

// Does not compile
C fast()
{
    C c;
    return c;
}

Даже с копией ellision fast все еще требует, чтобы конструктор копирования находился там, где slowвозвращает initialization list, который явно создает возвращаемое значение вызывающей стороной.Они могут или не могут в конечном итоге делать то же самое, но я считаю, что компилятор должен сделать некоторые хитрости, чтобы определить, так ли это.

Существует подробное сообщение в блоге, которое дает некоторую интересную информацию по этой теме

https://akrzemi1.wordpress.com/2018/05/16/rvalues-redefined/

Однако поведение изменилось в C ++ 17

, тогда как

#include <array>

class C
{
    std::array<char, 7> a{};

    public:
    C(){}

    private:
    C(const C & c){}
};

C slow()
{
    return {};
}

C fast()
{
    return C();
}

fast не скомпилируетсяв C ++ 11 теперь он компилируется в C ++ 17

https://godbolt.org/z/JG2PkD

Причина в том, что значение return C() меняется от возврата временного к явному конструированию объекта вкадр звонящего.

Так что теперь в C ++ 17 есть большая разница между

C fast(){
    C c;
    return c;
}

и

C fast(){
    return C();
}

, потому что во втором вы надеваетедаже не нужен конструктор копирования или перемещения, чтобы быть доступным.

https://godbolt.org/z/i2eZnf

Определенно не C ++ 101

...