C ++タプルと構造体


95

std::tupleとデータのみの使用に違いはありstructますか?

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

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

私がオンラインで見つけたものから、2つの大きな違いがあることがわかりました。1つstructは読みやすく、もう1つはtuple使用できる多くの汎用関数を持っていることです。パフォーマンスに大きな違いはありますか?また、データレイアウトは相互に互換性がありますか(交換可能にキャストされます)?


キャストの質問を忘れていたと言いました。の実装はtuple定義された実装であるため、実装によって異なります。個人的には、私はそれを当てにしません
Matthieu M.

回答:


31

タプルと構造体についても同様の議論があり、同僚の1人の助けを借りていくつかの簡単なベンチマークを作成し、タプルと構造体のパフォーマンスの違いを特定します。まず、デフォルトの構造体とタプルから始めます。

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.

現在、構造体はタプルよりもわずかに高速です(clangで約3%、gccで1%未満)が、すべての構造体に対してカスタマイズされたスワップ関数を作成する必要があります。


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です。それよりも、各構造体と各メンバー変数に対してまったく新しいファンクターを作成するのではなく、すべてのタプルに対してこれを行うことができます。NxMファンクターであるM個のメンバー変数を持つN個の構造体がある場合、1つの小さなコードに凝縮できる(最悪のシナリオ)を作成する必要があります。

当然、タプルの方法を使用する場合は、それらを操作するための列挙型も作成する必要があります。

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

タプルにはデフォルトが組み込まれています(==および!=の場合、すべての要素を比較します。<。<= ...が最初に比較され、同じ場合は2番目に比較されます...)コンパレータ:http//en.cppreference.com/w/ cpp / Utility / tuple / operator_cmp

編集:コメントに記載されているように、C ++ 20宇宙船演算子は、この機能を1行のコード(醜いですが、それでも1行だけ)で指定する方法を提供します。


1
C ++ 20では、これは宇宙船オペレーターを使用して最小限の定型文で修正されました。
ジョンマクファーレーン

6

さて、これはstruct operator ==()内にタプルの束を構築しないベンチマークです。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();
}

これをありがとう。で最適化すると-O3tuplesよりも時間がかからないことに気づきましたstructs
Simog

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 2014

@ G.Samaras:質問は、タプルとの違いについてですstruct。特に、構造体を操作するアルゴリズムがないのに対して、タプルを操作するアルゴリズムが豊富にあります(フィールドを反復処理することから始めます)。この回答は、Boost.Fusionを使用してこのギャップを埋めることができstruct、タプルにあるのと同じ数のアルゴリズムを実現できることを示しています。尋ねられた正確な2つの質問に小さな宣伝文を追加しました。
Matthieu M.

3

また、データレイアウトは相互に互換性がありますか(交換可能にキャストされます)?

奇妙なことに、質問のこの部分に対する直接の回答を見ることができません。

答えは:いいえ。または、タプルのレイアウトが指定されていないため、少なくとも確実ではありません。

まず、構造体は標準レイアウトタイプです。メンバーの順序、パディング、および配置は、標準とプラットフォームABIの組み合わせによって明確に定義されています。

タプルが標準のレイアウトタイプであり、フィールドがタイプが指定された順序でレイアウトされていることがわかっている場合、構造体と一致するという確信があるかもしれません。

タプルは通常、継承を使用して、古いLoki / Modern C ++ Design再帰スタイルまたは新しい可変個引数スタイルのいずれかの方法で実装されます。どちらも次の条件に違反するため、どちらも標準レイアウトタイプではありません。

  1. (C ++ 14より前)

    • 非静的データメンバーを持つ基本クラスがない、または

    • 最も派生したクラスに非静的データメンバーがなく、非静的データメンバーを持つ基本クラスが最大で1つあります

  2. (C ++ 14以降の場合)

    • すべての非静的データメンバーとビットフィールドが同じクラスで宣言されています(すべてが派生クラスにあるか、すべてが一部のベースにあります)

各リーフ基本クラスには単一のタプル要素が含まれているため(注:単一要素のタプルはおそらく は、あまり有用ではありません、標準のレイアウトタイプです)。したがって、標準では、タプルが構造体と同じパディングまたは配置を持つことを保証していません

さらに、古い再帰スタイルのタプルは通常、データメンバーを逆の順序でレイアウトすることに注意してください。

逸話的に、これは過去に一部のコンパイラーおよびフィールド・タイプの組み合わせで実際に機能することがありました(1つのケースでは、フィールドの順序を逆にした後、再帰的なタプルを使用します)。現在は(コンパイラ、バージョンなどで)確実に機能するわけではなく、そもそも保証されていません。


1

パフォーマンスの違いがあってはなりません(わずかな違いでも)。少なくとも通常の場合、それらは同じメモリレイアウトになります。それにもかかわらず、それらの間のキャストはおそらく機能する必要はありません(通常はかなりの可能性があると思いますが)。


4
実は少し違いがあるのではないかと思います。Aはstruct私がと思いながら、各サブオブジェクトのために、少なくとも1バイトを割り当てる必要がありtuple、空のオブジェクトを最適化して逃げることができます。また、パッキングと位置合わせに関しては、タプルに余裕がある可能性があります。
Matthieu M.

1

私の経験では、時間の経過とともに、純粋なデータホルダーであったタイプ(POD構造体など)に機能が忍び寄り始めます。データの内部知識を必要としない特定の変更、不変条件の維持など。

それは良いことです。それはオブジェクト指向の基礎です。これが、クラス付きのCが発明された理由です。タプルのような純粋なデータコレクションを使用することは、そのような論理的な拡張に対して開かれていません。構造体はです。そのため、ほとんどの場合、構造体を選択します。

関連するのは、すべての「オープンデータオブジェクト」と同様に、タプルは情報隠蔽パラダイムに違反しているということです。タプルの卸売りを捨てずに後で変更することはできません。構造体を使用すると、アクセス機能に向かって徐々に移動できます。

もう1つの問題は、型安全性と自己文書化コードです。関数がタイプのオブジェクトを受け取るinbound_telegramか、location_3Dそれが明確である場合。unsigned char *またはを受け取った場合tuple<double, double, double>それがない:電文が送信することができ、タプルは、おそらく翻訳の代わりに、位置、または長い週末から最低温度の読みであってもよいです。はい、typedefを使用して意図を明確にすることができますが、実際には温度の通過を妨げることはありません。

これらの問題は、特定のサイズを超えるプロジェクトで重要になる傾向があります。タプルの欠点と手の込んだクラスの利点は見えなくなり、実際、小さなプロジェクトではオーバーヘッドになります。目立たない小さなデータ集計であっても、適切なクラスから始めると、遅れて配当が支払われます。

もちろん、実行可能な戦略の1つは、純粋なデータホルダーを、そのデータに対する操作を提供するクラスラッパーの基になるデータプロバイダーとして使用することです。


1

速度やレイアウトについて心配する必要はありません。これはナノ最適化であり、コンパイラーに依存します。決定に影響を与えるほどの違いはありません。

意味のあるものが一緒になって全体を形成するために構造体を使用します。

偶然に一緒になっているものにはタプルを使用します。コードでタプルを自発的に使用できます。


1

他の回答から判断すると、パフォーマンスに関する考慮事項はせいぜい最小限です。

したがって、それは実際には実用性、読みやすさ、および保守性に帰着する必要があります。またstruct、読みやすく理解しやすい型が作成されるため、一般的に優れています。

場合によっては、非常に一般的な方法でコードを処理するためにstd::tuple(またはstd::pair)が必要になることがあります。たとえば、可変個引数パラメータパックに関連する一部の操作は、のようなものがないと不可能std::tupleです。std::tieいつstd::tupleコードを改善できるか(C ++ 20より前)の良い例です。

ただし、を使用できる場所では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
これは、あなたが取り組んでいることのように聞こえますが、その特定の質問に対する答えではありませんか?
ディーターミームケン2018

タプルでmemcpyを使用することを妨げるものは何もありません。
ピーター–モニカを復活させる
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.