【问题标题】:Fast String tokenization in C/C++C/C++ 中的快速字符串标记化
【发布时间】:2011-04-14 15:33:09
【问题描述】:

我正在开发一个 C/C++ 应用程序(在 Visual Studio 2010 中),我需要在其中标记一个逗号分隔的字符串,我希望这尽可能快。目前我正在使用strtok_s。我对strtok_ssscanf 进行了一些测试,似乎strtok_s 更快(除非我写了一个糟糕的实现:))但我想知道是否有人可以提出更快的替代方案。

【问题讨论】:

  • 在 C 语言中,我认为您找不到比strtok() 更快的替代方案(甚至是strtok_s()——不管是什么)。如果strtok() 适合您,请使用它。
  • Nitpick:没有这样的语言叫做“C/C++”。是 C 还是 C++?它们是不同的。
  • 感谢大家的快速解答,最初我的思路和pmg一样。不过,为了彻底起见,我将对这些答案进行原型制作,然后对它们进行计时,并将结果发布以供将来参考。
  • 许多事情的最佳实践在 C 和 C++ 之间是不同的。如果您正在使用一种语言,您需要该语言的最佳实践。如果您同时使用这两种方法,您可能需要两种不同的最佳做法。
  • @David 没错,我基本上是想问这些是什么,哪个是最快的。我只是想比较它们,因为我可以使用这两个选项,最重要的是我需要它尽可能快。我稍后会发布结果 :)

标签: c++ c string visual-studio-2010 tokenize


【解决方案1】:

对于纯粹的运行速度,boost.spirit.qi 是一个很好的候选者。

【讨论】:

    【解决方案2】:

    您能做的最好的事情是确保您只遍历字符串一次,并即时构建输出。开始将字符提取到临时缓冲区中,当遇到分隔符时,将临时缓冲区保存到输出集合中,清除临时缓冲区,冲洗并重复。

    这是执行此操作的基本实现。

    template<class C=char>
    struct basic_token
    {
        typedef std::basic_string<C> token_string;
        typedef unsigned long size_type;
        token_string token_, delim_;
        basic_token(const token_string& token, const token_string& delim = token_string());
    };
    
    template<class C>
    basic_token<C>::basic_token(const token_string& token, const token_string& delim)
    :   token_(token),
        delim_(delim)
    {
    }
    
    typedef basic_token<char> token;
    
    template<class Char, class Iter> void tokenize(const std::basic_string<Char>& line, const Char* delims, Iter itx)
    {
        typedef basic_token<Char> Token;
        typedef std::basic_string<Char> TString;
    
        for( TString::size_type tok_begin = 0, tok_end = line.find_first_of(delims, tok_begin);
            tok_begin != TString::npos; tok_end = line.find_first_of(delims, tok_begin) )
        {
            if( tok_end == TString::npos )
            {
                (*itx++) = Token(TString(&line[tok_begin]));
                tok_begin = tok_end;
            }
            else
            {
                (*itx++) = Token(TString(&line[tok_begin], &line[tok_end]), TString(1, line[tok_end]));
                tok_begin = tok_end + 1;
            }
        }
    }
    
    template<class Char, class Iter> void tokenize(const Char* line, const Char* delim, Iter itx)
    {
        tokenize(std::basic_string<Char>(line), delim, itx);
    }
    template<class Stream, class Token> Stream& operator<<(Stream& os, const Token& tok)
    {
        os << tok.token_ << "\t[" << tok.delim_ << "]";
        return os;
    }
    

    ...你会像这样使用它:

    string raw = "35=BW|49=TEST|1346=REQ22|1355=2|1182=88500|1183=88505|10=087^";
    vector<stoken> tokens;
    tokenize(raw, "|", back_inserter(tokens));
    copy(tokens.begin(), tokens.end(), ostream_iterator<stoken>(cout, "\n"));
    

    输出是:

    35=BW   [|]
    49=TEST [|]
    1346=REQ22      [|]
    1355=2  [|]
    1182=88500      [|]
    1183=88505      [|]
    10=087^ []
    

    【讨论】:

      【解决方案3】:

      我会提醒您,使用 strtok 及其类似工具存在风险 您可以取回与您想要的不同数量的令牌。

      one|two|three  would yield 3 tokens
      

      同时

      one|||three    would yield 2.
      

      【讨论】:

        【解决方案4】:

        mmhmm的测试没有正确使用精神,他的语法有缺陷。

        #include <cstdio> 
        #include <cstring>   
        
        #include <iostream>
        #include <string>
        
        #include <boost/fusion/include/adapt_struct.hpp>
        #include <boost/fusion/include/io.hpp>
        
        #include <boost/spirit/include/qi.hpp>    
        
        /****************************strtok_r************************/
        typedef struct sTokenDataC {
            char *time;
            char *symb;
            float bid;
            float ask;
            int bidSize;
            int askSize;
        } tokenDataC;
        
        tokenDataC parseTick( char *line, char *parseBuffer )
        {
            tokenDataC tokenDataOut;
        
            tokenDataOut.time = strtok_r( line,",", &parseBuffer );
            tokenDataOut.symb = strtok_r( nullptr,",", &parseBuffer );
            tokenDataOut.bid = atof(strtok_r( nullptr,",", &parseBuffer ));
            tokenDataOut.ask = atof(strtok_r( nullptr , ",", &parseBuffer ));
            tokenDataOut.bidSize = atoi(strtok_r( nullptr,",", &parseBuffer ));
            tokenDataOut.askSize = atoi(strtok_r( nullptr, ",", &parseBuffer  ));
        
            return tokenDataOut;
        }
        
        void test_strcpy_s(int iteration)
        {
            char *testStringC = new char[64];
            char *lineBuffer = new char[64];
        
            printf("test_strcpy_s....\n");
            strcpy(testStringC,"09:30:00,TEST,13.24,15.32,10,14");
            {
                timeEstimate<> es;
                tokenDataC tokenData2;
                for(int i = 0; i < iteration; i++)
                {
                    strcpy(lineBuffer, testStringC);//this is more realistic since this has to happen because I want to preserve the line
                    tokenData2 = parseTick(lineBuffer, testStringC);
                    //std::cout<<*tokenData2.time<<", "<<*tokenData2.symb<<",";
                    //std::cout<<tokenData2.bid<<", "<<tokenData2.ask<<", "<<tokenData2.bidSize<<", "<<tokenData2.askSize<<std::endl;
                }
            }
        
            delete[] lineBuffer;
            delete[] testStringC;
        }
        /****************************strtok_r************************/
        
        /****************************spirit::qi*********************/
        namespace qi = boost::spirit::qi;
        
        struct tokenDataCPP
        {
            std::string time;
            std::string symb;
            float bid;
            float ask;
            int bidSize;
            int askSize;
        
            void clearTimeSymb(){
                time.clear();
                symb.clear();
            }
        };
        
        BOOST_FUSION_ADAPT_STRUCT(
                tokenDataCPP,
                (std::string, time)
                (std::string, symb)
                (float, bid)
                (float, ask)
                (int, bidSize)
                (int, askSize)
                )
        
        void test_spirit_qi(int iteration)
        {
            std::string const strs("09:30:00,TEST,13.24,15.32,10,14");
            tokenDataCPP data;        
        
            auto myString = *~qi::char_(",");
            auto parser = myString >> "," >> myString >> "," >> qi::float_ >> "," >> qi::float_ >> "," >> qi::int_  >> "," >> qi::int_;
            {
                std::cout<<("test_spirit_qi....\n");
                timeEstimate<> es;
                for(int i = 0; i < iteration; ++i){
                    qi::parse(std::begin(strs), std::end(strs), parser, data);
                    //std::cout<<data.time<<", "<<data.symb<<", ";
                    //std::cout<<data.bid<<", "<<data.ask<<", "<<data.bidSize<<", "<<data.askSize<<std::endl;
                    data.clearTimeSymb();
                }
            }
        }
        /****************************spirit::qi*********************/
        
        int main()
        {
            int const ITERATIONS = 500 * 10000;
            test_strcpy_s(ITERATIONS);
            test_spirit_qi(ITERATIONS);
        }
        

        由于clang++没有strtok_s,我用strtok_r代替 迭代500 * 10k,次数为

        • test_strcpy_s : 1.40951
        • test_spirit_qi : 1.34277

        他们的时间几乎相同,差别不大。

        编译器,clang++ 3.2,-O2

        timeEstime的代码

        【讨论】:

          【解决方案5】:

          这应该很快,没有临时缓冲区,它也分配空令牌。

          template <class char_t, class char_traits_t, 
                  class char_allocator_t, class string_allocator_t>
          inline void _tokenize(
              const std::basic_string<char_t, char_traits_t, char_allocator_t>& _Str, 
              const char_t& _Tok, 
              std::vector<std::basic_string<char_t, char_traits_t, char_allocator_t>, 
                  string_allocator_t>& _Tokens, 
              const size_t& _HintSz=10)
          {
              _Tokens.reserve(_HintSz);
          
              const char_t* _Beg(&_Str[0]), *_End(&_Str[_Str.size()]); 
          
              for (const char_t* _Ptr=_Beg; _Ptr<_End; ++_Ptr)
              {
                  if (*_Ptr == _Tok)
                  {
                      _Tokens.push_back(
                          std::basic_string<char_t, char_traits_t,
                              char_allocator_t>(_Beg, _Ptr));
          
                      _Beg = 1+_Ptr;
                  }
              }
          
              _Tokens.push_back(
                  std::basic_string<char_t, char_traits_t,
                      char_allocator_t>(_Beg, _End));
          }
          

          【讨论】:

            【解决方案6】:

            在对每个建议的候选者进行测试和计时后,结果是 strtok 显然是最快的。尽管我对测试的热爱并不感到惊讶,但值得探索其他选择。 [注意:代码被放在一起编辑欢迎:)]

            给定:

            typedef struct sTokenDataC {
                char *time;
                char *symb; 
                float bid;
                float ask;
                int bidSize;
                int askSize;
            } tokenDataC;
            
            tokenDataC parseTick( char *line, char *parseBuffer )
            {
                tokenDataC tokenDataOut;
            
                tokenDataOut.time = strtok_s( line,",", &parseBuffer );
                tokenDataOut.symb = strtok_s( null,",", &parseBuffer );
                tokenDataOut.bid = atof(strtok_s( null,",", &parseBuffer ));
                tokenDataOut.ask = atof(strtok_s( null , ",", &parseBuffer ));
                tokenDataOut.bidSize = atoi(strtok_s( null,",", &parseBuffer ));
                tokenDataOut.askSize = atoi(strtok_s( null, ",", &parseBuffer  ));
            
                return tokenDataOut; 
            }
            
            char *testStringC = new char[64];
                strcpy(testStringC,"09:30:00,TEST,13.24,15.32,10,14");
            
            int _tmain(int argc, _TCHAR* argv[])
            {
            char *lineBuffer = new char[64];
                printf("Testing method2....\n");
                for(int i = 0; i < ITERATIONS; i++)
                {
                    strcpy(lineBuffer,testStringC);//this is more realistic since this has to happen because I want to preserve the line
                    tokenData2 = parseTick(lineBuffer,parseBuffer);
            
                }
            }
            

            vs 通过以下方式调用 John Diblings impl:

                struct sTokenDataCPP
                {
                    std::basic_string<char> time;
                    std::basic_string<char> symb; 
                    float bid;
                    float ask;
                    int bidSize;
                    int askSize;
                };
                    std::vector<myToken> tokens1;
                        tokenDataCPP tokenData;
                        printf("Testing method1....\n");
                        for(int i = 0; i < ITERATIONS; i++)
                        {
            tokens1.clear();
                            tokenize(raw, ",", std::back_inserter(tokens1));
            
                            tokenData.time.assign(tokens1.at(0).token_);
                            tokenData.symb.assign(tokens1.at(1).token_);
                            tokenData.ask = atof(tokens1.at(2).token_.c_str());
                            tokenData.bid = atof(tokens1.at(3).token_.c_str());
                            tokenData.askSize = atoi(tokens1.at(4).token_.c_str());
                            tokenData.bidSize = atoi(tokens1.at(5).token_.c_str());
            
                        }
            

            与定义语法的简单 boost.spirit.qi 实现如下:

            template <typename Iterator>
            struct tick_parser : grammar<Iterator, tokenDataCPP(), boost::spirit::ascii::space_type>
            {
            
                tick_parser() : tick_parser::base_type(start)
                {
                    my_string %= lexeme[+(boost::spirit::ascii::char_ ) ];
            
                    start %=
                        my_string >> ','
                        >>  my_string >> ','
                        >>  float_ >> ','
                        >>  float_ >> ','
                        >>  int_ >> ','
                        >>  int_
                        ;
                }
            
                rule<Iterator, std::string(), boost::spirit::ascii::space_type> my_string;
                rule<Iterator, sTokenDataCPP(), boost::spirit::ascii::space_type> start;
            };
            

            将迭代次数设置为 500k: strtok 版本:2s 约翰的版本:115s 提升:172s

            如果人们想要这个,我可以发布完整的代码,我只是不想占用大量空间

            【讨论】:

            • strchr() 的基础上滚动您自己的strtok() 版本以获得更快的版本。你可以让原型更简单:char *strtokchr(char *src, int ch);;您不需要为几个“中断”字符编码;也许您可以以不同的方式处理连续的“中断”字符...
            • @pmg 编写了一个基于 strchr 的函数,该函数直接写入我的目标结构,并且能够在 1 秒内进行 1 次磨机迭代。太棒了 :)
            • 耶!好的。但是不要像我在之前的评论中那样命名该函数。该标识符以str 和一个小写字母开头,因此保留用于实现。试试chrstrtok :)
            • 你的代码无法运行,尤其是精神语法,无法编译。1:命名空间错误——你的规则前没有添加命名空间; 2:struct的综合属性和“start”规则应该是同一类型。 3:精神的语法和strtok_s做的不一样。 4:你的语法有问题,它会解析所有的字符,但不是你想要的模式。结论:在发布结果之前仔细测试你的结果
            猜你喜欢
            • 1970-01-01
            • 2011-08-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-03-23
            • 1970-01-01
            • 2020-03-26
            相关资源
            最近更新 更多