【问题标题】:boost::tokenizer vs boost::splitboost::tokenizer 与 boost::split
【发布时间】:2011-12-17 08:44:27
【问题描述】:

我正在尝试将每个 '^' 字符上的 c++ 字符串解析为向量标记。我一直使用 boost::split 方法,但我现在正在编写性能关键代码,想知道哪一个能提供更好的性能。

例如:

string message = "A^B^C^D";
vector<string> tokens;
boost::split(tokens, message, boost::is_any_of("^"));

对比

boost::char_separator<char> sep("^");
boost::tokenizer<boost::char_separator<char> > tokens(text, sep);

哪一个会提供更好的性能,为什么?

【问题讨论】:

  • 描述它,然后告诉我们。
  • 第二个看起来不做任何内存分配,所以我猜这会更快。不过,只有一种方法可以确定。
  • Boost.Spirit.Qi 将大大优于两者。
  • @ildjarn:同时将编译时间提高了一个数量级。
  • @Nicol : 预编译的头文件多年来一直是标准的。如果你不利用它们,那是你的问题。 ;-]

标签: c++ boost


【解决方案1】:

最佳选择取决于几个因素。如果您只需要扫描一次令牌,那么 boost::tokenizer 在运行时和空间性能方面都是不错的选择(这些令牌向量可能会占用大量空间,具体取决于输入数据。)

如果您要经常扫描令牌,或者需要具有高效随机访问的向量,那么将 boost::split 转换为向量可能是更好的选择。

例如,在标记长度为 1 字节的“A^B^C^...^Z”输入字符串中,boost::split/vector&lt;string&gt; 方法将消耗至少 2 *N-1 个字节。使用大多数 STL 实现中存储字符串的方式,您可以计算出它需要超过 8 倍的数量。将这些字符串存储在向量中会耗费大量内存和时间。

我在我的机器上进行了一次快速测试,具有 1000 万个令牌的类似模式如下所示:

  • boost::split = 2.5s~620MB
  • boost::tokenizer = 0.9s0MB

如果您只是对令牌进行一次性扫描,那么显然令牌生成器会更好。 但是,如果您要分解成一个结构并希望在应用程序的生命周期内重用,那么最好使用标记向量。

如果你想走矢量路线,那么我建议不要使用vector&lt;string&gt;,而是使用 string::iterators 的矢量。只需分解成一对迭代器并保留一大串令牌以供参考。例如:

using namespace std;
vector<pair<string::const_iterator,string::const_iterator> > tokens;
boost::split(tokens, s, boost::is_any_of("^"));
for(auto beg=tokens.begin(); beg!=tokens.end();++beg){
   cout << string(beg->first,beg->second) << endl;
}

这个改进的版本需要 1.6s390MB 在同一台服务器上进行测试。而且,最重要的是,这个向量的内存开销与令牌的数量成线性关系——不依赖于令牌的长度,而 std::vector&lt;string&gt; 存储每个令牌。

【讨论】:

    【解决方案2】:

    我发现使用 clang++ -O3 -std=c++11 -stdlib=libc++ 的结果完全不同。

    首先,我将一个包含约 470k 单词的文本文件提取为一个巨大的字符串,其中用逗号分隔,没有换行符,如下所示:

    path const inputPath("input.txt");
    
    filebuf buf;
    buf.open(inputPath.string(),ios::in);
    if (!buf.is_open())
        return cerr << "can't open" << endl, 1;
    
    string str(filesystem::file_size(inputPath),'\0');
    buf.sgetn(&str[0], str.size());
    buf.close();
    

    然后我运行各种定时测试,将结果存储到一个预先确定大小的向量中,在运行之间清除,例如,

    void vectorStorage(string const& str)
    {
        static size_t const expectedSize = 471785;
    
        vector<string> contents;
        contents.reserve(expectedSize+1);
    
        ...
    
        {
            timed _("split is_any_of");
            split(contents, str, is_any_of(","));
        }
        if (expectedSize != contents.size()) throw runtime_error("bad size");
        contents.clear();
    
        ...
    }
    

    作为参考,定时器就是这样的:

    struct timed
    {
        ~timed()
        {
            auto duration = chrono::duration_cast<chrono::duration<double, ratio<1,1000>>>(chrono::high_resolution_clock::now() - start_);
    
            cout << setw(40) << right << name_ << ": " << duration.count() << " ms" << endl;
        }
    
        timed(std::string name="") :
            name_(name)
        {}
    
    
        chrono::high_resolution_clock::time_point const start_ = chrono::high_resolution_clock::now();
        string const name_;
    };
    

    我还记录了一次迭代(无向量)。结果如下:

    Vector: 
                                  hand-coded: 54.8777 ms
                             split is_any_of: 67.7232 ms
                         split is_from_range: 49.0215 ms
                                   tokenizer: 119.37 ms
    One iteration:
                                   tokenizer: 97.2867 ms
                              split iterator: 26.5444 ms
                split iterator back_inserter: 57.7194 ms
                    split iterator char copy: 34.8381 ms
    

    tokenizer 比split 慢得多,单次迭代的数字甚至不包括字符串副本:

    {
        string word;
        word.reserve(128);
    
        timed _("tokenizer");
        boost::char_separator<char> sep(",");
        boost::tokenizer<boost::char_separator<char> > tokens(str, sep);
    
        for (auto range : tokens)
        {}
    }
    
    {
        string word;
    
        timed _("split iterator");
        for (auto it = make_split_iterator(str, token_finder(is_from_range(',', ',')));
             it != decltype(it)(); ++it)
        {
            word = move(copy_range<string>(*it));
        }
    }
    

    明确的结论:使用split

    【讨论】:

      【解决方案3】:

      这可能取决于您的 boost 版本以及您的功能。

      我们在使用 boost::split 1.41.0 处理数千或数十万个较小字符串(预计少于 10 个令牌)的某些逻辑中遇到了性能问题。当我通过性能分析器运行代码时,我们发现在 boost::split 中花费了令人惊讶的 39% 的时间。

      我们尝试了一些不会对性能产生实质性影响的简单“修复”,例如“我们知道每次传递不会超过 10 个项目,因此将向量预设为 10 个项目”。

      由于我们实际上并不需要向量并且可以迭代标记并完成相同的工作,因此我们将代码更改为 boost::tokenize 并且相同的代码部分减少了运行时间的 1%。

      【讨论】:

        【解决方案4】:

        在生成令牌时对其进行处理是关键。我有一个带有正则表达式的设置,它似乎和 boost::tokenizer 一样快。如果我将匹配项存储在一个向量中,它至少会慢 50 倍

        【讨论】:

          猜你喜欢
          • 2012-10-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-03-31
          • 2021-12-08
          • 1970-01-01
          相关资源
          最近更新 更多