C ++ Tuple против Struct

96

Есть ли разница между использованием a std::tupleи только данных struct?

typedef std::tuple<int, double, bool> foo_t;

struct bar_t {
    int id;
    double value;
    bool dirty;
}

Из того, что я нашел в Интернете, я обнаружил, что есть два основных отличия: structболее читабельный и tupleимеет много общих функций, которые можно использовать. Должна ли быть значительная разница в производительности? Кроме того, совместимы ли макеты данных друг с другом (взаимозаменяемые)?

Алекс Коай
источник
Я просто заметил, что забыл о вопросе о приведении : реализация tupleis определяется реализацией, поэтому это зависит от вашей реализации. Лично я бы на это не рассчитывал.
Matthieu M.

Ответы:

32

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

struct StructData {
    int X;
    int Y;
    double Cost;
    std::string Label;

    bool operator==(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) == std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }

    bool operator<(const StructData &rhs) {
        return X < rhs.X || (X == rhs.X && (Y < rhs.Y || (Y == rhs.Y && (Cost < rhs.Cost || (Cost == rhs.Cost && Label < rhs.Label)))));
    }
};

using TupleData = std::tuple<int, int, double, std::string>;

Затем мы используем Celero для сравнения производительности нашей простой структуры и кортежа. Ниже приведен тестовый код и результаты производительности, собранные с использованием gcc-4.9.2 и clang-4.0.0:

std::vector<StructData> test_struct_data(const size_t N) {
    std::vector<StructData> data(N);
    std::transform(data.begin(), data.end(), data.begin(), [N](auto item) {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<> dis(0, N);
        item.X = dis(gen);
        item.Y = dis(gen);
        item.Cost = item.X * item.Y;
        item.Label = std::to_string(item.Cost);
        return item;
    });
    return data;
}

std::vector<TupleData> test_tuple_data(const std::vector<StructData> &input) {
    std::vector<TupleData> data(input.size());
    std::transform(input.cbegin(), input.cend(), data.begin(),
                   [](auto item) { return std::tie(item.X, item.Y, item.Cost, item.Label); });
    return data;
}

constexpr int NumberOfSamples = 10;
constexpr int NumberOfIterations = 5;
constexpr size_t N = 1000000;
auto const sdata = test_struct_data(N);
auto const tdata = test_tuple_data(sdata);

CELERO_MAIN

BASELINE(Sort, struct, NumberOfSamples, NumberOfIterations) {
    std::vector<StructData> data(sdata.begin(), sdata.end());
    std::sort(data.begin(), data.end());
    // print(data);

}

BENCHMARK(Sort, tuple, NumberOfSamples, NumberOfIterations) {
    std::vector<TupleData> data(tdata.begin(), tdata.end());
    std::sort(data.begin(), data.end());
    // print(data);
}

Результаты производительности собраны с помощью clang-4.0.0

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    196663.40000 |            5.08 | 
Sort            | tuple           | Null            |              10 |               5 |         0.92471 |    181857.20000 |            5.50 | 
Complete.

И результаты производительности, собранные с помощью gcc-4.9.2

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    219096.00000 |            4.56 | 
Sort            | tuple           | Null            |              10 |               5 |         0.91463 |    200391.80000 |            4.99 | 
Complete.

Из приведенных выше результатов ясно видно, что

  • Кортеж быстрее, чем структура по умолчанию

  • Производительность двоичного кода с помощью clang выше, чем у gcc. clang-vs-gcc не является целью этого обсуждения, поэтому я не буду вдаваться в подробности.

Все мы знаем, что написание оператора == или <или> для каждого отдельного определения структуры будет болезненной и ошибочной задачей. Давайте заменим наш собственный компаратор на std :: tie и повторно запустим наш тест.

bool operator<(const StructData &rhs) {
    return std::tie(X,Y,Cost, Label) < std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
}

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    200508.20000 |            4.99 | 
Sort            | tuple           | Null            |              10 |               5 |         0.90033 |    180523.80000 |            5.54 | 
Complete.

Теперь мы видим, что использование std :: tie делает наш код более элегантным и в нем труднее ошибиться, однако мы потеряем примерно 1% производительности. Я пока останусь с решением std :: tie, так как я также получаю предупреждение о сравнении чисел с плавающей запятой с настроенным компаратором.

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

struct StructData {
    int X;
    int Y;
    double Cost;
    std::string Label;

    bool operator==(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) == std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }

    void swap(StructData & other)
    {
        std::swap(X, other.X);
        std::swap(Y, other.Y);
        std::swap(Cost, other.Cost);
        std::swap(Label, other.Label);
    }  

    bool operator<(const StructData &rhs) {
        return std::tie(X,Y,Cost, Label) < std::tie(rhs.X, rhs.Y, rhs.Cost, rhs.Label);
    }
};

Результаты производительности собраны с помощью clang-4.0.0

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    176308.80000 |            5.67 | 
Sort            | tuple           | Null            |              10 |               5 |         1.02699 |    181067.60000 |            5.52 | 
Complete.

И результаты производительности, собранные с помощью gcc-4.9.2

Celero
Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
Sort            | struct          | Null            |              10 |               5 |         1.00000 |    198844.80000 |            5.03 | 
Sort            | tuple           | Null            |              10 |               5 |         1.00601 |    200039.80000 |            5.00 | 
Complete.

Теперь наша структура немного быстрее, чем кортеж (около 3% с clang и менее 1% с gcc), однако нам действительно нужно написать нашу настраиваемую функцию подкачки для всех наших структур.

Hungptit
источник
24

Если вы используете в своем коде несколько разных кортежей, вы можете сократить количество используемых функторов. Я говорю это потому, что часто использовал следующие формы функторов:

template<int N>
struct tuple_less{
    template<typename Tuple>
    bool operator()(const Tuple& aLeft, const Tuple& aRight) const{
        typedef typename boost::tuples::element<N, Tuple>::type value_type;
        BOOST_CONCEPT_REQUIRES((boost::LessThanComparable<value_type>));

        return boost::tuples::get<N>(aLeft) < boost::tuples::get<N>(aRight);
    }
};

Это может показаться излишним, но для каждого места в структуре мне пришлось бы создать совершенно новый объект-функтор, используя структуру, но для кортежа я просто меняю N. Более того, я могу сделать это для каждого кортежа, а не создавать совершенно новый функтор для каждой структуры и для каждой переменной-члена. Если у меня есть N структур с M переменными-членами, которые мне понадобятся функторы NxM (худший сценарий), которые можно сжать до одного небольшого фрагмента кода.

Естественно, если вы собираетесь пойти по пути Tuple, вам также понадобится создать Enum для работы с ними:

typedef boost::tuples::tuple<double,double,double> JackPot;
enum JackPotIndex{
    MAX_POT,
    CURRENT_POT,
    MIN_POT
};

и бум, ваш код полностью читаем:

double guessWhatThisIs = boost::tuples::get<CURRENT_POT>(someJackPotTuple);

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

пшеница
источник
8
Эээ ... В C ++ есть указатели на функции, так что это template <typename C, typename T, T C::*> struct struct_less { template <typename C> bool operator()(C const&, C const&) const; };должно быть возможно. Написание его немного менее удобно, но оно написано только один раз.
Matthieu M.
17

Кортеж имеет встроенные компараторы по умолчанию (для == и! = Он сравнивает каждый элемент, для <. <= ... сначала сравнивает, если то же самое сравнивает второй ...) компараторы: http://en.cppreference.com/w/ cpp / утилита / кортеж / оператор_cmp

edit: как указано в комментарии Оператор космического корабля C ++ 20 дает вам способ указать эту функциональность с помощью одной (уродливой, но все же всего одной) строки кода.

NoSenseEtAl
источник
1
В C ++ 20 это исправлено с помощью минимального шаблона с использованием оператора космического корабля .
Джон Макфарлейн
6

Что ж, вот тест, который не строит кучу кортежей внутри struct operator == (). Оказывается, использование кортежей оказывает довольно значительное влияние на производительность, как и следовало ожидать, учитывая, что использование POD вообще не влияет на производительность. (Преобразователь адресов находит значение в конвейере команд еще до того, как его увидит логический блок.)

Общие результаты запуска этого на моем компьютере с VS2015CE с использованием настроек Release по умолчанию:

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

Пожалуйста, обезьяна с этим, пока не будете удовлетворены

#include <iostream>
#include <string>
#include <tuple>
#include <vector>
#include <random>
#include <chrono>
#include <algorithm>

class Timer {
public:
  Timer() { reset(); }
  void reset() { start = now(); }

  double getElapsedSeconds() {
    std::chrono::duration<double> seconds = now() - start;
    return seconds.count();
  }

private:
  static std::chrono::time_point<std::chrono::high_resolution_clock> now() {
    return std::chrono::high_resolution_clock::now();
  }

  std::chrono::time_point<std::chrono::high_resolution_clock> start;

};

struct ST {
  int X;
  int Y;
  double Cost;
  std::string Label;

  bool operator==(const ST &rhs) {
    return
      (X == rhs.X) &&
      (Y == rhs.Y) &&
      (Cost == rhs.Cost) &&
      (Label == rhs.Label);
  }

  bool operator<(const ST &rhs) {
    if(X > rhs.X) { return false; }
    if(Y > rhs.Y) { return false; }
    if(Cost > rhs.Cost) { return false; }
    if(Label >= rhs.Label) { return false; }
    return true;
  }
};

using TP = std::tuple<int, int, double, std::string>;

std::pair<std::vector<ST>, std::vector<TP>> generate() {
  std::mt19937 mt(std::random_device{}());
  std::uniform_int_distribution<int> dist;

  constexpr size_t SZ = 1000000;

  std::pair<std::vector<ST>, std::vector<TP>> p;
  auto& s = p.first;
  auto& d = p.second;
  s.reserve(SZ);
  d.reserve(SZ);

  for(size_t i = 0; i < SZ; i++) {
    s.emplace_back();
    auto& sb = s.back();
    sb.X = dist(mt);
    sb.Y = dist(mt);
    sb.Cost = sb.X * sb.Y;
    sb.Label = std::to_string(sb.Cost);

    d.emplace_back(std::tie(sb.X, sb.Y, sb.Cost, sb.Label));
  }

  return p;
}

int main() {
  Timer timer;

  auto p = generate();
  auto& structs = p.first;
  auto& tuples = p.second;

  timer.reset();
  std::sort(structs.begin(), structs.end());
  double stSecs = timer.getElapsedSeconds();

  timer.reset();
  std::sort(tuples.begin(), tuples.end());
  double tpSecs = timer.getElapsedSeconds();

  std::cout << "Structs took " << stSecs << " seconds.\nTuples took " << tpSecs << " seconds.\n";

  std::cin.get();
}
Khatharr
источник
Спасибо за это. Я заметил , что когда оптимизированы -O3, tuplesменьше времени , чем structs.
Simog
3

Что ж, структура POD часто может использоваться (ab) при чтении и сериализации непрерывных блоков нижнего уровня. Как вы сказали, кортеж может быть более оптимизирован в определенных ситуациях и поддерживать больше функций.

Используйте то, что более подходит для ситуации, нет общих предпочтений. Я думаю (но не тестировал), что разница в производительности не будет значительной. Макет данных, скорее всего, несовместим и не зависит от реализации.

orlp
источник
3

Что касается "общей функции", Boost.Fusion заслуживает некоторой любви ... и особенно BOOST_FUSION_ADAPT_STRUCT .

Копирование со страницы: ABRACADBRA

namespace demo
{
    struct employee
    {
        std::string name;
        int age;
    };
}

// demo::employee is now a Fusion sequence
BOOST_FUSION_ADAPT_STRUCT(
    demo::employee
    (std::string, name)
    (int, age))

Это означает, что все алгоритмы Fusion теперь применимы к структуре demo::employee.


РЕДАКТИРОВАТЬ : Что касается разницы в производительности или совместимости макета, tupleмакет определяется реализацией, поэтому не совместим (и, следовательно, вы не должны использовать одно из представлений), и в целом я не ожидал бы никакой разницы в производительности (по крайней мере, в версии) благодаря встраивание get<N>.

Матье М.
источник
16
Я не верю, что это лучший ответ. Он даже не отвечает на вопрос. Речь идет о tuples и structs, а не о повышении!
gsamaras
@ G.Samaras: Вопрос в разнице между кортежами и struct, в частности, в изобилии алгоритмов для управления кортежами в отличие от отсутствия алгоритмов для управления структурами (начиная с итерации по его полям). Этот ответ показывает, что этот пробел можно преодолеть с помощью Boost.Fusion, что позволяет использовать structстолько же алгоритмов, сколько и для кортежей. Я добавил небольшую аннотацию к двум заданным вопросам.
Matthieu M.
3

Кроме того, совместимы ли макеты данных друг с другом (взаимозаменяемы)?

Как ни странно, я не вижу прямого ответа на эту часть вопроса.

Ответ: нет . Или, по крайней мере, ненадежно, поскольку макет кортежа не указан.

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

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

Кортеж обычно реализуется с использованием наследования одним из двух способов: в старом рекурсивном стиле Loki / Modern C ++ Design или в новом вариативном стиле. Ни один из них не является типом стандартного макета, поскольку оба нарушают следующие условия:

  1. (до C ++ 14)

    • не имеет базовых классов с нестатическими членами данных, или

    • не имеет нестатических членов данных в наиболее производном классе и не более одного базового класса с нестатическими членами данных

  2. (для C ++ 14 и новее)

    • Имеет все нестатические элементы данных и битовые поля, объявленные в одном классе (либо все в производном, либо все в некоторой базе)

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

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

Как ни странно, в прошлом он иногда работал на практике для некоторых компиляторов и комбинаций типов полей (в одном случае с использованием рекурсивных кортежей после изменения порядка полей). Сейчас это определенно не работает надежно (с компиляторами, версиями и т. Д.) И никогда не гарантировалось изначально.

Бесполезный
источник
1

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

Джерри Гроб
источник
4
На самом деле я думаю, может быть небольшая разница. A structдолжен выделить как минимум 1 байт для каждого подобъекта, в то время как я думаю, что A tupleможет уйти с оптимизацией пустых объектов. Кроме того, что касается упаковки и выравнивания, возможно, у кортежей будет больше свободы действий.
Matthieu M.
1

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

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

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

Другая проблема - безопасность типов и самодокументируемый код. Если ваша функция получает объект типа inbound_telegramили location_3Dон ясен; если он получает unsigned char *или tuple<double, double, double>нет: телеграмма может быть исходящей, а кортеж может быть переводом вместо местоположения или, возможно, минимальными показаниями температуры за длинные выходные. Да, вы можете набрать typedef, чтобы прояснить намерения, но на самом деле это не мешает вам передавать значения температуры.

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

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

Питер - Восстановить Монику
источник
1

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

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

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

скряга729
источник
1

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

Так что все должно сводиться к практичности, удобочитаемости и ремонтопригодности. И, structкак правило, лучше, потому что создает типы, которые легче читать и понимать.

Иногда std::tuple(или даже std::pair) может быть необходимо иметь дело с кодом в очень общем виде. Например, некоторые операции, связанные с пакетами переменных параметров, были бы невозможны без чего-то вроде std::tuple. std::tie- отличный пример того, когда std::tupleможно улучшить код (до C ++ 20).

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

// hard to get wrong; easy to understand
cat.arms = 0;
cat.legs = 4;

// easy to get wrong; hard to understand
std::get<0>(cat) = 0;
std::get<1>(cat) = 4;
Джон Макфарлейн
источник
0

Я знаю, что это старая тема, однако сейчас я собираюсь принять решение относительно части моего проекта: следует ли мне использовать кортеж или структуру. После прочтения этой ветки у меня появилось несколько идей.

  1. О пшенице и тесте производительности: обратите внимание, что обычно вы можете использовать memcpy, memset и подобные приемы для структур. Это сделало бы производительность НАМНОГО лучше, чем для кортежей.

  2. Я вижу в кортежах некоторые преимущества:

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

Я поискал в Интернете и в итоге добрался до этой страницы: https://arne-mertz.de/2017/03/smelly-pair-tuple/

В целом я согласен с окончательным выводом из вышесказанного.

Том К
источник
1
Это больше похоже на то, над чем вы работаете, а не на ответ на этот конкретный вопрос, или?
Дитер Мемкен
Ничто не мешает вам использовать memcpy с кортежами.
Питер - Восстановить Монику