Действительно ли передача фундаментальных значений с помощью константных ссылок вредит производительности? - PullRequest
0 голосов
/ 11 января 2019

Я пишу библиотеку, которая выполняет числовые вычисления. Я использую шаблоны, чтобы конечный пользователь мог выбрать точность, которую он хочет. Мне бы хотелось, чтобы это работало как с базовыми типами (double, float), так и с типами классов высокой точности (например, boost::multiprecision). Мне интересно, если типы аргументов должны быть T или const & T.

В SO / google много сообщений о передаче значения по ссылке. Одно из «эмпирических правил» выглядит так:

  • Передать фундаментальные типы по значению
  • Передайте все остальное по константной ссылке

Тем не менее, это становится грязным, если у вас есть шаблон:

template<typename T>
T doSomething(T x, T y)
{
   return x + y;
}

против

template<typename T>
T doSomething(const T & x, const T & y)
{
   return x + y;
}

Для boost::multiprecision вы почти наверняка захотите пройти по константной ссылке. Вопрос в том, хуже ли передать double на const &, чем по значению. Во многих SO-ответах говорится, что const & "не лучше, а может и хуже" ... но я не смог найти хороших ссылок.

Я сделал следующий тест

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

Есть возможность сделать что-то вроде:

#include <type_traits>

template<typename T>
using choose_arg_type =
    typename std::conditional<std::is_fundamental<T>::value,
                              T,
                              const T &>::type;

template <typename T>
T someFunc(choose_arg_type<T> arg)
{
    return arg + arg;
}

int main()
{
    auto result = someFunc<double>(0.0);

    return 0;
}

Но если это не приносит никакой пользы, это добавляет сложности, и вы теряете вычет типа ( Есть ли способ исправить это вычитание типа? )

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

Какой лучший способ справиться с этим?

Ответы [ 5 ]

0 голосов
/ 12 января 2019

Это сложный вопрос, который зависит от архитектуры, оптимизации компилятора и многих других особенностей, как показывают различия в ответах. Поскольку OP предназначен для написания шаблонных функций, существует также возможность управления тем, какая функция вызывается с помощью SFINAE.

#include <iostream>

template <typename T, typename = typename std::enable_if_t<std::is_fundamental_v<T>> >
void f(T t) {
    std::cout << "Pass by value\n";
}

template <typename T, typename = typename std::enable_if_t<not std::is_fundamental_v<T>> >
void f(T const &t) {
    std::cout << "Pass by const ref.\n";
}

class myclass {};

int main() {
    float x;
    int i;
    myclass c;

    std::cout << "float: ";
    f(x);

    std::cout << "int: ";
    f(i);

    std::cout << "myclass: ";
    f(c);

    return 0;
}

Выход:

float: Pass by value
int: Pass by value
myclass: Pass by const ref.
0 голосов
/ 12 января 2019

Существует как минимум одно обстоятельство, при котором передача по ссылке const может отключить оптимизацию. Тем не менее, самые популярные компиляторы предоставляют способ их повторного включения.

Давайте посмотрим на эту функцию:

int cryptographicHash( int& salt, const int& plaintext )
{
  salt = 4; // Chosen by fair dice roll
            // guaranteed to be random
  return plaintext; // If we tell them there's a salt,
                    // this is the last hash function they'll
                    // ever suspect!
}

Выглядит довольно безопасно, верно? Но, поскольку мы пишем на C ++, это так быстро, как могло бы быть? ( Определенно, что мы хотим в криптографическом хэше. )

Нет, потому что, если вы называете это с:

int x = 0xFEED;
const int y = cryptographicHash( x, x );

Теперь параметры, передаваемые по псевдониму ссылки на один и тот же объект, поэтому функция должна, как написано, возвращать 4, а не 0xFEED. Это означает, что катастрофически компилятор больше не может оптимизировать & в своем параметре const int&.

Однако самые популярные компиляторы (включая GCC , clang, Intel C ++ и Visual C ++ 2015 и более поздние ) поддерживают расширение __restrict. Итак, измените сигнатуру функции на int cryptographicHash( int& salt, const int& __restrict plaintext ) и все проблемы с ней будут решены навсегда.

Поскольку это расширение не является частью стандарта C ++, вы можете улучшить переносимость следующим образом:

#if ( __GNUC__ || __clang__ || __INTEL_COMPILER || __ICL || _MSC_VER >= 1900 )
#  define RESTRICT __restrict
#else
#  define RESTRICT /**/
#endif

int cryptographicHash( int& salt, const int& RESTRICT plaintext );

(В GCC и clang это не влияет на сгенерированный код.)

0 голосов
/ 11 января 2019

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

0 голосов
/ 12 января 2019

Если аргумент тривиально конструируем и не изменяется, передайте по значению. Соглашение о вызовах автоматически передает большие структуры по ссылке.

struct alignas(4096) page {unsigned char bytes[4096];};

[[nodiscard]] constexpr page operator^(page l, page r) noexcept {
    for (int i = 0; i < 4096; ++i)
        l.bytes[i] = l.bytes[i] ^ r.bytes[i];
    return l;
}

Аргументы, которые были изменены и / или возвращены по неконстантной ссылке, должны передаваться по неконстантной ссылке.

constexpr page& operator^=(page& l, page r) noexcept {return l = l ^ r;}

Передать любой аргумент, возвращенный с семантикой константных ссылок, по константной ссылке.

using buffer = std::vector<unsigned char>;

[[nodiscard]] std::string_view to_string_view(const buffer& b) noexcept {
    return {reinterpret_cast<const char*>(b.data()), b.size()};
}

Передать любой аргумент, глубоко скопированный в другой тип по константной ссылке.

[[nodiscard]] std::string to_string(const buffer& b) {
    return std::string{to_string_view(b)};
}

Передайте любой нетривиально конструируемый, неизмененный и не глубоко скопированный аргумент с помощью константной ссылки.

std::ostream& operator<<(std::ostream& os, const buffer& b) {
    os << std::hex;
    for (const unsigned short u8 : b)
        os << u8 << ',';
    return os << std::dec;
}

Передать любой аргумент, глубоко скопированный в значение того же типа по значению. Нет смысла передавать по ссылке аргумент, который все равно копируется, и конструктор для возвращаемой копии оптимизируется. См https://en.cppreference.com/w/cpp/language/copy_elision

[[nodiscard]] buffer operator^(buffer l, const buffer& r) {
    const auto lsize = l.size();
    const auto rsize = r.size();
    const auto minsize = std::min(lsize, rsize);

    for (buffer::size_type i = 0; i < minsize; ++i)
        l[i] = l[i] ^ r[i];
    if (lsize < rsize)
        l.insert(l.end(), r.begin() + minsize, r.end());

    return l;
}

Сюда также входят функции шаблонов.

template<typename T>
[[nodiscard]]
constexpr T clone(T t) noexcept(std::is_nothrow_constructible_v<T, T>) {
    return t;
}

В противном случае получить аргументы типа параметра шаблона путем пересылки ссылки (&&). Примечание: && имеет семантику перенаправления (универсальной) ссылки только в аргументах типа параметра шаблона и / или для auto&& или decltype(auto)&&.

template<typename T>
constexpr bool nt = noexcept(std::is_nothrow_constructible_v<int, T&&>);

template<typename T>
[[nodiscard]]
constexpr int to_int(T&& t) noexcept(nt<T>) {return static_cast<int>(t);}

const auto to_int_lambda = [](auto&& t) noexcept(to_int(t)) {return to_int(t);};
0 голосов
/ 11 января 2019

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

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

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

В конечном счете, ответом будет «профилирование соответствующих и репрезентативных вариантов использования», если вы не верите в компилятор (ы).


Редактировать (удалено решение для макроса): Как предполагает Jarod42, в C ++ будет использоваться шаблон псевдонимов. Это также позволяет избежать отсутствия вычетов, с которыми сталкивался аскер с их оригинальным подходом :

template<class T>
using CONSTREF = const T&; // Or just T for benchmarking.

https://godbolt.org/z/mopZ6B

Как сказано в cppreference :

Шаблоны псевдонимов никогда не выводятся путем вывода аргументов шаблона при выводе параметра шаблона шаблона.

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