【问题标题】:Best way to concatenate and condense a std::vector<std::string>连接和压缩 std::vector<std::string> 的最佳方法
【发布时间】:2020-04-20 10:11:52
【问题描述】:

免责声明:这个问题更多是理论上的,而不是实际的兴趣。我想找出各种不同的方法来做这件事,速度就像新年蛋糕上的糖霜。

问题

我希望能够存储一个字符串列表,并能够在需要时将它们快速组合为 1。
简而言之,我想浓缩一个结构(目前是std::vector&lt;std::string&gt;),看起来像

["Hello, ", "good ", "day ", " to", " you!"]

["Hello, good day to you!"]
  • 有没有什么惯用的方法来实现这一点,ala python 的[ ''.join(list_of_strings) ]
  • 就时间而言,在 C++ 中实现这一目标的最佳方法是什么?

可能的方法

我的第一个想法是

  • 遍历向量,
  • 将每个元素附加到第一个元素,
  • 同时删除元素。

We will be concatenating with += and reserve()。我假设max_size() will not be reached

方法 1(贪婪方法)

之所以这么称呼是因为它忽略了约定并就地运行。

#if APPROACH == 'G'

    // Greedy Approach
    void condense(std::vector< std::string >& my_strings, int total_characters_in_list)
    {
        // Reserve the size for all characters, less than max_size()
        my_strings[0].reserve(total_characters_in_list);

        // There are strings left, ...
        for(auto itr = my_strings.begin()+1; itr != my_strings.end();)
        {
            // append, and...
            my_strings[0] += *itr;
            // delete, until...
            itr = my_strings.erase(itr);
        }
    }

#endif

现在我知道了,你会说这很冒险而且很糟糕。所以:

  • 循环遍历向量,
  • 将每个元素附加到另一个std::string
  • 清除向量并使字符串成为向量的第一个元素。

方法 2(“安全”避风港)

之所以这么称呼,是因为它在迭代容器时不会修改容器。

#if APPROACH == 'H'

    // Safe Haven Approach
    void condense(std::vector< std::string >& my_strings, int total_characters_in_list)
    {
        // Store the whole vector here
        std::string condensed_string;
        condensed_string.reserve(total_characters_in_list);

        // There are strings left...
        for(auto itr = my_strings.begin(); itr != my_strings.end(); ++itr)
        {
            // append, until...
            condensed_string += *itr;
        }
        // remove all elements except the first
        my_strings.resize(1);
        // and set it to condensed_string
        my_strings[0] = condensed_string;
    }

#endif

现在是标准算法...

使用来自&lt;algorithm&gt;std::accumulate

方法 3(成语?)

之所以这么叫,是因为它是单线的。

#if APPROACH == 'A'

    // Accumulate Approach
    void condense(std::vector< std::string >& my_strings, int total_characters_in_list)
    {
        // Reserve the size for all characters, less than max_size()
        my_strings[0].reserve(total_characters_in_list);

        // Accumulate all the strings
        my_strings[0] = std::accumulate(my_strings.begin(), my_strings.end(), std::string(""));
        // And resize
        my_strings.resize(1);
    }

#endif

为什么不尝试将其全部存储在流中?

使用来自&lt;sstream&gt;std::stringstream

方法 4(字符串流)

之所以这样称呼,是因为 C++ 的流与水流的类比。

#if APPROACH == 'S'

    // Stringstream Approach
    void condense(std::vector< std::string >& my_strings, int) // you can remove the int
    {
        // Create out stream
        std::stringstream buffer(my_strings[0]);

        // There are strings left, ...
        for(auto itr = my_strings.begin(); itr != my_strings.end(); ++itr)
        {
            // add until...
            buffer << *itr;
        }

        // resize and assign
        my_strings.resize(1);
        my_strings[0] = buffer.str();
    }

#endif

但是,也许我们可以使用另一个容器而不是std::vector

那么,还有什么?

(可能)方法 5(伟大的印度“绳索”技巧)

我听说过rope data structure,但不知道是否(以及如何)可以在这里使用它。


基准和判断:

按他们的时间效率排序(目前和令人惊讶的)是1

Approaches          Vector Size: 40     Vector Size: 1600   Vector Size: 64000

SAFE_HAVEN:         0.1307962699997006  0.12057728999934625 0.14202970000042114 
STREAM_OF_STRINGS:  0.12656566000077873 0.12249500000034459 0.14765803999907803
ACCUMULATE_WEALTH:  0.11375975999981165 0.12984520999889354 3.748660090001067   
GREEDY_APPROACH:    0.12164988000004087 0.13558526000124402 22.6994204800023    

2

NUM_OF_ITERATIONS = 100
test_cases = [ 'greedy_approach', 'safe_haven' ]
for approach in test_cases:
    time_taken = timeit.timeit(
        f'system("{approach + ".exe"}")',
        'from os import system',
        number = NUM_OF_ITERATIONS
    )
    print(approach + ": ", time_taken / NUM_OF_ITERATIONS)

我们可以做得更好吗?

更新:我用 4 种方法测试了它(到目前为止),因为我可以在我的小时间里做到这一点。更多即将到来。折叠代码会更好,这样可以在这篇文章中添加更多方法,但是it was declined

1 请注意,这些读数仅用于粗略估计。有a lot of things that influence the execution time,注意这里也有一些不一致的地方。

2 这是旧代码,仅用于测试前两种方法。当前的代码更长,而且集成度更高,所以我不确定是否应该在此处添加。

结论:

  • 删除元素的成本很高。
  • 您应该将字符串复制到某处,然后调整向量的大小。
  • 事实上,如果复制到另一个字符串,最好也保留足够的空间。

【问题讨论】:

  • 我将始终使用标准算法方法,如Combining a vector of strings 的答案中所述。只有当这真的是一个瓶颈时,我才会尝试优化它。但我不认为你可以在那里做很多关于性能的事情,所以我会选择最易读的一个。
  • @t.niese 有趣。让我对它进行基准测试。任何其他想法也将不胜感激。
  • 在创建基准时始终需要小心。它们不一定反映这些方法在实际应用中的表现。大多数情况下,只有在实际应用程序中进行基准测试才有意义,但即便如此,也会有很多副作用,影响结果,因为这些副作用不容易正确解释。
  • 环境变量、链接库、启动路径……所有这些都会影响内存布局和缓存,因此会对性能产生很大影响。因此,这不仅会影响这些基准,还会影响实际用例。这是一个完整的讨论:"Performance Matters" by Emery Berger
  • 您对std::vector::erase 的使用会调用未定义的行为。将其返回值分配给itr 以解决此问题。

标签: c++ string performance time


【解决方案1】:

默认情况下,我会使用std::stringstream。只需构造蒸汽,从向量中输入所有字符串,然后返回输出字符串。它不是很有效,但很清楚它的作用。

在大多数情况下,处理字符串和打印时不需要快速方法 - 因此“易于理解和安全”的方法更好。另外,现在的编译器擅长在简单的情况下优化低效率。

最有效的方法……这是一个难题。一些应用需要多方面的效率。在这些情况下,您可能需要使用多线程。

【讨论】:

    【解决方案2】:

    使用range-v3(很快使用C++20 ranges),您可以这样做:

    std::vector<std::string> v{"Hello, ", "good ", "day ", " to", " you!"};
    std::string s = v | ranges::view::join;
    

    Demo

    【讨论】:

    • 另一种有趣的方法。在我也测试这种方法的同时,请耐心等待。谢谢!
    【解决方案3】:

    就个人而言,我会构造第二个向量来保存单个“压缩”字符串,构造压缩字符串,然后在完成后交换向量。

     void Condense(std::vector<std::string> &strings)
     {
          std::vector<std::string> condensed(1);          // one default constructed std::string
          std::string &constr = &condensed.begin();      // reference to first element of condensed
          for (const auto &str : strings)
             constr.append(str);
    
          std::swap(strings, condensed);                //  swap newly constructed vector into original
     }
    

    如果由于某种原因抛出异常,则原始向量保持不变,并进行清理 - 即此函数提供了强大的异常保证。

    可选地,为了减少“压缩”字符串的大小调整,在上面初始化 constr 之后,可以这样做

           //  optional:  compute the length of the condensed string and reserve
          std::size_t total_characters_in_list = 0;
          for (const auto &str : strings)
              total_characters_in_list += str.size();
          constr.reserve(total_characters_in_list);
           //   end optional reservation
    

    这与替代方案相比效率如何,这取决于。我也不确定它是否相关 - 如果字符串继续被附加到向量,并且需要被附加,那么从某处获取字符串(并将它们附加到向量)的代码很有可能会有一个对程序性能的影响比压缩它们的行为更大。

    【讨论】:

    • “从某个地方获取字符串(并将它们附加到向量)的代码很有可能对程序性能产生比压缩它们更大的影响......”和这也是我要测试的。但请期待明天之前不会有任何回复。
    【解决方案4】:

    你也可以试试std::accumulate:

    auto s = std::accumulate(my_strings.begin(), my_strings.end(), std::string());
    

    不会更快,但至少更紧凑。

    【讨论】:

    • 事实上,我倾向于将其称为成语。谢谢!
    猜你喜欢
    • 2017-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-17
    • 2015-10-20
    相关资源
    最近更新 更多