完整格式連結:https://blog.imakiseki.cf/2022/03/07/techdev/python-cpp-string-find-perf-test/
背景
最近在備戰一場演算法競賽,語言誤選了 Python ,無奈只能著手對常見場景進行語言遷移。而字串查詢的場景在演算法競賽中時有出現。本文即對此場景在 Python 和競賽常用語言 C++ 下的速度進行對比,並提供相關引數和執行結果供他人蔘考。
引數
硬體和作業系統
-` root@<hostname>
.o+` ------------
`ooo/ OS: Arch Linux ARM aarch64
`+oooo: Host: Raspberry Pi 4 Model B
`+oooooo: Kernel: 5.16.12-1-aarch64-ARCH
-+oooooo+: Uptime: 3 hours, 32 mins
`/:-:++oooo+: Packages: 378 (pacman)
`/++++/+++++++: Shell: zsh 5.8.1
`/++++++++++++++: Terminal: /dev/pts/0
`/+++ooooooooooooo/` CPU: (4) @ 1.500GHz
./ooosssso++osssssso+` Memory: 102MiB / 7797MiB
.oossssso-````/ossssss+`
-osssssso. :ssssssso.
:osssssss/ osssso+++.
/ossssssss/ +ssssooo/-
`/ossssso+/:- -:/+osssso+-
`+sso+:-` `.-/+oso:
`++:. `-/+/
.` `/
編譯環境和解釋環境
- Python
- 直譯器:Python 3.10.2 (main, Jan 23 2022, 21:20:14) [GCC 10.2.0] on linux
- 互動環境:IPython 8.0.1
- C++
- 編譯器:g++ (GCC) 11.2.0
- 編譯命令:
g++ test.cpp -Wall -O2 -g -std=c++11 -o test
場景
本次實測設定兩個場景:場景 1 的源串字元分佈使用偽隨機數生成器生成,表示字串查詢的平均情況;場景 2 的源串可連續分割成 20,000 個長度為 50 的字元片段,其中第 15,001 個即為模式串,形如“ab…b”(1 個“a”,49 個 “b”),其餘的字元片段形如“ab…c”(1 個“a”,48 個“b”,1 個“c”)。
專案 | 場景 1:平均情況 | 場景 2:較壞情況 |
---|---|---|
字符集 | 小寫字母 | abc |
字元分佈 | random.choice |
有較強規律性 |
源串長度 | 1,000,000 | 1,000,000 |
模式串長度 | 1,000 | 50 |
模式串出現位置 | 250,000、500,000、750,000 | 750,000 |
模式串出現次數 | 1 | 1 |
測試方法
本次實測中,Python 語言使用內建型別 str
的 .find()
成員函式,C++ 語言分別使用 string
類的 .find()
成員函式、strstr
標準庫函式和使用者實現的 KMP 演算法。
測試物件 | 核心程式碼 |
---|---|
Python | src.find(pat) |
C++ - test.cpp |
src.find(pat) |
C++ - test_strstr.cpp |
strstr(src, pat) |
C++ - test_kmp.cpp |
KMP(src, pat) |
原始碼
生成源串和模式串
import random
# 場景 1:
# 源串
s = "".join(chr(random.choice(range(ord("a"), ord("z") + 1))) for _ in range(1000000))
# 模式串列表,三個元素各對應一個模式串
p = [s[250000:251000], s[500000:501000], s[750000:751000]]
# 場景 2:
# 模式串
p = 'a' + 'b' * 49
# 其他字元片段
_s = "a" + "b" * 48 + "c"
# 源串
s = _s * 15000 + p + _s * 4999
# 儲存到檔案,便於 C++ 程式獲取
with open('source.in', 'w') as f:
f.write(s)
with open('pattern.in', 'w') as f:
f.write(p[0])
測試程式碼
Python
In []: %timeit s.find(p[0])
C++ - test.cpp
#include <chrono>
#include <iostream>
#include <cstring>
#include <fstream>
#define LOOP_COUNT (1000)
using namespace std;
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
double test(string s, string p, size_t* pos_ptr) {
auto t1 = high_resolution_clock::now();
*pos_ptr = s.find(p);
auto t2 = high_resolution_clock::now();
duration<double, milli> ms_double = t2 - t1;
return ms_double.count();
}
int main() {
string s, p;
size_t pos;
ifstream srcfile("source.in");
ifstream patfile("pattern.in");
srcfile >> s;
patfile >> p;
double tot_time = 0;
for (int i = 0; i < LOOP_COUNT; ++i) {
tot_time += test(s, p, &pos);
}
cout << "Loop count: " << LOOP_COUNT << endl;
cout << "Source string length: " << s.length() << endl;
cout << "Pattern string length: " << p.length() << endl;
cout << "Search result: " << pos << endl;
cout << "Time: " << tot_time / LOOP_COUNT << " ms" << endl;
return 0;
}
C++ - test_strstr.cpp
#include <chrono>
#include <iostream>
#include <cstring>
#include <fstream>
#define LOOP_COUNT (1000)
using namespace std;
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
char s[1000005], p[1005], *pos=NULL;
double test(char* s, char* p, char** pos_ptr) {
auto t1 = high_resolution_clock::now();
*pos_ptr = strstr(s, p);
auto t2 = high_resolution_clock::now();
duration<double, milli> ms_double = t2 - t1;
return ms_double.count();
}
int main() {
ifstream srcfile("source.in");
ifstream patfile("pattern.in");
srcfile >> s;
patfile >> p;
double tot_time = 0;
for (int i = 0; i < LOOP_COUNT; ++i) {
tot_time += test(s, p, &pos);
}
cout << "Loop count: " << LOOP_COUNT << endl;
cout << "Source string length: " << strlen(s) << endl;
cout << "Pattern string length: " << strlen(p) << endl;
cout << "Search result: " << pos - s << endl;
cout << "Time: " << tot_time / LOOP_COUNT << " ms" << endl;
return 0;
}
C++ - test_kmp.cpp
#include <chrono>
#include <iostream>
#include <cstring>
#include <fstream>
#include <cstdlib>
#define LOOP_COUNT (1000)
using namespace std;
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
int dp[1005];
int KMP(string s, string p) {
int m = s.length(), n = p.length();
if (n == 0) return 0;
if (m < n) return -1;
memset(dp, 0, sizeof(int) * (n+1));
for (int i = 1; i < n; ++i) {
int j = dp[i+1];
while (j > 0 && p[j] != p[i]) j = dp[j];
if (j > 0 || p[j] == p[i]) dp[i+1] = j + 1;
}
for (int i = 0, j = 0; i < m; ++i)
if (s[i] == p[j]) { if (++j == n) return i - j + 1; }
else if (j > 0) {
j = dp[j];
--i;
}
return -1;
}
double test(string s, string p, int* pos_ptr) {
auto t1 = high_resolution_clock::now();
*pos_ptr = KMP(s, p);
auto t2 = high_resolution_clock::now();
duration<double, milli> ms_double = t2 - t1;
return ms_double.count();
}
int main() {
string s, p;
int pos;
ifstream srcfile("source.in");
ifstream patfile("pattern.in");
srcfile >> s;
patfile >> p;
double tot_time = 0;
for (int i = 0; i < LOOP_COUNT; ++i) {
tot_time += test(s, p, &pos);
}
cout << "Loop count: " << LOOP_COUNT << endl;
cout << "Source string length: " << s.length() << endl;
cout << "Pattern string length: " << p.length() << endl;
cout << "Search result: " << pos << endl;
cout << "Time: " << tot_time / LOOP_COUNT << " ms" << endl;
return 0;
}
結果
IPython 的 %timeit
魔法命令可以輸出程式碼多次執行的平均時間和標準差,在此取平均時間。C++ 的程式碼對每個模式串固定執行 1,000 次後取平均時間。
以下時間若無特別說明,均以微秒為單位,保留到整數位。
場景 | 模式串出現位置 | Python | C++ - test.cpp |
C++ - test_strstr.cpp |
C++ - test_kmp.cpp |
---|---|---|---|---|---|
場景 1 | 250,000 | 105 | 523 | 155 | 2564 |
場景 1 | 500,000 | 183 | 1053 | 274 | 3711 |
場景 1 | 750,000 | 291 | 1589 | 447 | 4900 |
場景 2 | 750,000 | 2630* | 618 | 353 | 3565 |
* 原輸出為“2.63 ms”。IPython 的 %timeit
輸出的均值保留 3 位有效數字,由於此時間已超過 1 毫秒,微秒位被捨棄。此處仍以微秒作單位,數值記為“2630”。
侷限性
本次實測時使用的裝置硬體上劣於演算法競賽中的標準配置機器,實測結果中的“絕對數值”參考性較低。
總結
根據上表中的結果,在給定環境和相關引數條件下,場景 1 中 Python 的執行時間大約為 C++ 中 string::find
的五分之一,與 std:strstr
接近;而在場景 2 中 Python 的執行時間明顯增長,但 C++ 的前兩種測試方法的執行時間與先前接近甚至更短。四次測試中,C++ 的使用者實現的 KMP 演算法執行時間均較長,長於同條件下 Python 的情況。
Python 中的內建型別 str
的快速查詢(.find()
)和計數(.count()
)演算法基於 Boyer-Moore 演算法和 Horspool 演算法的混合,其中後者是前者的簡化,而前者與 Knuth-Morris-Pratt 演算法有關。
有關 C++ 的 string::find
比 std::strstr
執行時間長的相關情況,參見 Bug 66414 - string::find ten times slower than strstr。
值得關注的是:C++ 中自行實現的 KMP 演算法的執行時間竟然遠長於 C++ 標準庫甚至 Python 中的演算法。這也類似於常說的“自己設計彙編程式碼執行效率低於編譯器”的情況。Stack Overflow 的一個問題 strstr faster than algorithms? 下有人回答如下:
Why do you think
strstr
should be slower than all the others? Do you know what algorithmstrstr
uses? I think it's quite likely thatstrstr
uses a fine-tuned, processor-specific, assembly-coded algorithm of theKMP
type or better. In which case you don't stand a chance of out-performing it inC
for such small benchmarks.
KMP 演算法並非是所有線性複雜度演算法中最快的。在不同的環境(軟硬體、測試資料等)下,KMP 與其變種乃至其他線性複雜度演算法,孰優孰劣都無法判斷。編譯器在設計時考慮到諸多可能的因素,儘可能使不同環境下都能有相對較優的策略來得到結果。因而,在保證結果正確的情況下,與其根據演算法原理自行編寫,不如直接使用標準庫中提供的函式。
同時本次實測也在執行時間角度再次印證 Python 並不適合在演算法競賽中取得高成績的說法。
參考
- https://stackoverflow.com/questions/22387586/measuring-execution-time-of-a-function-in-c
- https://www.cplusplus.com/reference/string/string/find/
- https://stackoverflow.com/questions/681649/how-is-string-find-implemented-in-cpython
- https://github.com/python/cpython/blob/main/Objects/stringlib/fastsearch.h#L5
- https://stackoverflow.com/questions/8869605/c-stringfind-complexity
- https://stackoverflow.com/questions/19506571/can-it-be-faster-to-find-the-minimum-periodic-string-inside-another-string-in-te
- https://gcc.gnu.org/onlinedocs/gcc-9.4.0/libstdc++/api/a17342_source.html
- https://opensource.apple.com/source/tcl/tcl-10/tcl/compat/strstr.c.auto.html
- https://gist.github.com/hsinewu/44a1ce38a1baf47893922e3f54807713
- https://stackoverflow.com/questions/11799956/performance-comparison-strstr-vs-stdstringfind
- https://stackoverflow.com/questions/7586990/strstr-faster-than-algorithms
- https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66414
- http://0x80.pl/notesen/2016-10-08-slow-std-string-find.html