我正在尝试将一些 Python 代码转换成 C + + ,以便获得一点速度,并提高我生疏的 C + + 技能。昨天,当一个从 stdin 读取代码行的幼稚实现在 Python 中比 C + + 快得多时,我感到非常震惊(参见 这个)。今天,我终于弄明白了如何在 C + + 中使用合并分隔符来分隔字符串(与 python 的 split ()语义相似) ,现在我有种似曾相识的感觉!我的 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,8 GB,四核) ,这并不重要。我正在测试一个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 的性能与 split1C + + 实现大致相同。
/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 语言重新实现我的整个项目,所以这将只是一个字符串分解速度的实验。)
感谢大家的帮助。
最终编辑/解决方案:
请看看阿尔夫接受的答案。由于 python 严格地通过引用处理字符串,并且常常复制 STL 字符串,因此普通的 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 + + 所需的代码量。
从这个问题和昨天的标准输入行阅读问题(链接在上面)中得到的教训之一是,人们应该始终进行基准测试,而不是对语言的相对“默认”性能做出幼稚的假设。我很感激你的教育。
再次感谢大家的建议!