Optymalizacja w gcc

O niekorzystnych efektach stosowania optymalizacji na błędnym kodzie już kiedyś było. A teraz będzie jeszcze raz bo znalazłem kolejny ciekawy przykład. Znalazłem go na tej stronie przy okazji szukania materiałów o sygnałach w Linuksie. Kod po minimalnej zmianie wygląda tak:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

static int exit_flag = 0;

static void hdl (int sig)
{
  exit_flag = 1;
}

int main (int argc, char *argv[])
{
  struct sigaction act;

  memset (&act, '\0', sizeof(act));
  act.sa_handler = &hdl;
  sigaction(SIGTERM, &act, NULL);

  while (!exit_flag)
    ;

  return 0;
}

Co to robi:

  • tworzy statyczną zmienną globalną typu int
  • deklaruje funkcję która będzie wywoływana w momencie otrzymania sygnału
    • funkcja zmienia naszą zmienną na wartość 1
  • w main:
    • tworzymy strukturę typu sigaction której używa się do obsługi sygnałów
    • czyścimy strukturę
    • ustawiamy w odpowiednim polu naszą funkcję jako handler
    • “instalujemy” naszą strukturę, także teraz odebranie sygnału SIGTERM spowoduje wywołanie funkcji podanej w strukturze, czyli naszej hdl
    • wywołujemy pętlę która kręci się w kółko dopóki wartość naszej zmiennej globalnej jest równa zero

Czyli w teorii po zainstalowaniu handlera dla SIGTERM powinniśmy tkwić w pętli, aż do momentu kiedy po otrzymaniu SIGTERM wartość zmieni się na jeden i program kulturalnie skończy działanie. A jak jest w praktyce? Jeśli skompilujemy program bez optymalizacji to wszystko działa tak jak chcieliśmy. Jednak jeśli dodamy jakiekolwiek flagi optymalizacyjne to program nawet po otrzymaniu SIGTERM nie wychodzi. Dlaczego? Zobaczmy jak wygląda kod assemblerowy naszego programu:

  40058b:       mov    0x200a9f(%rip),%eax
  400591:       test   %eax,%eax
  400593:       je     40059f <main+0x3f>
  400595:       xor    %eax,%eax
  400597:       add    $0xa8,%rsp
  40059e:       retq
  40059f:       jmp    40059f <main+0x3f>

Powyższy kod jest tylko interesującym nas fragmentem, pominąłem wszystko, aż do zainstalowania handlera włącznie. W pierwszej linii nasza zmienna jest ładowana do rejestru, w drugiej następuje test naszego rejestru, w trzeciej linii jeśli rejestr był równy zero (a na początku jest) następuje skok pod adres 40059f, czyli do ostatniej linii. Ostatnia linia z kolei zawsze wykonuje skok pod ten sam adres czyli do samej siebie. I tak lądujemy w nieskończonej pętli. Dlaczego gcc tak robi? Nasz kod nigdy nie odnosi się jawnie do naszej funkcji (tylko poprzez wskaźnik, ale kompilator nie sprawdza takich rzeczy), zatem według gcc nasza zmienna nie może zostać nigdzie zmieniona, a skoro nigdy się nie zmieni to nie ma co jej więcej wczytywać z pamięci (co jest czasochłonne, ale w tym wypadku jednak konieczne) i testować.

Jak rozwiązać ten problem? Wyłączenie optymalizacji jest jednym z rozwiązań, ale kiepskim. Prawidłowym jest dopisanie słowa kluczowego “volatile” przed naszą zmienną. Jego znaczenie jest dokładnie tym czego tutaj potrzebujemy, czyli mówi kompilatorowi żeby danej zmiennej nie optymalizował pod względem dostępu do pamięci bo mimo, że na to nie wygląda to jej zawartość jednak może się zmienić. Po dodaniu tego jednego słowa kod wygenerowany wygląda bardzo podobnie, ale jednak działa zupełnie inaczej:

  40058b:       nopl   0x0(%rax,%rax,1)
  400590:       mov    0x200a9a(%rip),%eax        # 601030 <exit_flag>
  400596:       test   %eax,%eax
  400598:       je     400590 <main+0x30>
  40059a:       xor    %eax,%eax
  40059c:       add    $0xa8,%rsp
  4005a3:       retq

Jak widać tak samo wczytuje zmienną do rejestru, tak samo ją testuje, a potem skacze. Jednak adres skoku jest nie pod instrukcję skaczącą do samej siebie (której tutaj w ogóle nie ma), tylko znowu do instrukcji wczytania. Jeśli zmienna zmieni swoją wartość na inną niż zero to skok nie nastąpi i program skończy działanie.

Ciekawym przykładem jak dogłębnie wykorzystywane są cechy architektury podczas optymalizacji w wykonaniu gcc, jest pierwsza instrukcja (“nopl”) z drugiego listingu. Ale o tym więcej tutaj (koniecznie przeczytać komentarz z prawej strony).

Comment are closed.