C ++ 튜플 대 구조체 int

a std::tuple와 데이터 전용을 사용하는 데 차이가 struct있습니까?

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

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

온라인에서 찾은 내용에서 두 가지 주요 차이점이 있음을 발견했습니다. 즉 struct, 더 읽기 쉽고, tuple사용할 수있는 일반 함수가 많습니다. 상당한 성능 차이가 있어야합니까? 또한 데이터 레이아웃이 서로 호환됩니까 (교체 가능)?



답변

우리는 튜플과 struct에 대해 비슷한 논의를하고 있으며, 튜플과 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 솔루션을 사용하겠습니다.

지금까지 구조체 코드를 더 빠르게 실행할 수있는 솔루션이 없습니다. 스왑 함수를 살펴보고 성능을 얻을 수 있는지 다시 작성해 보겠습니다.

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 % 미만), 모든 구조체에 대해 사용자 지정 스왑 함수를 작성해야합니다.


답변

코드에서 여러 개의 다른 튜플을 사용하는 경우 사용중인 펑터의 수를 압축 할 수 있습니다. 다음과 같은 형태의 펑터를 자주 사용했기 때문에 이렇게 말합니다.

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

이것은 과잉처럼 보일 수 있지만 구조체 내의 각 위치에 대해 구조체를 사용하여 완전히 새로운 functor 객체를 만들어야하지만 튜플의 경우 변경 N합니다. 그보다 더 좋은 점은 각 구조체와 각 멤버 변수에 대해 완전히 새로운 펑터를 만드는 것과는 반대로 모든 단일 튜플에 대해이 작업을 수행 할 수 있다는 것입니다. NxM 펑터가있는 M 멤버 변수가있는 N 구조체가있는 경우 코드 하나로 압축 할 수있는 (최악의 경우 시나리오) 생성해야합니다.

당연히 튜플 방식을 사용하려는 경우 작업을위한 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 사용으로 인한 성능 영향이 전혀 없다는 점을 고려할 때 튜플을 사용하면 성능에 상당한 영향을 미칩니다. (주소 해석기는 로직 유닛이 값을보기 전에 명령어 파이프 라인에서 값을 찾습니다.)

기본 ‘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();
}


답변

글쎄, POD 구조체는 종종 (ab) 저수준 연속 청크 읽기 및 직렬화에 사용될 수 있습니다. 말했듯이 튜플은 특정 상황에서 더 최적화되고 더 많은 기능을 지원할 수 있습니다.

상황에 더 적합한 것을 사용하십시오. 일반적인 선호 사항은 없습니다. 성능 차이는 크지 않을 것이라고 생각합니다 (하지만 벤치마킹하지는 않았습니다). 데이터 레이아웃은 대부분 호환되지 않고 구현에 따라 다릅니다.


답변

“일반적인 함수”에 관한 한, 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>.


답변

또한 데이터 레이아웃이 서로 호환됩니까 (교환 가능)?

이상하게도 질문의이 부분에 대한 직접적인 응답을 볼 수 없습니다.

대답은 아니오 입니다. 또는 튜플의 레이아웃이 지정되지 않았기 때문에 적어도 신뢰할 수 없습니다.

첫째, 구조체는 표준 레이아웃 유형 입니다. 멤버의 순서, 패딩 및 정렬은 표준 및 플랫폼 ABI의 조합에 의해 잘 정의됩니다.

튜플이 표준 레이아웃 유형이고 유형이 지정된 순서로 필드가 배치되었음을 알고 있다면 구조체와 일치 할 것이라는 확신을 가질 수 있습니다.

튜플은 일반적으로 이전 Loki / Modern C ++ 디자인 재귀 스타일 또는 최신 가변 스타일 중 하나로 상속을 사용하여 구현됩니다. 둘 다 다음 조건을 위반하므로 둘 다 표준 레이아웃 유형이 아닙니다.

  1. (C ++ 14 이전)

    • 비 정적 데이터 멤버가있는 기본 클래스가 없거나

    • 가장 많이 파생 된 클래스에 비 정적 데이터 멤버가없고 비 정적 데이터 멤버가있는 기본 클래스가 하나만 있습니다.

  2. (C ++ 14 이상)

    • 모든 비 정적 데이터 멤버와 비트 필드가 동일한 클래스에 선언되어 있습니다 (모두 파생 또는 일부 기본에 모두 있음).

각 리프 기본 클래스에는 단일 튜플 요소가 포함되어 있기 때문입니다 (주의 : 단일 요소 튜플 유용하지는 않지만 표준 레이아웃 유형일 수 있음). 따라서 표준이 튜플이 구조체와 동일한 패딩 또는 정렬을 갖도록 보장 하지 않는다는 것을 알고 있습니다.

또한 이전의 재귀 스타일 튜플은 일반적으로 데이터 멤버를 역순으로 배치한다는 점에 주목할 가치가 있습니다.

일화로, 과거에 일부 컴파일러 및 필드 유형 조합에서 실제로 작동했습니다 (한 경우에는 필드 순서를 반대로 한 후 재귀 튜플 사용). 지금은 확실히 (컴파일러, 버전 등에서) 안정적으로 작동하지 않으며 처음부터 보장되지 않았습니다.