C ++元组与结构


95

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

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

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

从网上发现的地方,我发现有两个主要区别:struct更具可读性,而则tuple具有许多可以使用的通用功能。是否应该有明显的性能差异?此外,数据布局是否彼此兼容(可互换转换)?


我刚刚说过我已经忘记了强制转换问题:tupleis的实现是实现定义的,因此它取决于您的实现。就个人而言,我不会指望它。
Matthieu M.

Answers:


31

我们对元组和结构进行了类似的讨论,在我的一位同事的帮助下,我编写了一些简单的基准测试,以确定元组和结构在性能方面的差异。我们首先从默认结构和元组开始。

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.

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

  • 元组比默认结构快

  • lang生成的二进制文件具有比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%),但是,我们确实需要为所有结构编写自定义交换函数。


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。更好的是,我可以为每个元组执行此操作,而不是为每个结构和每个成员变量创建一个全新的函子。如果我有N个结构,带有Mx个函子的M个成员变量,则需要创建(更糟糕的情况),可以将其压缩为一点点代码。

自然地,如果您要使用元组方式,则还需要创建枚举来与它们一起工作:

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.


6

好吧,这是一个基准测试,没有在struct operator ==()内部构造一堆元组。事实证明,使用元组会对性能产生相当大的影响,因为使用POD完全不会对性能产生影响,因此人们可以预期。(地址解析器在逻辑单元甚至没有看到它之前就在指令管道中找到该值。)

使用默认的“ Release”设置在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结构通常可以(不)用于低级别的连续块读取和序列化。如您所说,在某些情况下,元组可能会更优化,并支持更多功能。

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


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算法现在都适用于struct demo::employee


编辑:关于性能差异或布局兼容性,tuple的布局是实现定义的,因此不兼容(因此您不应该在任何一种表示形式之间进行转换),并且总体上,我希望性能方面没有差异(至少在Release中),这要归功于内联get<N>


16
我不认为这是投票最多的答案。它甚至没有回答这个问题。问题是关于tuples和structs,而不是升压!
gsamaras 2014年

@ G.Samaras:问题是关于元组和的区别struct,特别是关于操作元组的算法很多,而缺少缺少对结构的算法(从迭代其字段开始)。这个答案表明,可以使用Boost.Fusion弥合这种差距,从而使struct元组中的算法数量达到s个。我在提出的确切两个问题上添加了一个简短的内容。
Matthieu M.

3

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

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

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

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

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

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

  1. (C ++ 14之前)

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

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

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

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

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

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

有趣的是,在过去,有时在某些编译器和字段类型组合中实际上可以工作(在一种情况下,在反转字段顺序后使用递归元组)。它现在绝对不能可靠地运行(跨编译器,版本等),并且从来没有保证过。


1

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


4
实际上,我认为可能会有很小的差异。Astruct必须为每个子对象分配至少1个字节,而我认为atuple可以优化空对象。另外,在打包和对齐方面,元组可能还有更多的余地。
Matthieu M.

1

我的经验是,随着时间的流逝,功能开始逐渐发展为以前是纯数据持有人的类型(例如POD结构)。诸如某些修改之类的事情,不需要内部数据知识,保持不变性等。

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

与此相关的是,像所有“开放数据对象”一样,元组违反了信息隐藏范式。您以后不能更改它,而不必扔掉元组批发。使用结构,您可以逐步转向访问功能。

另一个问题是类型安全和自记录代码。如果你的函数接收类型的对象inbound_telegram还是location_3D很明显; 如果收到unsigned char *tuple<double, double, double>:电报可能是外拨,元组可能是翻译而不是位置,或者可能是漫长周末的最低温度读数。是的,您可以使用typedef明确意图,但这实际上并不能阻止您通过温度测试。

这些问题在超过一定规模的项目中往往变得很重要。元组的缺点和精致类的优点变得不明显,在小项目中确实是开销。即使对于不起眼的少量数据聚合,也要从适当的类开始,这将带来后期收益。

当然,一种可行的策略是将纯数据持有者用作提供该数据操作的类包装器的基础数据提供者。


1

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

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

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


1

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

因此,它实际上应该归结为实用性,可读性和可维护性。和struct一般比较好,因为它创建更易于阅读和理解的类型。

有时,可能需要一个std::tuple(甚至一个std::pair)以高度通用的方式处理代码。例如,如果没有类似之类的信息,与可变参数包相关的某些操作将是不可能的std::tuplestd::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. 我看到了元组的一些优点:

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

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

通常,我同意上面的最后结论。


1
这听起来更像是您正在从事的工作,而不是该特定问题的答案,或者?
Dieter Meemken

没有什么可以阻止您将memcpy与元组一起使用。
彼得-恢复莫妮卡
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.