Array o Malloc?

13

Estoy usando el siguiente código en mi aplicación, y está funcionando bien. Pero me pregunto si es mejor hacerlo con malloc o dejarlo como está.

function (int len)
{
char result [len] = some chars;
send result over network
}
Bolsa de desarrollo
fuente
2
¿Se supone que el código está destinado a un entorno no integrado?
tehnyit
stackoverflow.com/questions/16672322/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Respuestas:

28

La principal diferencia es que los VLA (matrices de longitud variable) no proporcionan ningún mecanismo para detectar fallas de asignación.

Si declaras

char result[len];

y lenexcede la cantidad de espacio de pila disponible, el comportamiento de su programa no está definido. No existe un mecanismo de lenguaje para determinar de antemano si la asignación tendrá éxito o para determinar después del hecho si tuvo éxito.

Por otro lado, si escribes:

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

entonces puede manejar las fallas con gracia, o al menos garantizar que su programa no intente continuar ejecutándose después de una falla.

(Bueno, sobre todo. En los sistemas Linux, malloc()puede asignar una porción de espacio de direcciones incluso si no hay un almacenamiento correspondiente disponible; los intentos posteriores de usar ese espacio pueden invocar el OOM Killer . Pero verificar malloc()fallas sigue siendo una buena práctica).

Otro problema, en muchos sistemas, es que hay más espacio disponible (posiblemente mucho más espacio) disponible malloc()que para objetos automáticos como VLA.

Y como la respuesta de Philip ya mencionó, los VLA se agregaron en C99 (Microsoft en particular no los admite).

Y los VLA se hicieron opcionales en C11. Probablemente la mayoría de los compiladores de C11 los admitirán, pero no puede contar con eso.

Keith Thompson
fuente
14

Las matrices automáticas de longitud variable se introdujeron en C en C99.

A menos que tenga dudas acerca de la comparabilidad con los estándares anteriores, está bien.

En general, si funciona, no lo toques. No optimices con anticipación. No se preocupe por agregar funciones especiales o formas inteligentes de hacer las cosas, porque a menudo no las va a usar. Mantenlo simple.

Philip
fuente
77
Tengo que estar en desacuerdo con el dicho "si funciona, no lo toques". Creer falsamente que algún código "funciona" puede hacer que evite problemas en algún código que "funcione". La creencia debe ser reemplazada por la aceptación tentativa de que algún código funciona en este momento.
Bruce Ediger
2
No lo toques hasta que hagas una versión que tal vez "funcione" mejor ...
H_7
8

Si su compilador admite matrices de longitud variable, el único peligro es desbordar la pila en algunos sistemas, cuando lenes ridículamente grande. Si sabe con certeza que lenno será mayor que un número determinado y sabe que su pila no se desbordará incluso en la longitud máxima, deje el código como está; de lo contrario, reescríbalo con mallocy free.

dasblinkenlight
fuente
¿Qué pasa con esto en la función no c99 (char []) {resultado char [sizeof (char)] = algunos caracteres; enviar resultado a través de la red}
Dev Bag
@DevBag char result [sizeof(char)]es una matriz de tamaño 1(porque sizeof(char)es igual a uno), por lo que la asignación se va a truncar some chars.
dasblinkenlight
perdón por eso, lo digo de esta manera function (char str []) {char result [sizeof (str)] = algunos caracteres; enviar resultado a través de la red}
Dev Bag
44
@DevBag Esto tampoco funcionará: se str descompone en un puntero , por sizeoflo que será cuatro u ocho, dependiendo del tamaño del puntero en su sistema.
dasblinkenlight
2
Si está utilizando una versión de C sin matrices de longitud variable, puede hacerlo char* result = alloca(len);, que asigna en la pila. Tiene el mismo efecto básico (y los mismos problemas básicos)
Gort the Robot
6

Me gusta la idea de que puede tener una matriz asignada en tiempo de ejecución sin fragmentación de memoria, punteros colgantes, etc. Sin embargo, otros han señalado que esta asignación en tiempo de ejecución puede fallar silenciosamente. Así que probé esto usando gcc 4.5.3 en un entorno Cygwin bash:

#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
}

El resultado fue:

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

La longitud demasiado grande pasada en la segunda llamada claramente causó la falla (desbordando en el marcador []). Esto no significa que este tipo de verificación sea infalible (¡los tontos pueden ser inteligentes!) O que cumpla con los estándares de C99, pero podría ayudar si tiene esa preocupación.

Como de costumbre, YMMV.

Harold Bamford
fuente
1
+1 esto es muy útil: 3
Kokizzu
¡Siempre es bueno tener un código que acompañe las afirmaciones que hace la gente! Gracias ^ _ ^
Musa Al-hassy
3

En términos generales, la pila es el mejor y más fácil lugar para colocar sus datos.

Evitaría los problemas de los VLA simplemente asignando la matriz más grande que espera.

Sin embargo, hay casos en los que el montón es mejor y vale la pena jugar con Malloc.

  1. Cuando es una cantidad de datos grande pero variable. Grande depende de su entorno> 1K para sistemas integrados,> 10MB para un servidor Enterprise.
  2. Cuando desea que los datos persistan después de salir de su rutina, por ejemplo, si devuelve un puntero a sus datos. Utilizando
  3. Una combinación de puntero estático y malloc () suele ser mejor que definir una gran matriz estática;
James Anderson
fuente
3

En la programación integrada, siempre usamos una matriz estática en lugar de malloc cuando las operaciones malloc y gratuitas son frecuentes. Debido a la falta de administración de memoria en el sistema embebido, la asignación frecuente y las operaciones libres causarán fragmentos de memoria. Pero deberíamos utilizar algunos métodos complicados, como definir el tamaño máximo de la matriz y usar una matriz local estática.

Si su aplicación se ejecuta en Linux o Windows, no importa usar array o malloc. El punto clave radica en dónde usa su estructura de fecha y su lógica de código.

Steven Mou
fuente
1

Algo que nadie ha mencionado todavía es que la opción de matriz de longitud variable probablemente será mucho más rápida que malloc / free, ya que asignar un VLA es solo un caso de ajustar el puntero de la pila (al menos en GCC).

Entonces, si esta función se llama con frecuencia (que, por supuesto, determinará mediante la creación de perfiles), el VLA es una buena opción de optimización.

JeremyP
fuente
1
Parecerá bien hasta el punto en que te empuje a una situación de falta de espacio en la pila. Además, puede que no sea su código el que realmente alcance el límite de la pila; podría terminar mordiendo en una biblioteca o llamada al sistema (o interrumpir).
Donal Fellows
@Donal Performance siempre es una compensación de memoria contra velocidad. Si va a dar vueltas asignando matrices de varios megabytes, entonces tiene un punto, sin embargo, incluso por unos pocos kilobytes, siempre que la función no sea recursiva, es una buena optimización.
JeremyP
1

Esta es una solución C muy común que uso para el problema que podría ser de ayuda. A diferencia de los VLA, no enfrenta ningún riesgo práctico de desbordamiento de pila en casos patológicos.

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

Para usarlo en tu 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);

Lo que esto hace en el caso anterior es usar la pila si la cadena cabe en 512 bytes o menos. De lo contrario, utiliza una asignación de montón. Esto puede ser útil si, por ejemplo, el 99% del tiempo, la cadena se ajusta a 512 bytes o menos. Sin embargo, digamos que hay un caso exótico loco que ocasionalmente puede necesitar manejar donde la cadena es de 32 kilobytes donde el usuario se durmió en su teclado o algo así. Esto permite manejar ambas situaciones sin problemas.

La versión actual de uso en la producción también tiene su propia versión de reallocy callocy así sucesivamente, así como estructuras de datos conformes estándar de C ++ construidas en el mismo concepto, pero extrae el mínimo necesario para ilustrar el concepto.

Tiene la advertencia de que es peligroso copiar y no debe devolver los punteros asignados a través de él (podrían terminar siendo invalidados a medida que FastMemse destruye la instancia). Está destinado a ser utilizado para casos simples dentro del alcance de una función local donde estaría tentado a usar siempre la pila / VLA, de lo contrario, algún caso raro podría causar desbordamientos del búfer / pila. No es un asignador de propósito general y no debe usarse como tal.

De hecho, lo creé hace años en respuesta a una situación en una base de código heredada usando C89 que un antiguo equipo pensó que nunca sucedería cuando un usuario logró nombrar un elemento con un nombre que tenía más de 2047 caracteres (tal vez se quedó dormido en su teclado ) Mis colegas en realidad trataron de aumentar el tamaño de los arreglos asignados en varios lugares a 16,384 en respuesta, en ese momento pensé que se estaba volviendo ridículo y simplemente intercambiaba un mayor riesgo de desbordamiento de la pila a cambio de un menor riesgo de desbordamiento del búfer. Esto proporcionó una solución que fue muy fácil de conectar para solucionar esos casos simplemente agregando un par de líneas de código. Esto permitió que el caso común se manejara de manera muy eficiente y todavía utilizara la pila sin esos casos raros y locos que exigían que el montón bloqueara el software. Sin embargo, yo' Lo hemos encontrado útil desde entonces, incluso después de C99, ya que los VLA todavía no pueden protegernos contra los desbordamientos de la pila. Este puede, pero aún se agrupa de la pila para pequeñas solicitudes de asignación.


fuente
1

La pila de llamadas siempre es limitada. En sistemas operativos convencionales como Linux o Windows, el límite es de uno o unos pocos megabytes (y puede encontrar formas de cambiarlo). Con algunas aplicaciones de subprocesos múltiples, podría ser menor (porque los subprocesos podrían crearse con una pila más pequeña). En sistemas embebidos, podría ser tan pequeño como unos pocos kilobytes. Una buena regla general es evitar los marcos de llamadas de más de unos pocos kilobytes.

Por lo tanto, usar un VLA solo tiene sentido si está seguro de que lenes lo suficientemente pequeño (como máximo unas docenas de miles). De lo contrario, tiene un desbordamiento de pila y ese es un caso de comportamiento indefinido , una situación muy aterradora .

Sin embargo, el uso de la asignación manual de memoria dinámica C (por ejemplo, calloco malloc&free ) también tiene sus inconvenientes:

  • puede fallar y siempre debe probar la falla (por ejemplo, calloco mallocregresar NULL).

  • es más lento: una asignación exitosa de VLA toma unos pocos nanosegundos, una exitosa mallocpodría necesitar varios microsegundos (en el caso bueno, solo una fracción de microsegundo) o incluso más (en casos patológicos que involucran golpes , mucho más).

  • es mucho más difícil de codificar: freesolo puede hacerlo cuando está seguro de que la zona puntiaguda ya no se usa. En su caso, puede llamar a ambos callocy freeen la misma rutina.

Si sabe que la mayoría de las veces su result (un nombre muy pobre, nunca debe devolver la dirección de una variable automática VLA; por lo que estoy usando en buflugar de a resultcontinuación) es pequeño, podría usar un caso especial, por ejemplo

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

Sin embargo, el código anterior es menos legible y probablemente sea una optimización prematura. Sin embargo, es más robusto que una solución VLA pura.

PD. Algunos sistemas (por ejemplo, algunas distribuciones de Linux están habilitadas de forma predeterminada) tienen un compromiso excesivo de memoria (lo que hace que mallocdar algún puntero incluso si no hay suficiente memoria). Esta es una característica que no me gusta y que usualmente desactivo en mis máquinas Linux.

Basile Starynkevitch
fuente