Penggabungan string yang efisien di C ++

108

Saya mendengar beberapa orang mengungkapkan kekhawatiran tentang operator "+" di std :: string dan berbagai solusi untuk mempercepat penggabungan. Apakah semua ini benar-benar diperlukan? Jika ya, apa cara terbaik untuk menggabungkan string di C ++?

sneg
sumber
13
Pada dasarnya + BUKAN operator concatentation (karena menghasilkan string baru). Gunakan + = untuk penggabungan.
Martin York
1
Sejak C ++ 11, ada poin penting: operator + dapat memodifikasi salah satu operannya & mengembalikannya dengan gerakan jika operan itu diteruskan oleh referensi rvalue. libstdc++ melakukan ini, misalnya . Jadi, ketika memanggil operator + dengan temporaries, itu dapat mencapai kinerja yang hampir sama baiknya - mungkin sebuah argumen yang mendukung default, demi keterbacaan, kecuali jika seseorang memiliki tolok ukur yang menunjukkan bahwa itu adalah hambatan. Namun, variadic Standar append()akan menjadi optimal dan dapat dibaca ...
underscore_d

Jawaban:

85

Pekerjaan ekstra mungkin tidak sepadan, kecuali jika Anda benar-benar membutuhkan efisiensi. Anda mungkin akan memiliki efisiensi yang jauh lebih baik hanya dengan menggunakan operator + = sebagai gantinya.

Sekarang setelah pelepasan tanggung jawab hukum itu, saya akan menjawab pertanyaan Anda yang sebenarnya ...

Efisiensi kelas string STL bergantung pada implementasi STL yang Anda gunakan.

Anda dapat menjamin efisiensi dan memiliki kendali yang lebih besar dengan melakukan penggabungan secara manual melalui c fungsi bawaan.

Mengapa operator + tidak efisien:

Lihat antarmuka ini:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

Anda dapat melihat bahwa objek baru dikembalikan setelah setiap tanda +. Itu berarti buffer baru digunakan setiap saat. Jika Anda melakukan banyak + operasi tambahan, itu tidak efisien.

Mengapa Anda bisa membuatnya lebih efisien:

  • Anda menjamin efisiensi daripada mempercayai seorang delegasi untuk melakukannya secara efisien untuk Anda
  • kelas std :: string tidak tahu apa-apa tentang ukuran maksimal string Anda, atau seberapa sering Anda akan menggabungkannya. Anda mungkin memiliki pengetahuan ini dan dapat melakukan sesuatu berdasarkan informasi ini. Hal ini akan mengurangi alokasi ulang.
  • Anda akan mengontrol buffer secara manual sehingga Anda dapat yakin bahwa Anda tidak akan menyalin seluruh string ke buffer baru saat Anda tidak ingin hal itu terjadi.
  • Anda dapat menggunakan tumpukan untuk buffer Anda daripada heap yang jauh lebih efisien.
  • string + operator akan membuat objek string baru dan mengembalikannya menggunakan buffer baru.

Pertimbangan untuk implementasi:

  • Perhatikan panjang senar.
  • Pertahankan penunjuk ke akhir string dan awal, atau hanya awal dan gunakan awal + panjang sebagai offset untuk menemukan akhir string.
  • Pastikan buffer tempat Anda menyimpan string cukup besar sehingga Anda tidak perlu mengalokasikan ulang data
  • Gunakan strcpy sebagai pengganti strcat sehingga Anda tidak perlu mengulang panjang string untuk menemukan ujung string.

Struktur data tali:

Jika Anda membutuhkan penggabungan yang sangat cepat, pertimbangkan untuk menggunakan struktur data tali .

Brian R. Bondy
sumber
6
Catatan: "STL" mengacu pada pustaka sumber terbuka yang benar-benar terpisah, aslinya oleh HP, beberapa bagian di antaranya digunakan sebagai dasar untuk bagian-bagian Pustaka C ++ Standar ISO. "std :: string", bagaimanapun, tidak pernah menjadi bagian dari STL HP, jadi sangat salah untuk merujuk "STL dan" string "bersama-sama.
James Curran
1
Saya tidak akan mengatakan itu salah untuk menggunakan STL dan string bersama. Lihat sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy
1
Ketika SGI mengambil alih pemeliharaan STL dari HP, itu dipasang kembali agar sesuai dengan Perpustakaan Standar (itulah sebabnya saya mengatakan "jangan pernah menjadi bagian dari STL HP"). Namun demikian, pencetus std :: string adalah Komite ISO C ++.
James Curran
2
Catatan tambahan: Pegawai SGI yang bertugas memelihara STL selama bertahun-tahun adalah Matt Austern, yang, pada saat yang sama, mengepalai subkelompok Perpustakaan dari Komite Standardisasi ISO C ++.
James Curran
4
Bisakah Anda menjelaskan atau memberikan beberapa poin mengapa Anda dapat menggunakan tumpukan untuk buffer Anda daripada heap yang jauh lebih efisien. ? Dari manakah perbedaan efisiensi ini berasal?
h7r
76

Pesan ruang terakhir Anda sebelumnya, lalu gunakan metode append dengan buffer. Misalnya, Anda mengharapkan panjang string akhir Anda menjadi 1 juta karakter:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}
Carlos A. Ibarra
sumber
17

Aku tidak akan khawatir tentang hal itu. Jika Anda melakukannya dalam satu loop, string akan selalu mengalokasikan memori untuk meminimalkan realokasi - cukup gunakan operator+=dalam kasus itu. Dan jika Anda melakukannya secara manual, seperti ini atau lebih lama

a + " : " + c

Kemudian itu membuat temporaries - bahkan jika kompilator bisa menghilangkan beberapa salinan nilai yang dikembalikan. Itu karena dalam pemanggilan berturut-turut operator+tidak mengetahui apakah parameter referensi mereferensikan objek bernama atau sementara dikembalikan dari sub operator+pemanggilan. Saya lebih suka tidak khawatir tentang itu sebelum tidak membuat profil terlebih dahulu. Tapi mari kita ambil contoh untuk menunjukkannya. Kami pertama kali memperkenalkan tanda kurung untuk memperjelas pengikatan. Saya meletakkan argumen langsung setelah deklarasi fungsi yang digunakan untuk kejelasan. Di bawah itu, saya tunjukkan apa ekspresi yang dihasilkan kemudian:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

Sekarang, sebagai tambahan, tmp1adalah apa yang dikembalikan oleh panggilan pertama ke operator + dengan argumen yang ditampilkan. Kami menganggap kompilator benar-benar pintar dan mengoptimalkan salinan nilai kembalian. Jadi kita berakhir dengan satu string baru yang berisi penggabungan dari adan " : ". Sekarang, ini terjadi:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

Bandingkan dengan yang berikut ini:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

Ini menggunakan fungsi yang sama untuk sementara dan untuk string bernama! Jadi kompilator harus menyalin argumen ke string baru dan menambahkannya dan mengembalikannya dari badan operator+. Itu tidak bisa mengambil memori sementara dan menambahkannya. Semakin besar ekspresi, semakin banyak salinan string yang harus dilakukan.

Visual Studio dan GCC berikutnya akan mendukung semantik pemindahan c ++ 1x (melengkapi semantik salinan ) dan rvalue referensi sebagai tambahan eksperimental. Itu memungkinkan untuk mengetahui apakah parameter mereferensikan sementara atau tidak. Ini akan membuat penambahan seperti itu sangat cepat, karena semua hal di atas akan berakhir dalam satu "pipa tambahan" tanpa salinan.

Jika ternyata menjadi hambatan, Anda tetap bisa melakukannya

 std::string(a).append(" : ").append(c) ...

The appendpanggilan menambahkan argumen untuk *thisdan kemudian kembali referensi untuk diri mereka sendiri. Jadi tidak ada penyalinan sementara yang dilakukan di sana. Atau sebagai alternatif, operator+=dapat digunakan, tetapi Anda akan membutuhkan tanda kurung yang jelek untuk memperbaiki prioritas.

Johannes Schaub - litb
sumber
Saya harus memeriksa pelaksana stdlib benar-benar melakukan ini. : P libstdc++untuk operator+(string const& lhs, string&& rhs)melakukan return std::move(rhs.insert(0, lhs)). Kemudian jika keduanya adalah temporer, operator+(string&& lhs, string&& rhs)jika lhsmemiliki kapasitas yang memadai akan tersedia secara langsung append(). Di mana menurut saya risiko ini menjadi lebih lambat daripada operator+=jika lhstidak memiliki kapasitas yang cukup, karena kemudian jatuh kembali ke rhs.insert(0, lhs), yang tidak hanya harus memperluas buffer & menambahkan konten baru seperti append(), tetapi juga perlu menggeser konten asli sesuai keinginan rhs.
underscore_d
Bagian lain dari overhead yang dibandingkan operator+=adalah yang operator+masih harus mengembalikan nilai, jadi harus ke move()operan mana yang ditambahkan. Namun, saya rasa itu adalah overhead yang cukup kecil (menyalin beberapa petunjuk / ukuran) dibandingkan dengan menyalin seluruh string, jadi itu bagus!
underscore_d
11

Untuk sebagian besar aplikasi, itu tidak masalah. Cukup tulis kode Anda, tanpa menyadari bagaimana tepatnya operator + bekerja, dan hanya menangani masalah dengan tangan Anda sendiri jika itu menjadi hambatan yang nyata.

pesto
sumber
7
Tentu saja itu tidak layak untuk kebanyakan kasus, tetapi ini tidak benar-benar menjawab pertanyaannya.
Brian R. Bondy
1
ya. saya setuju hanya dengan mengatakan "profil lalu optimalkan" dapat dimasukkan sebagai komentar atas pertanyaan :)
Johannes Schaub - litb
6
Secara teknis, dia bertanya apakah ini "Diperlukan". Tidak, dan ini menjawab pertanyaan itu.
Samantha Branham
Cukup adil, tetapi pasti dibutuhkan untuk beberapa aplikasi. Jadi dalam aplikasi tersebut, jawabannya direduksi menjadi: 'menangani masalah dengan tangan Anda sendiri'
Brian R. Bondy
4
@Pesto Ada anggapan menyimpang di dunia pemrograman bahwa kinerja tidak penting dan kita bisa mengabaikan keseluruhan kesepakatan karena komputer terus menjadi lebih cepat. Masalahnya, bukan itu alasan orang memprogram dalam C ++ dan bukan itu sebabnya mereka memposting pertanyaan di stack overflow tentang penggabungan string yang efisien.
MrFox
7

Tidak seperti .NET System.Strings, std :: strings C ++ dapat berubah, dan oleh karena itu dapat dibangun melalui penggabungan sederhana secepat melalui metode lainnya.

James Curran
sumber
2
Terutama jika Anda menggunakan reserve () untuk membuat buffer cukup besar untuk hasil sebelum Anda mulai.
Mark Ransom
saya pikir dia berbicara tentang operator + =. itu juga menggabungkan, meskipun itu kasus yang merosot. james adalah vc ++ mvp jadi saya berharap dia memiliki beberapa petunjuk tentang c ++: p
Johannes Schaub - litb
1
Saya tidak ragu sedetik pun bahwa dia memiliki pengetahuan luas tentang C ++, hanya saja ada kesalahpahaman tentang pertanyaan itu. Pertanyaannya menanyakan tentang efisiensi operator + yang mengembalikan objek string baru setiap kali dipanggil, dan karenanya menggunakan buffer karakter baru.
Brian R. Bondy
1
ya. tapi kemudian dia minta operator case + lambat, cara apa yang terbaik adalah dengan melakukan penggabungan. dan di sini operator + = masuk ke dalam permainan. tapi saya setuju jawaban james agak pendek. membuatnya terdengar seperti kita semua bisa menggunakan operator + dan ini sangat efisien: p
Johannes Schaub - litb
@ BrianR.Bondy operator+tidak harus mengembalikan string baru. Pelaksana dapat mengembalikan salah satu operannya, dimodifikasi, jika operan itu diteruskan oleh referensi nilai r. libstdc++ melakukan ini, misalnya . Jadi, saat memanggil operator+dengan temporaries, itu dapat mencapai kinerja yang sama atau hampir sama baiknya - yang mungkin menjadi argumen lain yang mendukung default untuk itu kecuali jika seseorang memiliki tolok ukur yang menunjukkan bahwa itu mewakili kemacetan.
underscore_d
4

Dalam Imperfect C ++ , Matthew Wilson menyajikan penggabung string dinamis yang menghitung sebelumnya panjang string akhir agar hanya memiliki satu alokasi sebelum menggabungkan semua bagian. Kita juga bisa mengimplementasikan concatenator statis dengan bermain dengan template ekspresi .

Ide semacam itu telah diimplementasikan dalam implementasi STLport std :: string - yang tidak sesuai dengan standar karena peretasan yang tepat ini.

Luc Hermitte
sumber
Glib::ustring::compose()dari ikatan glibmm ke GLib melakukan itu: memperkirakan dan mengukur reserve()panjang akhir berdasarkan format string yang disediakan dan vararg, lalu append()masing-masing (atau penggantinya yang diformat) dalam satu lingkaran. Saya berharap ini adalah cara kerja yang cukup umum.
underscore_d
4

std::string operator+mengalokasikan string baru dan menyalin dua string operan setiap saat. ulangi berkali-kali dan itu menjadi mahal, O (n).

std::string appenddan operator+=di sisi lain, tingkatkan kapasitas sebesar 50% setiap kali tali perlu tumbuh. Yang mengurangi jumlah alokasi memori dan operasi penyalinan secara signifikan, O (log n).

timmerov
sumber
Saya tidak begitu yakin mengapa ini tidak disukai. Angka 50% tidak disyaratkan oleh Standar, tetapi IIRC itu atau 100% adalah ukuran umum pertumbuhan dalam praktiknya. Segala sesuatu yang lain dalam jawaban ini tampaknya tidak dapat dibantah.
underscore_d
Beberapa bulan kemudian, saya kira itu tidak terlalu akurat, karena ditulis lama setelah C ++ 11 memulai debutnya, dan kelebihan beban di operator+mana satu atau kedua argumen dilewatkan oleh referensi rvalue dapat menghindari alokasi string baru sama sekali dengan menggabungkan ke buffer yang ada dari salah satu operand (meskipun mungkin harus dialokasikan kembali jika kapasitasnya tidak mencukupi).
underscore_d
2

Untuk string kecil tidak masalah. Jika Anda memiliki string besar, sebaiknya Anda menyimpannya dalam bentuk vektor atau di koleksi lain sebagai bagian. Dan tambahkan algoritme Anda untuk bekerja dengan kumpulan data seperti itu, bukan dengan satu string besar.

Saya lebih suka std :: ostringstream untuk penggabungan kompleks.

Mykola Golubyev
sumber
2

Seperti kebanyakan hal, lebih mudah untuk tidak melakukan sesuatu daripada melakukannya.

Jika Anda ingin mengeluarkan string besar ke GUI, mungkin apa pun yang Anda hasilkan dapat menangani string dalam potongan lebih baik daripada sebagai string besar (misalnya, menggabungkan teks dalam editor teks - biasanya mereka membuat baris terpisah struktur).

Jika Anda ingin mengeluarkan ke file, streaming data daripada membuat string besar dan mengeluarkannya.

Saya tidak pernah menemukan kebutuhan untuk membuat penggabungan lebih cepat diperlukan jika saya menghapus penggabungan yang tidak perlu dari kode lambat.

Pete Kirkham
sumber
2

Mungkin performa terbaik jika Anda mengalokasikan (memesan) ruang sebelumnya dalam string yang dihasilkan.

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

Pemakaian:

std::string merged = concat("This ", "is ", "a ", "test!");
LanDenLabs
sumber
0

Larik karakter sederhana, yang dikemas dalam kelas yang melacak ukuran larik dan jumlah byte yang dialokasikan adalah yang tercepat.

Triknya adalah dengan melakukan satu alokasi besar di awal.

di

https://github.com/pedro-vicente/table-string

Tolak ukur

Untuk Visual Studio 2015, x86 debug build, peningkatan substansial melalui C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  
Pedro Vicente
sumber
1
OP tertarik pada bagaimana menggabungkan secara efisien std::string. Mereka tidak meminta kelas string alternatif.
underscore_d
0

Anda dapat mencoba yang ini dengan reservasi memori untuk setiap item:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
voltento.dll
sumber