为什么转置这个 std::vector<std::vector<std::string> > 这么慢?

Why transposing this std::vector<std::vector<std::string> > is so slow?

本文关键字:std gt vector lt string 转置 为什么      更新时间:2023-10-16

我有一个大约 400MB 的 1000 行文件,表示一些表示为字符串的数字数据。 我想转置数据,以便每行只有 1000 个字符串,(这样我就可以打开它并用 pandas 快速绘制它(。

我将整个文件导入到我要转置的字符串向量中(最终我想写回文件(。

我使用两个嵌套循环来遍历 2d 结构,并将其写入一些 std::ofstream 中。它很长。 然后我试着专注于换位,我写了以下代码:

//Read 400MB file, 90K strings per line and 1K lines, and store it into
std::vector<std::vector<std::string>> mData;
// ... 
// IO the file and populate mData with raw data 
// ...
//All rows have same number of string
size_t nbRows = mData.size();
size_t nbCols = mData[0].size();
std::vector<std::vector<std::string> > transposedData(nbCols);
for(size_t i = 0 ; i < nbCols  ; ++i)
{
transposedData[i].resize(nbRows);
for(size_t j = 0 ; j < nbRows ; ++j)
{
transposedData[i][j] = doc.mData[j][i];
}
}

我以为几秒钟就足够了,但这需要几分钟。 此外,我正在尝试使用不同的输入维度(对于 400MB 的相同文件大小,每行只有 3 行和更多的字符串(,而且速度要快得多。

编辑 1

根据人们的建议,我使用callgrind执行了分析。 我在此过程中收到此消息: ...线程 #1 中的 brk 段溢出:无法增长到 ...

我分析了结果并总结在这里:
25% 花在运算符 = 的 basic_string 21% 花在构造basic_string
上(只有 3% 的时间在新的(14% 花在运算符(([] 上 在外部向量上 11% 花在运算符((
[] 上 在内部向
量上

感谢您的建议。

首先,在对一段代码运行缓慢的原因提出任何声明之前,您应该真正测量它在机器上的性能,然后根据手头的数据推断出原因

也就是说,在这种情况下,我很有信心说问题可能在于您正在分配90k字符串向量,每个向量的大小为1k.如您所知,内存分配的成本很高,它可能会解释您的性能损失。

下面介绍如何仅使用预先分配的1D数组来实现代码。

size_t nbRows = mData.size();
size_t nbCols = mData[0].size();
auto get_idx = [](const int i, const int nr, const int j)
{
return i*nr+j;
};
std::vector<std::string> transposedData(nbCols*nbRows);  
for(size_t i = 0 ; i < nbCols  ; ++i)
{
for(size_t j = 0 ; j < nbRows ; ++j)
{
const int idx = get_idx(j, nbCols,i);
transposedData[idx] = std::move(mData[j][i]);
}
}
for(size_t i = 0 ; i < nbCols  ; ++i)
{
for(size_t j = 0 ; j < nbRows ; ++j)
{
const int idx = get_idx(j, nbCols,i);
cout<<transposedData[idx]<<" ";
}
cout<<endl;
}    

我想再次强调:分析你的代码。试用valgrind --tool= callgrindgprof等软件,这些软件可让您分析和可视化有关应用的性能数据。

该程序在多个层面上都有冗余。

显而易见的是,您无需转置矢量即可转置文件。

vector<vector<string> originalData;
// read the file to originalData
for(size_t i = 0 ; i < nbCols  ; ++i)
{
for(size_t j = 0 ; j < nbRows ; ++j)
{
cout << originalData[j][i] << " ";
}
cout<<endl;
}

假设由于某种原因确实需要生成转置向量,编写转置循环的一种方法是

vector<vector<string>> transposedData (nbCols);
for (size_t j = 0; j < nbCols; ++j)
{
transposedData[j].reserve(nrows);
for (size_t i = 0; i < nbRows; ++i) 
{
transposedData[j].emplace_back(originalData[i][j]);
// if keeping original veector is not needed ...
// transposedData[j].emplace_back(std::move(originalData[i][j]));
}
}

在我的(相当强大的(机器上,转置 1000x90000 的 3 个字符字符串矩阵大约需要 6-7 秒。这并不特别令人印象深刻,如果您不需要一天 24 小时转置数百万个元素的矩阵,它可以满足您的需求而不会产生太多开销。

惩罚可能来自您在 for 循环中过度使用调整大小的事实。

根据参考资料:

复杂性

当前大小和计数之间的差异呈线性。如果容量小于计数,则可能由于重新分配而增加复杂性

内存分配成本很高,因此您可能希望避免过度分配。

正如其他人所指出的,前期分配将是一种有趣的方法,可以避免每次都重新创建(调整(矢量大小。

我的计算机上没有足够的可用内存来执行此任务(见下文(。 将我的数据分成三部分,我在几秒钟内解决了任务。 这是检查内存的代码的输出:

free ram 2.5GB  
IO populating mData with raw data  
free ram 0.2GB  
Empty string capacity : 15 bytes  
Intending to allocate 1.4 GB  
terminate called after throwing an instance of 'std::bad_alloc'  
what() : std::bad_alloc  
Aborted