Ostatnio miałem następujące
struct data {
std::vector<int> V;
};
data get_vector(int n)
{
std::vector<int> V(n,0);
return {V};
}
Problem z tym kodem polega na tym, że po utworzeniu struktury następuje kopia, a zamiast tego rozwiązaniem jest napisanie return {std :: move (V)}
Czy istnieje liniowiec lub analizator kodu, który wykryłby takie fałszywe operacje kopiowania? Nie mogą tego zrobić ani cppcheck, cpplint, ani clang-tidy.
EDYCJA: Kilka kwestii, aby wyjaśnić moje pytanie:
- Wiem, że wystąpiła operacja kopiowania, ponieważ użyłem eksploratora kompilatora i wyświetla on wezwanie do memcpy .
- Mógłbym zidentyfikować, że wystąpiły operacje kopiowania, patrząc na standard tak. Ale moim początkowym błędnym pomysłem było to, że kompilator zoptymalizuje tę kopię. Myliłem się.
- Nie jest (prawdopodobnie) problemem kompilatora, ponieważ zarówno clang, jak i gcc produkują kod, który tworzy memcpy .
- Memcpy może być tani, ale nie mogę sobie wyobrazić okoliczności, w których kopiowanie pamięci i usuwanie oryginału jest tańsze niż przekazywanie wskaźnika przez std :: move .
- Dodanie std :: move jest operacją elementarną. Wyobrażam sobie, że analizator kodu mógłby zasugerować tę poprawkę.
c++
code-analysis
static-code-analysis
cppcheck
Mathieu Dutour Sikiric
źródło
źródło
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.Odpowiedzi:
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:
A teraz zacznijmy eksperymenty:
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 przenoszeniaPrzykł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 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łaniamemcpy
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.źródło
Czy umieściłeś całą aplikację w eksploratorze kompilatorów i czy włączono optymalizacje? Jeśli nie, to to, co zobaczyłeś w eksploratorze kompilatorów, może, ale nie musi, być tym, co dzieje się z twoją aplikacją.
Jednym z problemów z opublikowanym kodem jest to, że najpierw tworzysz
std::vector
, a następnie kopiujesz do instancjidata
. Lepiej byłoby zainicjalizowaćdata
za pomocą wektora:Ponadto, jeśli podasz kompilatorowi eksplorator definicji
data
iget_vector()
, i nic więcej, musi spodziewać się gorszego. Jeśli faktycznie podasz mu kod źródłowy, który używaget_vector()
, sprawdź, jaki zestaw jest generowany dla tego kodu źródłowego. Zobacz ten przykład, aby dowiedzieć się, co powyższa modyfikacja oraz faktyczne użycie i optymalizacje kompilatora mogą spowodować wygenerowanie kompilatora.źródło