Dlaczego enhanced GCC 6 optimizer łamie praktyczny kod C++?

GCC 6 ma nową funkcję optymalizatora : zakłada, że this zawsze nie jest null i optymalizuje się na tej podstawie.

Propagacja zakresu wartości zakłada teraz, że ten wskaźnik funkcji składowych C++ jest inny niż null. Eliminuje to typowe sprawdzanie wskaźnika null , ale również łamie niektóre niezgodne bazy kodu (takie jak Qt-5, Chromium, KDevelop) . Jako tymczasowy work-around-fno-delete-null-pointer-checks mogą być używane. Błędny kod można zidentyfikować za pomocą - fsanitize = undefined.

Dokument zmian wyraźnie określa to jako niebezpieczne, ponieważ łamie zaskakującą ilość często używanego kodu.

Dlaczego to nowe założenie łamie praktyczny kod C++? Czy istnieją konkretne wzorce, w których nieostrożni lub niedoinformowani Programiści polegają na tym konkretnym nieokreślonym zachowaniu? Nie wyobrażam sobie, żeby ktoś pisał if (this == NULL), bo to takie nienaturalne.

Author: boot4life, 2016-04-27

5 answers

Myślę, że pytanie, na które należy odpowiedzieć, dlaczego ludzie o dobrych intencjach w ogóle wypisują czeki.

Najczęstszym przypadkiem jest prawdopodobnie, jeśli masz klasę, która jest częścią naturalnie występującego wywołania rekurencyjnego.

Gdybyś miał:

struct Node
{
    Node* left;
    Node* right;
};

W C możesz napisać:

void traverse_in_order(Node* n) {
    if(!n) return;
    traverse_in_order(n->left);
    process(n);
    traverse_in_order(n->right);
}

W C++ dobrze jest zrobić z tego funkcję członkowską:

void Node::traverse_in_order() {
    // <--- What check should be put here?
    left->traverse_in_order();
    process();
    right->traverse_in_order();
}

W początkach C++ (przed standaryzacją) podkreślano, że członek funkcje były cukrem składniowym dla funkcji, w której parametr this jest niejawny. Kod został napisany w C++, skonwertowany do równoważnego C i skompilowany. Były nawet wyraźne przykłady, że porównywanie this do null było znaczące i oryginalny kompilator Cfront również z tego skorzystał. Tak więc, wychodząc z tła C, oczywistym wyborem dla czeku jest:

if(this == nullptr) return;      

Uwaga: Bjarne Stroustrup wspomina nawet, że zasady dla this zmieniły się na przestrzeni lat tutaj

I to działało na wielu kompilatorach przez wiele lat. Kiedy nastąpiła standaryzacja, to się zmieniło. Ostatnio Kompilatory zaczęły korzystać z funkcji member, gdzie this bycie nullptr jest niezdefiniowanym zachowaniem, co oznacza, że warunek ten jest zawsze false, a kompilator może go swobodnie pomijać.

Oznacza to, że aby wykonać dowolną trawersację tego drzewa, musisz albo:

  • Wykonaj wszystkie kontrole przed wywołaniem traverse_in_order

    void Node::traverse_in_order() {
        if(left) left->traverse_in_order();
        process();
        if(right) right->traverse_in_order();
    }
    

    Oznacza to również sprawdzenie na każdej stronie wywołania, czy możesz mieć null root.

  • Nie używaj funkcji member

    Oznacza to, że piszesz stary kod w stylu C (być może jako metodę statyczną) i wywołujesz go z obiektem jawnie jako parametrem. np. wracasz do pisania Node::traverse_in_order(node); zamiast node->traverse_in_order(); na stronie wywołania.

  • Uważam, że najłatwiej / najładniej naprawić ten konkretny przykład w sposób, który jest standardem zgodność polega na użyciu węzła sentinel zamiast nullptr.

    // static class, or global variable
    Node sentinel;
    
    void Node::traverse_in_order() {
        if(this == &sentinel) return;
        ...
    }
    

Żadna z dwóch pierwszych opcji nie wydaje się tak atrakcyjna, i chociaż kod mógł ujść na sucho, napisali zły kod za pomocą this == nullptr zamiast użyć odpowiedniej poprawki.

Zgaduję, że w ten sposób niektóre z tych baz kodowych ewoluowały, aby mieć this == nullptr w nich czeki.

 83
Author: jtlim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-19 16:16:05

Robi to, ponieważ "praktyczny" kod został złamany i wymagał niezdefiniowanego zachowania. Nie ma powodu, aby używać null this, innego niż mikro-optymalizacja, zwykle bardzo przedwczesna.

Jest to niebezpieczna praktyka, ponieważ korekta wskaźników ze względu na przechodzenie hierarchii klas może zmienić null this w non-null. Tak więc, klasa, której metody mają działać z null this musi być klasą końcową bez klasy bazowej: it nie można z niczego czerpać i nie można z tego czerpać. Szybko odchodzimy z praktycznego do brzydkiej krainy .

W praktyce kod nie musi być brzydki:

struct Node
{
  Node* left;
  Node* right;
  void process();
  void traverse_in_order() {
    traverse_in_order_impl(this);
  }
private:
  static void traverse_in_order_impl(Node * n)
    if (!n) return;
    traverse_in_order_impl(n->left);
    n->process();
    traverse_in_order_impl(n->right);
  }
};

Jeśli masz puste drzewo (np. root to nullptr), To rozwiązanie nadal opiera się na niezdefiniowanym zachowaniu przez wywołanie traverse_in_order z nullptr.

Jeśli drzewo jest puste, a. k. a. A null Node* root, nie powinieneś wywoływać żadnych niestatycznych metod na nim. Kropka. Dobrze jest mieć podobny do C kod drzewa, który pobiera wskaźnik instancji za pomocą jawnego parametru.

Tutaj argument sprowadza się do konieczności zapisu niestatycznych metod na obiektach, które można wywołać ze wskaźnika instancji null. Nie ma takiej potrzeby. Sposób pisania takiego kodu w C-with-objects jest nadal o wiele ładniejszy w świecie C++, ponieważ może być co najmniej bezpieczny. Zasadniczo null this jest taką mikro-optymalizacją, z tak wąskim polem zastosowania, to jest IMHO w porządku. Żadne publiczne API nie powinno zależeć od null this.

 64
Author: Kuba Ober,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-12 07:31:24

Dokument zmian wyraźnie określa to jako niebezpieczne, ponieważ łamie zaskakującą ilość często używanego kodu.

Dokument nie nazywa tego niebezpiecznym. Nie twierdzi też, że łamie zaskakującą ilość kodu . Po prostu wskazuje kilka popularnych baz kodu, które, jak twierdzi, są znane z tego, że opierają się na tym nieokreślonym zachowaniu I ulegną zniszczeniu z powodu zmiany, chyba że zostanie użyta opcja obejścia problemu.

Dlaczego to nowe założenie złamać praktyczny kod C++?

Jeśli praktyczny kod c++ opiera się na niezdefiniowanym zachowaniu, to zmiany w tym niezdefiniowanym zachowaniu mogą go złamać. Dlatego należy unikać UB, nawet jeśli program oparty na nim wydaje się działać zgodnie z przeznaczeniem.

Czy istnieją konkretne wzorce, w których nieostrożni lub niedoinformowani Programiści polegają na tym konkretnym nieokreślonym zachowaniu?

Nie wiem czy jest szeroko rozpowszechniony anty -wzorzec, ale niedoinformowany programista może pomyśleć, że mogą naprawić swój program z awarii, wykonując:

if (this)
    member_variable = 42;

Gdy rzeczywisty błąd przekierowuje wskaźnik null gdzie indziej.

Jestem pewien, że jeśli programista będzie wystarczająco niedoinformowany, będzie w stanie wymyślić bardziej zaawansowane (anty) wzorce, które opierają się na tym UB.

Nie wyobrażam sobie, żeby ktoś pisał if (this == NULL) ponieważ to takie nienaturalne.

Mogę.
 35
Author: user2079303,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-22 00:04:33

Niektóre" praktyczne "(zabawny sposób na pisanie" buggy") kody, które zostały złamane, wyglądały tak:]}

void foo(X* p) {
  p->bar()->baz();
}

I zapomniał wyjaśnić fakt, że p->bar() czasami zwraca wskaźnik null, co oznacza, że odwołanie go do wywołania baz() jest niezdefiniowane.

Nie wszystkie złamane kody zawierały jawne if (this == nullptr) lub if (!p) return; kontrole. Niektóre przypadki były po prostu funkcjami, które nie miały dostępu do żadnych zmiennych członkowskich, więc okazało się działać poprawnie. Na przykład:

struct DummyImpl {
  bool valid() const { return false; }
  int m_data;
};
struct RealImpl {
  bool valid() const { return m_valid; }
  bool m_valid;
  int m_data;
};

template<typename T>
void do_something_else(T* p) {
  if (p) {
    use(p->m_data);
  }
}

template<typename T>
void func(T* p) {
  if (p->valid())
    do_something(p);
  else 
    do_something_else(p);
}

W tym kodzie, gdy wywołujesz {[8] } ze wskaźnikiem null, istnieje "koncepcyjna" dereferencja wskaźnika do wywołania p->DummyImpl::valid(), ale w rzeczywistości funkcja member zwraca false bez dostępu do *this. To return false może być inlined, więc w praktyce wskaźnik nie musi być w ogóle dostępny. Więc z niektórymi kompilatorami wydaje się działać OK: nie ma segfault dla dereferencji null, p->valid() jest false, więc kod wywołuje do_something_else(p), który sprawdza wskaźniki null, i tak robi nic. Nie obserwuje się awarii ani nieoczekiwanego zachowania.

Z GCC 6 nadal otrzymujesz wywołanie p->valid(), ale kompilator wnioskuje z tego wyrażenia, że p musi być nie-null (w przeciwnym razie p->valid() byłoby niezdefiniowanym zachowaniem) i notuje te informacje. Ta wywnioskowana informacja jest używana przez optymalizator tak, że jeśli wywołanie do_something_else(p) zostanie zainlinowane, sprawdzenie if (p) jest teraz uważane za zbędne, ponieważ kompilator pamięta, że nie jest null, a więc wprowadza kod do:

template<typename T>
void func(T* p) {
  if (p->valid())
    do_something(p);
  else {
    // inlined body of do_something_else(p) with value propagation
    // optimization performed to remove null check.
    use(p->m_data);
  }
}

To teraz naprawdę nie dereference wskaźnik null, i tak kod, który wcześniej wydawał się działać przestaje działać.

W tym przykładzie błąd znajduje się w func, który powinien najpierw sprawdzić, czy nie ma null (lub wywoływacze nigdy nie powinni wywoływać go z null):

template<typename T>
void func(T* p) {
  if (p && p->valid())
    do_something(p);
  else 
    do_something_else(p);
}

Ważnym punktem do zapamiętania jest to, że większość optymalizacji tego typu nie jest przypadkiem, gdy kompilator mówi: "ach, programista testował ten wskaźnik przeciwko null, usunę go tylko po to, aby być irytujące". Dzieje się tak, że różne optymalizacje run-of-the-mill, takie jak inlining i propagacja zakresu wartości, łączą się, aby te kontrole były zbędne, ponieważ pojawiają się po wcześniejszym sprawdzeniu lub dereferencji. Jeśli kompilator wie, że wskaźnik nie jest null w punkcie a w funkcji, a wskaźnik nie jest zmieniany przed późniejszym punktem B w tej samej funkcji, to wie, że jest również null w punkcie B. gdy następuje inlining, punkty A i B mogą być fragmentami kodu, które pierwotnie były w funkcji. osobne funkcje, ale są teraz połączone w jeden kawałek kodu, a kompilator jest w stanie zastosować swoją wiedzę, że wskaźnik nie jest null w wielu miejscach. Jest to podstawowa, ale bardzo ważna optymalizacja, a gdyby Kompilatory tego nie zrobiły, codzienny kod byłby znacznie wolniejszy, a ludzie narzekaliby na niepotrzebne gałęzie, aby wielokrotnie testować te same warunki.

 25
Author: Jonathan Wakely,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-28 15:54:19

Standard C++ jest łamany w istotny sposób. Niestety, zamiast chronić użytkowników przed tymi problemami, deweloperzy GCC zdecydowali się użyć niezdefiniowanego zachowania jako pretekstu do wdrożenia marginalnych optymalizacji, nawet jeśli zostało im jasno wyjaśnione, jak szkodliwe jest to.

Tutaj o wiele mądrzejsza osoba, niż wyjaśniam szczegółowo. (Mówi O C ale sytuacja jest taka sama tam). Dlaczego jest szkodliwy?

Po prostu przekompilowanie wcześniej działającego, bezpiecznego kodu z nowszą wersją kompilatora może wprowadzić luki w zabezpieczeniach . Podczas gdy nowe zachowanie można wyłączyć za pomocą flagi, istniejące pliki Makefile oczywiście nie mają tej flagi ustawionej. A ponieważ nie ma ostrzeżenia, nie jest oczywiste dla dewelopera, że dotychczas rozsądne zachowanie uległo zmianie.

W tym przykładzie programista włączył sprawdzanie przepełnienia liczb całkowitych, używając assert, które zakończy program, jeśli podana zostanie Nieprawidłowa długość. Zespół GCC usunął sprawdzanie na podstawie tego, że przepełnienie integer jest niezdefiniowane, dlatego sprawdzanie może zostać usunięte. Spowodowało to, że prawdziwe in-the-wild instancje tej bazy kodowej zostały ponownie narażone po problemie naprawione.

Czytaj całość. To wystarczy, żebyś płakał. OK, ale co z tym?

Dawno temu, był dość powszechny idiom, który wyglądał mniej więcej tak:

 OPAQUEHANDLE ObjectType::GetHandle(){
    if(this==NULL)return DEFAULTHANDLE;
    return mHandle;

 }

 void DoThing(ObjectType* pObj){
     osfunction(pObj->GetHandle(), "BLAH");
 }

Więc idiom brzmi: Jeśli pObj nie jest null, używasz uchwytu, który zawiera, w przeciwnym razie używasz domyślnego uchwytu. Jest to zamknięte w funkcji GetHandle.

Sztuczka polega na tym, że wywołanie nie-wirtualna funkcja nie wykorzystuje wskaźnika this, więc nie ma naruszenia dostępu.

I still don ' t get it

Istnieje wiele kodu, który jest napisany w ten sposób. Jeśli ktoś po prostu ją przekompiluje, bez zmiany linii, każde wywołanie DoThing(NULL) jest błędem-jeśli masz szczęście.

Jeśli nie masz szczęścia, połączenia z awariami stają się lukami w zdalnej realizacji.

Może to nastąpić nawet automatycznie. Masz automatyczny zbuduj system, tak? Uaktualnienie go do najnowszego kompilatora jest nieszkodliwe, prawda? Ale teraz nie jest - nie, jeśli twoim kompilatorem jest GCC. OK, więc powiedz im! Powiedziano im. Robią to z pełną świadomością konsekwencji. Ale... dlaczego?

Kto może powiedzieć? Być może:

    [11]}cenią idealną czystość języka C++ nad rzeczywistym kodem Wierzą, że ludzie powinni być karani za nieprzestrzeganie standardu.]}
  • nie mają zrozumienie rzeczywistości świata
  • Są ... celowo Wprowadzamy błędy. Może dla obcego rządu. Gdzie mieszkasz? Wszystkie rządy są obce większości świata, a większość jest wrogo nastawiona do części świata.
A może coś innego. Kto wie?
 -27
Author: Ben,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-27 18:02:30