我试图比较字符串向量和指向字符串的向量的STL排序的性能。
我预计指针版本会跑赢大盘,但500万随机生成的字符串的实际结果是
字符串向量:12.06秒
指向字符串的向量:16.75秒
这种行为有什么解释?我希望交换指向字符串的指针应该比交换字符串对象更快。
通过转换随机整数生成了500万个字符串
编译(gcc 4.9.3):g++ -std=c++11 -Wall
CPU:Xeon X5650
// sort vector of strings
int main(int argc, char *argv[])
{
const int numElements=5000000;
srand(time(NULL));
vector<string> vec(numElements);
for (int i = 0; i < numElements; i++)
vec[i] = std::to_string(rand() % numElements);
unsigned before = clock();
sort(vec.begin(), vec.end());
cout<< "Time to sort: " << clock() - before << endl;
for (int i = 0; i < numElements; i++)
cout << vec[i] << endl;
return 0;
}
// sort vector of pointers to strings
bool comparePtrToString (string *s1, string *s2)
{
return (*s1 < *s2);
}
int main(int argc, char *argv[])
{
const int numElements=5000000;
srand(time(NULL));
vector<string *> vec(numElements);
for (int i = 0; i < numElements; i++)
vec[i] = new string( to_string(rand() % numElements));
unsigned before = clock();
sort(vec.begin(), vec.end(), comparePtrToString);
cout<< "Time to sort: " << clock() - before << endl;
for (int i = 0; i < numElements; i++)
cout << *vec[i] << endl;
return 0;
}
答案 0 :(得分:5)
这是因为sort
在strings
上执行的所有操作都是移动和交换。移动和交换std::string
都是常量时间操作,这意味着它们只涉及更改一些指针。
因此,对于这两种类型的数据移动具有相同的性能开销。但是,在指向字符串的情况下,您需要支付一些额外的费用来取消引用每个比较的指针,这会导致它明显变慢。
答案 1 :(得分:2)
在第一种情况下,字符串表示的内部指针是交换,而不是复制的完整数据。
您不应期望使用指针实现任何好处,实际上速度较慢,因为指针必须解除引用,以执行比较。< / p>
答案 2 :(得分:2)
这种行为有什么解释?我期望交换指向字符串的指针 应该比交换字符串对象更快。
这里发生的各种事情可能影响绩效。
交换相对便宜两种方式。交换字符串往往是一个浅层操作(只是交换POD,如指针和积分),对于大字符串,可能深度为小字符串(但仍然相当便宜 - 依赖于实现)。因此,交换字符串总体而言相当便宜,并且通常不比简单地交换指针更昂贵*。
[sizeof(string)
大于sizeof(string*)
,但它并不是一个天文上的差异,基本上操作仍然是
在这种情况下,它会在固定时间内发生,并且相当便宜
当字符串字段必须被提取到更快的形式时
比较器的记忆,给我们时间局部性
尊重其领域。]
无论如何都必须访问字符串内容。甚至比较器的指针版本也必须检查字符串内容(包括指定size
和{{1}的字段})。结果,我们最终支付了为字符串内容获取数据的内存成本。当然,如果您只是通过指针地址对字符串进行排序(例如:不使用比较器)而不是字符串内容的字典比较,则性能边缘应该转向指针版本,因为这样可以大大减少访问的数据量,同时改善空间locality(更多的指针可以放在缓存行中而不是字符串,例如)。
指针版本散布(或至少增加内存中字符串字段的步幅)。对于指针版本,您需要在免费商店中分配每个字符串(除了可以在免费商店分配或不分配的字符串内容)。这可以分散内存并减少引用的局部性,因此您可能会在比较器中以更高的缓存未命中率增加成本。即使这种顺序分配导致分配了一组非常连续的页面(理想情况),从一个字符串的字段到下一个字段的步幅往往至少会大一点,因为分配元数据/对齐开销(并非所有分配器都要求元数据直接存储在块中,但通常它们至少会为块大小增加一些小的开销)。
将此归因于取消引用指针的成本可能更简单,但与执行内存寻址的capacity
指令的成本相差不大(在此相对中)从没有缓存/分页的较慢/较大形式的内存加载到更快,更小的内存。在免费商店中单独分配每个字符串通常会增加此成本,无论是由于连续性丢失还是每个字符串条目之间的较大恒定步幅(在理想情况下)。
即使在基本级别上,如果不太努力地诊断内存级别发生的事情,这也会增加机器必须查看的数据的总大小(字符串内容/字段+指针地址)。除了减少局部性/更大或可变步幅(通常,如果增加访问的数据量,它必须至少具有改进的局部性以便有可能获益)。如果您只是将指针排序到连续分配的字符串(不是根据我们无法控制的字符串内容,而是根据相邻字符串对象本身的连续性),您可能会开始看到更多可比较的时间 - 有效地指向存储在数组中的字符串)。然后,除了将相关数据打包在一个连续的空间内之外,至少还要为字符串字段取回空间位置。
交换较小的数据类型(如索引或指针)有时可以提供一个好处,但它们通常需要避免检查它们引用的数据的原始内容或提供明显更便宜的交换/移动行为(在这种情况下,字符串已经很便宜,在这种情况下考虑时间局部性或两者都变得更便宜。
答案 3 :(得分:1)
嗯,std::string
通常是std::string*
的3-4倍
因此,只需直接交换两个前洗牌内存的更多内存。
但这与以下影响相形见绌:
std::string
每个分配的指针加簿记。两者都对缓存提出了额外的要求,前者甚至无法预取。
答案 4 :(得分:0)
交换容器只更改容器的内容,在字符串情况下是指向字符串第一个字符的指针,而不是整个字符串。
如果是字符串指针的向量,则执行一个额外的步骤 - 转换指针