字符串匹配性能:gcc对比CPython
字符串匹配性能:gcc对比CPython
在研究Python和C++的性能权衡时,我设计了一个小示例,主要关注愚蠢的子字符串匹配。
下面是相关的C++代码:
using std::string; std::vector matches; std::copy_if(patterns.cbegin(), patterns.cend(), back_inserter(matches), [&fileContents] (const string &pattern) { return fileContents.find(pattern) != string::npos; } );
上述代码是使用-O3编译的。
这里是Python:
def getMatchingPatterns(patterns, text): return filter(text.__contains__, patterns)
它们都使用一组相对较大的模式和输入文件,并使用愚蠢的子字符串搜索筛选出在文件中找到的匹配的模式。
版本如下:
- gcc- 4.8.2(Ubuntu)和4.9.2(cygwin)
- python- 2.7.6(Ubuntu)和2.7.8(cygwin)
让我感到惊讶的是性能。我在低规格的Ubuntu上运行它们,Python快了大约20%。在中规格的cygwin上也是如此- Python快了两倍。
剖析器显示99%以上的周期都用于字符串匹配(字符串复制和列表推导不重要)。
显然,Python的实现是本地C,我预期大致与C ++相同,但没有预期它会那么快。
任何有关与gcc相比的CPython优化的见解都将不胜感激。
为了参考,这里是完整的示例。输入只需要一组50K个HTML文件(每个测试都从磁盘读取所有文件,没有特殊缓存):
Python:
import sys def getMatchingPatterns(patterns, text): return filter(text.__contains__, patterns) def serialScan(filenames, patterns): return zip(filenames, [getMatchingPatterns(patterns, open(filename).read()) for filename in filenames]) if __name__ == "__main__": with open(sys.argv[1]) as filenamesListFile: filenames = filenamesListFile.read().split() with open(sys.argv[2]) as patternsFile: patterns = patternsFile.read().split() resultTuple = serialScan(filenames, patterns) for filename, patterns in resultTuple: print ': '.join([filename, ','.join(patterns)])
C ++:
#include #include #include #include #include #include #include using namespace std; using MatchResult = unordered_map<string, vector>; static const size_t PATTERN_RESERVE_DEFAULT_SIZE = 5000; MatchResult serialMatch(const vector &filenames, const vector &patterns) { MatchResult res; for (auto &filename : filenames) { ifstream file(filename); const string fileContents((istreambuf_iterator(file)), istreambuf_iterator()); vector matches; std::copy_if(patterns.cbegin(), patterns.cend(), back_inserter(matches), [&fileContents] (const string &pattern) { return fileContents.find(pattern) != string::npos; } ); res.insert(make_pair(filename, std::move(matches))); } return res; } int main(int argc, char **argv) { vector filenames; ifstream filenamesListFile(argv[1]); std::copy(istream_iterator(filenamesListFile), istream_iterator(), back_inserter(filenames)); vector patterns; patterns.reserve(PATTERN_RESERVE_DEFAULT_SIZE); ifstream patternsFile(argv[2]); std::copy(istream_iterator(patternsFile), istream_iterator(), back_inserter(patterns)); auto matchResult = serialMatch(filenames, patterns); for (const auto &matchItem : matchResult) { cout << matchItem.first << ": "; for (const auto &matchString : matchItem.second) cout << matchString << ","; cout << endl; } }
Python 3.4代码b'abc' in b'abcabc'
(或者在你的例子中为b'abcabc'.__contains__(b'abc')
)执行bytes_contains
方法,该方法调用内联函数stringlib_find
;它将搜索代理给FASTSEARCH
。
FASTSEARCH
函数然后使用简化的Boyer-Moore搜索算法(Boyer-Moore-Horspool):
快速搜索/计数实现,基于Boyce-Moore和Horspool的组合,并且增加一些其他功能。有关更多背景,请参见:http://effbot.org/zone/stringlib.htm
还有一些修改,如注释所示:
注意:fastsearch可能会访问
s[n]
,在使用Python的普通字符串类型时不会有问题,但如果在其他上下文中使用此代码,则可能会出现问题。另外,如果在目标字符串中不可能有匹配项,则计数模式返回-1
,如果实际上已经检查过匹配项但没有找到,则返回0
。调用者请注意!
中文翻译:
GNU C++标准库basic_string
的实现尽可能通用(且愚蠢); 它只是愚蠢地在每个连续字符位置上尝试匹配模式,直到找到匹配为止。
TL;DR: C++标准库之所以与Python相比速度较慢,是因为它在std::basic_string
的基础上尝试进行通用算法,但对于更有趣的情况,无法有效地执行; 而在Python中,程序员基于每种情况使用最有效的算法是免费的。