C ++ 'da, bir fonksiyondan vektör döndürmek hala kötü bir uygulama mı?


103

Kısa versiyon: Vektörler / diziler gibi büyük nesneleri birçok programlama dilinde döndürmek yaygındır. Sınıfın bir hareket oluşturucusu varsa, bu stil artık C ++ 0x'de kabul edilebilir mi, yoksa C ++ programcıları bunu garip / çirkin / iğrenç olarak mı düşünüyor?

Uzun versiyon: C ++ 0x'de bu hala kötü form olarak kabul ediliyor mu?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

Geleneksel versiyon şu şekilde görünür:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

Daha yeni sürümde, döndürülen değer bir r değeridir BuildLargeVector, bu nedenle v std::vector, (N) RVO'nun yer almadığı varsayılarak, ' nin move yapıcısı kullanılarak oluşturulur .

C ++ 0x'den önce bile, (N) RVO nedeniyle ilk biçim genellikle "verimli" olurdu. Ancak, (N) RVO, derleyicinin takdirine bağlıdır. Artık rvalue referanslarımız olduğuna göre, derin bir kopyanın olmayacağı garanti edilmektedir .

Düzenleme : Soru gerçekten optimizasyonla ilgili değil. Gösterilen her iki form da gerçek dünya programlarında neredeyse aynı performansa sahiptir. Oysa geçmişte, ilk biçim, büyük ölçüde daha kötü performansa sahip olabilirdi. Sonuç olarak, ilk biçim C ++ programlamasında uzun süre büyük bir kod kokusuydu. Artık değil, umarım?


18
Başlamanın kötü bir form olduğunu kim söyledi?
Edward Strange

7
Benim geldiğim "eski günlerde" kesinlikle kötü bir kod kokusuydu. :-)
Nate

1
Umarım öyledir! Geçiş değerinin daha popüler hale geldiğini görmek isterim. :)
sellibitze

Yanıtlar:


73

Dave Abrahams, değerlerin geçme / geri dönme hızının oldukça kapsamlı bir analizine sahip .

Kısa cevap, bir değer döndürmeniz gerekiyorsa, bir değer döndürün. Çıktı referanslarını kullanmayın çünkü derleyici bunu zaten yapar. Tabii ki uyarılar var, bu yüzden o makaleyi okumalısınız.


24
"derleyici yine de yapar": derleyicinin bunu yapması gerekmez == belirsizlik == kötü fikir (% 100 kesinlik gerekir). "kapsamlı analiz" Bu analizle ilgili çok büyük bir sorun var - bilinmeyen derleyicideki belgelenmemiş / standart olmayan dil özelliklerine dayanır ("Her ne kadar standart tarafından kopya ayrımı gerekmiyorsa da"). Dolayısıyla, çalışsa bile, onu kullanmak iyi bir fikir değildir - amaçlandığı gibi çalışacağına dair kesinlikle hiçbir garanti yoktur ve her derleyicinin her zaman bu şekilde çalışacağına dair hiçbir garanti yoktur. Bu belgeye güvenmek kötü bir kodlama uygulamasıdır, IMO. Performansı kaybedecek olsanız bile.
SigTerm

5
@SigTerm: Bu harika bir yorum !!! atıfta bulunulan makalenin çoğu, üretimde kullanımı düşünülmeyecek kadar belirsizdir. İnsanlar, Red In-Deepth kitap yazan bir yazarın müjde olduğunu ve daha fazla düşünmeden veya analiz edilmeden ona uyulması gereken her şeyi düşünüyor. ATM Pazarda, Abrahams'ın makalede kullandığı örnekler kadar çeşitli kopya-elison sağlayan bir derleyici yok.
Hippicoder

13
@SigTerm, orada var çok derleyici yapmak gerekli olmadığını, ancak yine de yapar varsayalım. Derleyiciler değişikliğine "Gerekli" değildir x / 2etmek x >> 1için ints, ancak o olacak varsayıyorum. Standart ayrıca, derleyicilerin başvuruları uygulamak için nasıl gerekli olduğu hakkında hiçbir şey söylemiyor, ancak bunların işaretçiler kullanılarak verimli bir şekilde ele alındığını varsayıyorsunuz. Standart ayrıca v-tabloları hakkında hiçbir şey söylemez, bu nedenle sanal işlev çağrılarının da verimli olduğundan emin olamazsınız. Esasen, derleyiciye zaman zaman biraz güvenmeniz gerekir.
Peter Alexander

16
@Sig: Programınızın gerçek çıktısı dışında çok az şey garanti edilir. Her seferinde ne olacağı konusunda% 100 kesinlik istiyorsanız, o zaman doğrudan farklı bir dile geçmeniz daha iyi olur.
Dennis Zickefoose

6
@SigTerm: "Gerçek durum senaryosu" üzerinde çalışıyorum. Derleyicinin ne yaptığını test ediyorum ve bununla çalışıyorum. "Daha yavaş çalışabilir" diye bir şey yoktur. Basitçe daha yavaş çalışmaz çünkü standart gerektirsin ya da etmesin derleyici RVO uygular. Ifs yok, amas veya maybes yok, bu sadece basit bir gerçek.
Peter Alexander

37

En azından IMO, genellikle kötü bir fikir, ancak verimlilik nedeniyle değil . Bu kötü bir fikir çünkü söz konusu işlev genellikle çıktısını bir yineleyici aracılığıyla üreten genel bir algoritma olarak yazılmalıdır. Yineleyiciler üzerinde çalışmak yerine bir kabı kabul eden veya geri döndüren hemen hemen her kod şüpheli olarak değerlendirilmelidir.

Beni yanlış anlamayın: Koleksiyon benzeri nesnelerin (örneğin dizgiler) etrafından dolaşmanın mantıklı olduğu zamanlar vardır, ancak bahsedilen örnek için, vektörü geçirmeyi veya iade etmeyi kötü bir fikir olarak değerlendiririm.


6
Yineleyici yaklaşımındaki sorun, koleksiyon öğesi türü bilindiğinde bile, işlevleri ve yöntemleri şablon haline getirmenizi gerektirmesidir. Bu rahatsız edicidir ve söz konusu yöntem sanal olduğunda imkansızdır. Unutmayın, cevabınıza kendi başına katılmıyorum, ancak pratikte C ++ 'da biraz hantal hale geliyor.
jon-hanson

22
Katılmamalıyım. Çıktı için yineleyiciler kullanmak bazen uygun olabilir, ancak genel bir algoritma yazmıyorsanız, genel çözümler genellikle gerekçelendirilmesi zor olan kaçınılmaz ek yük sağlar. Hem kod karmaşıklığı hem de gerçek performans açısından.
Dennis Zickefoose

1
@Dennis: Deneyimlerimin tam tersi olduğunu söylemeliyim: İşin içinde olan türleri önceden bildiğimde bile şablon olarak çok sayıda şey yazıyorum çünkü bunu yapmak daha basit ve performansı artırıyor.
Jerry Coffin

9
Şahsen bir konteyneri iade ediyorum. Niyet açık, kod daha kolay, yazarken performansı pek umursamıyorum (sadece erken kötümserlikten kaçınıyorum). Bir çıktı yineleyici kullanmanın amacımı daha net hale getirip getirmeyeceğinden emin değilim ... ve mümkün olduğunca şablon olmayan koda ihtiyacım var, çünkü büyük bir proje bağımlılıkları gelişmeyi öldürüyor.
Matthieu M.

1
@Dennis: Bunu kavramsal olarak varsayacağım, asla bir aralığa yazmak yerine bir kapsayıcı oluşturmamalısınız. Bir kap tam da budur - bir kap. Endişeniz (ve kodunuzun endişesi) kapla değil, içerikle ilgili olmalıdır.
Jerry Coffin

18

Ana fikir şudur:

Kopya Elision ve RVO olabilir "korkutucu kopya" önlemek (derleyici bu optimizasyonları uygulamak için gerekli değildir ve bazı durumlarda bunun uygulanamaz)

C ++ 0x RValue başvuruları , bunu garanti eden bir dize / vektör uygulamalarına izin verir .

Eski derleyicileri / STL uygulamalarını terk edebilirseniz, vektörleri özgürce geri döndürün (ve kendi nesnelerinizin de bunu desteklediğinden emin olun). Kod tabanınızın "daha az" derleyicileri desteklemesi gerekiyorsa, eski stile bağlı kalın.

Ne yazık ki, bunun arayüzleriniz üzerinde büyük etkisi var. C ++ 0x bir seçenek değilse ve garantilere ihtiyacınız varsa, bunun yerine bazı senaryolarda referans sayılan veya yazma üzerine kopyalanan nesneleri kullanabilirsiniz. Yine de multithreading ile olumsuz yönleri var.

(C ++ 'da tek bir cevabın basit, anlaşılır ve koşulsuz olmasını diliyorum).


11

Gerçekten de, C ++ 11, maliyeti kopyalamastd::vector çoğu durumda gitti.

Bununla birlikte, yeni vektörü inşa etmenin (sonra onu yok etmenin) maliyetinin hala var olduğu ve vektörün kapasitesini yeniden kullanmak istediğinizde değere göre dönmek yerine çıktı parametrelerini kullanmanın hala yararlı olduğu unutulmamalıdır. Bu, C ++ Temel Yönergelerinin F.20'sinde bir istisna olarak belgelenmiştir .

Hadi karşılaştıralım:

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

ile:

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

Şimdi, bu yöntemleri aramamız gerektiğini varsayalım numIter sıkı bir döngü içinde kez ve bazı eylemler gerçekleştirmemiz . Örneğin, tüm elemanların toplamını hesaplayalım.

Kullanarak BuildLargeVector1şunları yaparsınız:

size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
    std::vector<int> v = BuildLargeVector1(vecSize);
    sum1 = std::accumulate(v.begin(), v.end(), sum1);
}

Kullanarak BuildLargeVector2şunları yaparsınız:

size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
    BuildLargeVector2(/*out*/ v, vecSize);
    sum2 = std::accumulate(v.begin(), v.end(), sum2);
}

İlk örnekte, halihazırda tahsis edilmiş belleği yeniden kullanarak eski yöntemle bir çıktı parametresi kullanılarak ikinci örnekte önlenen birçok gereksiz dinamik ayırma / serbest bırakma gerçekleşmektedir. Bu optimizasyonun yapmaya değip değmeyeceği, değerlerin hesaplanması / değiştirilmesinin maliyeti ile karşılaştırıldığında tahsis / serbest bırakmanın göreceli maliyetine bağlıdır.

Kıyaslama

Değerleri ile Let oyun vecSizeve numIter. VecSize * numIter sabit tutacağız, böylece "teoride", aynı zamanı almalı (= aynı değerlere sahip aynı sayıda atama ve ekleme var) ve zaman farkı yalnızca maliyetten gelebilir ayırmalar, serbest bırakmalar ve önbelleğin daha iyi kullanımı.

Daha spesifik olarak, vecSize * numIter = 2 ^ 31 = 2147483648'i kullanalım, çünkü 16GB RAM'im var ve bu sayı, 8GB'den daha fazla ayrılmamasını sağlıyor (sizeof (int) = 4), diske takas etmememi sağlıyor ( diğer tüm programlar kapatıldı, testi çalıştırırken ~ 15GB boş alanım vardı).

İşte kod:

#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

class Timer {
    using clock = std::chrono::steady_clock;
    using seconds = std::chrono::duration<double>;
    clock::time_point t_;

public:
    void tic() { t_ = clock::now(); }
    double toc() const { return seconds(clock::now() - t_).count(); }
};

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

int main() {
    Timer t;

    size_t vecSize = size_t(1) << 31;
    size_t numIter = 1;

    std::cout << std::setw(10) << "vecSize" << ", "
              << std::setw(10) << "numIter" << ", "
              << std::setw(10) << "time1" << ", "
              << std::setw(10) << "time2" << ", "
              << std::setw(10) << "sum1" << ", "
              << std::setw(10) << "sum2" << "\n";

    while (vecSize > 0) {

        t.tic();
        size_t sum1 = 0;
        {
            for (int i = 0; i < numIter; ++i) {
                std::vector<int> v = BuildLargeVector1(vecSize);
                sum1 = std::accumulate(v.begin(), v.end(), sum1);
            }
        }
        double time1 = t.toc();

        t.tic();
        size_t sum2 = 0;
        {
            std::vector<int> v;
            for (int i = 0; i < numIter; ++i) {
                BuildLargeVector2(/*out*/ v, vecSize);
                sum2 = std::accumulate(v.begin(), v.end(), sum2);
            }
        } // deallocate v
        double time2 = t.toc();

        std::cout << std::setw(10) << vecSize << ", "
                  << std::setw(10) << numIter << ", "
                  << std::setw(10) << std::fixed << time1 << ", "
                  << std::setw(10) << std::fixed << time2 << ", "
                  << std::setw(10) << sum1 << ", "
                  << std::setw(10) << sum2 << "\n";

        vecSize /= 2;
        numIter *= 2;
    }

    return 0;
}

Ve işte sonuç:

$ g++ -std=c++11 -O3 main.cpp && ./a.out
   vecSize,    numIter,      time1,      time2,       sum1,       sum2
2147483648,          1,   2.360384,   2.356355, 2147483648, 2147483648
1073741824,          2,   2.365807,   1.732609, 2147483648, 2147483648
 536870912,          4,   2.373231,   1.420104, 2147483648, 2147483648
 268435456,          8,   2.383480,   1.261789, 2147483648, 2147483648
 134217728,         16,   2.395904,   1.179340, 2147483648, 2147483648
  67108864,         32,   2.408513,   1.131662, 2147483648, 2147483648
  33554432,         64,   2.416114,   1.097719, 2147483648, 2147483648
  16777216,        128,   2.431061,   1.060238, 2147483648, 2147483648
   8388608,        256,   2.448200,   0.998743, 2147483648, 2147483648
   4194304,        512,   0.884540,   0.875196, 2147483648, 2147483648
   2097152,       1024,   0.712911,   0.716124, 2147483648, 2147483648
   1048576,       2048,   0.552157,   0.603028, 2147483648, 2147483648
    524288,       4096,   0.549749,   0.602881, 2147483648, 2147483648
    262144,       8192,   0.547767,   0.604248, 2147483648, 2147483648
    131072,      16384,   0.537548,   0.603802, 2147483648, 2147483648
     65536,      32768,   0.524037,   0.600768, 2147483648, 2147483648
     32768,      65536,   0.526727,   0.598521, 2147483648, 2147483648
     16384,     131072,   0.515227,   0.599254, 2147483648, 2147483648
      8192,     262144,   0.540541,   0.600642, 2147483648, 2147483648
      4096,     524288,   0.495638,   0.603396, 2147483648, 2147483648
      2048,    1048576,   0.512905,   0.609594, 2147483648, 2147483648
      1024,    2097152,   0.548257,   0.622393, 2147483648, 2147483648
       512,    4194304,   0.616906,   0.647442, 2147483648, 2147483648
       256,    8388608,   0.571628,   0.629563, 2147483648, 2147483648
       128,   16777216,   0.846666,   0.657051, 2147483648, 2147483648
        64,   33554432,   0.853286,   0.724897, 2147483648, 2147483648
        32,   67108864,   1.232520,   0.851337, 2147483648, 2147483648
        16,  134217728,   1.982755,   1.079628, 2147483648, 2147483648
         8,  268435456,   3.483588,   1.673199, 2147483648, 2147483648
         4,  536870912,   5.724022,   2.150334, 2147483648, 2147483648
         2, 1073741824,  10.285453,   3.583777, 2147483648, 2147483648
         1, 2147483648,  20.552860,   6.214054, 2147483648, 2147483648

Karşılaştırma sonuçları

(Intel i7-7700K @ 4.20GHz; 16GB DDR4 2400Mhz; Kubuntu 18.04)

Gösterim: mem (v) = v.size () * sizeof (int) = v.size () * 4 platformumda.

Şaşırtıcı olmayan bir şekilde, numIter = 1(yani mem (v) = 8GB) olduğunda, zamanlar tamamen aynıdır. Aslında, her iki durumda da belleğe sadece bir kez 8GB'lık devasa bir vektör ayırıyoruz. Bu aynı zamanda BuildLargeVector1 () kullanılırken hiçbir kopya olmadığını kanıtlıyor: Kopyayı yapmak için yeterli RAM'e sahip olmayacaktım!

Ne zaman numIter = 2 yerine vektör kapasitesini yeniden ikinci bir vektör 1.37x daha hızlı yeniden tahsis.

Ne zaman numIter = 256, (... tekrar tekrar 256 kez vektör ayırmayı kaldırma / yerine tahsis) vektörü kapasitesini yeniden hızlı 2.45x olduğunu :)

Biz zm1 den oldukça fazla sabit olduğunu fark olabilir numIter = 1için numIter = 2568 GB bir büyük vektör tahsis oldukça fazla 32MB 256 vektörleri tahsis olarak pahalı olduğu anlamına gelir. Bununla birlikte, 8GB'lık devasa bir vektör tahsis etmek, 32MB'lık bir vektör tahsis etmekten kesinlikle daha pahalıdır, bu nedenle vektörün kapasitesinin yeniden kullanılması performans kazanımları sağlar.

Gönderen numIter = 512(Mem (v) = 16MB) için numIter = 8M(Mem (v) = 1kb) tatlı nokta: iki yöntem de daha hızlı numIter ve vecSize tüm diğer kombinasyonlardan daha hızlı tam, ve. Bunun muhtemelen işlemcimin L3 önbellek boyutunun 8MB olmasıyla ilgisi var, böylece vektör hemen hemen tamamen önbelleğe sığacak. Ani sıçramanın neden time1mem (v) = 16MB için olduğunu gerçekten açıklamıyorum , mem (v) = 8MB olduğunda hemen sonra gerçekleşmesi daha mantıklı görünüyor. Şaşırtıcı bir şekilde, bu tatlı noktada, kapasiteyi yeniden kullanmamanın aslında biraz daha hızlı olduğunu unutmayın! Bunu gerçekten açıklamıyorum.

Ne zaman numIter > 8Mişler çirkin olsun başlar. Her iki yöntem de yavaşlar ancak vektörü değere göre döndürmek daha da yavaşlar. En kötü durumda, tek bir tek içeren bir vektörle int, değere göre dönmek yerine kapasiteyi yeniden kullanmak 3,3 kat daha hızlıdır. Muhtemelen bu, hakim olmaya başlayan malloc () 'un sabit maliyetlerinden kaynaklanmaktadır.

Time2 eğrisinin time1 eğrisinden nasıl daha pürüzsüz olduğuna dikkat edin: sadece vektör kapasitesinin yeniden kullanılması genel olarak daha hızlı değil, belki daha da önemlisi, daha öngörülebilirdir .

Ayrıca tatlı noktada ~ 0,5 saniyede 2 milyar 64 bit tam sayı eklemeyi gerçekleştirebildik, bu da 4.2Ghz 64bit işlemcide oldukça ideal. Tüm 8 çekirdeği kullanmak için hesaplamayı paralel hale getirerek daha iyisini yapabilirdik (yukarıdaki test bir seferde yalnızca bir çekirdek kullanıyor, bunu CPU kullanımını izlerken testi yeniden çalıştırarak doğruladım). En iyi performans, L1 önbelleğinin büyüklük sırası olan mem (v) = 16kB olduğunda elde edilir (i7-7700K için L1 veri önbelleği 4x32kB'dir).

Elbette, veriler üzerinde gerçekte ne kadar çok hesaplama yapmanız gerekiyorsa, farklılıklar giderek daha az alakalı hale gelir. Biz değiştirirseniz Aşağıda sonuçlarıdır sum = std::accumulate(v.begin(), v.end(), sum);tarafından for (int k : v) sum += std::sqrt(2.0*k);:

Ölçüt 2

Sonuçlar

  1. Çıkış parametreleri kullanarak yerine değeriyle dönen edebilir yeniden kullanarak kapasite ile performans kazançlarını sağlarlar.
  2. Modern bir masaüstü bilgisayarda, bu yalnızca büyük vektörler (> 16MB) ve küçük vektörler (<1kB) için geçerli görünüyor.
  3. Milyonlarca / milyarlarca küçük vektörü (<1kB) ayırmaktan kaçının. Mümkünse kapasiteyi yeniden kullanın veya daha iyisi mimarinizi farklı şekilde tasarlayın.

Sonuçlar diğer platformlarda farklılık gösterebilir. Her zaman olduğu gibi, performans önemliyse, özel kullanım durumunuz için ölçütler yazın.


6

Hala kötü bir uygulama olduğunu düşünüyorum ancak ekibimin MSVC 2008 ve GCC 4.1 kullandığını belirtmek gerekir, bu yüzden en son derleyicileri kullanmıyoruz.

Daha önce, MSVC 2008 ile vtune'da gösterilen birçok etkin nokta dize kopyalamaya gelmişti. Şöyle bir kodumuz vardı:

String Something::id() const
{
    return valid() ? m_id: "";
}

... kendi String tipimizi kullandığımıza dikkat edin (bu gerekliydi çünkü eklenti yazarlarının farklı derleyiciler ve dolayısıyla farklı, uyumsuz std :: string / std :: wstring uygulamaları kullanabileceği bir yazılım geliştirme kiti sağlıyoruz).

String :: String (const String &) gösteren çağrı grafiği örnekleme profil oluşturma oturumuna yanıt olarak basit bir değişiklik yaptım ve önemli miktarda zaman alıyor. Yukarıdaki örnekte olduğu gibi yöntemler en çok katkıda bulunanlardır (aslında profil oluşturma oturumu bellek ayırma ve serbest bırakmanın en büyük sıcak noktalardan biri olduğunu gösterdi, String copy yapıcısı ayırmalara birincil katkıda bulunuyordu).

Yaptığım değişiklik basitti:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

Yine de bu bir dünya fark yarattı! Hotspot sonraki profil oluşturucu oturumlarında ortadan kalktı ve buna ek olarak, uygulama performansımızı takip etmek için birçok kapsamlı birim testi yapıyoruz. Bu basit değişikliklerden sonra her türlü performans testi süreleri önemli ölçüde düştü.

Sonuç: Mutlak en son derleyicileri kullanmıyoruz, ancak yine de derleyicinin, değer bazında güvenilir bir şekilde geri dönmek için kopyalamayı optimize etmesine güvenemiyoruz (en azından her durumda değil). Bu, MSVC 2010 gibi daha yeni derleyiciler kullananlar için geçerli olmayabilir. C ++ 0x'i ne zaman kullanabileceğimizi ve sadece rvalue referanslarını ne zaman kullanabileceğimizi ve karmaşık geri dönerek kodumuza zarar verdiğimizden endişelenmemizi dört gözle bekliyorum. değere göre sınıflar.

[Düzenle] Nate'in belirttiği gibi, RVO bir işlevin içinde oluşturulan geçicilerin döndürülmesi için geçerlidir. Benim durumumda, bu tür geçiciler yoktu (boş bir dize oluşturduğumuz geçersiz dal hariç) ve bu nedenle RVO uygulanamazdı.


3
Mesele bu: RVO derleyiciye bağımlıdır, ancak bir C ++ 0x derleyicisi , RVO kullanmamaya karar verirse (bir hareket oluşturucu olduğunu varsayarak) hareket anlamını kullanmalıdır. Trigraf operatörünü kullanmak RVO'yu bozar. Peter'ın bahsettiği cpp-next.com/archive/2009/09/move-it-with-rvalue-references bakın . Ancak örneğiniz, geçici olarak geri dönmediğiniz için zaten taşıma semantiği için uygun değil.
Nate

@ Stinky472: Bir üyeyi değere göre döndürmek her zaman referanstan daha yavaş olacaktı. Rdeğer referansları, orijinal üyeye bir referans döndürmekten daha yavaş olacaktır (arayan kişi bir kopyaya ihtiyaç duymak yerine bir referans alabilirse). Buna ek olarak, rvalue referanslarına göre kaydedebileceğiniz birçok kez vardır, çünkü bağlamınız vardır. Örneğin, String newstring yapabilirsiniz; newstring.resize (string1.size () + string2.size () + ...); yeni dize + = dize1; yeni dize + = dize2; vb. Bu hala değerlere göre önemli bir tasarruftur.
Puppy

@DeadMG, RVO uygulayan C ++ 0x derleyicileriyle bile ikili operatöre göre önemli bir tasarruf mu? Eğer öyleyse, bu utanç verici. Sonunda, birleştirilmiş dizeyi hesaplamak için bir geçici oluşturmak zorunda kaldığımız için, + = doğrudan yeni dizeye birleştirebiliriz.
stinky472

Şöyle bir duruma ne dersiniz: string newstr = str1 + str2; Hareket semantiğini uygulayan bir derleyicide, bunun şu kadar hızlı veya daha hızlı olması gerektiği görülmektedir: string newstr; yenistr + = str1; yenistr + = dizi2; Rezerv yok, tabiri caizse (yeniden boyutlandırmak yerine rezerve etmek istediğinizi varsayıyorum).
stinky472

5
@Nate: Ne kafa karıştırıcı olduğunu düşünüyorum üç karakterli gibi <::ya ??!ile koşullu operatör ?: (bazen üçlü operatörü ).
fredoverflow

3

Kısaca özetlemek gerekirse: birçok programlama dilinde dizileri fonksiyonlardan döndürmek yaygın değildir. Çoğunda, diziye bir başvuru döndürülür. C ++ 'da, en yakın benzetme geri dönmek olacaktırboost::shared_array


4
@Billy: std :: vector, kopyalama anlamlarına sahip bir değer türüdür. Mevcut C ++ standardı, (N) RVO'nun hiçbir zaman uygulanacağına dair hiçbir garanti sunmamaktadır ve uygulamada, uygulanmadığı zamanlarda birçok gerçek hayat senaryosu vardır.
Nemanja Trifunovic

3
@Billy: Yine, en son derleyicilerin bile NRVO uygulamadığı bazı çok gerçek senaryolar var: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic

3
@Billy ONeal:% 99 yeterli değil,% 100'e ihtiyacınız var. Murphy yasası - "Bir şeyler ters giderse, olur". Bir tür bulanık mantıkla uğraşıyorsanız belirsizlik iyidir, ancak geleneksel yazılım yazmak için bu iyi bir fikir değildir. Kodun düşündüğünüz gibi çalışmama olasılığı% 1 bile olsa, bu kodun sizi kovulacak kritik bir hata oluşturacağını beklemelisiniz. Üstelik standart bir özellik değil. Belgelenmemiş özellikleri kullanmak kötü bir fikirdir - eğer bir yıl içinde derleyici özelliği bırakacaksa ( standart gereği yoktur , değil mi?), Başınız belada olacak.
SigTerm

4
@SigTerm: Davranışın doğruluğundan bahsediyor olsaydık, sana katılırdım. Ancak bir performans optimizasyonundan bahsediyoruz. Bu tür şeyler% 100'den daha az kesinlik ile iyidir.
Billy ONeal

2
@Nemanja: Burada neye "güvenildiğini" anlamıyorum. Uygulamanız, RVO veya NRVO kullanılsa da aynı şekilde çalışır. Yine de kullanılırlarsa, daha hızlı çalışacaktır. Uygulamanız belirli bir platformda çok yavaşsa ve değeri kopyalamayı döndürmek için onu geriye doğru izlediyseniz, o zaman kesinlikle değiştirin, ancak bu, en iyi uygulamanın yine de dönüş değerini kullanmak olduğu gerçeğini değiştirmez. Kopyalama olmadığından kesinlikle emin olmanız gerekiyorsa, vektörü a içine sarın shared_ptrve bir gün olarak adlandırın.
Billy ONeal

2

Performans gerçek bir sorunsa, hareket anlamının kopyalamadan her zaman daha hızlı olmadığını anlamalısınız . Örneğin, küçük dizgi optimizasyonunu kullanan bir dizeniz varsa, küçük dizeler için bir hareket oluşturucu, normal bir kopya oluşturucu ile tam olarak aynı miktarda iş yapmalıdır.


1
NRVO, yalnızca hareket oluşturucuları eklendiği için kaybolmaz.
Billy ONeal

1
@Billy, gerçek ama alakasız, soru C ++ 0x iyi uygulamaları değişti ve NRVO nedeniyle C ++ 0x için değişmedi
Motti
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.