将向量的向量转换为具有相反存储顺序的单个连续向量的更快方法

时间:2019-03-19 02:43:20

标签: c++ performance caching vector

我有一个std::vector<std::vector<double>>,我试图尽快将其转换为单个连续向量。我的向量的形状大致为4000 x 50

问题是,有时我需要按列主要连续顺序输出输出向量(只是将2d输入向量的内部向量连接起来),有时我需要按行主要连续顺序输出输出向量,实际上需要转置。

我发现一个朴素的for循环对于转换为以列为主的向量非常快:

auto to_dense_column_major_naive(std::vector<std::vector<double>> const & vec)
    -> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    for (size_t i = 0; i < n_col; ++i)
        for (size_t j = 0; j < n_row; ++j)
            out_vec[i * n_row + j] = vec[i][j];
    return out_vec;
}

但是显然,由于所有的高速缓存未命中,因此类似的方法对于逐行转换非常慢。因此,对于逐行转换,我认为提高缓存局部性的阻止策略可能是我最好的选择:

auto to_dense_row_major_blocking(std::vector<std::vector<double>> const & vec)
    -> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    size_t block_side = 8;

    for (size_t l = 0; l < n_col; l += block_side) {
        for (size_t k = 0; k < n_row; k += block_side) {
            for (size_t j = l; j < l + block_side && j < n_col; ++j) {
                auto const &column = vec[j];
                for (size_t i = k; i < k + block_side && i < n_row; ++i)
                    out_vec[i * n_col + j] = column[i];
            }
        }
    }
    return out_vec;
}

这比主要行转换的朴素循环要快得多,但在我的输入大小上仍然比朴素列主要循环要慢近一个数量级。

enter image description here

我的问题,是否存在一种更快的方法来将双精度矢量的(列主)矢量转换为单个连续的行主矢量?我正在努力思考该代码的速度限制应该是多少,从而质疑我是否缺少明显的东西。我的假设是,阻塞会给我比实际带来的更大的加速。


该图表是使用QuickBench生成的(并通过我的计算机上的GBench进行了某种程度的验证),其代码如下:(Clang 7,C ++ 20,-O3)

auto to_dense_column_major_naive(std::vector<std::vector<double>> const & vec)
    -> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    for (size_t i = 0; i < n_col; ++i)
        for (size_t j = 0; j < n_row; ++j)
            out_vec[i * n_row + j] = vec[i][j];
    return out_vec;
}

auto to_dense_row_major_naive(std::vector<std::vector<double>> const & vec)
    -> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    for (size_t i = 0; i < n_col; ++i)
        for (size_t j = 0; j < n_row; ++j)
            out_vec[j * n_col + i] = vec[i][j];
    return out_vec;
}

auto to_dense_row_major_blocking(std::vector<std::vector<double>> const & vec)
    -> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    size_t block_side = 8;

    for (size_t l = 0; l < n_col; l += block_side) {
        for (size_t k = 0; k < n_row; k += block_side) {
            for (size_t j = l; j < l + block_side && j < n_col; ++j) {
                auto const &column = vec[j];
                for (size_t i = k; i < k + block_side && i < n_row; ++i)
                    out_vec[i * n_col + j] = column[i];
            }
        }
    }
    return out_vec;
}

auto to_dense_column_major_blocking(std::vector<std::vector<double>> const & vec)
    -> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    size_t block_side = 8;

    for (size_t l = 0; l < n_col; l += block_side) {
        for (size_t k = 0; k < n_row; k += block_side) {
            for (size_t j = l; j < l + block_side && j < n_col; ++j) {
                auto const &column = vec[j];
                for (size_t i = k; i < k + block_side && i < n_row; ++i)
                    out_vec[j * n_row + i] = column[i];
            }
        }
    }
    return out_vec;
}

auto make_vecvec() -> std::vector<std::vector<double>>
{
  std::vector<std::vector<double>> vecvec(50, std::vector<double>(4000));
  std::mt19937 mersenne {2019};
  std::uniform_real_distribution<double> dist(-1000, 1000);
  for (auto &vec: vecvec)
   for (auto &val: vec)
       val = dist(mersenne);
  return vecvec;
}

static void NaiveColumnMajor(benchmark::State& state) {
  // Code before the loop is not measured

  auto vecvec = make_vecvec();
  for (auto _ : state) {
    benchmark::DoNotOptimize(to_dense_column_major_naive(vecvec));
  }
}
BENCHMARK(NaiveColumnMajor);

static void NaiveRowMajor(benchmark::State& state) {
  // Code before the loop is not measured

  auto vecvec = make_vecvec();
  for (auto _ : state) {
    benchmark::DoNotOptimize(to_dense_row_major_naive(vecvec));
  }
}
BENCHMARK(NaiveRowMajor);

static void BlockingRowMajor(benchmark::State& state) {
  // Code before the loop is not measured

  auto vecvec = make_vecvec();
  for (auto _ : state) {
    benchmark::DoNotOptimize(to_dense_row_major_blocking(vecvec));
  }
}
BENCHMARK(BlockingRowMajor);

static void BlockingColumnMajor(benchmark::State& state) {
  // Code before the loop is not measured

  auto vecvec = make_vecvec();
  for (auto _ : state) {
    benchmark::DoNotOptimize(to_dense_column_major_blocking(vecvec));
  }
}
BENCHMARK(BlockingColumnMajor);

2 个答案:

答案 0 :(得分:8)

首先,只要某物被认定为“显而易见”,我就会畏缩。这个词通常用来掩盖自己推论的一个缺点。

  

但是显然,由于所有的高速缓存未命中,因此类似的方法对于逐行转换非常慢。

我不确定哪个应该是显而易见的:将显示逐行转换,或者由于高速缓存未命中而导致速度变慢。无论哪种情况,我都觉得不太明显。毕竟,这里有两个缓存注意事项,不是吗?一种阅读,一种写作?让我们从阅读的角度来看代码:

row_major_naive

for (size_t i = 0; i < n_col; ++i)
    for (size_t j = 0; j < n_row; ++j)
        out_vec[j * n_col + i] = vec[i][j];

vec的连续读取是对连续内存的读取:vec[i][0]后跟vec[i][1],以此类推。非常适合缓存。所以...缓存未命中?慢? :)可能不太明显。

仍然,可以从中收集一些东西。仅通过“明显”主张才是错误的主张。存在非本地性问题,但它们发生在写作端。 (成功的写入被50个double值的空格所抵消。)经验测试证实了这种缓慢性。因此,也许一种解决方案是翻转所谓的“显而易见”的东西?

行主要翻转

for (size_t j = 0; j < n_row; ++j)
    for (size_t i = 0; i < n_col; ++i)
        out_vec[j * n_col + i] = vec[i][j];

我在这里所做的只是扭转循环。从字面上交换那两行代码的顺序,然后调整缩进。现在,连续的读取可能遍及整个地方,因为它们是从不同的向量读取的。但是,连续写入现在是对连续的内存块的。从某种意义上说,我们处于与以前相同的情况。但是就像以前一样,人们应该在假设“快”或“慢”之前测量性能。

NaiveColumnMajor:3.4秒
NaiveRowMajor:7.7秒
FlippedRowMajor:4.2秒
BlockingRowMajor:4.4秒
BlockingColumnMajor:3.9秒

仍然比朴素的列主要转换要慢。但是,这种方法不仅比朴素的行专业快,而且比 blocking 行专业快。至少在我的计算机上(显然使用gcc -O3 :P进行了数千次迭代)。里程可能会有所不同。我不知道花哨的剖析工具会说些什么。关键是有时候越简单越好。

对于有趣的事情,我进行了一个尺寸互换的测试(从40个元素的50个向量更改为50个元素的4000个向量)。所有方法都受到这种伤害,但是“ NaiveRowMajor”受到的打击最大。值得一提的是,“翻转行专业”落后于阻止版本。因此,正如人们可能期望的那样,最适合该工作的工具取决于该工作到底是什么。

NaiveColumnMajor:3.7秒
NaiveRowMajor: 16
FlippedRowMajor:5.6秒
BlockingRowMajor:4.9秒
BlockingColumnMajor:4.5秒

(顺便说一句,我还尝试了阻止版本的翻转技巧。更改很小-约为0.2,与翻转朴素版本相反。也就是说,“翻转阻止”的速度慢于对该问题的4000个向量中的50个向量进行“阻止”,但对我的4000个50个变体进行“阻止”。微调可能会改善结果。)


更新:我对阻止版本的翻转技巧进行了更多测试。这个版本有四个循环,因此“翻转”不像只有两个循环时那样简单。似乎交换两个外部循环的顺序对性能不利,而交换两个内部循环的循环则有利。 (起初,我两者都做过,并且得到了混合的结果。)当我只交换内部循环时,我测量了 3.8秒(在4000-of-50情况下为4.1秒),这使其成为最佳选择我的测试中的行优先选项。

行主要混合动力

for (size_t l = 0; l < n_col; l += block_side)
    for (size_t i = 0; i < n_row; ++i)
        for (size_t j = l; j < l + block_side && j < n_col; ++j)
            out_vec[i * n_col + j] = vec[j][i];

(交换内部循环后,我合并了中间循环。)

关于背后的理论,我想这相当于试图一次写入一个缓存块。写入块后,请尝试重新使用向量(vec[j]),然后再将其从缓存中弹出。耗尽所有源向量后,继续使用新的一组源向量,再次一次写入完整的块。

答案 1 :(得分:0)

我刚刚添加了并行版本的东西的两个功能

#include <ppl.h>

auto ppl_to_dense_column_major_naive(std::vector<std::vector<double>> const & vec)
-> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);

    size_t vecLen = out_vec.size();
    concurrency::parallel_for(size_t(0), vecLen, [&](size_t i)
    {
        size_t row = i / n_row;
        size_t column = i % n_row;

        out_vec[i] = vec[row][column];
    });

    return out_vec;
}

auto ppl_to_dense_row_major_naive(std::vector<std::vector<double>> const & vec)
-> std::vector<double>
{
    auto n_col = vec.size();
    auto n_row = vec[0].size();
    std::vector<double> out_vec(n_col * n_row);
    size_t vecLen = out_vec.size();


    concurrency::parallel_for(size_t(0), vecLen, [&](size_t i)
    {
        size_t column = i / n_col;
        size_t row = i % n_col;

        out_vec[i] = vec[row][column];
    });

    return out_vec;
}

以及所有其他基准代码

template< class _Fn, class ... Args >
auto callFncWithPerformance( std::string strFnName,  _Fn toCall, Args&& ...args )
{
    auto start = std::chrono::high_resolution_clock::now();
    auto toRet = toCall( std::forward<Args>(args)... );
    auto end = std::chrono::high_resolution_clock::now();

    std::chrono::duration<double> diff = end - start;

    std::cout << strFnName << ": " << diff.count() << " s" << std::endl;

    return toRet;
}

template< class _Fn, class ... Args >
auto second_callFncWithPerformance(_Fn toCall, Args&& ...args)
{
    std::string strFnName(typeid(toCall).name());

    auto start = std::chrono::high_resolution_clock::now();
    auto toRet = toCall(std::forward<Args>(args)...);
    auto end = std::chrono::high_resolution_clock::now();

    std::chrono::duration<double> diff = end - start;

    std::cout << strFnName << ": " << diff.count() << " s";

    return toRet;
}


#define MAKEVEC( FN, ... ) callFncWithPerformance( std::string( #FN ) , FN  , __VA_ARGS__ )


int main()
{
    //prepare vector
    auto vec = make_vecvec();

    std::vector< double > vecs[]
    {
        std::vector<double>(MAKEVEC(to_dense_column_major_naive, vec)),
        std::vector<double>(MAKEVEC(to_dense_row_major_naive, vec)),
        std::vector<double>(MAKEVEC(ppl_to_dense_column_major_naive, vec)),
        std::vector<double>(MAKEVEC(ppl_to_dense_row_major_naive, vec)),
        std::vector<double>(MAKEVEC(to_dense_row_major_blocking, vec)),
        std::vector<double>(MAKEVEC(to_dense_column_major_blocking, vec)),
    };

    //system("pause");

    return 0;
}

以及这些结果的下方

  

调试x64

     

to_dense_column_major_naive:0.166859 s
  to_dense_row_major_naive:0.192488 s
  ppl_to_dense_column_major_naive:0.0557423 s
  ppl_to_dense_row_major_naive:0.0514017 s
  to_dense_column_major_blocking:0.118465 s
  to_dense_row_major_blocking:0.117732 s

     

调试x86

     

to_dense_column_major_naive:0.15242 s
  to_dense_row_major_naive:0.158746 s
  ppl_to_dense_column_major_naive:0.0534966 s
  ppl_to_dense_row_major_naive:0.0484076 s
  to_dense_column_major_blocking:0.111217 s
  to_dense_row_major_blocking:0.107727 s

     

发布x64

     

to_dense_column_major_naive:0.000874 s
  to_dense_row_major_naive:0.0011973 s
  ppl_to_dense_column_major_naive:0.0054639 s
  ppl_to_dense_row_major_naive:0.0012034 s
  to_dense_column_major_blocking:0.0008023 s
  to_dense_row_major_blocking:0.0010282 s

     

发布x86

     

to_dense_column_major_naive:0.0007156 s
  to_dense_row_major_naive:0.0012538 s
  ppl_to_dense_column_major_naive:0.0053351 s
  ppl_to_dense_row_major_naive:0.0013022 s
  to_dense_column_major_blocking:0.0008761 s
  to_dense_row_major_blocking:0.0012404 s

您说得对,要并行处理的数据集太小。
而且也太小了。
尽管我将发布帖子供其他人参考这些功能。