Matriz ou Malloc?

12

Estou usando o seguinte código no meu aplicativo e está funcionando bem. Mas estou pensando se é melhor fazê-lo com malloc ou deixá-lo como está?

function (int len)
{
char result [len] = some chars;
send result over network
}
Dev Bag
fonte
2
A suposição de que o código é direcionado para um ambiente não incorporado?
Tehnyit # 8/12

Respostas:

27

A principal diferença é que os VLAs (matrizes de comprimento variável) não fornecem mecanismo para detectar falhas de alocação.

Se você declarar

char result[len];

e lenexcede a quantidade de espaço disponível na pilha, o comportamento do seu programa é indefinido. Não existe um mecanismo de linguagem para determinar antecipadamente se a alocação será bem-sucedida ou para determinar após o fato se foi bem-sucedida.

Por outro lado, se você escrever:

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

você pode lidar com falhas normalmente ou, pelo menos, garantir que seu programa não tente continuar executando após uma falha.

(Bem, principalmente. Nos sistemas Linux, é malloc()possível alocar uma parte do espaço de endereço, mesmo que não haja armazenamento correspondente disponível; tentativas posteriores de usar esse espaço podem invocar o OOM Killer . Mas verificar malloc()falhas ainda é uma boa prática.)

Outra questão, em muitos sistemas, é que há mais espaço (possivelmente muito mais espaço) disponível do malloc()que para objetos automáticos como VLAs.

E, como a resposta de Philip já mencionou, os VLAs foram adicionados no C99 (a Microsoft em particular não os suporta).

E os VLAs foram feitos opcionais no C11. Provavelmente a maioria dos compiladores C11 os suportará, mas você não pode contar com isso.

Keith Thompson
fonte
14

Matrizes automáticas de comprimento variável foram introduzidas em C em C99.

A menos que você tenha preocupações sobre comparabilidade retroativa com padrões mais antigos, tudo bem.

Em geral, se funcionar, não toque. Não otimize com antecedência. Não se preocupe em adicionar recursos especiais ou maneiras inteligentes de fazer as coisas, porque muitas vezes você não vai usá-las. Mantenha simples.

Philip
fonte
7
Eu tenho que discordar do ditado "se funcionar, não toque". Acreditar falsamente que algum código "funciona" pode causar problemas em algum código que "funciona". A crença deve ser substituída pela tentativa de aceitação de que algum código funciona no momento.
Bruce Ediger
2
Não tocá-lo até que você faça uma versão talvez "funciona" melhor ...
H_7
8

Se o seu compilador suportar matrizes de tamanho variável, o único perigo será transbordar a pilha em alguns sistemas, quando isso lenfor ridiculamente grande. Se você tem certeza de que lennão será maior que um determinado número e sabe que sua pilha não excederá o tamanho máximo, deixe o código como está; caso contrário, reescreva-o com malloce free.

dasblinkenlight
fonte
e quanto a isso na função não c99 (char []) {char result [sizeof (char)] = alguns caracteres; enviar resultado pela rede}
Dev Bag
@DevBag char result [sizeof(char)]é uma matriz de tamanho 1(porque sizeof(char)é igual a um), portanto, a atribuição será truncada some chars.
precisa saber é o seguinte
desculpe por isso, quero dizer desta forma function (char str []) {char result [sizeof (str)] = alguns caracteres; enviar resultado pela rede}
Dev Bag
4
@DevBag Isso também não funciona - str decai para um ponteiro , então sizeofserá quatro ou oito, dependendo do tamanho do ponteiro no seu sistema.
precisa saber é o seguinte
2
Se você estiver usando uma versão de C sem matrizes de comprimento variável, poderá fazer isso char* result = alloca(len);, que é alocado na pilha. Tem o mesmo efeito básico (e mesmos problemas básicos)
Gort o robô
6

Gosto da ideia de que você pode ter uma matriz alocada em tempo de execução sem fragmentação da memória, ponteiros oscilantes etc. No entanto, outros apontaram que essa alocação em tempo de execução pode falhar silenciosamente. Então, eu tentei isso usando o gcc 4.5.3 em um ambiente de 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
}

A saída foi:

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

O tamanho excessivamente grande passado na segunda chamada causou claramente a falha (transbordando para o marcador []). Isso não significa que esse tipo de verificação seja à prova de idiotas (os tolos podem ser inteligentes!) Ou que atenda aos padrões da C99, mas pode ajudar se você tiver essa preocupação.

Como sempre, YMMV.

Harold Bamford
fonte
1
+1 isto é muito útil: 3
Kokizzu 15/01
É sempre bom ter algum código para acompanhar as reivindicações que as pessoas fazem! Obrigado ^ _ ^
Musa Al-hassy
3

De um modo geral, a pilha é o melhor e mais fácil local para colocar seus dados.

Eu evitaria os problemas dos VLAs simplesmente alocando a maior matriz que você espera.

No entanto, existem casos em que a pilha é melhor e mexer com o malloc vale o esforço.

  1. Quando sua quantidade grande, mas variável, de dados. Grande depende do seu ambiente> 1K para sistemas incorporados,> 10MB para um servidor Enterprise.
  2. Quando você deseja que os dados persistam depois de sair de sua rotina, por exemplo, se você retornar um ponteiro para seus dados. Usando
  3. Uma combinação de ponteiro estático e malloc () geralmente é melhor do que definir uma grande matriz estática;
James Anderson
fonte
3

Na programação incorporada, sempre usamos matriz estática em vez de malloc quando as operações malloc e free são frequentes. Por causa da falta de gerenciamento de memória no sistema incorporado, a alocação frequente e as operações livres causarão fragmento de memória. Mas devemos utilizar alguns métodos complicados, como definir o tamanho máximo da matriz e usar a matriz local estática.

Se seu aplicativo estiver em execução no Linux ou Windows, não importa o uso de matriz ou malloc. O ponto principal é onde você usa sua estrutura de datas e sua lógica de código.

Steven Mou
fonte
1

Algo que ninguém mencionou ainda é que a opção de matriz de comprimento variável provavelmente será muito mais rápida que malloc / free, pois alocar um VLA é apenas um caso de ajustar o ponteiro da pilha (pelo menos no GCC).

Portanto, se essa função for chamada com frequência (que você naturalmente determinará por criação de perfil), o VLA é uma boa opção de otimização.

JeremyP
fonte
1
Parece bom até o momento em que o leva a uma situação de falta de espaço na pilha. Além disso, pode não ser o seu código que realmente atinge o limite da pilha; pode acabar mordendo uma biblioteca ou chamada do sistema (ou interromper).
Donal Fellows
O @Donal Performance é sempre uma troca de memória e velocidade. Se você vai alocar matrizes de vários megabytes, no entanto, você tem um ponto, mesmo que alguns kilobytes, desde que a função não seja recursiva, é uma boa otimização.
9132 JeremyP
1

Esta é uma solução C muito comum que uso para o problema que pode ser útil. Ao contrário dos VLAs, ele não enfrenta nenhum risco prático de estouro de pilha em 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 usá-lo no seu 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);

O que isso faz no caso acima é usar a pilha se a string se encaixar em 512 bytes ou menos. Caso contrário, ele usa uma alocação de heap. Isso pode ser útil se, digamos, 99% do tempo, a string se encaixar em 512 bytes ou menos. No entanto, digamos que exista algum caso exótico maluco que você possa ocasionalmente precisar controlar onde a string tem 32 kilobytes em que o usuário adormeceu no teclado ou algo assim. Isso permite que ambas as situações sejam tratadas sem problemas.

A versão real eu uso na produção também tem a sua própria versão do realloce calloce assim por diante, bem como estruturas de dados padrão conformes C ++ construídos no mesmo conceito, mas eu extraído o mínimo necessário para ilustrar o conceito.

Ele tem a ressalva de que é perigoso copiar e você não deve retornar ponteiros alocados por ele (eles podem acabar sendo invalidados à medida que a FastMeminstância é destruída). Ele deve ser usado para casos simples no escopo de uma função local, onde você seria tentado a sempre usar a pilha / VLAs, caso contrário, em casos raros, isso poderá causar estouros de buffer / pilha. Não é um alocador de uso geral e não deve ser usado como tal.

Na verdade, eu o criei há tempos em resposta a uma situação em uma base de código herdada usando o C89 que uma equipe anterior pensava que nunca aconteceria onde um usuário conseguisse nomear um item com um nome com mais de 2047 caracteres (talvez ele tenha adormecido no teclado ) Meus colegas realmente tentaram aumentar o tamanho das matrizes alocadas em vários locais para 16.384 em resposta, quando pensei que estava ficando ridículo e apenas trocando um risco maior de estouro de pilha em troca de menor risco de estouro de buffer. Isso forneceu uma solução muito fácil de conectar para corrigir esses casos, apenas adicionando algumas linhas de código. Isso permitiu que o caso comum fosse tratado com muita eficiência e ainda utilizasse a pilha, sem os casos malucos e raros que exigiam que a pilha travasse o software. No entanto, eu achei útil desde então, mesmo depois do C99, já que os VLAs ainda não podem nos proteger contra estouros de pilha. Este pode, mas ainda assim, agrupar da pilha para pequenas solicitações de alocação.


fonte
1

A pilha de chamadas é sempre limitada. Nos sistemas operacionais convencionais, como Linux ou Windows, o limite é de um ou alguns megabytes (e você pode encontrar maneiras de alterá-lo). Com alguns aplicativos multithread, ele pode ser menor (porque os threads podem ser criados com uma pilha menor). Em sistemas embarcados, pode ser tão pequeno quanto alguns kilobytes. Uma boa regra é evitar quadros de chamada maiores que alguns kilobytes.

Portanto, usar um VLA faz sentido apenas se você tiver certeza de que lené pequeno o suficiente (no máximo algumas dezenas de milhares). Caso contrário, você terá um estouro de pilha e esse é um caso de comportamento indefinido , uma situação muito assustadora .

No entanto, o uso manual da alocação de memória dinâmica C (por exemplo, callocou malloc&free ) também tem suas desvantagens:

  • pode falhar e você deve sempre testar a falha (por exemplo, callocou mallocretornar NULL).

  • é mais lento: uma alocação bem-sucedida de VLA leva alguns nanossegundos, uma bem-sucedida mallocpode precisar de vários microssegundos (nos bons casos, apenas uma fração de microssegundo) ou até mais (em casos patológicos que envolvem trocas , muito mais).

  • é muito mais difícil codificar: você pode freeapenas quando tiver certeza de que a zona apontada não será mais usada. No seu caso, você pode ligar para os dois calloce freena mesma rotina.

Se você sabe que na maioria das vezes o seu result (um nome muito ruim, você nunca deve retornar o endereço de uma variável automática VLA; portanto, eu estou usando em bufvez do resultabaixo) é pequeno, você pode especial, por exemplo:

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

No entanto, o código acima é menos legível e provavelmente é uma otimização prematura. No entanto, é mais robusto que uma solução VLA pura.

PS. Alguns sistemas (por exemplo, algumas distribuições Linux estão ativando por padrão) têm um comprometimento excessivo da memória (o que torna o mallocfornecimento de algum ponteiro, mesmo que não haja memória suficiente). Esse é um recurso que eu não gosto e geralmente desabilito em minhas máquinas Linux.

Basile Starynkevitch
fonte