元组与结构

使用 std::tuple和只使用数据的 struct有什么区别吗?

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


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

从我在线发现的情况来看,我发现有两个主要的区别: struct更具可读性,而 tuple有许多可以使用的通用函数。 是否应该存在显著的性能差异? 此外,数据布局是否彼此兼容(可交换转换) ?

49644 次浏览

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

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

不应该存在性能差异(即使是微不足道的性能差异)。至少在正常情况下,它们将导致相同的内存布局。尽管如此,两者之间的选角可能并不一定会奏效(尽管我猜想通常情况下会奏效的可能性很大)。

如果您在代码中使用了几个不同的元组,那么可以压缩所使用的函数的数量。之所以这样说,是因为我经常使用以下函数形式:

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

这可能看起来有点夸张,但是对于 struct 中的每个位置,我都必须使用 struct 创建一个全新的函数对象,但是对于 tuple,我只需更改 N。比这更好的是,我可以对每个元组都这样做,而不是为每个结构和每个成员变量创建一个全新的函数。如果我有 N 个带 M 个成员变量的结构,我需要创建 NxM 函数(更糟糕的情况) ,它们可以压缩成一小段代码。

Naturally, if you're going to go with the Tuple way, you're also going to need to create Enums for working with them:

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

然后你的代码就完全可读了:

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

因为当您希望获取其中包含的项时,它会自我描述。

至于“通用功能”去,Boost。融合值得一些爱... 特别是 融合

从页面上撕下: [咒语]

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


EDIT: Regarding the performance difference or layout compatibility, tuple's layout is implementation defined so not compatible (and thus you should not cast between either representation) and in general I would expect no difference performance-wise (at least in Release) thanks to the inlining of get<N>.

Tuple 已经默认构建(for = = and!= 它比较 < 的每个元素。< = ... 比较第一个,如果相同比较第二个...)比较器: Http://en.cppreference.com/w/cpp/utility/tuple/operator_cmp

编辑: 正如注释 C + + 20中提到的,宇宙飞船操作符为您提供了一种用一行(丑陋的,但仍然只有一行)代码指定此功能的方法。

我们对 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 来比较简单 struct 和 tuple 的性能。下面是使用 gcc-4.9.2和 clang-4.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);
}

Performance results collected with 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.

从以上结果我们可以清楚地看到

  • 元组比默认结构 更快

  • Binary produce by clang has higher performance that that of gcc. clang-vs-gcc is not the purpose of this discussion so I won't dive into the detail.

我们都知道,为每一个结构定义编写 a = = 或 < 或 > 操作符将是一项痛苦且充满错误的任务。让我们使用 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.

Now we can see that using std::tie makes our code more elegant and it is harder to make mistake, however, we will loose about 1% performance. I will stay with the std::tie solution for now since I also receive a warning about comparing floating point numbers with the customized comparator.

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

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

Performance results collected using 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.

And the performance results collected using 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.

现在我们的 struct 比 tuple 稍微快一点(clang 大约3% ,gcc 小于1%) ,但是,我们需要为所有的 struct 编写定制的交换函数。

这里有一个基准,它不会在 struct 操作符 = = ()中构造一堆元组。事实证明,使用 tuple 会对性能产生相当大的影响,因为使用 POD 根本不会对性能产生任何影响。(地址解析器在逻辑单元看到之前就能在指令管线化中找到值。)

使用默认的“发布”设置在我的 VS2015CE 机器上运行这个程序的常见结果是:

Structs took 0.0814905 seconds.
Tuples took 0.282463 seconds.

Please monkey with it until you're satisfied.

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

我知道这是一个古老的主题,但是我现在要对我的项目的一部分做出决定: 我应该采用 tuple-way 还是 struct-way。 读完这篇帖子后,我有了一些想法。

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

  2. 我看到了元组的一些优势:

    • 可以使用元组从函数或方法返回变量集合,并减少所使用的类型。
    • 基于 tuple 具有预定义的 < ,= = ,> 操作符这一事实,您还可以在 map 或 hash _ map 中使用 tuple 作为键,这比在需要实现这些操作符的 struct 中更具成本效益。

I have searched the web and eventually reached this page: https://arne-mertz.de/2017/03/smelly-pair-tuple/

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

My experience is that over time functionality starts to creep up on types (like POD structs) which used to be pure data holders. Things like certain modifications which shouldn't require inside knowledge of the data, maintaining invariants etc.

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

相关的是,像所有“开放数据对象”一样,元组违反了信息隐藏范例。您 cannot稍后更改它,而不需要批量抛出元组。使用 struct,您可以逐渐转向访问函数。

Another issue is type safety and self-documenting code. If your function receives an object of type inbound_telegram or location_3D it's clear; if it receives a unsigned char * or tuple<double, double, double> it is not: the telegram could be outbound, and the tuple could be a translation instead of a location, or perhaps the minimum temperature readings from the long weekend. Yes, you can typedef to make intentions clear but that does not actually prevent you from passing temperatures.

这些问题往往在超过一定规模的项目中变得重要; 元组的缺点和复杂类的优点变得不明显,实际上是小型项目中的一个开销。从合适的类别开始,即使是不起眼的小数据集合也会带来后期红利。

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

此外,数据布局是否彼此兼容(可交换转换) ?

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

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

首先,您的结构是 标准布局类型。成员的排序、填充和对齐由标准和平台 ABI 的组合来定义。

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

The tuple is normally implemented using inheritance, in one of two ways: the old Loki/Modern C++ Design recursive style, or the newer variadic style. Neither is a Standard Layout type, because both violate the following conditions:

  1. (C + + 14之前)

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

    • 在派生程度最高的类中没有非静态数据成员,而且最多只有一个具有非静态数据成员的基类

  2. (对于 C + + 14及更高版本)

    • 具有在同一类中声明的所有非静态数据成员和位字段(要么全部在派生类中,要么全部在某个基类中)

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

另外,值得注意的是,较老的递归样式的元组通常会以相反的顺序布局数据成员。

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

不要担心速度或布局,这是纳米级优化,取决于编译器,没有足够的差异来影响你的决定。

您使用一个结构来表示有意义地属于一起的事物,从而形成一个整体。

You use a tuple for things that are together coincidentally. You can use a tuple spontaneously in your code.

从其他答案来看,性能考虑最多只是最小的。

因此,它确实应该归结为实用性、可读性和可维护性。struct通常更好,因为它创建的类型更容易阅读和理解。

Sometimes, a std::tuple (or even std::pair) might be necessary to deal with code in a highly generic way. For example, some operations related to variadic parameter packs would be impossible without something like std::tuple. std::tie is a great example of when std::tuple can improve code (prior to C++20).

但是,无论你在哪里使用 可以,你可能 应该使用 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;

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