【问题标题】:performance comparsion between vector and raw c-style array矢量和原始 c 样式数组之间的性能比较
【发布时间】:2018-10-31 10:06:45
【问题描述】:

我已经分析了 c++ 向量和 c 样式数组之间的性能。结果有点出乎意料,因为文献说向量的性能应该非常接近原始数组,但事实并非如此。我在分析中做错了什么吗?

void getVector1(int n)
{
    if (n < 0)
    {
        throw std::invalid_argument(std::string("negative argument n:") + std::to_string(n));
    }

    auto tp1 = std::chrono::steady_clock::now();
    std::vector<int> ivec(n);
    int i = 0;
    for (auto& x : ivec)
    {
        x = ++i;
    }

    auto tp2 = std::chrono::steady_clock::now();
    std::chrono::duration<double, std::micro> dd = tp2 - tp1;

    printf("spend %6.2f us time to create: %d elements vector inside %s() at %s:%d \n", dd.count(), n, __func__, __FILE__, __LINE__);
}


void getVector2(int n)
{
    if (n < 0)
    {
        throw std::invalid_argument(std::string("negative argument n:") + std::to_string(n));
    }

    auto tp1 = std::chrono::steady_clock::now();
    auto pvec = new int[n];

    for (int i = 0; i < n; ++i)
    {
        pvec[i] = i;
    }

    auto tp2 = std::chrono::steady_clock::now();
    std::chrono::duration<double, std::micro> dd = tp2 - tp1;

    delete[] pvec;
    printf("spend %6.2f us time to create: %d elements vector inside %s() at %s:%d \n", dd.count(), n, __func__, __FILE__, __LINE__);
}



int main()
{
    int n = 10000000;
    getVector1(n);
    getVector2(n);

    return 0;
}

代码是使用带有 -O3 选项的 g++ 编译的。

花费 11946.38 我们的时间来创建:在 testVectorSpeed.cpp 中的 getVector1() 内的 10000000 个元素向量

花费 7298.66 我们的时间来创建:在 testVectorSpeed.cpp 中的 getVector2() 内的 10000000 个元素向量

【问题讨论】:

  • 将创建时间与填充它的循环分开可能会有所帮助。
  • 我希望一个好的编译器能够意识到你不使用向量或数组,尝试使用它们(在时间之外)。此外,您应该多次运行测量并更改调用顺序
  • @user463035818 堆省略在 gcc 中还没有 AFAIK,所以副作用应该仍然起作用。
  • 创建向量时,会调用memset(),因为整数是默认构造的。如果你改用reserve()emplace_back(),你可能会得到更好的性能。
  • 一个真正的区别是向量中的元素是默认初始化的,但在动态数组中没有太多。试试vec = new int[n]();

标签: c++ performance vector


【解决方案1】:

此成本归结为向量通过其分配器将内存清零。


首先,使用像google benchmark 这样的基准测试库总是一个好主意,而不是滚动您自己的基准测试。我们可以使用quick-bench.com 快速使用该库。重写代码以使用它:

// Just the benchmark code:
void getVector1(benchmark::State& state)
{
    int n = state.range(0);

    for (auto _ : state) {
      std::vector<int> ivec(n);

      // This is the same operation that you are doing
      std::iota(ivec.begin(), ivec.end(), 1);

      // We don't want the compiler to see that we aren't
      // using `ivec` and thus optimize away the entire
      // loop body
      benchmark::DoNotOptimize(ivec);
    }
}

void getArray1(benchmark::State& state)
{
    int n = state.range(0);

    for (auto _ : state) {
      auto pvec = new int[n];

      std::iota(pvec, pvec + n, 1);

      benchmark::DoNotOptimize(pvec);

      delete[] pvec;
    }
}

// Smaller number still reproduces it
BENCHMARK(getVector1)->Arg(10000);
BENCHMARK(getArray1)->Arg(10000);

Click on image for quick-bench link

稍微玩弄一下,我们可以发现成本差异只是用std::uninitialized_fill (on quick-bench) 将内存归零的成本。

确实,如果我们改用an allocator that leaves the memory uninitialized,两者之间没有可衡量的区别:

// Allocator from https://stackoverflow.com/a/41049640
template <typename T, typename A = std::allocator<T>>
class default_init_allocator : public A {
    typedef std::allocator_traits<A> a_t;
public:
    // http://en.cppreference.com/w/cpp/language/using_declaration
    using A::A; // Inherit constructors from A

    template <typename U> struct rebind {
        using other =
            default_init_allocator
            <  U, typename a_t::template rebind_alloc<U>  >;
    };

    template <typename U>
    void construct(U* ptr)
        noexcept(std::is_nothrow_default_constructible<U>::value) {
        ::new(static_cast<void*>(ptr)) U;
    }

    template <typename U, typename...Args>
    void construct(U* ptr, Args&&... args) {
        a_t::construct(static_cast<A&>(*this),
            ptr, std::forward<Args>(args)...);
    }
};

void getVector1(benchmark::State& state)
{
    int n = state.range(0);

    for (auto _ : state) {
      std::vector<int, default_init_allocator<int>> ivec(n);

      std::iota(ivec.begin(), ivec.end(), 1);

      benchmark::DoNotOptimize(ivec);
    }
}

void getArray1(benchmark::State& state)
{
    int n = state.range(0);

    for (auto _ : state) {
      auto pvec = new int[n];

      std::iota(pvec, pvec + n, 1);

      benchmark::DoNotOptimize(pvec);

      delete[] pvec;
    }
}

BENCHMARK(getVector1)->Arg(10000);
BENCHMARK(getArray1)->Arg(10000);

Click on image for quick-bench link

【讨论】:

  • 老实说,我怀疑你可以使用 reserveemplace_back 来获得相同的结果。
  • @Mgetz 使用reserveemplace_back is actually really slow(即使我使用std::back_inserterstd::iota,这略有不同,但仍然......)。我也是这么想的,不过比原来的向量代码还要差
  • 有异味,我想直接分析一下以获取热线
  • @Mgetz 请做!我真的很想知道为什么会更糟。我一直在自己的代码中到处使用reserve + emplace_back。我很想看看你能找到什么
  • @Mgetz 查看the disassembly on godbolt,对于gcc,代码似乎一直在检查是否需要增加容量,即使我们知道它不需要。对于 clang,clang 似乎无法使用 reserve + emplace_back 版本生成矢量化指令
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-25
  • 1970-01-01
  • 2021-02-19
  • 1970-01-01
  • 2015-11-26
相关资源
最近更新 更多