簡體   English   中英

C++ 元組與結構

[英]C++ 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有很多通用函數可以使用。 是否應該有任何顯着的性能差異? 此外,數據布局是否相互兼容(可互換)?

我們對 tuple 和 struct 進行了類似的討論,我在一位同事的幫助下編寫了一些簡單的基准測試,以確定 tuple 和 struct 在性能方面的差異。 我們首先從一個默認結構和一個元組開始。

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 解決方案,因為我還會收到有關將浮點數與自定義比較器進行比較的警告。

到目前為止,我們還沒有任何解決方案可以讓我們的結構代碼運行得更快。 讓我們看看交換 function 並重寫它,看看我們是否可以獲得任何性能:

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%),但是,我們確實需要為我們所有的結構編寫自定義交換 function。

如果你在你的代碼中使用了幾個不同的元組,你可以通過壓縮你正在使用的函子的數量來逃避。 我這樣說是因為我經常使用以下 forms 的仿函數:

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

這可能看起來有點矯枉過正,但對於結構中的每個地方,我都必須使用結構創建一個全新的仿函數 object 但對於元組,我只需更改N 比這更好的是,我可以為每個元組執行此操作,而不是為每個結構和每個成員變量創建一個全新的仿函數。 如果我有 N 個帶有 M 個成員變量的結構,我需要創建 NxM 仿函數(更糟糕的情況),可以將其壓縮為一點點代碼。

當然,如果您要使用 Tuple 方式訪問 go,您還需要創建 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);

因為當您想要獲取其中包含的項目時,它會描述自己。

元組已內置默認值(對於 == 和,= 它比較每個元素。對於 <.<=..,首先比較。如果相同比較第二個..:) 比較器: http://en.cppreference.com/w/ cpp/實用程序/元組/operator_cmp

編輯:如評論中所述,C++20 宇宙飛船運算符為您提供了一種使用一行(丑陋,但仍然只有一行)代碼來指定此功能的方法。

好吧,這是一個基准,它不會在 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();
}

此外,數據布局是否相互兼容(可互換)?

奇怪的是,我看不到對這部分問題的直接回應。

答案是:沒有 或者至少不可靠,因為元組的布局是未指定的。

首先,您的結構是 標准布局類型 成員的排序、填充和 alignment 由標准和您的平台 ABI 的組合明確定義。

如果元組是標准布局類型,並且我們知道字段是按照指定類型的順序排列的,那么我們可能有信心它會匹配結構。

元組通常使用 inheritance 以兩種方式之一實現:舊的 Loki/現代 C++ 設計遞歸樣式或更新的可變參數樣式。 兩者都不是標准布局類型,因為兩者都違反以下條件:

  1. (在 C++14 之前)

    • 沒有具有非靜態數據成員的基類,或

    • 在最派生的 class 中沒有非靜態數據成員,並且最多有一個具有非靜態數據成員的基本 class

  2. (對於 C++14 及更高版本)

    • 具有在同一 class 中聲明的所有非靜態數據成員和位字段(全部在派生中或全部在某個基中)

因為每個葉基 class 包含單個元組元素(注意。單個元素元組可能一種標准布局類型,盡管不是很有用)。 因此,我們知道標准不保證元組具有與結構相同的填充或 alignment。

此外,值得注意的是,較舊的遞歸式元組通常會以相反的順序排列數據成員。

有趣的是,過去它有時在某些編譯器和字段類型組合的實踐中起作用(在一種情況下,在反轉字段順序后使用遞歸元組)。 它現在肯定不能可靠地工作(跨編譯器、版本等),並且從一開始就沒有得到保證。

好吧,POD 結構通常可以(ab)用於低級連續塊讀取和序列化。 正如您所說,元組在某些情況下可能會更優化並支持更多功能。

使用更適合情況的任何東西,沒有普遍的偏好。 我認為(但我沒有對其進行基准測試)性能差異不會很大。 數據布局很可能不兼容並且特定於實現。

就“通用函數”go 而言,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的布局是實現定義的,因此不兼容(因此你不應該在兩種表示之間進行轉換),一般來說,我希望性能方面沒有差異(至少在發布中),這要歸功於get<N>的內聯。

從其他答案來看,性能考慮充其量是最小的。

所以它真的應該歸結為實用性、可讀性和可維護性。 struct通常更好,因為它創建的類型更易於閱讀和理解。

有時,可能需要std::tuple (甚至std::pair )以高度通用的方式處理代碼。 例如,如果沒有std::tuple之類的東西,一些與可變參數包相關的操作將是不可能的。 std::tiestd::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;

我的經驗是,隨着時間的推移,功能開始逐漸出現在曾經是純數據持有者的類型(如 POD 結構)上。 諸如某些不需要了解數據內部知識的修改、維護不變量等事情。

這是一件好事; 它是 object 定位的基礎。 這就是發明帶類的 C 的原因。 使用純數據 collections 之類的元組不對這種邏輯擴展開放; 結構是。 這就是為什么我幾乎總是選擇結構。

相關的是,像所有“開放數據對象”一樣,元組違反了信息隱藏范式。 如果不丟棄元組批發,您以后無法更改它。 使用結構,您可以逐漸轉向訪問功能。

另一個問題是類型安全和自記錄代碼。 如果您的 function 收到inbound_telegramlocation_3D類型的 object ,則很清楚; 如果它接收到unsigned char *tuple<double, double, double>則不是:電報可能是出站的,並且元組可能是翻譯而不是位置,或者可能是長周末的最低溫度讀數。 是的,您可以使用 typedef 來明確意圖,但這實際上並不能阻止您通過溫度。

這些問題往往在超過一定規模的項目中變得重要; 元組的缺點和精細類的優點變得不可見,並且在小型項目中確實是一種開銷。 即使對於不顯眼的小數據聚合,也可以從適當的類開始支付后期紅利。

當然,一種可行的策略是使用純數據持有者作為 class 包裝器的底層數據提供者,該包裝器提供對該數據的操作。

不要擔心速度或布局,這是納米優化,並且取決於編譯器,並且永遠不會有足夠的差異來影響您的決定。

您將結構用於有意義地屬於一起以形成整體的事物。

您將元組用於巧合在一起的事物。 您可以在代碼中自發使用元組。

不應該存在性能差異(即使是微不足道的差異)。 至少在正常情況下,它們會產生相同的 memory 布局。 盡管如此,它們之間的轉換可能不需要工作(盡管我猜它通常會有相當大的機會)。

我知道這是一個古老的主題,但是我現在要對我的項目的一部分做出決定:我應該 go 元組方式還是結構方式。 讀完這篇文章后,我有了一些想法。

  1. 關於小麥和性能測試:請注意,您通常可以對結構使用 memcpy、memset 和類似的技巧。 這將使性能比元組好得多。

  2. 我在元組中看到了一些優點:

    • 您可以使用元組從 function 或方法返回一組變量,並減少使用的類型數量。
    • 基於元組已預定義的 <,==,> 運算符這一事實,您還可以將元組用作 map 或 hash_map 中的鍵,這比您需要實現這些運算符的結構更具成本效益。

我搜索了 web 並最終到達此頁面: https://arne-mertz.de/2017/03/smelly-pair-tuple/

一般來說,我同意上面的最終結論。

沒有兼容C memory布局等的包袱,更有利於優化。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM