Po pierwsze: wiem, że większość błędów optymalizacyjnych wynika z błędów programistycznych lub z faktów, które mogą się zmieniać w zależności od ustawień optymalizacyjnych (wartości zmiennoprzecinkowe, problemy z wielowątkowością, ...).Błąd optymalizatora lub błąd programowania?
Jednak doświadczyłem bardzo trudnego do znalezienia błędu i nie jestem pewien, czy istnieje sposób, aby zapobiec występowaniu tego rodzaju błędów, bez wyłączania optymalizacji. Czy czegoś brakuje? Czy to naprawdę może być błąd optymalizatora? Oto uproszczony przykład:
struct Data {
int a;
int b;
double c;
};
struct Test {
void optimizeMe();
Data m_data;
};
void Test::optimizeMe() {
Data * pData; // Note that this pointer is not initialized!
bool first = true;
for (int i = 0; i < 3; ++i) {
if (first) {
first = false;
pData = &m_data;
pData->a = i * 10;
pData->b = i * pData->a;
pData->c = pData->b/2;
} else {
pData->a = ++i;
} // end if
} // end for
};
int main(int argc, char *argv[]) {
Test test;
test.optimizeMe();
return 0;
}
Rzeczywisty program oczywiście ma o wiele więcej do zrobienia niż to. Wszystko sprowadza się jednak do tego, że zamiast bezpośredniego dostępu do m_data używany jest wskaźnik (poprzednio ujednolicony). Jak tylko dodać wystarczającą oświadczenia do if (first)
-part optymalizator wydaje się, aby zmienić kod na coś wzdłuż tych linii:
if (first) {
first = false;
// pData-assignment has been removed!
m_data.a = i * 10;
m_data.b = i * m_data.a;
m_data.c = m_data.b/m_data.a;
} else {
pData->a = ++i; // This will crash - pData is not set yet.
} // end if
Jak widać, zastępuje niepotrzebnego wskaźnik dereference z bezpośredniego zapisu do struktura członkowska. Jednak nie robi tego w gałęzi else
. Usuwa również funkcję-pData
. Ponieważ wskaźnik jest teraz nadal unitaryzowany, program ulegnie awarii w gałęzi else
.
Oczywiście istnieje wiele rzeczy, które można poprawić tutaj, więc można zrzucić winę na programatorze:
- Zapomnij o wskaźnik i robić to, co robi optymalizator - użyj
m_data
bezpośrednio. - Zainicjuj pData na nullptr - w ten sposób optymalizator wie, że
else
-branch zakończy się niepowodzeniem, jeśli wskaźnik nie zostanie nigdy przypisany. Przynajmniej wydaje się, że rozwiązuje problem w moim środowisku testowym. - Przenieś przypisanie wskaźnika przed pętlą (efektywnie inicjując
pData
z&m_data
, która może być również referencją zamiast wskaźnika (dla dokładnej miary) .To ma sens, ponieważ pData jest potrzebna we wszystkich przypadkach, więc nie ma potrzeby powód, aby to zrobić wewnątrz pętli
Kod jest oczywiście śmierdzący, co najmniej, a ja nie staram się „winić” optymalizator za robienie tego, ale pytam:.. Co am I robić niewłaściwy? Program może być brzydki, ale to ważny kod ...
należy dodać, że używam VS2012 z C + +/CLI i v110_xp-Zestaw narzędzi. Optymalizacja jest ustawiona na/O2. Zwróć też uwagę, że jeśli naprawdę chcesz odtworzyć problem (nie jest to naprawdę kwestia tego pytania), musisz pogodzić się ze złożonością programu. Jest to bardzo uproszczony przykład, a optymalizator czasami nie usuwa przypisania wskaźnika. Ukrywanie &m_data
za funkcją wydaje się "pomagać".
EDIT:
Q: Skąd mam wiedzieć, że kompilator optymalizuje go do czegoś podobnego przykładu warunkiem?
A: Nie jestem bardzo dobry w czytaniu asemblera, I spojrzał na nią jednak i dokonały 3 obserwacje, które sprawiają mi uwierzyć, że to zachowuje się w ten sposób:
- Jak tylko kopnięcia optymalizacja (dodawanie kolejnych przydziałów zwykle wykonuje lewę) przypisanie wskaźnika nie ma powiązanej instrukcji asemblera. Nie został również przeniesiony do deklaracji, więc tak naprawdę pozostało niezainicjalizowane (przynajmniej dla mnie).
- W przypadkach, gdy program się zawiesza, debugger pomija instrukcję przypisania. W przypadkach, gdy program działa bezproblemowo, debugger zatrzymuje się w tym miejscu.
- Gdybym oglądać zawartość
pData
oraz zawartościm_data
podczas debugowania, to wyraźnie pokazuje, że wszystkie zadania wif
-branch mieć wpływ nam_data
im_data
odbiera poprawne wartości. Sam wskaźnik nadal wskazuje na niezainicjowaną wartość od początku. Dlatego muszę założyć, że w rzeczywistości nie używa on wskaźnika do wykonania zadań.
P: Czy musi coś zrobić z i (rozwijanie pętli)?
A: Nie, rzeczywisty program faktycznie używa do {...} while(), aby zapętlić zestaw wyników SQL SELECT, więc liczba iteracji jest całkowicie specyficzna dla środowiska wykonawczego i nie może być z góry określona przez kompilator.
Zredukuj to do [SSCCE] (http://sscce.org). – djechlin
@dłamlin: Błędy z optymalizacją mogą być trudne do zredukowania do małych próbek kodu. Pytanie jednoznacznie stwierdza, że jest to już uproszczony przykład. –
nie potrafię jeszcze jednoznacznie wyjaśnić pomysłu, ale czy działa on tak samo, jeśli tworzysz klasę zamiast struct? – evilruff