为什么这个普通的数组实现比STD ::向量实现性能慢

Why is this plain array implementation slower than the std::vector implementation performance?

本文关键字:实现 STD 向量 性能 数组 为什么      更新时间:2023-10-16

为什么此普通数组实现比std :: vector实现性能慢?

由于我在正在从事的事情上看到了一些Weired结果,因此我决定编写一个简化的测试以比较std::vector与纯阵列效率。

我有一个我以这两种方式实现的结构,

1使用普通阵列(不同尺寸)

typedef struct {
    uint16_t index;
     uint16_t nvals;
     uint16_t vals[50];
     double mean;
} a_segment_t;

2使用stl

 typedef struct {
      uint16_t index;
      uint16_t nvals;
      vector<uint16_t> vals;
      uint32_t mean;
} b_segment_t;

在内存中创建此对象并不是我感兴趣的(所以我不介意push_back()),一旦此对象在内存中,它将用于操作,并且效率为我正在分析什么。vals充满了一些随机数据。

操作 通过存储在每个段中的阀门,在这种情况下是一个简单的平均计算。测试如下:

using namespace std;
#include <stdint.h>
#include <stdlib.h> // srand, rand
#include <time.h>
#include <iostream>
#include <iomanip>
#include <vector>
#include <array>
#define NSEGMENTS 100
#define MAX_NPXS 50
#define N 10000
// plain array approach
typedef struct {
    uint16_t index;
    uint16_t nvals;
    uint16_t vals[MAX_NPXS];
    double mean;
} a_segment_t;
uint16_t operation(uint16_t, a_segment_t*);
uint16_t print(uint16_t nsegments, a_segment_t* p_segments);
// stl vector approach
typedef struct {
    uint16_t index;
    uint16_t nvals;
    vector<uint16_t> vals;
    uint32_t mean;
} b_segment_t;
uint16_t operation(uint16_t, vector<b_segment_t>*);
uint16_t print(uint16_t nsegments, vector<b_segment_t>*);
void delta_time(struct timespec*, struct timespec*, struct timespec*);
uint16_t operation(uint16_t nsegments, a_segment_t* p_segments) {
    // the operation (plain array approach)
    uint64_t sum;
    for( uint16_t nsegment = 0; nsegment < nsegments; ++nsegment ) {
        sum = 0;
        for(uint16_t nval = 0; nval < p_segments[nsegment].nvals; ++nval){
            sum = sum + p_segments[nsegment].vals[nval];
        }
        p_segments[nsegment].mean = sum/p_segments[nsegment].nvals;
    }
    return nsegments;
}
uint16_t print(uint16_t nsegments, a_segment_t* p_segments) {
    // print data (plain array approach)
    for( uint16_t nsegment = 0; nsegment < nsegments; ++nsegment ) {
        cout << "index : " << setfill('0') << setw(3) << p_segments[nsegment].index;
        cout << "tnval : " << setfill('0') << setw(3) << p_segments[nsegment].nvals;
        cout << "tvals : [";
        for(uint16_t nval = 0; nval < p_segments[nsegment].nvals; ++nval){
            cout << p_segments[nsegment].vals[nval] << ",";
        }
        cout << "b]" << endl;
    }
    return nsegments;
}
uint16_t operation(uint16_t nsegments, vector<b_segment_t>* p_segments) {
    // the operation (stl vector approach)
    uint32_t sum;
    for (vector<b_segment_t>::iterator p_segment = p_segments->begin(); p_segment<p_segments->end(); ++p_segment) {
        sum = 0;
        for (vector<uint16_t>::iterator p_val = (p_segment->vals).begin(); p_val<(p_segment->vals).end(); ++p_val) {
            sum = sum + (*p_val);
        }
        p_segment->mean = sum/(p_segment->nvals);
    }
    return nsegments;
}
uint16_t print(uint16_t nsegments, vector<b_segment_t>* p_segments) {
    // print data (stl vector approach)
    for (vector<b_segment_t>::iterator p_segment = p_segments->begin(); p_segment<p_segments->end(); ++p_segment) {
        cout << "index : " << setfill('0') << setw(3) << p_segment->index;
        cout << "tnval : " << setfill('0') << setw(3) << p_segment->nvals;
        cout << "tvals : [";
        for (vector<uint16_t>::iterator p_val = (p_segment->vals).begin(); p_val<(p_segment->vals).end(); ++p_val) {
            cout << *p_val << ",";
        }
        cout << "b]" << endl;
    }
    return nsegments;
}
void delta_time(struct timespec* t1, struct timespec* t2, struct timespec* dt) {
    if ((t2->tv_nsec - t1->tv_nsec) < 0) {
        dt->tv_sec = t2->tv_sec - t1->tv_sec - 1;
        dt->tv_nsec = t2->tv_nsec - t1->tv_nsec + 1000000000;
    } else {
        dt->tv_sec = t2->tv_sec - t1->tv_sec;
        dt->tv_nsec = t2->tv_nsec - t1->tv_nsec;
    }
    return;
}
int main(int argc, char const *argv[]) {
    uint16_t nsegments = NSEGMENTS;
    uint16_t nsegment = 0;
    uint16_t i = 0;
    //create an populate the segments with dummy data (plain array approach)
    a_segment_t* a_segments = new a_segment_t[nsegments];
    for( nsegment = 0; nsegment < nsegments; ++nsegment ) {
        a_segments[nsegment].index = nsegment;
        srand(nsegment);
        a_segments[nsegment].nvals = rand() % MAX_NPXS + 1;
        for(uint16_t nval = 0; nval < a_segments[nsegment].nvals; ++nval){
            a_segments[nsegment].vals[nval] = nval;
        }
    }
    //create an populate the segments with dummy data (stl vector approach)
    nsegment = 0;
    vector<b_segment_t> b_segments(nsegments);
    for (vector<b_segment_t>::iterator p_segment = b_segments.begin(); p_segment<b_segments.end(); ++p_segment) {
        p_segment->index = nsegment;
        srand(nsegment);
        p_segment->nvals = rand() % MAX_NPXS + 1;
        for(uint16_t nval = 0; nval < p_segment->nvals; ++nval){
            p_segment->vals.push_back(nval);
        }
        nsegment++;
    }
    // print(nsegments, a_segments);
    // cout << "===================================" << endl;
    // print(nsegments, &b_segments);
    // cout << "===================================" << endl;
    // ======================= plain array timing measure ========================
    struct timespec a_times[N];
    for(i = 0; i < N; i++) {
        nsegments = operation(nsegments, a_segments);
        clock_gettime(CLOCK_REALTIME, &(a_times[i]));
    }
    // ===========================================================================
    // ========================= vector timing measure ===========================
    struct timespec b_times[N];
    for(i = 0; i < N; i++) {
        nsegments = operation(nsegments, &b_segments);
        clock_gettime(CLOCK_REALTIME, &(b_times[i]));
    }
    // ===========================================================================
    // =========================== timing console log ============================
    struct timespec a_deltatime[N], a_elapsedtime[N], b_deltatime[N], b_elapsedtime[N];
    cout << "tt  plain arraytt       stl vector" << endl;
    cout << "frame #telapsedtimetdeltatimetelapsedtimetdeltatime" << endl;
    for(i = 0; i < N-1; i=i+1000) {
        delta_time(&(a_times[0]), &(a_times[i]), &(a_elapsedtime[i]));
        delta_time(&(a_times[i]), &(a_times[i+1]), &(a_deltatime[i]));
        delta_time(&(b_times[0]), &(b_times[i]), &(b_elapsedtime[i]));
        delta_time(&(b_times[i]), &(b_times[i+1]), &(b_deltatime[i]));
        cout << i << ",t"
        << a_elapsedtime[i].tv_sec << "." << setfill('0') << setw(9) << a_elapsedtime[i].tv_nsec << ",t"
        << a_deltatime[i].tv_sec << "." << setfill('0') << setw(9) << a_deltatime[i].tv_nsec << ",t"
        << b_elapsedtime[i].tv_sec << "." << setfill('0') << setw(9) << b_elapsedtime[i].tv_nsec << ",t"
        << b_deltatime[i].tv_sec << "." << setfill('0') << setw(9) << b_deltatime[i].tv_nsec << endl;
    }
    // ===========================================================================
}

在线版本。注意:所有测试均使用-O3

编译
  • 有人可以指出为什么普通数组实现比std::vector实现慢?

  • 不应该更快地实现吗?

  • 我该怎么做才能提高普通数组实现的速度?

如果您用迭代器表达算法,编译器将在优化代码方面做得更好。原因之一是它可以对数组索引的大小和溢出特性做出假设(该索引的大小和溢出特性(将其转化为机器代码中的偏移索引)。

重构以迭代器(可以是指示器)来表达operation()print()

#include <stdint.h>
#include <stdlib.h> // srand, rand
#include <time.h>
#include <iostream>
#include <iomanip>
#include <vector>
#include <array>
#include <numeric>
using namespace std;
#define NSEGMENTS 100
#define MAX_NPXS 50
#define N 10000
// plain array approach
typedef struct {
    uint16_t index;
    uint16_t nvals;
    uint16_t vals[MAX_NPXS];
    double mean;
} a_segment_t;
// stl vector approach
typedef struct {
    uint16_t index;
    uint16_t nvals;
    vector<uint16_t> vals;
    uint32_t mean;
} b_segment_t;
void delta_time(struct timespec*, struct timespec*, struct timespec*);
template<class Iter>
uint16_t operation(Iter first, Iter last) {
    auto result = std::uint16_t(std::distance(first, last));
    // the operation (plain array approach)
    for( ; first != last ; ++first ) {
        auto sum = std::accumulate(std::begin(first->vals), std::begin(first->vals) + first->nvals, uint64_t(0), std::plus<>());
        first->mean = sum / first->nvals;
    }
    return result;
}

template<class Iter>
uint16_t print(Iter first, Iter last) {
    auto result = std::uint16_t(std::distance(first, last));
    // print data (plain array approach)
    for( ; first != last ; ++first ) {
        cout << "index : " << setfill('0') << setw(3) << first->index;
        cout << "tnval : " << setfill('0') << setw(3) << first->nvals;
        cout << "tvals : [";
        for_each(std::begin(first->vals), std::begin(first->vals) + first->nvals, [](const auto& val)
        {
            cout << val << ",";
        });
        cout << "b]" << endl;
    }
    return result;
}
void delta_time(struct timespec* t1, struct timespec* t2, struct timespec* dt) {
    if ((t2->tv_nsec - t1->tv_nsec) < 0) {
        dt->tv_sec = t2->tv_sec - t1->tv_sec - 1;
        dt->tv_nsec = t2->tv_nsec - t1->tv_nsec + 1000000000;
    } else {
        dt->tv_sec = t2->tv_sec - t1->tv_sec;
        dt->tv_nsec = t2->tv_nsec - t1->tv_nsec;
    }
    return;
}
int main(int argc, char const *argv[]) {
    uint16_t nsegments = NSEGMENTS;
    uint16_t nsegment = 0;
    uint16_t i = 0;
    //create an populate the segments with dummy data (plain array approach)
    a_segment_t* a_segments = new a_segment_t[nsegments];
    for( nsegment = 0; nsegment < nsegments; ++nsegment ) {
        a_segments[nsegment].index = nsegment;
        srand(nsegment);
        a_segments[nsegment].nvals = rand() % MAX_NPXS + 1;
        for(uint16_t nval = 0; nval < a_segments[nsegment].nvals; ++nval){
            a_segments[nsegment].vals[nval] = nval;
        }
    }
    //create an populate the segments with dummy data (stl vector approach)
    nsegment = 0;
    vector<b_segment_t> b_segments(nsegments);
    for (vector<b_segment_t>::iterator p_segment = b_segments.begin(); p_segment<b_segments.end(); ++p_segment) {
        p_segment->index = nsegment;
        srand(nsegment);
        p_segment->nvals = rand() % MAX_NPXS + 1;
        for(uint16_t nval = 0; nval < p_segment->nvals; ++nval){
            p_segment->vals.push_back(nval);
        }
        nsegment++;
    }
    // print(a_segments, a_segments + nsegments);
    // cout << "===================================" << endl;
    // print(b_segments.begin(), b_segments.end());
    // cout << "===================================" << endl;
    // ======================= plain array timing measure ========================
    struct timespec a_times[N];
    for(i = 0; i < N; i++) {
        nsegments = operation(a_segments, a_segments + nsegments);
        clock_gettime(CLOCK_REALTIME, &(a_times[i]));
    }
    // ===========================================================================
    // ========================= vector timing measure ===========================
    struct timespec b_times[N];
    for(i = 0; i < N; i++) {
        nsegments = operation(b_segments.begin(), b_segments.begin() + nsegments);
        clock_gettime(CLOCK_REALTIME, &(b_times[i]));
    }
    // ===========================================================================
    // =========================== timing console log ============================
    struct timespec a_deltatime[N], a_elapsedtime[N], b_deltatime[N], b_elapsedtime[N];
    cout << "tt  plain arraytt       stl vector" << endl;
    cout << "frame #telapsedtimetdeltatimetelapsedtimetdeltatime" << endl;
    for(i = 0; i < N-1; i=i+1000) {
        delta_time(&(a_times[0]), &(a_times[i]), &(a_elapsedtime[i]));
        delta_time(&(a_times[i]), &(a_times[i+1]), &(a_deltatime[i]));
        delta_time(&(b_times[0]), &(b_times[i]), &(b_elapsedtime[i]));
        delta_time(&(b_times[i]), &(b_times[i+1]), &(b_deltatime[i]));
        cout << i << ",t"
        << a_elapsedtime[i].tv_sec << "." << setfill('0') << setw(9) << a_elapsedtime[i].tv_nsec << ",t"
        << a_deltatime[i].tv_sec << "." << setfill('0') << setw(9) << a_deltatime[i].tv_nsec << ",t"
        << b_elapsedtime[i].tv_sec << "." << setfill('0') << setw(9) << b_elapsedtime[i].tv_nsec << ",t"
        << b_deltatime[i].tv_sec << "." << setfill('0') << setw(9) << b_deltatime[i].tv_nsec << endl;
    }
    // ===========================================================================
}

产生预期结果:

          plain array              stl vector
frame # elapsedtime deltatime   elapsedtime deltatime
0,  0.000000000,    0.000002000,    0.000000000,    0.000002000
1000,   0.001533000,    0.000001000,    0.001551000,    0.000002000
2000,   0.003061000,    0.000002000,    0.003096000,    0.000002000
3000,   0.004589000,    0.000001000,    0.004771000,    0.000002000
4000,   0.006255000,    0.000001000,    0.006433000,    0.000002000
5000,   0.007785000,    0.000002000,    0.007975000,    0.000001000
6000,   0.009326000,    0.000002000,    0.009494000,    0.000001000
7000,   0.010893000,    0.000002000,    0.011012000,    0.000001000
8000,   0.012435000,    0.000002000,    0.012650000,    0.000002000
9000,   0.014024000,    0.000002000,    0.014273000,    0.000001000

这两个版本实际上并不等于。

首先,您的"数组版本"具有mean作为double,而" STL版本"的meanuint32_t。为了使这两个函数远程等效,mean的计算必须相同。

第二,您的"数组版本"使用阵列订阅,而STL版本则进行了增量和呈现迭代器。由于编译器/优化器将需要在数组版本中允许更多的疑虑(例如指针混叠),因此它可能无法尽可能多地优化性能。

尝试将您的数组版本变成;

之类的东西
uint16_t operation(uint16_t nsegments, a_segment_t* p_segments)
{
    uint64_t sum;
    for(a_segment *pseg = p_segments, *eseg = p_segments + nsegments; pseg < eseg; ++pseg)
    {
        sum = 0;
        for(uint16_t *val = pseg->vals, *eval = pseg->vals + pseg->nvals; val < eval; ++val)
        {
            sum = sum + (*val);
        }
        p_seg->mean = sum/(pseg->nvals);
    }
    return nsegments;
}

这将(禁止我在转换为此形式时犯的错误 - 我没有测试过)给出相同的结果,但至少会给编译器一个能够将相同类型的性能优化的战斗机会关于" stl版本"的"数组版本"。

这是(几个)C 标准算法与迭代器一起使用的原因之一,而不是在vector之类的容器上进行索引。编译器有更好的机会优化性能。请注意,指针是一种迭代器。