Array ou Malloc?

13

J'utilise le code suivant dans mon application, et cela fonctionne bien. Mais je me demande s'il vaut mieux le faire avec du malloc ou le laisser tel quel?

function (int len)
{
char result [len] = some chars;
send result over network
}
Sac de développement
la source
2
Est-ce l'hypothèse que le code est ciblé pour un environnement non intégré?
tehnyit
stackoverflow.com/questions/16672322/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Réponses:

28

La principale différence est que les VLA (tableaux de longueur variable) ne fournissent aucun mécanisme pour détecter les échecs d'allocation.

Si vous déclarez

char result[len];

et lendépasse la quantité d'espace disponible sur la pile, le comportement de votre programme n'est pas défini. Il n'y a pas de mécanisme linguistique pour déterminer à l'avance si l'allocation réussira ou pour déterminer après coup si elle a réussi.

En revanche, si vous écrivez:

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

alors vous pouvez gérer les échecs avec élégance, ou au moins garantir que votre programme ne tentera pas de continuer à s'exécuter après un échec.

(Eh bien, la plupart du temps. Sur les systèmes Linux, malloc()peut allouer un morceau d'espace d'adressage même s'il n'y a pas de stockage correspondant disponible; les tentatives ultérieures d'utiliser cet espace peuvent invoquer OOM Killer . Mais la recherche d' malloc()échec est toujours une bonne pratique.)

Un autre problème, sur de nombreux systèmes, est qu'il y a plus d'espace (peut-être beaucoup plus d'espace) disponible malloc()que pour les objets automatiques comme les VLA.

Et comme la réponse de Philip l'a déjà mentionné, des VLA ont été ajoutés dans C99 (Microsoft en particulier ne les prend pas en charge).

Et les VLA ont été rendus facultatifs dans C11. La plupart des compilateurs C11 les prendront probablement en charge, mais vous ne pouvez pas compter dessus.

Keith Thompson
la source
14

Les tableaux automatiques de longueur variable ont été introduits dans C en C99.

À moins que vous n'ayez des inquiétudes quant à la comparabilité en amont avec les anciennes normes, tout va bien.

En général, si cela fonctionne, ne le touchez pas. N'optimisez pas à l'avance. Ne vous inquiétez pas de l'ajout de fonctionnalités spéciales ou de façons intelligentes de faire les choses, car vous n'allez souvent pas l'utiliser. Rester simple.

Philippe
la source
7
Je dois être en désaccord avec le dicton «si cela fonctionne, ne le touchez pas». Croire à tort que certains codes "fonctionnent" peuvent vous amener à contourner les problèmes de certains codes qui "fonctionnent". La croyance doit être remplacée par l'acceptation provisoire que certains codes fonctionnent actuellement.
Bruce Ediger
2
Ne le touchez pas avant d'avoir fait une version qui "fonctionne" mieux ...
H_7
8

Si votre compilateur prend en charge les tableaux de longueur variable, le seul danger est de faire déborder la pile sur certains systèmes, lorsque la lentaille est ridiculement grande. Si vous savez avec certitude que cela lenne sera pas supérieur à un certain nombre et que vous savez que votre pile ne va pas déborder même à la longueur maximale, laissez le code tel quel; sinon, réécrivez-le avec mallocet free.

dasblinkenlight
la source
qu'en est-il de la fonction non c99 (char []) {char result [sizeof (char)] = certains chars; envoyer le résultat sur le réseau}
Dev Bag
@DevBag char result [sizeof(char)]est un tableau de taille 1(car sizeof(char)égal à un), donc l'affectation va être tronquée some chars.
dasblinkenlight
désolé, je le pense de cette façon function (char str []) {char result [sizeof (str)] = some chars; envoyer le résultat sur le réseau}
Dev Bag
4
@DevBag Cela ne fonctionnera pas non plus - str se désintègre en un pointeur , donc ça sizeofva être quatre ou huit, selon la taille du pointeur sur votre système.
dasblinkenlight
2
Si vous utilisez une version de C sans tableaux de longueur variable, vous pourrez peut-être le faire char* result = alloca(len);, qui alloue sur la pile. Il a le même effet de base (et les mêmes problèmes de base)
Gort the Robot
6

J'aime l'idée que vous pouvez avoir un tableau alloué au moment de l'exécution sans fragmentation de la mémoire, pointeurs pendants, etc. Cependant, d'autres ont souligné que cette allocation au moment de l'exécution peut échouer en silence. J'ai donc essayé ceci en utilisant gcc 4.5.3 dans un environnement 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
}

Le résultat était:

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

La longueur trop grande passée dans le deuxième appel a clairement causé l'échec (débordement dans le marqueur []). Cela ne signifie pas que ce type de vérification est infaillible (les imbéciles peuvent être intelligents!) Ou qu'il répond aux normes de C99, mais cela pourrait aider si vous avez cette préoccupation.

Comme d'habitude, YMMV.

Harold Bamford
la source
1
+1 c'est très utile: 3
Kokizzu
C'est toujours agréable d'avoir du code pour accompagner les affirmations des gens! Merci ^ _ ^
Musa Al-hassy
3

De manière générale, la pile est l'endroit le plus simple et le meilleur pour mettre vos données.

J'éviterais les problèmes des VLA en allouant simplement la plus grande baie que vous attendiez.

Il y a cependant des cas où le tas est meilleur et jouer avec malloc en vaut la chandelle.

  1. Quand sa quantité de données est grande mais variable. Large dépend de votre environnement> 1 Ko pour les systèmes embarqués,> 10 Mo pour un serveur d'entreprise.
  2. Lorsque vous souhaitez que les données persistent après avoir quitté votre routine, par exemple si vous renvoyez un pointeur vers vos données. En utilisant
  3. Une combinaison de pointeur statique et malloc () est généralement meilleure que de définir un grand tableau statique;
James Anderson
la source
3

Dans la programmation intégrée, nous utilisons toujours un tableau statique au lieu de malloc lorsque les opérations malloc et free sont fréquentes. En raison du manque de gestion de la mémoire dans le système embarqué, les opérations d'allocation et de libération fréquentes entraîneront un fragment de mémoire. Mais nous devons utiliser certaines méthodes délicates telles que la définition de la taille maximale du tableau et l'utilisation d'un tableau local statique.

Si votre application s'exécute sous Linux ou Windows, peu importe l'utilisation de array ou malloc. Le point clé réside dans l'endroit où vous utilisez votre structure de date et votre logique de code.

Steven Mou
la source
1

Quelque chose que personne n'a encore mentionné est que l'option de tableau de longueur variable sera probablement beaucoup plus rapide que malloc / free car l'allocation d'un VLA est juste un cas d'ajustement du pointeur de pile (dans GCC au moins).

Donc, si cette fonction est appelée fréquemment (que vous déterminerez bien sûr par profilage), le VLA est une bonne option d'optimisation.

JeremyP
la source
1
Cela vous semblera bon jusqu'au moment où il vous poussera dans une situation d'espace hors pile. De plus, il pourrait ne pas être votre code qui fait frappe la limite de la pile; cela pourrait finir par mordre dans une bibliothèque ou un appel système (ou une interruption).
Donal Fellows
@Donal Performance est toujours un compromis entre la mémoire et la vitesse. Si vous allez allouer des tableaux de plusieurs mégaoctets, vous avez un point, cependant, même pour quelques kilo-octets, tant que la fonction n'est pas récursive, c'est une bonne optimisation.
JeremyP
1

Il s'agit d'une solution C très courante que j'utilise pour le problème qui pourrait être utile. Contrairement aux VLA, il ne fait face à aucun risque pratique de débordement de pile dans les cas pathologiques.

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

Pour l'utiliser dans votre cas:

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

Dans le cas ci-dessus, cela utilise la pile si la chaîne tient en 512 octets ou moins. Sinon, il utilise une allocation de tas. Cela peut être utile si, disons, 99% du temps, la chaîne tient en 512 octets ou moins. Cependant, disons qu'il y a un cas exotique fou que vous pourriez parfois avoir besoin de gérer lorsque la chaîne fait 32 kilo-octets où l'utilisateur s'est endormi sur son clavier ou quelque chose comme ça. Cela permet de gérer les deux situations sans problème.

La version actuelle que j'utilise dans la production a également sa propre version de reallocet callocet ainsi de suite, ainsi que des structures de données conformes au standard C ++ construit sur le même concept, mais j'extrait le minimum nécessaire pour illustrer le concept.

Il a la mise en garde qu'il est dangereux de copier et vous ne devez pas renvoyer les pointeurs alloués à travers lui (ils pourraient finir par être invalidés lorsque l' FastMeminstance est détruite). Il est destiné à être utilisé pour des cas simples dans la portée d'une fonction locale où vous seriez tenté de toujours utiliser la pile / VLA, sinon où certains cas rares pourraient provoquer des débordements de tampon / pile. Ce n'est pas un allocateur à usage général et ne doit pas être utilisé comme tel.

Je l'ai en fait créé il y a longtemps en réponse à une situation dans une base de code héritée utilisant C89 qu'une ancienne équipe pensait ne jamais arriver lorsqu'un utilisateur parviendrait à nommer un élément avec un nom de plus de 2047 caractères (peut-être s'est-il endormi sur son clavier ). Mes collègues ont en fait essayé d'augmenter la taille des tableaux alloués à divers endroits à 16384 en réponse, à quel point je pensais que cela devenait ridicule et échangeait simplement un plus grand risque de débordement de pile en échange d'un moindre risque de débordement de tampon. Cela a fourni une solution très facile à brancher pour corriger ces cas en ajoutant simplement quelques lignes de code. Cela a permis de gérer le cas commun très efficacement et d'utiliser toujours la pile sans ces rares cas fous qui exigeaient que le tas écrase le logiciel. Cependant, je' Je l'ai trouvé utile depuis lors, même après C99, car les VLA ne peuvent toujours pas nous protéger contre les débordements de pile. Celui-ci peut mais toujours des pools de la pile pour les petites demandes d'allocation.


la source
1

La pile d'appels est toujours limitée. Sur les systèmes d'exploitation courants comme Linux ou Windows, la limite est de un ou de quelques mégaoctets (et vous pouvez trouver des moyens de la changer). Avec certaines applications multithreads, il peut être inférieur (car les threads peuvent être créés avec une pile plus petite). Sur les systèmes embarqués, elle peut atteindre quelques kilo-octets. Une bonne règle d'or consiste à éviter les trames d'appel supérieures à quelques kilo-octets.

Donc, utiliser un VLA n'a de sens que si vous êtes sûr qu'il lenest assez petit (au plus quelques dizaines de milliers). Sinon, vous avez un débordement de pile et c'est un cas de comportement indéfini , une situation très effrayante .

Cependant, l'utilisation de l' allocation de mémoire dynamique C manuelle (par exemple callocou malloc&free ) a aussi ses inconvénients:

  • il peut échouer et vous devez toujours tester l'échec (par exemple callocou mallocrevenir NULL).

  • elle est plus lente: une allocation VLA réussie prend quelques nanosecondes, une réussite mallocpeut nécessiter plusieurs microsecondes (dans les bons cas, seulement une fraction de microseconde) ou même plus (dans les cas pathologiques impliquant une raclée , bien plus).

  • c'est beaucoup plus difficile à coder: vous ne pouvez le faire freeque lorsque vous êtes sûr que la zone pointée n'est plus utilisée. Dans votre cas, vous pourriez appeler les deux callocet freedans la même routine.

Si vous savez que la plupart du temps votre result (un nom très médiocre, vous ne devriez jamais retourner l'adresse d'une variable automatique VLA; donc j'utilise bufau lieu de resultci - dessous) est petite, vous pouvez le cas particulier, par exemple

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

Cependant, le code ci-dessus est moins lisible et est probablement une optimisation prématurée. Elle est cependant plus robuste qu'une solution VLA pure.

PS. Certains systèmes (par exemple, certaines distributions Linux activent par défaut) ont un sur-engagement de mémoire (ce qui permet de mallocdonner un pointeur même s'il n'y a pas assez de mémoire). C'est une fonctionnalité que je n'aime pas et que je désactive généralement sur mes machines Linux.

Basile Starynkevitch
la source