【问题标题】:How is the vector element access performance向量元素访问性能如何
【发布时间】:2015-12-02 22:55:12
【问题描述】:

我有一个功能。它将多次访问向量中的相同元素。我想知道访问元素的性能。例如

Void f()
{
   // v[i] appears many times, each time use v[i] to access it
   // the other way is introduce a variable to record it like 
   auto& e = v[i];
}

哪个更好?或者它们是一样的。

【问题讨论】:

  • 不要猜测,只需尝试两者并测量(当然要启用优化)。如果您无法衡量差异,那也没关系。
  • 编写优雅的代码。让优化器担心性能。 担心性能的时候是当你拥有最优雅的算法、没有阻塞、良好的缓存并发并且你的用户抱怨 CPU 负载时​​。相信我,那是几年之后的事了。
  • 请注意,使用引用可能很危险,因为如果由于插入操作而重新分配底层内存,您可能会得到一个悬空引用。
  • 简短的回答是“视情况而定”。如果没有看到实际的代码,我们真的无法猜测。 (向量是否有变化?是否有代码调用可能会改变向量但碰巧没有?)

标签: c++ performance stl


【解决方案1】:

一般来说,当您使用v[i] 时,您会调用成员函数std::vector::operator[]。如果您多次使用它,那么您会多次调用一个函数。多次调用非内联函数会导致堆栈切换,因此需要更多时间。而将结果(引用)存储在变量中只调用一次函数。

现在,大多数体面的编译器都可以优化调用(甚至内联它),因此您很可能不会看到差异。但是,最好的方法是使用分析器对其进行测试并查看结果。

EDIT1请阅读this。它讨论了类似的事情,在循环条件内多次调用函数可能会导致性能下降。

EDIT2 我在我的机器(gcc5、i5 8GB RAM)上测试了您的代码并对其进行了计时,代码如下。打开优化器后,没有区别(g++)。如果没有优化,参考版本的速度是原来的两倍。

#include <iostream>
#include <chrono>
#include <vector>

auto timing = [](auto && F, auto && ... params)
{
    auto start = std::chrono::steady_clock::now();
    std::forward<decltype(F)>(F)(std::forward<decltype(params)>(params)...);
    return std::chrono::duration_cast<std::chrono::milliseconds>(
               std::chrono::steady_clock::now() - start).count();
};

std::vector<int> v(1024);

long long f(std::size_t i) // we pick the i-th element
{
    auto& e = v[i];
    long long sum = 0;
    for(volatile std::size_t k = 0 ; k < 1000000000; ++k)
        sum += e;
    return sum;
}

long long g(std::size_t i)
{
    long long sum = 0;
    for(volatile std::size_t k = 0 ; k < 1000000000; ++k)
        sum += v[i];
    return sum;
}

int main()
{
    for(std::size_t i = 0; i < 1024; ++i)
        v[i] = i * i;

    auto time0 = timing(f, 42);
    auto time1 = timing(g, 42);

    std::cout << time0 << std::endl;
    std::cout << time1 << std::endl;
}

Live on Coliru, with optimizations turned on Live on Coliru, no optimizations

【讨论】:

  • “多次调用非内联函数会导致上下文切换”——这是什么废话?
  • @KarolyHorvath 我使用了错误的术语,已更正。我不知道它是怎么调用的,但基本上你切换堆栈框架。
  • @vsoftco 优化器将忽略对 operator[] 的所有冗余调用。这不是问题。
  • 即使没有明确标记inline,编译器也很可能会内联调用
  • @M.M 这似乎确实发生了,并且在答案中也提到了。在我的机器上,开启优化时没有区别。
【解决方案2】:

信不信由你,它可以产生巨大的影响(即使考虑到缓存命中,使得对同一元素的一次又一次访问非常有效)。

这是一些测试代码:

#include <vector>
#include <cstdint>
#include <iostream>

static inline std::uint64_t RDTSC()
{
  unsigned int hi, lo;
  __asm__ volatile("rdtsc" : "=a" (lo), "=d" (hi));
  return ((uint64_t)hi << 32) | lo;
}

void v1(const std::vector<double> & v, std::vector<double> & ov)
{
    for(int i=0 ; i<100000000 ; ++i)
        ov.push_back(v[5]);
}

void v2(const std::vector<double> & v, std::vector<double> & ov)
{
    auto fixed_var = v[5];
    for(int i=0 ; i<100000000 ; ++i)
        ov.push_back(fixed_var);
}

void v3(const std::vector<double> & v, std::vector<double> & ov)
{
    const double fixed_var = v[5];
    for(int i=0 ; i<100000000 ; ++i)
        ov.push_back(fixed_var);
}

void flush_cache()
{
    //Flush L1 and L2 cache by thrashing it with garbage
    const int cache_size = 256*1024*1024;
    auto garbage = new char[cache_size];
    for(int i=0 ; i < 48; ++i)
    {
        for (int j=0 ; j<cache_size ; j++)
            garbage[j] = i*j;
    }
    delete[] garbage;
    std::cout << "flushed cache\n";
}

                 int main(void)
{
    std::vector<double> v;
    std::vector<double> ov;
    for(int i=0 ; i<10000000 ; ++i)
    {
        v.push_back(i/(i+1000000));
    }

    //try v1
    auto start = RDTSC();
    v1(v,ov);
    auto end = RDTSC();
    auto v1t = end-start;
    std::cout << "V1: 1.0x\n";

    //flush and clear
    ov.clear();
    flush_cache();

    //try v2
    start = RDTSC();
    v2(v,ov);
    end = RDTSC();
    auto v2t = end-start;
    std::cout << "V2: " << ((double)v2t)/v1t << "x\n";

    //flush and clear
    ov.clear();
    flush_cache();

    //try v3
    start = RDTSC();
    v3(v,ov);
    end = RDTSC();
    auto v3t = end-start;
    std::cout << "V3: " << ((double)v3t)/v1t << "x\n";
}

我们看到,幼稚的方法和使用变量之间实际上存在巨大差异:

V1: 1.0x
flushed cache
V2: 0.221311x
flushed cache
V3: 0.222199x

我仔细检查了程序集,以确保没有因为我们没有使用结果而被优化掉。

我们还使用 RTDSC 指令来确保 CPU 周期的时序一致。

V2 和 V3 之间的差异并不显着:它们在任何给定的运行中交换位置。但是不每次都访问数组绝对是速度提升了 70-80%。

请注意,如果您在运行之间不刷新 L1/L2 缓存并在 V2 和 V3 之后运行 V1,它们都会有相似的时序。因此,刷新缓存非常重要。

更多测试表明,g++ 和 c++ 不会对此进行优化(只是将值存储在寄存器中),但英特尔 C++ 会。去图...

【讨论】:

    猜你喜欢
    • 2011-02-13
    • 2021-12-28
    • 2012-01-05
    • 1970-01-01
    • 2012-08-08
    • 1970-01-01
    • 2018-12-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多