Garbage Collector dla C i C++

Prostota C

Język C, z założenia skupiający się na prostocie w swej budowie obarcza programistę sporą ilością pracy i koniecznością dbania o detale, o których istnieniu możemy zapomnieć pisząc w nowocześniejszych językach.

Takie podejście pozwala lepiej zrozumieć maszyny i ich tendencję do bycia głupim, niewykwalifikowanym, choć dokładnym pracownikiem. O ile pozwala to, na lepsze wykorzystanie zasobów maszyny i dokładniejszą optymalizację, to trudno nazwać to zadanie wygodnym.

Kolekcjonowanie odpadków

Programiści języków takich jak C#, Java czy Python o alokację i zwalnianie pamięci troszczyć się nie muszą — tym zadaniem obarczone jest środowisko uruchomieniowe, które dba o sprzątanie całego bajzlu który zostawił po sobie programista, dlaczego więc nie wykorzystać podobnego mechanizmu w C? Z pomocą przychodzi biblioteka GC.

GC udostępnia nam pięć funkcji (makr):

  • GC_INIT() — inicjalizuje bibliotekę
  • GC_MALLOC_ATOMIC() — odpowiednik malloc()
  • GC_MALLOC() — odpowiednik calloc()
  • GC_REALLOC() — odpowiednik realloc()
  • GC_FREE() — odpowiednik free(), w większości przypadków całkowicie zbędny.

Ich definicje znajdziemy w pliku gc.h.

Przykład wykorzystania GC

Zbudujmy więc prosty program operujący na nich. Dla wygody przykryjemy standardowe funkcje C ich odpowiednikami z biblioteki GC.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <gc.h>

#define malloc(x) GC_MALLOC_ATOMIC(x)
#define calloc(x) GC_MALLOC(x)
#define realloc(x, y) GC_REALLOC(x, y)
#define free(x) GC_FREE(x)

int main(void) {
    unsigned char i;
    char *str;

    GC_INIT();

    for(i = 0; i < 10; i++){ 
        char *str = malloc(16);
        strcpy(str,"Hello World!\n");
        printf("%s",str);
    }

    return EXIT_SUCCESS;
}

Kompilacja i jej efekty

Oczywiście “hello world” wcale nie musi używać dynamicznie alokowanej pamięci, jednak jest przykładem wystarczającym do zobrazowania sytuacji. Wywołanie GC_INIT() na Linuksach jest opcjonalne. Kompilacja odbywa się poprzez wywołanie clang -pedantic -lgc -o hello hello.c. Nic nadzwyczajnego, wykonanie programu też nie jest jakąś zagadką. Jednak warto zerknąć na dokumentację GC, a konkretniej README.environment. Po zdefiniowaniu zmiennej środowiskowej GC_PRINT_STATS program podczas działania będzie wyświetlał na standardowe wyjście statystyki alokacji i zwalniania pamięci. Jak widać, używanie free() stało się zbędne.

Wnioski?

O ile w tak prostym przykładzie ręczne zwalnianie pamięci nie jest problemem, to już przy dłuższym kodzie automatyzacja staje się bardzo wygodnym narzędziem, a także rozwiązuje problematykę alokowania pamięci wewnątrz funkcji która zwraca wskaźnik na tę pamięć. Czasem późniejsze zwolnienie jej jest utrudnione przez wymogi reszty kodu, a programista może zwyczajnie zapomnieć o wywołaniu free(). Dzięki bibliotece GC możesz zapomnieć o wyciekach pamięci, a samo działanie Garbage Collectora nie jest aż tak obciążające dla współczesnego sprzętu, by rezygnować z niego w imię optymalizacji.