Saya mencoba mengonversi beberapa kode dari Python ke C ++ dalam upaya untuk mendapatkan sedikit kecepatan dan mempertajam keterampilan C ++ saya yang berkarat. Kemarin saya terkejut ketika implementasi naif membaca baris dari stdin jauh lebih cepat dengan Python daripada C ++ (lihat ini ). Hari ini, saya akhirnya menemukan cara membagi string di C ++ dengan pembatas penggabungan (semantik mirip dengan split python ()), dan sekarang saya mengalami deja vu! Kode C ++ saya membutuhkan waktu lebih lama untuk melakukan pekerjaan itu (meskipun bukan urutan besarnya, seperti yang terjadi pada pelajaran kemarin).
Kode 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('')
Kode 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
Perhatikan bahwa saya mencoba dua implementasi pemisahan yang berbeda. One (split1) menggunakan metode string untuk mencari token dan mampu menggabungkan banyak token serta menangani banyak token (berasal dari sini ). Yang kedua (split2) menggunakan getline untuk membaca string sebagai aliran, tidak menggabungkan pembatas, dan hanya mendukung satu karakter pembatas (yang diposting oleh beberapa pengguna StackOverflow sebagai jawaban untuk pertanyaan pemisahan string).
Saya menjalankan ini beberapa kali dalam berbagai pesanan. Mesin uji saya adalah Macbook Pro (2011, 8GB, Quad Core), bukan itu masalahnya. Saya menguji dengan file teks baris 20 juta dengan tiga kolom terpisah spasi yang masing-masing terlihat mirip dengan ini: "foo.bar 127.0.0.1 home.foo.bar"
Hasil:
$ /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
Apa yang saya lakukan salah? Apakah ada cara yang lebih baik untuk melakukan pemisahan string dalam C ++ yang tidak bergantung pada pustaka eksternal (yaitu, tidak ada dorongan), mendukung penggabungan urutan pembatas (seperti pemisahan python), aman untuk utas (jadi tidak ada strtok), dan yang kinerjanya setidaknya setara dengan python?
Edit 1 / Solusi Parsial ?:
Saya mencoba membuatnya menjadi perbandingan yang lebih adil dengan meminta python mengatur ulang daftar boneka dan menambahkannya setiap kali, seperti yang dilakukan C ++. Ini masih belum persis seperti yang dilakukan kode C ++, tetapi ini sedikit lebih dekat. Pada dasarnya, loop sekarang:
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
Kinerja python sekarang hampir sama dengan implementasi 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
Saya masih terkejut bahwa, meskipun Python begitu dioptimalkan untuk pemrosesan string (seperti yang disarankan Matt Joiner), implementasi C ++ ini tidak akan lebih cepat. Jika ada yang punya ide tentang cara melakukan ini dengan cara yang lebih optimal menggunakan C ++, silakan bagikan kode Anda. (Saya pikir langkah saya selanjutnya akan mencoba menerapkan ini dalam C murni, meskipun saya tidak akan menukar produktivitas pemrogram untuk menerapkan ulang keseluruhan proyek saya di C, jadi ini hanya akan menjadi eksperimen untuk kecepatan pemisahan string.)
Terima kasih untuk semua atas bantuan Anda.
Edit / Solusi Akhir:
Silakan lihat jawaban Alf yang diterima. Karena python menangani string secara ketat dengan referensi dan string STL sering disalin, kinerja lebih baik dengan implementasi vanilla python. Sebagai perbandingan, saya mengumpulkan dan menjalankan data saya melalui kode Alf, dan berikut adalah kinerja pada mesin yang sama dengan semua yang berjalan, pada dasarnya identik dengan implementasi python naif (meskipun lebih cepat daripada implementasi python yang menyetel ulang / menambahkan daftar, seperti ditampilkan di edit di atas):
$ /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
Keluhan kecil saya yang tersisa hanyalah mengenai jumlah kode yang diperlukan untuk mendapatkan C ++ untuk bekerja dalam kasus ini.
Salah satu pelajaran di sini dari masalah ini dan masalah membaca garis stdin kemarin (ditautkan di atas) adalah bahwa seseorang harus selalu membuat tolok ukur daripada membuat asumsi naif tentang kinerja relatif "default" bahasa. Saya menghargai pendidikan.
Terima kasih sekali lagi untuk semua saran Anda!
g++ -Wall -O3 -o split1 split_1.cpp
@JJC: Bagaimana benchmark Anda saat Anda benar-benar menggunakandummy
danspline
masing - masing, mungkin Python menghapus panggilan keline.split()
karena tidak memiliki efek samping?Jawaban:
Sebagai tebakan, string Python adalah referensi yang dihitung sebagai string yang tidak dapat diubah, sehingga tidak ada string yang disalin dalam kode Python, sementara C ++
std::string
adalah jenis nilai yang dapat berubah, dan disalin pada peluang terkecil.Jika tujuannya adalah pemisahan cepat, maka seseorang akan menggunakan operasi substring waktu konstan, yang berarti hanya merujuk pada bagian dari string asli, seperti di Python (dan Java, dan C #…).
Kelas C ++
std::string
memiliki satu fitur penebusan, meskipun: ini standar , sehingga dapat digunakan untuk meneruskan string dengan aman dan mudah dibawa di mana efisiensi bukanlah pertimbangan utama. Tapi cukup ngobrol. Kode - dan di mesin saya ini tentu saja lebih cepat daripada Python, karena penanganan string Python diimplementasikan di C yang merupakan bagian dari C ++ (he he):Penafian: Saya harap tidak ada bug. Saya belum menguji fungsionalitasnya, tetapi hanya memeriksa kecepatannya. Tapi saya pikir, meskipun ada satu atau dua bug, mengoreksi itu tidak akan mempengaruhi kecepatan secara signifikan.
sumber
StringRef
, Anda dapat menyalin substring ke filestd::string
dengan sangat mudah, hanyastring( sr.begin(), sr.end() )
.PyString_FromStringAndSize()
panggilan ituPyObject_MALLOC()
. Jadi tidak ada pengoptimalan dengan representasi bersama yang mengeksploitasi bahwa string tidak dapat diubah dalam Python.Saya tidak memberikan solusi yang lebih baik (setidaknya dari segi performa), tetapi beberapa data tambahan yang mungkin menarik.
Menggunakan
strtok_r
(varian reentrant daristrtok
):Selain itu menggunakan string karakter untuk parameter, dan
fgets
untuk input:Dan, dalam beberapa kasus, di mana pemusnahan string input dapat diterima:
Pengaturan waktu untuk ini adalah sebagai berikut (termasuk hasil saya untuk varian lain dari pertanyaan dan jawaban yang diterima):
Seperti yang bisa kita lihat, solusi dari jawaban yang diterima masih paling cepat.
Bagi siapa pun yang ingin melakukan tes lebih lanjut, saya juga memasang repo Github dengan semua program dari pertanyaan, jawaban yang diterima, jawaban ini, dan tambahan Makefile dan skrip untuk menghasilkan data pengujian: https: // github. com / tobbez / string-splitting .
sumber
memcpy
, bukanstrcpy
, jika kompilator gagal memperhatikan pengoptimalan itu.strcpy
biasanya menggunakan strategi startup yang lebih lambat yang memberikan keseimbangan antara cepat untuk string pendek vs. meningkatkan SIMD penuh untuk string panjang.memcpy
mengetahui ukurannya segera, dan tidak harus menggunakan trik SIMD apa pun untuk memeriksa akhir string panjang implisit. (Bukan masalah besar pada x86 modern). Membuatstd::string
objek dengan(char*, len)
konstruktor mungkin lebih cepat juga, jika Anda bisa mengeluarkannyasaveptr-token
. Jelas akan menjadi yang tercepat untuk hanya menyimpanchar*
token: PSaya menduga bahwa ini karena cara
std::vector
diubah ukurannya selama proses pemanggilan fungsi push_back (). Jika Anda mencoba menggunakanstd::list
ataustd::vector::reserve()
menyediakan ruang yang cukup untuk kalimat, Anda akan mendapatkan kinerja yang jauh lebih baik. Atau Anda bisa menggunakan kombinasi keduanya seperti di bawah ini untuk split1 ():EDIT : Hal yang jelas lainnya saya lihat adalah bahwa variabel Python
dummy
akan ditugaskan setiap kali tetapi tidak dimodifikasi. Jadi ini bukan perbandingan yang adil terhadap C ++. Anda harus mencoba memodifikasi kode Python Anda menjadidummy = []
untuk menginisialisasi dan kemudian melakukannyadummy += line.split()
. Dapatkah Anda melaporkan runtime setelah ini?EDIT2 : Untuk membuatnya lebih adil, Anda dapat memodifikasi loop sementara dalam kode C ++ menjadi:
sumber
Saya pikir kode berikut lebih baik, menggunakan beberapa fitur C ++ 17 dan C ++ 14:
Pilihan wadah:
std::vector
.Dengan asumsi ukuran awal dari array internal yang dialokasikan adalah 1, dan ukuran akhirnya adalah N, Anda akan mengalokasikan dan membatalkan alokasi untuk log2 (N) kali, dan Anda akan menyalin (2 ^ (log2 (N) + 1) - 1) = (2N - 1) kali. Seperti yang ditunjukkan di Apakah kinerja yang buruk dari std :: vector karena tidak memanggil realoc beberapa kali logaritmik? , ini dapat memiliki kinerja yang buruk jika ukuran vektor tidak dapat diprediksi dan bisa sangat besar. Tapi, jika Anda bisa memperkirakan ukurannya, ini tidak akan menjadi masalah.
std::list
.Untuk setiap push_back, waktu yang dikonsumsi adalah konstan, tetapi mungkin akan membutuhkan lebih banyak waktu daripada std :: vector pada masing-masing push_back. Menggunakan kumpulan memori per utas dan pengalokasi khusus dapat meredakan masalah ini.
std::forward_list
.Sama seperti std :: list, tetapi menggunakan lebih sedikit memori per elemen. Memerlukan kelas pembungkus agar berfungsi karena kurangnya push_back API.
std::array
.Jika Anda dapat mengetahui batas pertumbuhan, maka Anda dapat menggunakan std :: array. Penyebabnya, Anda tidak dapat menggunakannya secara langsung, karena tidak memiliki push_back API. Tetapi Anda dapat menentukan pembungkus, dan menurut saya ini adalah cara tercepat di sini dan dapat menghemat memori jika perkiraan Anda cukup akurat.
std::deque
.Opsi ini memungkinkan Anda untuk menukar memori dengan kinerja. Tidak akan ada (2 ^ (N + 1) - 1) kali salinan elemen, hanya alokasi N kali, dan tidak ada deallocation. Selain itu, Anda akan memiliki waktu akses acak yang konstan, dan kemampuan untuk menambahkan elemen baru di kedua ujungnya.
Menurut std :: deque-cppreference
atau Anda dapat menggunakan kombinasi ini:
std::vector< std::array<T, 2 ^ M> >
Ini mirip dengan std :: deque, perbedaannya hanya wadah ini tidak mendukung untuk menambahkan elemen di depan. Tetapi ini masih lebih cepat dalam kinerjanya, karena fakta bahwa itu tidak akan menyalin std :: array yang mendasari untuk (2 ^ (N + 1) - 1) kali, itu hanya akan menyalin array pointer untuk (2 ^ (N - M + 1) - 1) kali, dan mengalokasikan array baru hanya jika arus sudah penuh dan tidak perlu membatalkan alokasi apa pun. Omong-omong, Anda bisa mendapatkan waktu akses acak yang konstan.
std::list< std::array<T, ...> >
Sangat meringankan tekanan framentation memori. Ini hanya akan mengalokasikan array baru ketika arus sudah penuh, dan tidak perlu menyalin apa pun. Anda masih harus membayar harga untuk penunjuk tambahan yang sebanding dengan kombo 1.
std::forward_list< std::array<T, ...> >
Sama seperti 2, tetapi harganya sama dengan memori kombo 1.
sumber
N
. Sayang sekali std :: vector tidak dapat digunakanrealloc
untuk memungkinkan pemetaan lebih banyak halaman di akhir alokasi saat ini , jadi sekitar 2x lebih lambat.stringview::remove_prefix
semurah hanya melacak posisi Anda saat ini dalam string normal?std::basic_string::find
memiliki argumenpos = 0
ke- 2 opsional untuk memungkinkan Anda mulai mencari dari offset.log2(size - 1) + 2
, menggunakan log integer). Alokasi pertama memindahkan 0 string, yang kedua memindahkan 1, lalu 2, lalu 4, lalu 8, lalu akhirnya 16, dengan total 31 gerakan (2^(log2(size - 1) + 1) - 1)
). Ini adalah O (n), bukan O (2 ^ n). Ini akan sangat mengunggulistd::list
.Anda membuat asumsi yang salah bahwa implementasi C ++ yang Anda pilih tentu lebih cepat daripada implementasi Python. Penanganan string dengan Python sangat dioptimalkan. Lihat pertanyaan ini untuk lebih lanjut: Mengapa operasi std :: string berkinerja buruk?
sumber
Jika Anda mengambil implementasi split1 dan mengubah tanda tangan agar lebih cocok dengan split2, dengan mengubah ini:
untuk ini:
Anda mendapatkan perbedaan yang lebih dramatis antara split1 dan split2, dan perbandingan yang lebih adil:
sumber
sumber
Saya menduga bahwa ini terkait dengan buffering pada sys.stdin dengan Python, tetapi tidak ada buffering dalam implementasi C ++.
Lihat posting ini untuk detail tentang cara mengubah ukuran buffer, lalu coba perbandingan lagi: Menetapkan ukuran buffer yang lebih kecil untuk sys.stdin?
sumber