Tablica czy Malloc?

13

Korzystam z następującego kodu w mojej aplikacji i działa dobrze. Ale zastanawiam się, czy lepiej zrobić to z malloc, czy zostawić tak, jak jest?

function (int len)
{
char result [len] = some chars;
send result over network
}
Dev Bag
źródło
2
Czy założenie, że kod jest przeznaczony dla środowiska niewbudowanego?
tehnyit
stackoverflow.com/questions/16672322/…
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Odpowiedzi:

28

Główną różnicą jest to, że VLA (tablice o zmiennej długości) nie zapewniają mechanizmu wykrywania błędów alokacji.

Jeśli zadeklarujesz

char result[len];

i lenprzekracza ilość dostępnego miejsca na stosie, zachowanie twojego programu jest niezdefiniowane. Nie ma mechanizmu językowego ani do wcześniejszego ustalenia, czy alokacja się powiedzie, ani do ustalenia po fakcie, czy się ona powiodła.

Z drugiej strony, jeśli napiszesz:

char *result = malloc(len);
if (result == NULL) {
    /* allocation failed, abort or take corrective action */
}

wtedy możesz poradzić sobie z awariami z wdziękiem lub przynajmniej zagwarantować, że Twój program nie będzie próbował kontynuować działania po awarii.

(Cóż, głównie. W systemach Linux malloc()można przydzielić część przestrzeni adresowej, nawet jeśli nie jest dostępna odpowiednia pamięć; późniejsze próby wykorzystania tej przestrzeni mogą wywołać OOM Killera . Ale sprawdzanie malloc()awarii jest nadal dobrą praktyką.)

Innym problemem na wielu systemach jest to, że jest więcej miejsca (być może o wiele więcej miejsca) malloc()niż na obiekty automatyczne, takie jak VLA.

I jak już wspomniano w odpowiedzi Philipa, VLA zostały dodane w C99 (w szczególności Microsoft ich nie obsługuje).

A VLA zostały opcjonalne w C11. Prawdopodobnie większość kompilatorów C11 będzie je obsługiwać, ale nie można na to liczyć.

Keith Thompson
źródło
14

Automatyczne tablice o zmiennej długości zostały wprowadzone do C w C99.

Jeśli nie masz wątpliwości co do wstecznej porównywalności ze starszymi standardami, wszystko jest w porządku.

Ogólnie, jeśli to działa, nie dotykaj go. Nie optymalizuj z wyprzedzeniem. Nie martw się o dodanie specjalnych funkcji lub sprytnych sposobów robienia rzeczy, ponieważ często nie będziesz z nich korzystać. Nie komplikuj.

Philip
źródło
7
Muszę się nie zgodzić z powiedzeniem „jeśli to działa, nie dotykaj go”. Fałszywe przekonanie, że jakiś kod „działa” może spowodować obejście problemów w kodzie, który „działa”. Wiara musi zostać zastąpiona wstępną akceptacją, że jakiś kod działa teraz.
Bruce Ediger,
2
Nie dotykaj go, dopóki nie
stworzysz
8

Jeśli Twój kompilator obsługuje tablice o zmiennej długości, jedynym niebezpieczeństwem jest przepełnienie stosu w niektórych systemach, gdy lenjest on absurdalnie duży. Jeśli wiesz na pewno, że lennie będzie większa niż pewna liczba i wiesz, że Twój stos nie przepełni się nawet przy maksymalnej długości, pozostaw kod bez zmian; w przeciwnym razie przepisz go za pomocą malloci free.

dasblinkenlight
źródło
co z tym w przypadku funkcji innej niż c99 (char []) {wynik char [sizeof (char)] = niektóre znaki; wyślij wynik przez sieć}
Dev Bag
@DevBag char result [sizeof(char)]jest tablicą wielkości 1(ponieważ sizeof(char)jest równa jeden), więc przypisanie zostanie obcięte some chars.
dasblinkenlight
przepraszam za to, mam na myśli to w ten sposób function (char str []) {char result [sizeof (str)] = niektóre znaki; wyślij wynik przez sieć}
Dev Bag
4
@DevBag To też nie zadziała - str rozpada się na wskaźnik , więc sizeofbędzie cztery lub osiem, w zależności od rozmiaru wskaźnika w twoim systemie.
dasblinkenlight
2
Jeśli używasz wersji C bez tablic o zmiennej długości, możesz to zrobić char* result = alloca(len);, co przydziela na stosie. Ma ten sam podstawowy efekt (i te same podstawowe problemy)
Gort the Robot
6

Podoba mi się pomysł, że możesz mieć tablicę alokowaną w czasie wykonywania bez fragmentacji pamięci, zwisających wskaźników itp. Jednak inni zauważyli, że ta alokacja w czasie wykonywania może po cichu zakończyć się niepowodzeniem. Wypróbowałem to, używając gcc 4.5.3 w środowisku bash Cygwin:

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

void testit (unsigned long len)
{
    char result [len*2];
    char marker[100];

    memset(marker, 0, sizeof(marker));
    printf("result's size: %lu\n", sizeof(result));
    strcpy(result, "this is a test that should overflow if no allocation");
    printf("marker's contents: '%s'\n", marker);
}

int main(int argc, char *argv[])
{
    testit(100);
    testit((unsigned long)-1);  // probably too big
}

Wynik był:

$ ./a.exe
result's size: 200
marker's contents: ''
result's size: 4294967294
marker's contents: 'should overflow if no allocation'

Zbyt duża długość przekazana w drugim wywołaniu wyraźnie spowodowała awarię (przelanie do znacznika []). Nie oznacza to, że ten rodzaj kontroli jest głupi (głupcy mogą być sprytni!) Lub że spełnia standardy C99, ale może pomóc, jeśli masz taką troskę.

Jak zwykle YMMV.

Harold Bamford
źródło
1
+1 to jest bardzo przydatne: 3
Kokizzu
Zawsze miło jest mieć jakiś kod zgodny z twierdzeniami ludzi! Dzięki ^ _ ^
Musa Al-hassy
3

Ogólnie mówiąc, stos jest najłatwiejszym i najlepszym miejscem do umieszczenia danych.

Unikałbym problemów VLA, po prostu przydzielając największą oczekiwaną macierz.

Zdarzają się jednak przypadki, gdy kupa jest najlepsza, a warto bawić się w malloc.

  1. Gdy jest duża, ale zmienna ilość danych. Duże zależy od środowiska> 1 KB dla systemów wbudowanych,> 10 MB dla serwera Enterprise.
  2. Gdy chcesz, aby dane pozostały po wyjściu z rutyny, np. Jeśli zwrócisz wskaźnik do danych. Za pomocą
  3. Kombinacja wskaźnika statycznego i malloc () jest zwykle lepsza niż definiowanie dużej tablicy statycznej;
James Anderson
źródło
3

W programowaniu wbudowanym zawsze używamy tablicy statycznej zamiast malloc, gdy malloc i operacje wolne są częste. Ze względu na brak zarządzania pamięcią w systemie osadzonym częste przydzielanie i wolne operacje powodują fragment pamięci. Ale powinniśmy skorzystać z pewnych trudnych metod, takich jak określenie maksymalnego rozmiaru tablicy i użycie statycznej lokalnej tablicy.

Jeśli aplikacja działa w systemie Linux lub Windows, nie ma znaczenia użycie tablicy lub malloc. Kluczowy punkt leży w tym, gdzie używasz struktury daty i logiki kodu.

Steven Mou
źródło
1

Coś, o czym nikt jeszcze nie wspomniał, to to, że opcja tablicy o zmiennej długości będzie prawdopodobnie znacznie szybsza niż malloc / free, ponieważ alokacja VLA jest tylko przypadkiem dostosowania wskaźnika stosu (przynajmniej w GCC).

Tak więc, jeśli ta funkcja jest często wywoływana (co oczywiście zostanie określone przez profilowanie), VLA jest dobrą opcją optymalizacji.

JeremyP
źródło
1
Będzie to wydawać się dobre aż do momentu, gdy popchnie cię do sytuacji braku miejsca na stosie. Co więcej, może to nie być twój kod, który faktycznie przekroczy limit stosu; może skończyć gryzienie w bibliotece lub wywołanie systemowe (lub przerwać).
Donal Fellows
@Donal Performance jest zawsze kompromisem pamięci w stosunku do prędkości. Jeśli zamierzasz obchodzić alokowanie tablic kilku megabajtów, masz jednak rację, nawet dla kilku kilobajtów, o ile funkcja nie jest rekurencyjna, jest to dobra optymalizacja.
JeremyP,
1

Jest to bardzo popularne rozwiązanie C, którego używam do rozwiązania problemu, który może być pomocny. W przeciwieństwie do VLA, nie ma praktycznego ryzyka przepełnienia stosu w przypadkach patologicznych.

/// Used for frequent allocations where the common case generally allocates
/// a small amount of memory, at which point a heap allocation can be
/// avoided, but rare cases also need to be handled which may allocate a
/// substantial amount. Note that this structure is not safe to copy as
/// it could potentially invalidate the 'data' pointer. Its primary use
/// is just to allow the stack to be used in common cases.
struct FastMem
{
    /// Stores raw bytes for fast access.
    char fast_mem[512];

    /// Points to 'fast_mem' if the data fits. Otherwise, it will point to a
    /// dynamically allocated memory address.
    void* data;
};

/// @return A pointer to a newly allocated memory block of the specified size.
/// If the memory fits in the specified fast memory structure, it will use that
/// instead of the heap.
void* fm_malloc(struct FastMem* mem, int size)
{
    // Utilize the stack if the memory fits, otherwise malloc.
    mem->data = (size < sizeof mem->fast_mem) ? mem->fast_mem: malloc(size);
    return mem->data;
}

/// Frees the specified memory block if it has been allocated on the heap.
void fm_free(struct FastMem* mem)
{
    // Free the memory if it was allocated dynamically with 'malloc'.
    if (mem->data != mem->fast_mem)
        free(mem->data);
    mem->data = 0;
}

Aby użyć go w swoim przypadku:

struct FastMem fm;

// `result` will be allocated on the stack if 'len <= 512'.
char* result = fm_malloc(&fm, len);

// send result over network.
...

// this function will only do a heap deallocation if 'len > 512'.
fm_free(&fm, result);

W powyższym przypadku stosuje się stos, jeśli ciąg mieści się w 512 bajtach lub mniej. W przeciwnym razie wykorzystuje przydział sterty. Może to być przydatne, jeśli, powiedzmy, w 99% przypadków, łańcuch mieści się w 512 bajtach lub mniej. Powiedzmy jednak, że istnieje jakiś szalony egzotyczny przypadek, który może czasem wymagać obsługi, gdzie ciąg znaków wynosi 32 kilobajty, gdy użytkownik zasnął na klawiaturze lub coś w tym rodzaju. Dzięki temu obie sytuacje mogą być obsługiwane bez problemów.

Rzeczywista wersja używam w produkcji ma również swoją wersję realloci calloci tak dalej, jak również standardowe zgodne ze standardem C ++ struktury danych zbudowane na tej samej koncepcji, ale ekstrakcji minimum niezbędne do zilustrowania koncepcji.

Ma zastrzeżenie, że kopiowanie go jest niebezpieczne i nie powinieneś zwracać wskaźników przydzielonych przez niego (mogą one zostać unieważnione, gdy FastMeminstancja zostanie zniszczona). Jest przeznaczony do stosowania w prostych przypadkach w zakresie funkcji lokalnej, w których kusiłoby cię, aby zawsze używać stosu / VLA, w przeciwnym razie rzadkie przypadki mogą spowodować przepełnienie bufora / stosu. Nie jest to alokator ogólnego przeznaczenia i nie powinien być używany jako taki.

Stworzyłem go wieki temu w odpowiedzi na sytuację w starej bazie kodu za pomocą C89, że były zespół myślał, że nigdy nie zdarzy się, gdy użytkownik zdoła nazwać element o nazwie o długości ponad 2047 znaków (być może zasnął na klawiaturze ). W rzeczywistości moi koledzy próbowali zwiększyć rozmiar tablic przydzielonych w różnych miejscach do 16 384, w którym to momencie pomyślałem, że robi się to śmieszne i po prostu wymieniam większe ryzyko przepełnienia stosu w zamian za mniejsze ryzyko przepełnienia bufora. Zapewniło to rozwiązanie, które bardzo łatwo było podłączyć, aby naprawić te przypadki, po prostu dodając kilka wierszy kodu. Pozwoliło to na bardzo skuteczne załatwienie zwykłego przypadku i nadal korzystało ze stosu bez tych szalonych rzadkich przypadków, które wymagały sterty awarii oprogramowania. Jednakże, ja' uznaliśmy ją za przydatną od tego czasu nawet po C99, ponieważ VLA wciąż nie mogą nas ochronić przed przepełnieniem stosu. Ten może, ale nadal pula ze stosu dla małych żądań alokacji.


źródło
1

Stos wywołań jest zawsze ograniczona. W głównych systemach operacyjnych, takich jak Linux lub Windows, limit wynosi jeden lub kilka megabajtów (i można znaleźć sposoby na jego zmianę). W przypadku niektórych aplikacji wielowątkowych może być niższa (ponieważ wątki można tworzyć przy użyciu mniejszego stosu). W systemach wbudowanych może być tak mały, jak kilka kilobajtów. Dobrą zasadą jest unikanie ramek połączeń większych niż kilka kilobajtów.

Korzystanie z VLA ma sens tylko wtedy, gdy masz pewność, że Twój komputer lenjest wystarczająco mały (najwyżej kilkadziesiąt tysięcy). W przeciwnym razie masz przepełnienie stosu, co jest przypadkiem niezdefiniowanego zachowania , bardzo przerażającej sytuacji.

Korzystanie z ręcznej dynamicznej alokacji pamięci C (np. callocLub malloc&free ) ma jednak również swoje wady:

  • może się nie powieść i zawsze powinieneś testować pod kątem awarii (np. calloclub mallocpowrotu NULL).

  • jest wolniejszy: pomyślny przydział VLA zajmuje kilka nanosekund, pomyślny mallocmoże potrzebować kilku mikrosekund (w dobrych przypadkach tylko ułamek mikrosekundy) lub nawet więcej (w patologicznych przypadkach obejmujących druzgocenie , znacznie więcej).

  • kodowanie jest znacznie trudniejsze: możesz to zrobić freetylko wtedy, gdy masz pewność, że wskazana strefa nie jest już używana. W twoim przypadku można nazwać zarówno calloci freew tej samej rutyny.

Jeśli wiesz, że przez większość czasu twoje result (bardzo kiepskie imię, nigdy nie powinieneś zwracać adresu automatycznej zmiennej VLA; więc używam bufzamiast resultniżej) jest małe, możesz to zrobić w specjalnym przypadku, np.

char tinybuf[256];
char *buf = (len<sizeof(tinybuf))?tinybuf:malloc(len);
if (!buf) { perror("malloc"); exit(EXIT_FAILURE); };
fill_buffer(buf, len);
send_buffer_on_network(buf, len);
if (buf != tinybuf) 
  free(buf);

Powyższy kod jest jednak mniej czytelny i prawdopodobnie przedwczesna optymalizacja. Jest jednak bardziej solidny niż czyste rozwiązanie VLA.

PS. Niektóre systemy (np. Niektóre dystrybucje Linuksa domyślnie włączają się) mają nadmierne zaangażowanie pamięci (co powoduje, że mallocpodanie pewnego wskaźnika nawet, jeśli nie ma wystarczającej ilości pamięci). Jest to funkcja, której nie lubię i zwykle wyłączam na moich komputerach z systemem Linux.

Basile Starynkevitch
źródło