Array o Malloc?


Risposte:


28

La differenza principale è che i VLA (array di lunghezza variabile) non forniscono alcun meccanismo per rilevare errori di allocazione.

Se dichiari

char result[len];

e lensupera la quantità di spazio di stack disponibile, il comportamento del programma non è definito. Non esiste un meccanismo linguistico né per determinare in anticipo se l'allocazione avrà successo, né per determinare dopo il fatto se abbia avuto successo.

D'altra parte, se scrivi:

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

allora puoi gestire gli errori con garbo, o almeno garantire che il tuo programma non proverà a continuare l'esecuzione dopo un errore.

(Beh, soprattutto. Sui sistemi Linux, è malloc()possibile allocare una porzione di spazio degli indirizzi anche se non è disponibile una memoria corrispondente; in seguito i tentativi di utilizzare tale spazio possono invocare OOM Killer . Ma verificare la presenza di malloc()errori è ancora una buona pratica.)

Un altro problema, su molti sistemi, è che c'è più spazio (forse molto più spazio) disponibile malloc()rispetto agli oggetti automatici come i VLA.

E come già menzionato nella risposta di Philip, VLA sono stati aggiunti in C99 (in particolare Microsoft non li supporta).

E i VLA sono stati resi opzionali in C11. Probabilmente la maggior parte dei compilatori C11 li supporterà, ma non puoi contare su di esso.


14

Le matrici automatiche a lunghezza variabile sono state introdotte in C in C99.

A meno che tu non abbia dubbi sulla comparabilità all'indietro con gli standard più vecchi, va bene.

In generale, se funziona, non toccarlo. Non ottimizzare in anticipo. Non preoccuparti di aggiungere funzionalità speciali o modi intelligenti di fare le cose, perché spesso non le userai. Mantienilo semplice.


7
Non sono d'accordo con il detto "se funziona, non toccarlo". Credere falsamente che alcuni codici "funzionino" possono farti aggirare i problemi in alcuni codici che "funzionano". La credenza deve essere sostituita dall'accettazione provvisoria che alcuni codici funzionino proprio ora.
Bruce Ediger,

2
Non toccarlo finché non fai una versione forse "funziona" meglio ...
H_7

8

Se il tuo compilatore supporta array di lunghezza variabile, l'unico pericolo è quello di traboccare lo stack su alcuni sistemi, quando lenè ridicolmente grande. Se sai per certo che lennon sarà più grande di un certo numero, e sai che il tuo stack non avrà un overflow anche alla massima lunghezza, lascia il codice così com'è; in caso contrario, riscriverlo con malloce free.


che dire di questo sulla funzione non c99 (char []) {char result [sizeof (char)] = alcuni caratteri; invia risultato via rete}
Dev Bag

@DevBag char result [sizeof(char)]è un array di dimensioni 1(perché sizeof(char)uguale a uno), quindi l'assegnazione verrà troncata some chars.
dasblinkenlight,

scusatemi, intendo in questo modo function (char str []) {char result [sizeof (str)] = some chars; invia risultato via rete}
Dev Bag

4
@DevBag Anche questo non funzionerà: str decade in un puntatore , quindi sizeofsarà quattro o otto, a seconda della dimensione del puntatore sul sistema.
dasblinkenlight,

2
Se stai usando una versione di C senza array di lunghezza variabile, potresti essere in grado di farlo char* result = alloca(len);, che alloca sullo stack. Ha lo stesso effetto di base (e gli stessi problemi di base)
Gort the Robot

6

Mi piace l'idea che sia possibile disporre di un array allocato in fase di runtime senza frammentazione della memoria, puntatori penzolanti, ecc. Tuttavia, altri hanno sottolineato che questa allocazione in fase di runtime può fallire silenziosamente. Quindi ho provato questo usando gcc 4.5.3 in un ambiente bash di 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
}

L'output è stato:

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

La lunghezza eccessiva della seconda chiamata ha causato chiaramente l'errore (traboccando nel marker []). Ciò non significa che questo tipo di controllo sia a prova di folle (gli sciocchi possono essere intelligenti!) O che soddisfa gli standard di C99, ma potrebbe essere utile se si ha quella preoccupazione.

Come al solito, YMMV.


1
+1 questo è molto utile: 3
Kokizzu,

È sempre bello avere un po 'di codice da abbinare alle affermazioni che la gente fa! Grazie ^ _ ^
Musa Al-hassy,

3

In generale, lo stack è il posto più semplice e migliore per mettere i tuoi dati.

Eviterei i problemi dei VLA semplicemente allocando l'array più grande che ci si aspetta.

Ci sono comunque casi in cui l'heap è il migliore e fare casino con malloc vale lo sforzo.

  1. Quando la sua grande ma variabile quantità di dati. Grandi dipendono dal tuo ambiente> 1K per i sistemi embedded,> 10 MB per un server Enterprise.
  2. Quando si desidera che i dati persistano dopo aver abbandonato la routine, ad esempio se si restituisce un puntatore ai dati. utilizzando
  3. Una combinazione di puntatore statico e malloc () è in genere migliore della definizione di un array statico di grandi dimensioni;

3

Nella programmazione integrata, utilizziamo sempre array statici anziché malloc quando le operazioni malloc e free sono frequenti. A causa della mancanza di gestione della memoria nel sistema incorporato, l'allocazione frequente e le operazioni libere causeranno frammenti di memoria. Ma dovremmo utilizzare alcuni metodi complicati come la definizione della dimensione massima dell'array e l'utilizzo di array locali statici.

Se l'applicazione è in esecuzione su Linux o Windows, non importa se si utilizza array o malloc. Il punto chiave sta nel punto in cui usi la struttura della data e la logica del codice.


1

Qualcosa che nessuno ha ancora menzionato è che l'opzione di array a lunghezza variabile sarà probabilmente molto più veloce di malloc / free poiché l'allocazione di un VLA è solo un caso di regolazione del puntatore dello stack (almeno in GCC).

Quindi, se questa funzione è chiamata frequentemente (cosa che, ovviamente, determinerai per profilazione), il VLA è una buona opzione di ottimizzazione.


1
Sembrerà buono fino al punto in cui ti spinge in una situazione di spazio fuori dallo stack. Inoltre, potrebbe non essere il codice a raggiungere effettivamente il limite dello stack; potrebbe finire per mordere in una libreria o una chiamata di sistema (o interrompere).
Donal Fellows

Le prestazioni @Donal sono sempre un compromesso della memoria contro la velocità. Se hai intenzione di andare in giro allocando array di diversi megabyte, allora hai un punto, anche per pochi kilobyte, purché la funzione non sia ricorsiva, è una buona ottimizzazione.
JeremyP,

1

Questa è una soluzione C molto comune che uso per il problema che potrebbe essere di aiuto. A differenza dei VLA, non presenta alcun rischio pratico di overflow dello stack in casi patologici.

/// 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;
}

Per usarlo nel tuo caso:

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);

Ciò che fa nel caso precedente è usare lo stack se la stringa si adatta a 512 byte o meno. Altrimenti utilizza un'allocazione di heap. Questo può essere utile se, diciamo, il 99% delle volte, la stringa si adatta a 512 byte o meno. Tuttavia, supponiamo che ci sia un pazzo caso esotico che potresti occasionalmente dover gestire in cui la stringa è di 32 kilobyte in cui l'utente si è addormentato sulla sua tastiera o qualcosa del genere. Ciò consente di gestire entrambe le situazioni senza problemi.

La versione attuale che uso nella produzione ha anche una propria versione di realloce callocecc così come le strutture di dati standard conformi C ++ costruito sullo stesso concetto, ma estratto il minimo necessario per illustrare il concetto.

Ha l'avvertenza che è pericoloso copiarlo e non dovresti restituire i puntatori allocati attraverso di esso (potrebbero finire per essere invalidati quando l' FastMemistanza viene distrutta). È pensato per essere usato per casi semplici nell'ambito di una funzione locale in cui si sarebbe tentati di usare sempre lo stack / VLA, altrimenti dove alcuni rari casi potrebbero causare overflow del buffer / stack. Non è un allocatore per tutti gli usi e non dovrebbe essere usato come tale.

In realtà l'ho creato secoli fa in risposta a una situazione in una base di codice legacy utilizzando C89 che un ex team pensava che non sarebbe mai accaduto in cui un utente è riuscito a nominare un elemento con un nome che era lungo oltre 2047 caratteri (forse si è addormentato sulla tastiera ). I miei colleghi hanno effettivamente cercato di aumentare le dimensioni degli array allocati in vari punti a 16.384 in risposta a quel punto ho pensato che stesse diventando ridicolo e scambiando solo un maggior rischio di overflow dello stack in cambio di un minor rischio di overflow del buffer. Ciò ha fornito una soluzione che era molto facile da collegare per risolvere quei casi semplicemente aggiungendo un paio di righe di codice. Ciò ha consentito di gestire il caso comune in modo molto efficiente e di utilizzare comunque lo stack senza quei rari casi folli che richiedevano l'heap che causava l'arresto anomalo del software. Tuttavia, io' l'ho trovato utile da allora anche dopo C99 poiché i VLA non possono ancora proteggerci dagli overflow dello stack. Questo può ancora raggruppare dallo stack per piccole richieste di allocazione.


1

Lo stack di chiamate è sempre limitato. Su sistemi operativi tradizionali come Linux o Windows il limite è di uno o pochi megabyte (e potresti trovare il modo di cambiarlo). Con alcune applicazioni multi-thread, potrebbe essere inferiore (perché i thread potrebbero essere creati con uno stack più piccolo). Sui sistemi integrati, potrebbe essere piccolo quanto pochi kilobyte. Una buona regola empirica è quella di evitare frame di chiamata più grandi di pochi kilobyte.

Quindi usare un VLA ha senso solo se sei sicuro che sei lenabbastanza piccolo (al massimo poche decine di migliaia). Altrimenti hai un overflow dello stack e questo è un caso di comportamento indefinito , una situazione molto spaventosa .

Tuttavia, l'utilizzo dell'allocazione manuale dinamica della memoria C (ad es. callocO malloc&free ) ha anche i suoi svantaggi:

  • può fallire e dovresti sempre verificare l'errore (ad es. calloco mallocrestituire NULL).

  • è più lento: un'allocazione VLA riuscita richiede pochi nanosecondi, un successo mallocpotrebbe richiedere diversi microsecondi (nei casi buoni, solo una frazione di microsecondo) o anche di più (in casi patologici che coinvolgono il thrashing , molto altro).

  • è molto più difficile da programmare: puoi freesolo quando sei sicuro che la zona appuntita non viene più utilizzata. Nel tuo caso potresti chiamare entrambi calloce freenella stessa routine.

Se sai che la maggior parte delle volte il tuo result (un nome molto scarso, non dovresti mai restituire l'indirizzo di una variabile automatica VLA; quindi sto usando al bufposto di resultsotto) è piccolo potresti farlo in casi particolari, ad es.

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);

Tuttavia, il codice sopra è meno leggibile ed è probabilmente un'ottimizzazione prematura. È tuttavia più robusto di una soluzione VLA pura.

PS. Alcuni sistemi (ad esempio alcune distribuzioni Linux sono abilitate per impostazione predefinita) hanno un sovraccarico di memoria (il che rende possibile mallocdare un puntatore anche se non c'è memoria sufficiente). Questa è una caratteristica che non mi piace e di solito disabilito sui miei computer Linux.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.