Qual a eficiência do malloc e como as implementações diferem?

8

Se eu usar malloc, mallocsempre usa o mesmo algoritmo, independentemente do que está alocando, ou olha os dados e seleciona um algoritmo apropriado?

Podemos tornar o malloc mais rápido ou mais inteligente escolhendo um algoritmo mais eficiente? Nos meus testes, o sistema oficial interno mallocdo Ubuntu é 10 vezes mais lento que um projeto escolar, se os resultados dos meus testes estiverem corretos. Qual é o problema? Estou surpreso que o mallocdesempenho tenha sido tão ruim nos testes, porque deve ser otimizado. Ele sempre usa o mesmo algoritmo? Existe uma implementação de referência malloc? Se eu quiser procurar a fonte malloc, para quem devo procurar? Os testes que eu executo são os seguintes:

/* returns an array of arrays of char*, all of which NULL */
char ***alloc_matrix(unsigned rows, unsigned columns) {
    char ***matrix = malloc(rows * sizeof(char **));
    unsigned row = 0;
    unsigned column = 0;
    if (!matrix) abort();

    for (row = 0; row < rows; row++) {
        matrix[row] = calloc(columns, sizeof(char *));
        if (!matrix[row]) abort();
        for (column = 0; column < columns; column++) {
            matrix[row][column] = NULL;
        }
    }
    return matrix;
}

/* deallocates an array of arrays of char*, calling free() on each */
void free_matrix(char ***matrix, unsigned rows, unsigned columns) {
    unsigned row = 0;
    unsigned column = 0;
    for (row = 0; row < rows; row++) {
        for (column = 0; column < columns; column++) {
            /*    printf("column %d row %d\n", column, row);*/
            free(matrix[row][column]);
        }
        free(matrix[row]);
    }
    free(matrix);
}


int main(int agrc, char **argv) {

    int x = 10000;
    char *** matrix = alloc_matrix(x, x);
    free_matrix(matrix, x, x);
    return (0);
}

O teste está bom? Eu também uso este teste:

   for (i = 0; i < 1000000; i++) {
        void *p = malloc(1024 * 1024 * 1024);
        free(p);
   }
  • atualizar

De acordo com o comentário, eu devo fazer pedaços de tamanhos variados e livres em ordem diferente da alocação, então tento:

int main(int agrc, char **argv) {
     int i;
    srand(time(NULL));
    int randomnumber;
    int size = 1024;
    void *p[size];
    for (i = 0; i < size; i++) {
        randomnumber = rand() % 10;
        p[i] = malloc(1024 * 1024 * randomnumber);
    }

    for (i = size-1; i >= 0; i--) {
        free(p[i]);
    }

    int x = 1024;
    char *** matrix = alloc_matrix(x, x);
    free_matrix(matrix, x, x);
    return (0);
}

Então meu malloc personalizado não é mais rápido:

$ time ./gb_quickfit 

real    0m0.154s
user    0m0.008s
sys 0m0.144s
dac@dac-Latitude-E7450:~/ClionProjects/omalloc/openmalloc/overhead$ time ./a.out 

real    0m0.014s
user    0m0.008s
sys 0m0.004s

O algoritmo que usei foi:

void *malloc_quick(size_t nbytes) {

    Header *moreroce(unsigned);
    int index, i;
    index = qindex(nbytes);

    /* 
     * Use another strategy for too large allocations. We want the allocation
     * to be quick, so use malloc_first().
     */

    if (index >= NRQUICKLISTS) {
        return malloc_first(nbytes);
    }

    /* Initialize the quick fit lists if this is the first run. */
    if (first_run) {
        for (i = 0; i < NRQUICKLISTS; ++i) {
            quick_fit_lists[i] = NULL;
        }
        first_run = false;
    }


    /*
     * If the quick fit list pointer is NULL, then there are no free memory
     * blocks present, so we will have to create some before continuing.
     */

    if (quick_fit_lists[index] == NULL) {
        Header* new_quick_fit_list = init_quick_fit_list(index);
        if (new_quick_fit_list == NULL) {
            return NULL;
        } else {
            quick_fit_lists[index] = new_quick_fit_list;
        }
    }

    /*
     * Now that we know there is at least one free quick fit memory block,
     * let's use return that and also update the quick fit list pointer so that
     * it points to the next in the list.
     */

    void* pointer_to_return = (void *)(quick_fit_lists[index] + 1);
    quick_fit_lists[index] = quick_fit_lists[index]->s.ptr;
   /* printf("Time taken %d seconds %d milliseconds", msec/1000, msec%1000);*/
    return pointer_to_return;
}
Niklas
fonte
7
Tente comparar o desempenho ao alocar e desalocar blocos de tamanho variável e quando a liberação não for chamada na mesma ordem que as alocações, e veja o que acontece com o projeto da sua escola.
Whatsisname 20/05
1
Você provavelmente encontrará a fonte da biblioteca C aqui: gnu.org/software/libc - ou poderá usar seu gerenciador de pacotes para baixar a fonte.
Kdgregory 20/05
1
Se você quiser comentários sobre por que seu alocador pode ser mais rápido ou mais lento que a biblioteca padrão, você deve mostrá-lo em vez de apenas o programa de teste. Suponho que você pré-aloque um grande bloco de memória e, em seguida, corte pedaços, o que significa que você não precisa pagar o preço de aumentar gradualmente o tamanho da pilha via sbrk(ou o que os alocadores modernos usam).
Kdgregory 20/05
1
E não relacionado, por que calloce depois explicitamente claro?
Kdgregory
@whatsisname Alterei o teste de acordo com o seu comentário e recebo o resultado razoável, pois meu costume mallocé mais lento. Isso é o que eu esperaria.
Niklas #

Respostas:

11

Existem várias implementações malloce elas podem usar algoritmos muito diferentes. Duas implementações muito usadas são jemalloc e dlmalloc . Nos dois casos, os links têm muitas informações sobre o processo de reflexão e as trocas feitas em um alocador de uso geral.

Lembre-se de que uma mallocimplementação tem muito pouca informação, apenas o tamanho da alocação solicitada. Uma freeimplementação possui apenas o ponteiro e quaisquer dados que 'malloc' possam ter secretamente anexado a ele. Como tal, acaba havendo uma quantidade razoável de heurísticas, isto é, suposições inspiradas ao decidir como alocar e desalocar.

Alguns problemas que um alocador precisa resolver são:

  1. alinhamento - alguns alinhamentos de memória são mais rápidos que outros
  2. fragmentação - alocação e liberação ingênuas podem deixar buracos que causam inchaço
  3. desempenho - voltar ao sistema operacional para obter mais memória pode ser caro
  4. Solicitações comuns - na prática, os aplicativos (especialmente C ++) costumam fazer muitas pequenas alocações, portanto, torná-las eficientes pode ajudar bastante

Levando tudo isso em consideração, o que você encontrará é que os alocadores tendem a ser peças complexas de software para que, em uso geral, tenham um bom desempenho. No entanto, em casos específicos, pode ser melhor gerenciar a memória fora do alocador se você souber muito mais sobre a estrutura dos dados. Obviamente, a desvantagem é que você precisa fazer o trabalho sozinho.

Alex
fonte
+1 no link para os bons artigos. Eu preciso estudar a teoria. Apenas tropeçou no vallocque eu nunca ouvi falar antes, preciso verificar o que é.
Niklas #
2
Não se esqueça da segurança da linha.
Sebastian Redl
vallocretorna a memória alinhada ao tamanho da página. Está obsoleto, como você pode usar memalignpara isso.
23416 Alex
19

Se você se preocupa apenas com eficiência, aqui está uma implementação em conformidade padrão e muito eficiente :

void* malloc(size_t sz) {
  errno = ENOMEM;
  return NULL;
}

void free(void*p) {
  if (p != NULL) abort();
}

Tenho certeza que você não encontrará mais rápido malloc.

Mas, embora ainda esteja em conformidade com o padrão, essa implementação é inútil (nunca aloca com êxito nenhuma memória heap). É uma implementação de piada.

Isso mostra que no mundo real, malloce freeimplementações precisa fazer trade-offs . E várias implementações estão fazendo várias compensações. Você encontrará muitos tutoriais sobre implementações de malloc . Para depurar vazamentos de memória em seus programas em C, convém usar o valgrind .

Observe que, nos sistemas Linux, pelo menos, (quase) todas as bibliotecas padrão C são software livre , para que você possa estudar o código fonte (em particular o código para malloc& free). musl-libc tem algum código fonte muito legível.

BTW, o código na sua pergunta está errado (e falhará com o meu mallocacima). Toda chamada para malloc pode falhar, e você deve testar isso.

Você pode ler a Programação Avançada do Linux e algum material mais geral sobre sistemas operacionais, por exemplo, Sistemas Operacionais: três partes fáceis .

Você provavelmente deve ler algo sobre coleta de lixo , pelo menos para obter os principais conceitos e terminologia; talvez lendo o manual do GC . Observe que a contagem de referência pode ser vista como uma forma de GC (baixa, que não lida com ciclos de referência ou gráficos cíclicos ...).

Você pode querer usar no coletor de lixo conservador do programa C de Boehm : você usaria GC_MALLOC(ou, para dados sem ponteiros como seqüências de caracteres ou matrizes numéricas GC_MALLOC_ATOMIC) , em vez de malloce não se preocupará em ligar freemais. Existem algumas advertências ao usar o GC da Boehm. Você pode considerar outros esquemas ou bibliotecas de GC ...

NB: Para testar a falha do malloc em um sistema Linux ( mallocàs vezes chama o sistema mmap (2) e / ou sbrk (2) , o Linux deve aumentar o espaço de endereço virtual , mas na maioria das vezes ele tenta reutilizar a freememória d anteriormente ) , você pode chamar o setrlimit (2) adequadamente com RLIMIT_ASe / ou RLIMIT_DATA, geralmente através doulimit bash embutido no shell do terminal. Use strace (1) para descobrir as chamadas do sistema feitas pelo seu (ou algum outro) programa.

Basile Starynkevitch
fonte
Eu me preocupo com a confiabilidade, mas é mais fácil entender a eficiência / velocidade. Eu li que o malloc pode travar se houver uma interrupção ou algo semelhante, mas ainda não sei o suficiente sobre isso. Obrigado por apontar que o código de teste está errado, achei que o resultado não era razoável. Mudei o código para alocação aleatória. Eu acho que a conclusão é que eu deveria estudar mais.
Niklas #
Existem implementações em que o malloc nunca falha (seu programa pode falhar, no entanto). No iOS, testar se o malloc retorna o ponteiro nulo S é inútil.
gnasher729
Eu sei disso (por exemplo, alguns computadores Linux têm supercomprometimento de memória), mas noto que essas implementações são contra o padrão C: elas mallocestão retornando um NULLponteiro (não ) que não pôde ser desreferenciado.
Basile Starynkevitch
6

Primeiro, malloc e trabalho livre juntos, portanto, testar o malloc por si só é enganador. Segundo, não importa quão bons sejam, eles podem facilmente ser o custo dominante em qualquer aplicativo, e a melhor solução para isso é chamá-los menos. Chamar menos é quase sempre a maneira vencedora de consertar programas limitados ao malloc . Uma maneira comum de fazer isso é reciclar objetos usados. Quando você é feito com um bloco, ao invés de livre -ing-lo, você encadear-lo em uma pilha de blocos usados e re uso-lo na próxima vez que você precisar de um.

Mike Dunlavey
fonte
5

O principal problema com sua malloc_quick()implementação é que ela não é segura para threads. E sim, se você omitir o suporte a threads do seu alocador, poderá obter um ganho de desempenho significativo. Vi uma aceleração semelhante com meu próprio alocador não thread-safe.

No entanto, uma implementação padrão precisa ser segura para threads. Ele precisa levar em conta todos os seguintes itens:

  • Threads diferentes usam malloc()e free()simultaneamente. Isso significa que a implementação não pode acessar o estado global sem sincronização interna.

    Como os bloqueios são muito caros, as malloc()implementações típicas tentam evitar o máximo possível de estado global, usando armazenamento local de encadeamento para quase todas as solicitações.

  • Um segmento que aloca um ponteiro não é necessariamente o segmento que o libera. Isso tem que ser resolvido.

  • Um thread pode alocar memória constantemente e passá-lo para outro thread para liberá-lo. Isso torna o manuseio do último ponto muito mais difícil, porque significa que blocos livres podem se acumular em qualquer armazenamento local de encadeamento. Isso força a malloc()implementação a fornecer meios para os encadeamentos trocarem blocos livres e provavelmente requer a captura de bloqueios de tempos em tempos.

Se você não se importa com os encadeamentos, todos esses pontos não apresentam problemas; portanto, um alocador que não seja seguro para encadeamentos não precisará pagar pelo tratamento com desempenho. Mas, como eu disse, uma implementação padrão não pode ignorar esses problemas e, consequentemente, tem que pagar por seu tratamento.

cmaster - restabelece monica
fonte
2

Eu acho que os dois SUT não são comparações diretas. Eu não ficaria surpreso com nenhuma diferença comparável quando você considera todas as variáveis: fabricação de memória, arquitetura da placa-mãe, versão do compilador (que compilou malloc), como é a aplicação do espaço de memória no SUT na época, etc etc etc ... .... Tente usar o seu equipamento de teste para ser mais representativo de como você usaria a memória em um projeto real - com algumas alocações grandes / pequenas e alguns aplicativos mantidos por um longo período de tempo e alguns liberados logo após serem executados.

Wayne Booth
fonte
2

Se você comparar uma implementação de malloc real com um projeto de escola, considere que um malloc real precisa gerenciar alocações, realocações e liberar memória de tamanhos enormemente diferentes, funcionando corretamente se as alocações ocorrerem em encadeamentos diferentes simultaneamente e a memória de realocação e liberação ocorrer em encadeamentos diferentes . Você também quer ter certeza de que qualquer tentativa de liberar memória que não foi alocada com o malloc seja detectada. E, finalmente, certifique-se de não comparar com uma implementação de malloc de depuração.

gnasher729
fonte