Корпус C ++ проти структури


95

Чи є якась різниця між використанням a std::tupleта лише даних struct?

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

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

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


Я тільки що зауважив, що забув про питання акторів : tupleреалізація визначена реалізацією, тому це залежить від вашої реалізації. Особисто я б на це не розраховував.
Matthieu M.

Відповіді:


31

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

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), однак нам потрібно написати нашу спеціальну функцію підкачки для всіх наших структур.


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-функтори, мені потрібно було б створити (гірший сценарій), які можна звести до однієї маленької частини коду.

Звичайно, якщо ви збираєтеся піти шляхом Кортежу, вам також потрібно буде створити 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);

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


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 / utility / кортеж / operator_cmp

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


1
У C ++ 20 це виправлено за допомогою мінімального шаблону за допомогою оператора космічного корабля .
Джон Макфарлейн,

6

Ну, ось орієнтир, який не створює купу кортежів всередині оператора struct == (). Виявляється, існує досить значний вплив на продуктивність від використання кортежу, як і слід було очікувати, враховуючи те, що від використання POD-систем взагалі не впливає на продуктивність. (Розпорядник адрес знаходить значення в конвеєрі інструкцій ще до того, як логічний блок його коли-небудь побачить.)

Поширені результати від запуску цього на моїй машині з VS2015CE з використанням налаштувань "Випуск" за замовчуванням:

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();
}

Дякую за це. Я помітив , що коли оптимізовані -O3, tuplesменше часу , ніж structs.
Сімог

3

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

Використовуйте все, що більше підходить для ситуації, загальних переваг немає. Я думаю (але я не оцінював це), що різниця в продуктивності не буде суттєвою. Макет даних, швидше за все, несумісний і конкретно реалізований.


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, приводячи до structs стільки алгоритмів, скільки існує на кортежах. Я додав невеличку роздумку щодо двох заданих питань.
Matthieu M.

3

Крім того, чи сумісний формат даних (взаємозамінний)?

Як не дивно, я не бачу прямої відповіді на цю частину запитання.

Відповідь така: ні . Або, принаймні, не надійно, оскільки компонування кортежу не визначено.

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

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

Зазвичай кортеж реалізується з використанням успадкування одним із двох способів: старий рекурсивний стиль Loki / Modern C ++ Design або новий варіадичний стиль. Це не стандартний тип макета, оскільки обидва вони порушують такі умови:

  1. (до C ++ 14)

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

    • не має нестатичних членів даних у найбільш похідному класі та щонайбільше одного базового класу з нестатичними членами даних

  2. (для C ++ 14 та пізніших версій)

    • Має всі нестатичні члени даних та бітові поля, оголошені в одному класі (або всі в похідному, або всі в якійсь базі)

оскільки кожен базовий клас листочка містить один елемент кортежу (Примітка. одноелементний кортеж, ймовірно, є стандартним типом макета, хоча і не дуже корисним). Отже, ми знаємо, що стандарт не гарантує, що кортеж має таке ж заповнення або вирівнювання, як структура.

Крім того, варто зазначити, що старший кортеж у рекурсивному стилі, як правило, розміщує елементи даних у зворотному порядку.

Анекдотично, що це іноді працювало на практиці для деяких компіляторів та комбінацій типів полів у минулому (в одному випадку, використовуючи рекурсивні кортежі, після зміни порядку полів). Зараз це точно не працює надійно (у компіляторах, версіях тощо) і ніколи не було гарантовано спочатку.


1

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


4
Насправді я думаю, що може бути невелика різниця. structПовинні виділити по крайней мере , 1 байт для кожного подоб'екти в той час як я думаю , що tupleможе піти з оптимізацією з порожніх об'єктів. Крім того, що стосується упаковки та вирівнювання, можливо, кортежі мають більше свободи.
Матьє М.,

1

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

Це добре; це основа орієнтації на об'єкти. Це причина, чому було винайдено С з класами. Використання чистих колекцій даних, таких як кортежі, не є відкритим для такого логічного розширення; структури є. Ось чому я майже завжди зупинявся на структурах.

Це пов’язано з тим, що, як і всі “об’єкти відкритих даних”, кортежі порушують парадигму приховування інформації. Ви не можете змінити це пізніше, не викинувши кортеж оптом. За допомогою структури ви можете поступово переходити до функцій доступу.

Інше питання - безпека типу та код самодокументування. Якщо ваша функція отримує об'єкт типу inbound_telegramабо location_3Dце зрозуміло; якщо він отримує unsigned char *абоtuple<double, double, double> ні, телеграма може бути вихідною, і кортеж може бути перекладом замість місця, або, можливо, показниками мінімальної температури з довгих вихідних. Так, ви можете ввести def, щоб пояснити наміри, але це насправді не заважає вам проходити температуру.

Ці проблеми, як правило, стають важливими в проектах, які перевищують певний розмір; недоліки кортежів та переваги складних класів стають невидимими і справді є накладними витратами в невеликих проектах. Починаючи з належних класів, навіть для малопомітних сукупних даних, дивіденди приносять пізно.

Звичайно, однією життєздатною стратегією було б використання чистого власника даних як основного постачальника даних для обгортки класів, що забезпечує операції з цими даними.


1

Не хвилюйтеся про швидкість або макет, це нано-оптимізація, і це залежить від компілятора, і ніколи не буває достатньої різниці, щоб вплинути на ваше рішення.

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

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


1

Судячи з інших відповідей, міркування щодо продуктивності в кращому випадку мінімальні.

Тож це справді має зводитися до практичності, читабельності та ремонтопридатності. І, structяк правило, кращий, оскільки створює типи, які легше читати і розуміти.

Іноді std::tuple(або навіть std::pair) може знадобитися для роботи з кодом вкрай загальним способом. Наприклад, деякі операції, пов’язані з варіатичними пакетами параметрів, були б неможливі без чогось подібного std::tuple. std::tieє чудовим прикладом того, коли std::tupleможна вдосконалити код (до C ++ 20).

Але скрізь, де ви можете використовувати a struct, ви, мабуть, повинні використовувати a 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. Я бачу деякі переваги в кортежах:

    • Ви можете використовувати кортежі для повернення колекції змінних з функції або методу та зменшення кількості використовуваних типів.
    • Виходячи з того, що кортеж має заздалегідь визначені оператори <, ==,>, ви також можете використовувати кортеж як ключ у map або hash_map, що набагато вигідніше, ніж структура, де вам потрібно реалізувати ці оператори.

Я шукав в Інтернеті і врешті-решт перейшов на цю сторінку: https://arne-mertz.de/2017/03/smelly-pair-tuple/

Як правило, я згоден з остаточним висновком зверху.


1
Це більше схоже на те, над чим ви працюєте, а не на відповідь на це конкретне запитання, чи?
Дітер Меемкен,

Ніщо не заважає вам використовувати memcpy з кортежами.
Пітер - Відновити Моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.