我有一个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;
}
这比主要行转换的朴素循环要快得多,但在我的输入大小上仍然比朴素列主要循环要慢近一个数量级。
我的问题,是否存在一种更快的方法来将双精度矢量的(列主)矢量转换为单个连续的行主矢量?我正在努力思考该代码的速度限制应该是多少,从而质疑我是否缺少明显的东西。我的假设是,阻塞会给我比实际带来的更大的加速。
该图表是使用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);
答案 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
您说得对,要并行处理的数据集太小。
而且也太小了。
尽管我将发布帖子供其他人参考这些功能。