STL排序在字符串矢量与字符串指针矢量上的性能比较

Performance comparison of STL sort on vector of strings vs. vector of string pointers

本文关键字:字符串 性能 比较 性能比 排序 STL 指针      更新时间:2023-10-16

我试图比较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;
    }

这是因为sortstrings执行的所有操作都是移动和交换。std::string的移动和交换都是恒定时间操作,这意味着它们只涉及更改一些指针。

因此,对于这两种类型,数据的移动具有相同的性能开销。然而,如果是指向字符串的指针,则需要在每次比较时支付一些额外的费用来取消引用指针,这会导致比较速度明显较慢。

在第一种情况下,到字符串表示的内部指针被交换,而不是复制完整的数据。

您不应该期望从使用指针的实现中获得任何好处,事实上,指针的实现速度较慢,因为为了执行比较,指针必须被取消引用

是什么解释了这种行为?我期望交换指向字符串的指针应该比交换字符串对象更快。

这里发生了各种可能影响性能的事情。

  1. 交换是相对便宜的两种方式对于大字符串,交换字符串往往是一种浅操作(只是交换指针和积分等POD),对于小字符串,可能是深操作(但仍然相当便宜——取决于实现)。因此,交换字符串总体上往往非常便宜,通常不会比简单地交换指向它们的指针贵多少。

    sizeof(string)肯定比sizeof(string*)大,但这基本上不是天文数字,因为运算仍然在恒定的时间内发生,在这种情况下要便宜得多当字符串字段已经被提取到一个更快的表单中时比较器的内存,为我们提供时间位置关于其领域。]

  2. 字符串内容必须双向访问即使是比较器的指针版本也必须检查字符串内容(包括指定sizecapacity的字段)。因此,不管怎样,我们最终都要为获取字符串内容的数据支付内存成本。当然,如果你只是按指针地址对字符串进行排序(例如:不使用比较器),而不是对字符串内容进行字典式比较,那么性能优势应该向指针版本转移,因为这将大大减少访问的数据量,同时提高空间局部性(例如,可以在缓存行中容纳比字符串更多的指针)

  3. 指针版本正在分散(或至少增加)内存中的字符串字段对于指针版本,您将在免费存储中分配每个字符串(除了可能在免费存储上分配或不分配的字符串内容)。这可以分散内存并减少引用的局部性,因此,通过增加缓存未命中,您可能会在比较器中产生更大的成本。即使这种顺序分配导致分配非常连续的页面集(理想情况),由于分配元数据/对齐开销(并非所有分配器都要求元数据直接存储在块中,但通常它们至少会给块大小增加一些小开销),因此从一个字符串的字段到下一个字符串字段的步长往往会变大一点。

    将其归因于取消引用指针的成本可能更简单,但与其说mov/load指令执行内存寻址的成本很高(在这种相对上下文中),不如说是从尚未缓存/分页的较慢/较大形式的内存加载到较快、较小的内存。在免费存储中单独分配每个字符串通常会增加这一成本,无论是由于相邻性的丢失还是每个字符串条目之间的较大恒定步长(在理想情况下)。

    即使在不太努力地诊断存储器级别上发生的事情的基本级别上,这增加了机器必须查看的数据的总大小(字符串内容/字段+指针地址),此外还减少了局部性/较大或可变的步幅(通常,如果你增加了访问的数据量,它至少必须具有改进的局部性,才有很好的机会受益)。如果您只是对指向连续分配的字符串的指针进行排序,您可能会开始看到更多可比较的时间(不是根据我们无法控制的字符串内容,而是根据相邻的字符串对象本身进行排序——实际上是指向存储在数组中的字符串的指示器)。然后,除了将关联得更紧密的数据打包在一个连续的空间中之外,您还可以至少为字符串字段返回空间位置。

交换较小的数据类型(如索引或指针)有时会带来好处,但它们通常需要避免检查它们所引用的数据的原始内容,或者提供明显更便宜的交换/移动行为(在这种情况下,字符串已经很便宜,在考虑时间位置的情况下会变得更便宜),或者两者兼而有之。

嗯,std::string通常是std::string*的3-4倍大
所以,只要直接交换前两个,就会打乱更多的记忆。

但这与以下影响相比相形见绌:

  1. 引用的位置。您需要再跟随一个指针到一个随机位置来读取字符串
  2. 更多的内存使用:每个std::string的每次分配都有一个指针加上记账

两者都对缓存提出了额外的要求,前者甚至无法预取。

交换容器只更改容器的内容,在字符串的情况下,指针指向字符串的第一个字符,而不是整个字符串。

在字符串指针的矢量的情况下,您执行了一个额外的步骤-强制转换指针