Cara efisien untuk mengembalikan std :: vector di c ++

106

Berapa banyak data yang disalin, ketika mengembalikan std :: vector dalam sebuah fungsi dan seberapa besar optimasi akan menempatkan std :: vector di free-store (di heap) dan mengembalikan pointer yaitu:

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

lebih efisien daripada:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 

?

Morten
sumber
3
Bagaimana kalau melewatkan vektor dengan referensi dan kemudian mengisinya di dalam f?
Kiril Kirov
4
RVO adalah pengoptimalan yang cukup mendasar yang dapat dilakukan oleh sebagian besar kompiler setiap saat.
Remus Rusanu
Saat jawaban mengalir masuk, ini dapat membantu Anda untuk mengklarifikasi apakah Anda menggunakan C ++ 03 atau C ++ 11. Praktik terbaik antara kedua versi tersebut sedikit berbeda.
Drew Dormann
@Kiril Kirov, Dapatkah saya melakukannya tanpa meletakkannya dalam daftar argumen fungsi yaitu. void f (std :: vector & result)?
Morten

Jawaban:

140

Di C ++ 11, ini adalah cara yang disukai:

std::vector<X> f();

Artinya, kembali dengan nilai.

Dengan C ++ 11, std::vectormemiliki semantik bergerak, yang berarti vektor lokal yang dideklarasikan dalam fungsi Anda akan dipindahkan saat kembali dan dalam beberapa kasus bahkan pemindahan tersebut dapat dieliminasi oleh kompilator.

Nawaz
sumber
13
@LeonidVolnitsky: Ya jika itu lokal . Nyatanya, return std::move(v);akan menonaktifkan move-elision meski pun bisa dengan hanya return v;. Jadi yang terakhir lebih disukai.
Nawaz
1
@juanchopanza: Sepertinya tidak. Sebelum C ++ 11, Anda dapat membantahnya karena vektor tidak akan dipindahkan; dan RVO adalah hal yang bergantung pada kompilator! Bicarakan tentang hal-hal dari tahun 80-an & 90-an.
Nawaz
2
Pemahaman saya tentang nilai kembali (berdasarkan nilai) adalah: alih-alih 'telah dipindahkan', nilai kembali di callee dibuat di tumpukan pemanggil, jadi semua operasi di callee ada di tempat, tidak ada yang dipindahkan di RVO . Apakah itu benar?
r0ng
2
@ r0ng: Ya, itu benar. Begitulah biasanya kompiler mengimplementasikan RVO.
Nawaz
1
@Nggak. Bahkan tidak ada lagi gerakan.
Lightness Races di Orbit
70

Anda harus kembali dengan nilai.

Standar memiliki fitur khusus untuk meningkatkan efisiensi pengembalian berdasarkan nilai. Ini disebut "penghapusan salinan", dan lebih khusus lagi dalam hal ini "bernama optimasi nilai pengembalian (NRVO)".

Compiler tidak harus mengimplementasikannya, tetapi compiler tidak harus mengimplementasikan function inlining (atau melakukan optimasi sama sekali). Tetapi kinerja pustaka standar bisa sangat buruk jika kompiler tidak dioptimalkan, dan semua kompiler serius menerapkan inlining dan NRVO (dan pengoptimalan lainnya).

Ketika NRVO diterapkan, tidak akan ada penyalinan pada kode berikut:

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

Tetapi pengguna mungkin ingin melakukan ini:

std::vector<int> myvec;
... some time later ...
myvec = f();

Penghapusan salinan tidak mencegah penyalinan di sini karena ini adalah tugas daripada inisialisasi. Namun, Anda tetap harus mengembalikan nilai. Di C ++ 11, tugas dioptimalkan oleh sesuatu yang berbeda, yang disebut "semantik bergerak". Di C ++ 03, kode di atas memang menyebabkan salinan, dan meskipun secara teori pengoptimal mungkin dapat menghindarinya, dalam praktiknya terlalu sulit. Jadi alih-alih myvec = f(), di C ++ 03 Anda harus menulis ini:

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

Ada opsi lain, yaitu menawarkan antarmuka yang lebih fleksibel kepada pengguna:

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

Anda juga dapat mendukung antarmuka berbasis vektor yang ada selain itu:

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

Ini mungkin kurang efisien daripada kode yang ada, jika kode yang ada digunakan reserve()dengan cara yang lebih kompleks daripada jumlah tetap di muka. Tetapi jika kode Anda yang ada pada dasarnya memanggil push_backvektor berulang kali, maka kode berbasis template ini seharusnya sama baiknya.

Steve Jessop
sumber
Suara positif untuk jawaban yang benar-benar terbaik dan terperinci. Namun, dalam varian swap () Anda ( untuk C ++ 03 tanpa NRVO ) Anda masih akan memiliki satu salinan copy-constructor yang dibuat di dalam f (): dari hasil variabel ke objek sementara tersembunyi yang akhirnya akan ditukar ke myvec .
JenyaKh
@JenyaKh: tentu, itu masalah kualitas implementasi. Standar tersebut tidak mengharuskan implementasi C ++ 03 mengimplementasikan NRVO, sama seperti standar tersebut tidak memerlukan fungsi inlining. Perbedaan dari function inlining, adalah bahwa inlining tidak mengubah semantik atau program Anda sedangkan NRVO tidak. Kode portabel harus berfungsi dengan atau tanpa NRVO. Kode yang dioptimalkan untuk implementasi tertentu (dan tanda compiler tertentu) dapat mencari jaminan terkait NRVO dalam dokumentasi implementasinya sendiri.
Steve Jessop
3

Sudah waktunya saya memposting jawaban tentang RVO , saya juga ...

Jika Anda mengembalikan objek berdasarkan nilai, kompilator sering kali mengoptimalkannya sehingga tidak dibangun dua kali, karena akan berlebihan jika membuatnya dalam fungsi sebagai sementara lalu menyalinnya. Ini disebut pengoptimalan nilai pengembalian: objek yang dibuat akan dipindahkan alih-alih disalin.


sumber
1

Idiom pra-C ++ 11 yang umum adalah meneruskan referensi ke objek yang sedang diisi.

Maka tidak ada penyalinan vektor.

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 
Drew Dormann
sumber
3
Itu tidak lagi menjadi idiom di C ++ 11.
Nawaz
1
@Nawaz Saya setuju. Saya tidak yakin apa praktik terbaik sekarang di SO mengenai pertanyaan tentang C ++ tetapi tidak secara khusus C ++ 11. Saya rasa saya harus cenderung memberikan jawaban C ++ 11 kepada siswa, C ++ 03 jawaban untuk seseorang yang memahami kode produksi. Apakah Anda memiliki pendapat?
Drew Dormann
7
Sebenarnya setelah keluarnya C ++ 11 (yang berumur 19 bulan), saya menganggap setiap pertanyaan menjadi pertanyaan C ++ 11, kecuali jika secara eksplisit dinyatakan sebagai pertanyaan C ++ 03.
Nawaz
1

Jika compiler mendukung Named Return Value Optimization ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ), Anda dapat langsung mengembalikan vektor asalkan tidak ada:

  1. Jalur berbeda mengembalikan objek bernama berbeda
  2. Beberapa jalur pengembalian (bahkan jika objek bernama yang sama dikembalikan di semua jalur) dengan status EH yang diperkenalkan.
  3. Objek bernama yang dikembalikan direferensikan dalam blok asm sebaris.

NRVO mengoptimalkan pemanggilan konstruktor salinan dan destruktor yang berlebihan dan dengan demikian meningkatkan kinerja secara keseluruhan.

Seharusnya tidak ada perbedaan nyata dalam contoh Anda.

taocp
sumber
0
vector<string> getseq(char * db_file)

Dan jika Anda ingin mencetaknya di main () Anda harus melakukannya dalam satu putaran.

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}
Akash Kandpal
sumber
-2

Betapapun bagusnya "pengembalian berdasarkan nilai", ini adalah jenis kode yang dapat menyebabkan kesalahan. Pertimbangkan program berikut:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • T: Apa yang akan terjadi jika hal di atas dijalankan? J: Sebuah coredump.
  • T: Mengapa kompilator tidak menangkap kesalahan? J: Karena programnya secara sintaksis, meskipun tidak secara semantik, benar.
  • T: Apa yang terjadi jika Anda memodifikasi vecFunc () untuk mengembalikan referensi? J: Program berjalan sampai selesai dan memberikan hasil yang diharapkan.
  • T: Apa bedanya? J: Kompilator tidak harus membuat dan mengelola objek anonim. Pemrogram telah menginstruksikan kompilator untuk menggunakan tepat satu objek untuk iterator dan untuk penentuan titik akhir, daripada dua objek berbeda seperti yang dilakukan contoh yang rusak.

Program yang salah di atas tidak akan menunjukkan kesalahan meskipun seseorang menggunakan opsi pelaporan GNU g ++ -Wall -Wextra -Weffc ++

Jika Anda harus menghasilkan nilai, hal berikut akan berfungsi sebagai pengganti pemanggilan vecFunc () dua kali:

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

Hal di atas juga tidak menghasilkan objek anonim selama iterasi loop, tetapi memerlukan operasi penyalinan yang mungkin (yang, seperti beberapa catatan, mungkin dioptimalkan dalam beberapa keadaan. Tetapi metode referensi menjamin bahwa tidak ada salinan yang akan dihasilkan. Percaya kompiler akan melakukan RVO bukanlah pengganti untuk mencoba membangun kode paling efisien yang Anda bisa.Jika Anda dapat memperdebatkan kebutuhan kompiler untuk melakukan RVO, Anda berada di depan permainan.

naga paman
sumber
3
Ini lebih merupakan contoh dari apa yang bisa salah jika pengguna tidak terbiasa dengan C ++ secara umum. Seseorang yang akrab dengan bahasa berbasis objek seperti .net atau javascript mungkin akan berasumsi bahwa vektor string selalu diteruskan sebagai penunjuk dan oleh karena itu dalam contoh Anda akan selalu menunjuk ke objek yang sama. vecfunc (). begin () dan vecfunc (). end () belum tentu cocok dengan contoh Anda karena mereka harus merupakan salinan dari vektor string.
Medran
-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
Amruth A
sumber