C++ 元组与结构

Posted

技术标签:

【中文标题】C++ 元组与结构【英文标题】:C++ Tuple vs Struct 【发布时间】:2011-08-16 16:08:59 【问题描述】:

使用std::tuple 和纯数据struct 之间有什么区别吗?

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

struct bar_t 
    int id;
    double value;
    bool dirty;

从网上查到的,我发现有两个主要区别:struct可读性更强,而tuple有很多通用函数可以使用。 是否应该有任何显着的性能差异? 另外,数据布局是否相互兼容(可互换)?

【问题讨论】:

我只是说我忘记了 cast 问题:tuple 的实现是实现定义的,因此它取决于您的实现。就个人而言,我不会指望它。 【参考方案1】:

我们对 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 解决方案,因为我还会收到有关将浮点数与自定义比较器进行比较的警告。

到目前为止,我们还没有任何解决方案可以让我们的结构代码运行得更快。让我们看一下swap函数并重写它,看看我们是否可以获得任何性能:

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%),但是,我们确实需要为所有结构体编写自定义交换函数。

【讨论】:

如果为结构添加元素交换会是一个更好的测试,因为 std::sort 中使用的 swap(tuple, tuple) 是使用元素交换实现的【参考方案2】:

如果您在代码中使用了多个不同的元组,则可以通过压缩所使用的仿函数的数量来摆脱困境。我这样说是因为我经常使用以下形式的函子:

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 仿函数(更糟糕的情况),可以将其压缩为一点点代码。

当然,如果您要使用元组方式,您还需要创建 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);

因为当您想要获取其中包含的项目时,它会描述自己。

【讨论】:

呃... C++ 有函数指针,所以template &lt;typename C, typename T, T C::*&gt; struct struct_less template &lt;typename C&gt; bool operator()(C const&amp;, C const&amp;) const; ; 应该是可能的。拼写不太方便,但只写一次。【参考方案3】:

Tuple 内置了默认(对于 == 和 != 它比较每个元素,对于 <. href="http://en.cppreference.com/w/cpp/utility/tuple/operator_cmp" rel="nofollow" target="_blank">http://en.cppreference.com/w/cpp/utility/tuple/operator_cmp

编辑:如评论中所述,C++20 宇宙飞船运算符为您提供了一种使用一行(丑陋,但仍然只有一行)代码来指定此功能的方法。

【讨论】:

在 C++20 中,这通过使用 the spaceship operator 的最小样板进行了补救。【参考方案4】:

好吧,这里有一个基准,它不会在 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();

【讨论】:

谢谢。我注意到当使用-O3 优化时,tuplesstructs 花费的时间更少。 是的,我添加了编辑。元组的速度几乎快了 7 倍。 godbolt.org/z/h3eaEPv8q【参考方案5】:

嗯,POD 结构通常可以(ab)用于低级连续块读取和序列化。正如您所说,元组在某些情况下可能会更优化并支持更多功能。

使用任何更适合情况的方法,没有普遍的偏好。我认为(但我没有对其进行基准测试)性能差异不会很大。数据布局很可能不兼容且不特定于实现。

【讨论】:

【参考方案6】:

另外,数据布局是否相互兼容(可互换)?

奇怪的是,我看不到对这部分问题的直接回应。

答案是:。 或者至少不可靠,因为元组的布局是未指定的。

首先,您的结构是Standard Layout Type。成员的排序、填充和对齐方式由标准和您的平台 ABI 的组合很好地定义。

如果元组是标准布局类型,并且我们知道字段按照指定类型的顺序进行布局,我们可能有信心它会匹配结构。

元组通常使用继承实现,采用以下两种方式之一:旧的 Loki/现代 C++ 设计递归风格,或新的可变参数风格。两者都不是标准布局类型,因为两者都违反了以下条件:

    (C++14 之前)

    没有具有非静态数据成员的基类,或者

    在派生最多的类中没有非静态数据成员,最多有一个具有非静态数据成员的基类

    (适用于 C++14 及更高版本)

    在同一个类中声明了所有非静态数据成员和位域(全部在派生类中或全部在某个基类中)

因为每个叶基类都包含一个元组元素(注意。单元素元组可能一种标准布局类型,尽管不是很有用)。因此,我们知道标准不保证元组具有与结构相同的填充或对齐方式。

此外,值得注意的是,较旧的递归样式元组通常会以相反的顺序排列数据成员。

有趣的是,过去它有时在某些编译器和字段类型组合的实践中有效(在一种情况下,在反转字段顺序后使用递归元组)。它现在肯定不能可靠地工作(跨编译器、版本等),并且从一开始就没有得到保证。

【讨论】:

【参考方案7】:

就“通用函数”而言,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&lt;N&gt; 的内联,性能方面的差异(至少在发布中)。

【讨论】:

我不相信这是票数最高的答案。它甚至不回答这个问题。问题是关于tuples 和structs,而不是提升! @G.Samaras:问题是关于元组和struct 之间的区别,尤其是处理元组的算法丰富,而没有处理结构的算法(从迭代其字段开始) )。这个答案表明,这个差距可以通过使用 Boost.Fusion 来弥补,为structs 带来与元组一样多的算法。我对所问的两个问题添加了一个小简介。【参考方案8】:

不要担心速度或布局,这是纳米优化,并且取决于编译器,并且永远不会有足够的差异来影响您的决定。

您将结构用于有意义地属于一起形成一个整体的事物。

您将元组用于巧合在一起的事物。您可以在代码中自发使用元组。

【讨论】:

【参考方案9】:

从其他答案来看,性能方面的考虑充其量是最少的。

所以它真的应该归结为实用性、可读性和可维护性。而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;

【讨论】:

【参考方案10】:

我的经验是,随着时间的推移,功能开始逐渐出现在曾经是纯数据持有者的类型(如 POD 结构)上。诸如某些不需要了解数据内部知识的修改、维护不变量等。

这是一件好事;它是面向对象的基础。这就是发明带有类的 C 的原因。使用像元组这样的纯数据集合对这种逻辑扩展是不开放的;结构是。这就是为什么我几乎总是选择结构。

与所有“开放数据对象”一样,元组违反了信息隐藏范式。您不能稍后更改它而不丢弃整个元组。使用结构体,您可以逐步转向访问函数。

另一个问题是类型安全和自记录代码。如果您的函数接收到inbound_telegramlocation_3D 类型的对象,那很清楚;如果它收到unsigned char *tuple&lt;double, double, double&gt;,则不是:电报可能是出站的,元组可能是翻译而不是位置,或者可能是长周末的最低温度读数。是的,您可以通过 typedef 来明确意图,但这实际上并不能阻止您通过温度。

这些问题往往在超过一定规模的项目中变得很重要;元组的缺点和复杂类的优点变得不可见,并且在小型项目中确实是一种开销。即使对于不起眼的小数据聚合,也可以从适当的类开始。

当然,一种可行的策略是使用纯数据持有者作为类包装器的底层数据提供者,该类包装器提供对该数据的操作。

【讨论】:

【参考方案11】:

不应该存在性能差异(即使是微不足道的差异)。至少在正常情况下,它们会导致相同的内存布局。尽管如此,它们之间的强制转换可能不需要工作(尽管我猜它通常会有相当大的机会)。

【讨论】:

其实我觉得可能会有一点小差别。 struct 必须为每个子对象分配至少 1 个字节,而我认为 tuple 可以通过优化空对象而侥幸逃脱。此外,关于打包和对齐,元组可能有更多的余地。【参考方案12】:

我知道这是一个古老的主题,但是我现在要对我的项目的一部分做出决定:我应该采用元组方式还是结构方式。 看完这篇文章,我有了一些想法。

    关于小麦和性能测试:请注意,您通常可以对结构使用 memcpy、memset 和类似的技巧。这将使性能比元组好得多。

    我看到了元组的一些优点:

    您可以使用元组从函数或方法返回一组变量,并减少您使用的类型数量。 基于 tuple 已预定义 运算符这一事实,您还可以将 tuple 用作 map 或 hash_map 中的键,这比您需要实现这些运算符的结构更具成本效益。

我已经在网上搜索并最终到达此页面: https://arne-mertz.de/2017/03/smelly-pair-tuple/

总的来说,我同意上面的最终结论。

【讨论】:

这听起来更像是您正在研究的内容,而不是针对特定问题的答案,或者? 没有什么能阻止你将 memcpy 与元组一起使用。【参考方案13】:

没有兼容C内存布局等的负担,更有利于优化。

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。

以上是关于C++ 元组与结构的主要内容,如果未能解决你的问题,请参考以下文章

Erlang复合数据结构基础之元组与列表

Python数据结构方法简介三————元组

列表 += 元组与列表 = 列表 + 元组

元组与列表的区别

python元组与字典

3.python元组与列表