Wierzę, że masz prawidłową obserwację, ale złą interpretację!
Kopiowanie nie nastąpi po zwróceniu wartości, ponieważ każdy normalny sprytny kompilator użyje w tym przypadku (N) RVO . Od C ++ 17 jest to obowiązkowe, więc nie możesz zobaczyć żadnej kopii, zwracając lokalnie wygenerowany wektor z funkcji.
OK, zagrajmy trochę z std::vector
tym, co stanie się podczas budowy lub wypełniając ją krok po kroku.
Po pierwsze, pozwólmy wygenerować typ danych, który sprawia, że każda kopia lub ruch jest widoczny jak ten:
template <typename DATA >
struct VisibleCopy
{
private:
DATA data;
public:
VisibleCopy( const DATA& data_ ): data{ data_ }
{
std::cout << "Construct " << data << std::endl;
}
VisibleCopy( const VisibleCopy& other ): data{ other.data }
{
std::cout << "Copy " << data << std::endl;
}
VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
{
std::cout << "Move " << data << std::endl;
}
VisibleCopy& operator=( const VisibleCopy& other )
{
data = other.data;
std::cout << "copy assign " << data << std::endl;
}
VisibleCopy& operator=( VisibleCopy&& other ) noexcept
{
data = std::move( other.data );
std::cout << "move assign " << data << std::endl;
}
DATA Get() const { return data; }
};
A teraz zacznijmy eksperymenty:
using T = std::vector< VisibleCopy<int> >;
T Get1()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
std::cout << "End init" << std::endl;
return vec;
}
T Get2()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec(4,0);
std::cout << "End init" << std::endl;
return vec;
}
T Get3()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
T Get4()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.reserve(4);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
int main()
{
auto vec1 = Get1();
auto vec2 = Get2();
auto vec3 = Get3();
auto vec4 = Get4();
// All data as expected? Lets check:
for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}
Co możemy zaobserwować:
Przykład 1) Tworzymy wektor z listy inicjalizatora i być może oczekujemy, że zobaczymy 4 razy konstrukcję i 4 ruchy. Ale dostajemy 4 kopie! Brzmi to trochę tajemniczo, ale powodem jest implementacja listy inicjalizacyjnej! Po prostu nie można poruszać się z listy, ponieważ iterator z listy const T*
uniemożliwia przenoszenie z niej elementów. Szczegółową odpowiedź na ten temat można znaleźć tutaj: lista_inicjalizatora i semantyka przenoszenia
Przykład 2) W tym przypadku otrzymujemy wstępną konstrukcję i 4 kopie wartości. To nic specjalnego i tego możemy się spodziewać.
Przykład 3) Również tutaj mamy konstrukcję i niektóre ruchy zgodnie z oczekiwaniami. Dzięki mojej implementacji stl wektor rośnie za każdym razem o współczynnik 2. Widzimy więc pierwszą konstrukcję, inną, a ponieważ wektor zmienia rozmiar z 1 na 2, widzimy ruch pierwszego elementu. Dodając 3, widzimy zmianę rozmiaru z 2 na 4, która wymaga przesunięcia pierwszych dwóch elementów. Wszystko zgodnie z oczekiwaniami!
Przykład 4) Teraz rezerwujemy miejsce i wypełniamy później. Teraz nie mamy już żadnej kopii ani żadnego ruchu!
We wszystkich przypadkach nie widzimy żadnego ruchu ani kopii, zwracając w ogóle wektor z powrotem do dzwoniącego! (N) RVO ma miejsce i na tym etapie nie są wymagane żadne dalsze działania!
Powrót do pytania:
„Jak znaleźć fałszywe operacje kopiowania w C ++”
Jak widać powyżej, możesz wprowadzić klasę pośrednią pomiędzy nimi w celu debugowania.
Ustanowienie prywatnego programu kopiującego-ctora może nie działać w wielu przypadkach, ponieważ możesz mieć niektóre poszukiwane kopie i niektóre ukryte. Jak wyżej, tylko kod na przykład 4 będzie działał z prywatnym narzędziem kopiującym! I nie mogę odpowiedzieć na pytanie, czy przykład 4 jest najszybszy, ponieważ pokój wypełniamy pokojem.
Przepraszam, że nie mogę zaoferować ogólnego rozwiązania w zakresie wyszukiwania „niechcianych” kopii. Nawet jeśli wykopiesz kod dla wywołań memcpy
, nie znajdziesz wszystkich, ponieważ memcpy
zostaną one również zoptymalizowane i zobaczysz bezpośrednio niektóre instrukcje asemblera wykonujące zadanie bez wywołania memcpy
funkcji biblioteki .
Moja wskazówka nie polega na skupieniu się na tak drobnym problemie. Jeśli masz rzeczywiste problemy z wydajnością, weź profiler i zmierz. Jest tak wielu potencjalnych zabójców wydajności, że inwestowanie dużo czasu w fałszywe memcpy
użycie nie wydaje się tak wartościowym pomysłem.
std::vector
jakikolwiek sposób nie jest tym, czym się wydaje . Twój przykład pokazuje wyraźną kopię, i jest to naturalne i prawidłowe podejście (ponownie imho), aby zastosować tęstd::move
funkcję, jak sugerujesz sobie, jeśli kopia nie jest tym, czego chcesz. Zauważ, że niektóre kompilatory mogą pominąć kopiowanie, jeśli flagi optymalizacji są włączone, a wektor pozostaje niezmieniony.