[英]Performance comparison of STL sort on vector of strings vs. vector of string pointers
我试图比较字符串向量和字符串指针向量的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;
}
这是因为对strings
执行sort
所有操作都是移动和交换。 移动和交换std::string
都是常量时间操作,这意味着它们只涉及更改一些指针。
因此,对于这两种类型的数据移动具有相同的性能开销。 但是,在指向字符串的情况下,您需要支付一些额外的费用来取消引用每个比较的指针,这会导致它明显变慢。
在第一种情况下, 交换字符串表示的内部指针 ,而不是复制的完整数据。
您不应期望使用指针实现任何好处,实际上速度较慢,因为指针必须另外解除引用 ,以执行比较。
什么解释了这种行为? 我期望交换指向字符串的指针应该比交换字符串对象更快。
这里有各种各样的事情会影响性能。
交换相对便宜两种方式。 对于大字符串,交换字符串往往是一个浅操作(只是交换像指针和积分的POD),对于小字符串来说可能很深(但仍然非常便宜 - 依赖于实现)。 因此,交换字符串整体上往往相当便宜,并且通常不比简单地交换指针更昂贵*。
[ sizeof(string)
比sizeof(string*)
大得多,但它并不是天文上的差异,因为操作仍然在恒定时间内发生,并且在这种情况下,当字符串字段必须被提取到时,相当便宜一些比较器的更快记忆形式,为我们提供了与其字段相关的时间局部性。
无论如何都必须以两种方式访问字符串内容。 甚至比较器的指针版本也必须检查字符串内容(包括指定size
和capacity
的字段)。 结果,我们最终支付了为字符串内容获取数据的内存成本。 当然,如果您只是通过指针地址对字符串进行排序(例如:不使用比较器)而不是字符串内容的字典比较,则性能边缘应该转向指针版本,因为这样可以大大减少访问的数据量,同时改善空间locality(更多的指针可以放在缓存行中而不是字符串,例如)。
指针版本散布(或至少增加了内存中字符串字段的步幅)。 对于指针版本,您要在免费商店中分配每个字符串(除了可以在免费商店上分配或不分配的字符串内容)。 这可以分散内存并减少引用的局部性,因此在比较器中可能会因增加缓存未命中而导致更高的成本。 即使这种顺序的顺序分配导致分配一组非常连续的页面(理想情况),由于分配元数据/对齐,从一个字符串的字段到下一个字段的步幅往往会变得更小。开销(并非所有分配器都要求元数据直接存储在块中,但通常它们至少会为块大小增加一些小开销)。
将此归因于解除引用指针的成本可能更简单,但这并不是执行内存寻址的mov/load
指令的代价,因为从较慢/较大形式的内存加载,这是昂贵的(在此相对上下文中)已经缓存/分页到更快,更小的内存。 在免费商店中单独分配每个字符串通常会增加此成本,无论是由于连续性丢失还是每个字符串条目之间的较大恒定步幅(在理想情况下)。
即使在基本级别上也没有太难以诊断内存级别发生的事情,这会增加机器必须查看的数据的总大小(字符串内容/字段+指针地址)以及减少的局部性/更大或变量步幅(通常如果增加访问的数据量,它必须至少具有改进的局部性以便有可能获益)。 如果您只是将指针排序到连续分配的字符串(不是根据我们无法控制的字符串内容,而是根据相邻字符串对象本身的连续性),您可能会开始看到更多可比较的时间 - 有效地指向存储在数组中的字符串)。 然后,除了将相关联的数据打包在一个连续的空间内之外,至少还要为字符串字段返回空间局部性。
交换较小的数据类型(如索引或指针)有时可以提供好处,但它们通常需要避免检查它们引用的数据的原始内容,或者提供明显更便宜的交换/移动行为(在这种情况下,字符串已经很便宜并且变得更便宜了考虑时间局部性的这种情况或两者兼而有之。
好吧, std::string
通常是std::string*
3-4倍。
所以,直接交换两个前洗牌,更多的记忆。
但这与以下影响相形见绌:
std::string
每个分配的指针加簿记。 两者都对缓存提出了额外的要求,前者甚至无法预取。
交换容器只更改容器的内容,在字符串情况下是指向字符串的第一个字符的指针,而不是整个字符串。
如果是字符串指针的向量,则执行一个额外的步骤 - 转换指针
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.