Применяется ли оптимизация к однострочным функциям?

Я не люблю повторяться в коде, но и не хочу терять производительность из-за простых функций. Предположим, что класс имеет operator+ и функцию Add с одинаковой функциональностью (рассматривая первый как удобный способ использования класса в выражениях, а последний как «явный» способ сделать это)

struct Obj {
   Obj operator+(float);
   Obj Add(float);
   /* some other state and behaviour */
};

Obj AddDetails(Obj const& a, float b) {
   return Obj(a.float_val + b, a.some_other_stuff);
}

Obj Obj::operator+(float b) {
   return AddDetails(*this, b);
}

Obj Obj::Add(float b) {
   return AddDetails(*this, b);
}

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

Но достаточно ли умен компилятор, чтобы исключить такие двойные вызовы?

Я тестировал на простых классах (которые содержат встроенные типы и указатели) и оптимизатор просто не вычисляет что-то ненужное, но как он ведет себя в больших системах (особенно с горячими вызовами)?

Если это то, где происходит RVO, то работает ли он в больших последовательностях вызовов (3-4), чтобы сложить его в 1 вызов?

P.S. Да-да, преждевременная оптимизация - корень всех зол, но все же хочется ответа


person Alexey Larionov    schedule 27.02.2019    source источник
comment
Очевидно, зависит от компилятора, я бы предположил, что да, но вам придется протестировать его с помощью вашего компилятора.   -  person john    schedule 27.02.2019
comment
Вы можете проверить на godbolt.org   -  person drescherjm    schedule 27.02.2019
comment
Соберите с включенной оптимизацией и посмотрите на сгенерированный код сборки. Возможно, компилятор встраивает ваши вызовы.   -  person Some programmer dude    schedule 27.02.2019
comment
Если код разбит на несколько TU, это будет сложнее и потребует LTO.   -  person Jarod42    schedule 27.02.2019
comment
Определение ваших функций в вашем классе или в заголовке с ключевым словом inline повысит шансы на то, что это будет оптимизировано.   -  person 1201ProgramAlarm    schedule 27.02.2019
comment
@ 1201ProgramAlarm встраивание ненадежно. С моей точки зрения, компилятор видит возможность для какого-то RVO, но так ли это на самом деле?   -  person Alexey Larionov    schedule 27.02.2019
comment
Любой достойный компилятор встроит эти вызовы.   -  person SergeyA    schedule 27.02.2019
comment
@AlexLarionov Если у каждой единицы перевода есть определение этих функций, их можно легко оптимизировать. Если они определены только в файле .CPP, вам придется полагаться на оптимизацию времени ссылки, которая является более сложным процессом.   -  person 1201ProgramAlarm    schedule 27.02.2019
comment
Применяется ли оптимизация к однострочным функциям? - Точно так же, как они применяются к многострочным функциям. Количество строк исходного кода C++ не влияет на то, какие оптимизации компилятор будет использовать или не использовать (ну, это упрощение, но в целом верно). А если в микс добавить LTO, то наверняка (одну строчку) функции можно оптимизировать еще больше.   -  person Jesper Juhl    schedule 27.02.2019
comment
@ Jarod42 Я не думаю, что удаление копии требует LTO. Возвращаемые объекты большего размера размещаются в стеке вызывающего объекта (зависит от соглашений о вызовах, но это происходит в 64-разрядной Windows), а вызываемый объект получает адрес в качестве аргумента. Задача удаления копии состоит в том, чтобы использовать этот адрес для всех возвращаемых объектов в цепочке вызовов, что не имеет отношения к встраиванию, LTO и тому подобному.   -  person Alexey B.    schedule 27.02.2019
comment
Зачем компилятору обрабатывать 1-строчные функции особым образом? Это просто функции. И одна строка (я предполагаю, оператор), которую они содержат, может включать в себя какой-то очень сложный код, который требует гораздо больше генерации кода и оптимизации, чем простой многострочный (оператор) функция. У компилятора нет причин специализироваться на однострочном случае.   -  person Jesper Juhl    schedule 27.02.2019
comment
@SergeyA Нет, если AddDetails хотя бы не объявлен со статической областью действия. При экспорте из TU модель стоимости для размера кода уже может блокировать встраивание на основе накладных расходов на расширение параметра. Конструктор, операторы и AddDetails объявлены в одном TU, а Obj с конструктором перемещения по умолчанию потребуется для почти гарантированного встраивания.   -  person Ext3h    schedule 27.02.2019
comment
@AlexeyB.: Действительно, исключение копии особенное, но я читаю вопрос в более общем плане (как assign/operator= или операторы сравнения, где (N) RVO не применяется).   -  person Jarod42    schedule 27.02.2019


Ответы (2)


Общий

Да См. инструкции clang, созданные на https://godbolt.org/z/VB23-W линия 21

   movsd   xmm0, qword ptr [rsp]   # xmm0 = mem[0],zero
   addsd   xmm0, qword ptr [rip + .LCPI3_0]

он просто применяет код AddDetails напрямую, вместо того, чтобы даже звонить вашему оператору +. Это называется встраиванием и работает даже для этой цепочки вызовов возврата значения.

Подробности

Не только оптимизация RVO может происходить с однострочными функциями, но и любая другая оптимизация, включая встраивание, см. https://godbolt.org/z/miX3u1 и https://godbolt.org/z/tNaSW.

Посмотрите на это, вы можете видеть, что gcc и clang сильно оптимизируют даже не встроенный объявленный код ( https://godbolt.org/z/8Wf3oR )

#include <iostream>

struct Obj {
    Obj(double val) : float_val(val) {}
    Obj operator+(float b) {
        return AddDetails(*this, b);
    }
    Obj Add(float b) {
        return AddDetails(*this, b);
    }
    double val() const {
        return float_val;
    }
private:
    double float_val{0};
    static inline Obj AddDetails(Obj const& a, float b);
};

Obj Obj::AddDetails(Obj const& a, float b) {
    return Obj(a.float_val + b);
}


int main() {
    Obj foo{32};
    Obj bar{foo + 1337};
    std::cout << bar.val() << "\n";
}

Даже без встраивания никаких дополнительных вызовов C-Tor нельзя увидеть с помощью

#include <iostream>

struct Obj {
    Obj(double val) : float_val(val) {}
    Obj operator+(float);
    Obj Add(float);
    double val() const {
        return float_val;
    }
private:
    double float_val{0};
    static Obj AddDetails(Obj const& a, float b);
};

Obj Obj::AddDetails(Obj const& a, float b) {
    return Obj(a.float_val + b);
}

Obj Obj::operator+(float b) {
    return AddDetails(*this, b);
}

Obj Obj::Add(float b) {
    return AddDetails(*this, b);
}

int main() {
    Obj foo{32};
    Obj bar{foo + 1337};
    std::cout << bar.val() << "\n";
}

Однако некоторая оптимизация выполняется из-за того, что компилятор знает, что значение не изменится, поэтому давайте изменим main на

int main() {
    double d{};
    std::cin >> d;
    Obj foo{d};
    Obj bar{foo + 1337};
    std::cout << bar.val() << "\n";
}

Но тогда вы все еще можете увидеть оптимизацию на обоих компиляторах https://godbolt.org/z/M2jaSH и https://godbolt.org/z/OyQfJI

person Superlokkus    schedule 27.02.2019

Насколько я понимаю, современные компиляторы должны применять исключение копирования в ваших случаях. Согласно https://en.cppreference.com/w/cpp/language/copy_elision, когда вы пишете return Obj(a.float_val + b, a.some_other_stuff), вызов конструктора является prvalue; возврат не приведет к созданию временного объекта, поэтому перемещение или копирование не произойдет.

person Alexey B.    schedule 27.02.2019
comment
@ Ext3h Это неправда. Удаление копирования произойдет, даже если у конструктора перемещения есть побочные эффекты. - person Alexey B.; 27.02.2019