为什么在C ++中分割字符串比Python慢?
我试图将Python中的一些代码转换为C ++,以获得一点速度并提高生锈的C ++技能。 昨天我惊讶地发现,从stdin的阅读界面的天真实现在Python中比在C ++中快得多(请参阅本文)。 今天,我终于想出了如何使用合并分隔符(类似的语义到python的split())来分割C ++中的字符串,现在我正在体验似曾相识的感觉! 我的C ++代码需要更长的时间来完成这项工作(尽管不像昨天的课程那样多一个数量级)。
Python代码:
#!/usr/bin/env python
from __future__ import print_function
import time
import sys
count = 0
start_time = time.time()
dummy = None
for line in sys.stdin:
dummy = line.split()
count += 1
delta_sec = int(time.time() - start_time)
print("Python: Saw {0} lines in {1} seconds. ".format(count, delta_sec), end='')
if delta_sec > 0:
lps = int(count/delta_sec)
print(" Crunch Speed: {0}".format(lps))
else:
print('')
C ++代码:
#include <iostream>
#include <string>
#include <sstream>
#include <time.h>
#include <vector>
using namespace std;
void split1(vector<string> &tokens, const string &str,
const string &delimiters = " ") {
// Skip delimiters at beginning
string::size_type lastPos = str.find_first_not_of(delimiters, 0);
// Find first non-delimiter
string::size_type pos = str.find_first_of(delimiters, lastPos);
while (string::npos != pos || string::npos != lastPos) {
// Found a token, add it to the vector
tokens.push_back(str.substr(lastPos, pos - lastPos));
// Skip delimiters
lastPos = str.find_first_not_of(delimiters, pos);
// Find next non-delimiter
pos = str.find_first_of(delimiters, lastPos);
}
}
void split2(vector<string> &tokens, const string &str, char delim=' ') {
stringstream ss(str); //convert string to stream
string item;
while(getline(ss, item, delim)) {
tokens.push_back(item); //add token to vector
}
}
int main() {
string input_line;
vector<string> spline;
long count = 0;
int sec, lps;
time_t start = time(NULL);
cin.sync_with_stdio(false); //disable synchronous IO
while(cin) {
getline(cin, input_line);
spline.clear(); //empty the vector for the next line to parse
//I'm trying one of the two implementations, per compilation, obviously:
// split1(spline, input_line);
split2(spline, input_line);
count++;
};
count--; //subtract for final over-read
sec = (int) time(NULL) - start;
cerr << "C++ : Saw " << count << " lines in " << sec << " seconds." ;
if (sec > 0) {
lps = count / sec;
cerr << " Crunch speed: " << lps << endl;
} else
cerr << endl;
return 0;
//compiled with: g++ -Wall -O3 -o split1 split_1.cpp
请注意,我尝试了两种不同的拆分实现。 一个(split1)使用字符串方法来搜索标记,并能够合并多个标记以及处理大量标记(它来自这里)。 第二个(split2)使用getline将字符串作为流读取,不合并分隔符,并且只支持单个分隔符(这是由几个StackOverflow用户在字符串分割问题的答案中发布的)。
我以各种顺序多次运行这个。 我的测试机器是Macbook Pro(2011,8GB,四核),并不重要。 我正在用一个20M行文本文件进行测试,其中包含三个空格分隔的列,每列都与此类似:“foo.bar 127.0.0.1 home.foo.bar”
结果:
$ /usr/bin/time cat test_lines_double | ./split.py
15.61 real 0.01 user 0.38 sys
Python: Saw 20000000 lines in 15 seconds. Crunch Speed: 1333333
$ /usr/bin/time cat test_lines_double | ./split1
23.50 real 0.01 user 0.46 sys
C++ : Saw 20000000 lines in 23 seconds. Crunch speed: 869565
$ /usr/bin/time cat test_lines_double | ./split2
44.69 real 0.02 user 0.62 sys
C++ : Saw 20000000 lines in 45 seconds. Crunch speed: 444444
我究竟做错了什么? 有没有更好的方法来做C ++中的字符串分割,它不依赖于外部库(即没有提升),支持合并分隔符序列(如python的分割),是线程安全的(因此没有strtok),并且其性能至少为与python相提并论?
编辑1 /部分解决方案?:
我试图通过让python重新设置虚拟列表并将其添加到每次,正如C ++所做的那样,使它更加公平。 这仍然不是C ++代码所做的,但它更接近一点。 基本上,循环现在是:
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
python的性能现在与split1 C ++实现大致相同。
/usr/bin/time cat test_lines_double | ./split5.py
22.61 real 0.01 user 0.40 sys
Python: Saw 20000000 lines in 22 seconds. Crunch Speed: 909090
我仍然很惊讶,即使Python对字符串处理进行了如此优化(如Matt Joiner所建议的),但是这些C ++实现不会更快。 如果有人有关于如何以更优化的方式使用C ++来实现这一点的想法,请分享您的代码。 (我认为我的下一步将尝试在纯C中实现这一点,尽管我不打算牺牲程序员的生产力来重新实现我在C中的整体项目,所以这只是一个实验,用于分割字符串的速度。)
感谢大家的帮助。
最终编辑/解决方案:
请看Alf的接受的答案。 由于python严格通过引用处理字符串,STL字符串经常被复制,因此使用vanilla python实现的性能会更好。 为了比较,我通过Alf的代码编译并运行了我的数据,这里是所有其他运行的同一台机器上的性能,基本上与朴素Python实现相同(尽管比重置/附加列表的Python实现更快,显示在上面的编辑中):
$ /usr/bin/time cat test_lines_double | ./split6
15.09 real 0.01 user 0.45 sys
C++ : Saw 20000000 lines in 15 seconds. Crunch speed: 1333333
我唯一剩下的小麻烦是关于在这种情况下让C ++执行所需的代码量。
从这个问题和昨天的stdin行阅读问题(上面链接)的一个教训是,应该总是基准,而不是对语言的相对“默认”性能做出天真的假设。 我很欣赏教育。
再次感谢您的建议!
作为一个猜测,Python字符串是引用计数的不可变字符串,所以Python代码中不会复制任何字符串,而C ++ std::string
是一个可变值类型,并且可以在最小的时机复制。
如果目标是快速分裂,那么可以使用恒定时间子串操作,这意味着只能引用原始字符串的部分,就像在Python中(以及Java和C#...)。
但是,C ++ std::string
类具有一个兑换功能:它是标准的,因此它可以用于安全地传递字符串,并且可以在效率不是主要考虑因素的位置移动。 但有足够的聊天。 代码 - 在我的机器上,这当然比Python更快,因为Python的字符串处理是用C语言实现的,它是C ++的一个子集(他):
#include <iostream>
#include <string>
#include <sstream>
#include <time.h>
#include <vector>
using namespace std;
class StringRef
{
private:
char const* begin_;
int size_;
public:
int size() const { return size_; }
char const* begin() const { return begin_; }
char const* end() const { return begin_ + size_; }
StringRef( char const* const begin, int const size )
: begin_( begin )
, size_( size )
{}
};
vector<StringRef> split3( string const& str, char delimiter = ' ' )
{
vector<StringRef> result;
enum State { inSpace, inToken };
State state = inSpace;
char const* pTokenBegin = 0; // Init to satisfy compiler.
for( auto it = str.begin(); it != str.end(); ++it )
{
State const newState = (*it == delimiter? inSpace : inToken);
if( newState != state )
{
switch( newState )
{
case inSpace:
result.push_back( StringRef( pTokenBegin, &*it - pTokenBegin ) );
break;
case inToken:
pTokenBegin = &*it;
}
}
state = newState;
}
if( state == inToken )
{
result.push_back( StringRef( pTokenBegin, &*str.end() - pTokenBegin ) );
}
return result;
}
int main() {
string input_line;
vector<string> spline;
long count = 0;
int sec, lps;
time_t start = time(NULL);
cin.sync_with_stdio(false); //disable synchronous IO
while(cin) {
getline(cin, input_line);
//spline.clear(); //empty the vector for the next line to parse
//I'm trying one of the two implementations, per compilation, obviously:
// split1(spline, input_line);
//split2(spline, input_line);
vector<StringRef> const v = split3( input_line );
count++;
};
count--; //subtract for final over-read
sec = (int) time(NULL) - start;
cerr << "C++ : Saw " << count << " lines in " << sec << " seconds." ;
if (sec > 0) {
lps = count / sec;
cerr << " Crunch speed: " << lps << endl;
} else
cerr << endl;
return 0;
}
//compiled with: g++ -Wall -O3 -o split1 split_1.cpp -std=c++0x
免责声明:我希望没有任何错误。 我没有测试过这个功能,但只检查了速度。 但我认为,即使存在一两个缺陷,纠正这一问题也不会显着影响速度。
我没有提供任何更好的解决方案(至少在性能方面),但一些额外的数据可能很有趣。
使用strtok_r
( strtok
可重入变体):
void splitc1(vector<string> &tokens, const string &str,
const string &delimiters = " ") {
char *saveptr;
char *cpy, *token;
cpy = (char*)malloc(str.size() + 1);
strcpy(cpy, str.c_str());
for(token = strtok_r(cpy, delimiters.c_str(), &saveptr);
token != NULL;
token = strtok_r(NULL, delimiters.c_str(), &saveptr)) {
tokens.push_back(string(token));
}
free(cpy);
}
另外使用字符串作为参数,输入fgets
:
void splitc2(vector<string> &tokens, const char *str,
const char *delimiters) {
char *saveptr;
char *cpy, *token;
cpy = (char*)malloc(strlen(str) + 1);
strcpy(cpy, str);
for(token = strtok_r(cpy, delimiters, &saveptr);
token != NULL;
token = strtok_r(NULL, delimiters, &saveptr)) {
tokens.push_back(string(token));
}
free(cpy);
}
而且,在某些情况下,销毁输入字符串是可以接受的:
void splitc3(vector<string> &tokens, char *str,
const char *delimiters) {
char *saveptr;
char *token;
for(token = strtok_r(str, delimiters, &saveptr);
token != NULL;
token = strtok_r(NULL, delimiters, &saveptr)) {
tokens.push_back(string(token));
}
}
这些时间如下(包括我对这个问题的其他变体的结果以及所接受的答案):
split1.cpp: C++ : Saw 20000000 lines in 31 seconds. Crunch speed: 645161
split2.cpp: C++ : Saw 20000000 lines in 45 seconds. Crunch speed: 444444
split.py: Python: Saw 20000000 lines in 33 seconds. Crunch Speed: 606060
split5.py: Python: Saw 20000000 lines in 35 seconds. Crunch Speed: 571428
split6.cpp: C++ : Saw 20000000 lines in 18 seconds. Crunch speed: 1111111
splitc1.cpp: C++ : Saw 20000000 lines in 27 seconds. Crunch speed: 740740
splitc2.cpp: C++ : Saw 20000000 lines in 22 seconds. Crunch speed: 909090
splitc3.cpp: C++ : Saw 20000000 lines in 20 seconds. Crunch speed: 1000000
正如我们所看到的,来自被接受的答案的解决方案仍然是最快的。
对于任何希望进行进一步测试的人,我还会提供一个Github回购库,其中包含问题的所有程序,接受的答案,此答案,另外还有一个生成测试数据的Makefile和脚本:https:// github。 COM / tobbez /字符串分割。
我怀疑这是因为std::vector
在push_back()函数调用过程中被调整大小的原因。 如果您尝试使用std::list
或std::vector::reserve()
为句子保留足够的空间,则应该获得更好的性能。 或者你可以使用下面的组合来表示split1():
void split1(vector<string> &tokens, const string &str,
const string &delimiters = " ") {
// Skip delimiters at beginning
string::size_type lastPos = str.find_first_not_of(delimiters, 0);
// Find first non-delimiter
string::size_type pos = str.find_first_of(delimiters, lastPos);
list<string> token_list;
while (string::npos != pos || string::npos != lastPos) {
// Found a token, add it to the list
token_list.push_back(str.substr(lastPos, pos - lastPos));
// Skip delimiters
lastPos = str.find_first_not_of(delimiters, pos);
// Find next non-delimiter
pos = str.find_first_of(delimiters, lastPos);
}
tokens.assign(token_list.begin(), token_list.end());
}
编辑:我看到的另一个明显的事情是,Python变量dummy
获取分配每一次,但没有修改。 所以这与C ++并不是一个公平的比较。 您应该尝试将您的Python代码修改为dummy = []
来初始化它,然后执行dummy += line.split()
。 你能在这之后报告运行时间吗?
编辑2 :为了使它更公平,你可以修改C ++代码中的while循环:
while(cin) {
getline(cin, input_line);
std::vector<string> spline; // create a new vector
//I'm trying one of the two implementations, per compilation, obviously:
// split1(spline, input_line);
split2(spline, input_line);
count++;
};
链接地址: http://www.djcxy.com/p/5393.html