С++ Tuple vs Struct

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

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

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

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

Ответ 1

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

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, вам также понадобится создать Enums для работы с ними:

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

и стрела, вы полностью понятный код:

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

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

Ответ 2

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

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 не является целью этого обсуждения, поэтому я не буду вдаваться в детали.

Мы все знаем, что написание == или & lt; Оператор or> для каждого отдельного определения структуры будет болезненной и ошибочной задачей. Давайте заменим наш пользовательский компаратор с помощью 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), однако нам нужно написать нашу собственную функцию подкачки для всех наших структур.

Ответ 3

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

Ответ 4

Ну, вот тест, который не создает кучу кортежей внутри оператора struct ==(). Оказывается там довольно значительное влияние производительности от использования кортежа, как и следовало ожидать, учитывая, что от использования 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();
}

Ответ 5

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

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

Ответ 6

Что касается "общей функции", 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 теперь применимы к struct demo::employee.


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

Ответ 7

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

Ответ 8

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

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

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

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

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

Вообще я согласен с окончательным выводом сверху.

Ответ 9

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

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

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

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

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

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

Ответ 10

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

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

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

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

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

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

  1. (до C++ 14)

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

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

  2. (для C++ 14 и позже)

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

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

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

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