使用std::tuple
和仅数据struct
之间有什么区别吗?
typedef std::tuple<int, double, bool> foo_t;
struct bar_t {
int id;
double value;
bool dirty;
}
根据我在网上找到的内容,我发现有两个主要区别:struct
更具可读性,而tuple
有许多可以使用的通用函数。
应该有任何显着的性能差异吗?
另外,数据布局是否相互兼容(可互换地转换)?
答案 0 :(得分:21)
如果您在代码中使用了几个不同的元组,那么您可以通过缩减所使用的仿函数来实现。我这样说是因为我经常使用以下形式的仿函数:
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个结构,其中包含我需要创建的NxM仿函数的M个成员变量(更糟糕的情况),可以简化为一小段代码。
当然,如果您要使用Tuple方式,那么您还需要创建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);
因为当你想要获取其中包含的项目时它会自我描述。
答案 1 :(得分:18)
我们有关于元组和结构的类似讨论,我在一位同事的帮助下写了一些简单的基准,以确定元组和结构之间的性能差异。我们首先从默认结构和元组开始。
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.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不是讨论的目的所以我不会深入细节。
我们都知道写一个==或&lt;或者&gt;每个结构定义的运算符将是一个痛苦和错误的任务。让我们使用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%),但是,我们需要为所有结构编写自定义交换函数。
答案 2 :(得分:14)
Tuple内置默认值(对于==和!=它比较每个元素,对于&lt;。&lt; = ...先比较,如果相同则比较第二个...)比较器: http://en.cppreference.com/w/cpp/utility/tuple/operator_cmp
答案 3 :(得分: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();
}
答案 4 :(得分:3)
嗯,POD结构通常可以(ab)用于低级连续的块读取和序列化。正如你所说,元组在某些情况下可能会更加优化并支持更多功能。
使用更合适的情况,没有一般偏好。我认为(但我没有对它进行基准测试)性能差异不会很大。数据布局很可能不兼容并且特定于实现。
答案 5 :(得分: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>
。
答案 6 :(得分:1)
不应该有性能差异(即使是微不足道的)。至少在正常情况下,它们将导致相同的内存布局。尽管如此,他们之间的演员可能不需要工作(虽然我猜它通常会有相当大的机会)。
答案 7 :(得分:1)
此外,数据布局是否彼此兼容(可互换转换)?
奇怪的是,我看不到对此部分问题的直接答复。
答案是:否。 或至少不可靠,因为未指定元组的布局。
首先,您的结构是Standard Layout Type。通过标准和平台ABI的结合,可以很好地定义成员的顺序,填充和对齐方式。
如果元组是标准布局类型,并且我们知道字段是按照指定类型的顺序进行布局的,那么我们可能会确信它会与结构匹配。
元组通常以两种方式之一使用继承来实现:旧的Loki /现代C ++设计递归样式,或较新的可变参数样式。都不是标准版式类型,因为违反了以下条件:
(C ++ 14之前的版本)
没有带有非静态数据成员的基类,或者
在最多派生的类中没有非静态数据成员,并且最多一个具有非静态数据成员的基类
(适用于C ++ 14和更高版本)
- 具有在同一类中声明的所有非静态数据成员和位域(全部在派生类中或全部在某个基类中)
由于每个叶基类都包含一个元组元素(注意,一个单元素元组可能是标准布局类型,尽管不是很有用)。因此,我们知道标准不能保证元组具有与结构相同的填充或对齐方式。
此外,值得注意的是,较旧的递归式元组通常会以相反的顺序布置数据成员。
有趣的是,过去有时在某些编译器和字段类型组合中有时会起作用(在一种情况下,在反转字段顺序后使用递归元组)。现在,它肯定无法可靠地运行(跨编译器,版本等),并且从来没有保证过。答案 8 :(得分:1)
不必担心速度或布局,它是纳米优化的,并且取决于编译器,并且永远不会有足够的差异来影响您的决策。
对于有意义地属于一个整体的事物使用结构。
将元组用于巧合的事物。您可以在代码中自发使用元组。
答案 9 :(得分: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;
答案 10 :(得分:0)
我知道这是一个古老的主题,但是我现在要对我的项目的一部分做出决定:我应该走元组还是结构。
阅读此主题后,我有一些想法。
1.关于缺点和性能测试-请注意,通常可以对结构使用memcpy,memset和类似的技巧。这样会使性能大大提高,而不是倒塌。
2.我看到了元组的一些优点:
a)您可以使用元组从函数或方法中返回变量的集合,并减少使用的类型,
b)基于元组具有预定义的<,==,>运算符的事实,您还可以将元组用作map或hash_map中的键,这比在需要实现这些运算符的结构上更具成本效益。
我已经开始挖掘www并最终到达此页面:
https://arne-mertz.de/2017/03/smelly-pair-tuple/
通常,我同意上面的最后结论。
答案 11 :(得分:0)
我的经验是,随着时间的流逝,功能(例如POD结构)开始逐渐成为一种纯粹的数据持有者。诸如某些修改之类的事情不需要内部数据知识,保持不变性等。
那是一件好事;这是面向对象的基础。这就是发明带有类的C的原因。使用像元组这样的纯数据集合并不适合这种逻辑扩展。结构是。这就是为什么我几乎总是选择结构。
相关的是,像所有“开放数据对象”一样,元组违反了信息隐藏范式。您不能稍后更改它,而不必扔掉元组批发。使用结构,您可以逐步使用访问功能。
另一个问题是类型安全和自我记录代码。如果您的函数接收到类型为inbound_telegram
或location_3D
的对象,那就很清楚了;如果它收到unsigned char *
或tuple<double, double, double>
,则不是:电报可能是出站的,元组可能是翻译而不是位置,或者可能是漫长周末的最低温度读数。是的,您可以使用typedef来使意图清楚,但这实际上并不能阻止您通过温度调节。
这些问题在超过一定规模的项目中往往变得很重要;元组的缺点和精心设计的类的优点变得不明显,在小型项目中确实是开销。即使对于不起眼的少量数据聚合,也要从适当的类开始,这会带来后期的红利。
当然,一种可行的策略是使用纯数据持有者作为提供此类数据操作的类包装的基础数据提供程序。