Implementando malloc() e free() — memória antiga tem preferência

You are viewing an old revision of this post, from September 18, 2024 @ 18:11:13. See below for differences between this version and the current revision.

No post anterior desta série sobre a implementação de malloc() e free(), mostramos como é possível reutilizar blocos de memória e reduzir o heap ao liberar os blocos mais novos. No entanto, a função atual tem um comportamento que sutilmente leva a um consumo de memória maior: ela escolhe blocos de memória mais recentes para reutilizar. Por que isso é um problema? Vamos analisar como isso ocorre.

Redução do heap com reaproveitamento de blocos recentes

Considere o cenário a seguir. Primeiro, alocamos quatro blocos de memória:

void *ptr1 = abmalloc(8);<br>void *ptr2 = abmalloc(8);<br>void *ptr3 = abmalloc(8);<br>void *ptr4 = abmalloc(8); Code language: HTML, XML (xml)

A estrutura em memória pode ser visualizada assim:

Lista ligada com quatro nós. Os nós mais novos apontam para os mais antigos. Todos os nós estão em uso, por isso todos estão com o rótulo Ocupado.

Agora, liberamos o primeiro e o terceiro bloco…

abfree(ptr1);<br>abfree(ptr3); Code language: HTML, XML (xml)

…resultando na seguinte estrutura:

Lista ligada com quatro nós. Os nós mais novos apontam para os mais antigos. O segundo e quarto nós da lista (ou seja, o segundo mais novo e o mais antigo de todos) foram liberados com free(), então estão rotulados com a palavra Livre.

Em seguida, alocamos outro bloco do mesmo tamanho:

void *ptr5 = abmalloc(8); Code language: JavaScript (javascript)

Como função abmalloc() começa a busca pelo bloco livre mais recente, ela reutiliza o bloco que está no topo. Se agora liberarmos o último bloco:

Lista ligada com quatro nós. Os nós mais novos apontam para os mais antigos. Agora apenas o últmo nó está liberado e rotulado com a palavra Livre

Se, agora, liberamos o último bloco…

abfree(ptr4);

…poderemos reduzir o tamanho do heap em apenas um bloco de 8 bytes, já que o penúltimo bloco não está mais livre:

Lista ligada com três nós. Os nós mais novos apontam para os mais antigos. O último nó está liberado e rotulado com a palavra Livre

Reaproveitamento de blocos antigos

Agora, imagine o mesmo cenário, mas com uma modificação: nossa função busca blocos livres começando pelo mais antigo.

A estrutura inicial será a mesma…

Lista ligada com quatro nós. Os nós mais antigos apontam para os mais novos. Todos os nós estão em uso e rotulados com a palavra Ocupado.

…e novamente liberamos o primeiro e o terceiro bloco de memória:

Lista ligada com quatro nós. Os nós mais antigos apontam para os mais novos. O primeiro e terceiro nós (isto é, o mais antigo e o terceiro mais antigo/segundo mais novo) foram desalocados e estão rotulados com a palavra Livre

Dessa vez, o primeiro bloco será reutilizado:

Lista ligada com quatro nós. Os nós mais antigos apontam para os mais novos. O terceiro nós (isto é, o mais antigo e o terceiro mais antigo/segundo mais novo) foi desalocado. Por isso, está rotulado com a palavra Livre

Agora, ao liberarmos o último bloco, teremos dois blocos livres no topo, permitindo reduzir o heap em dois blocos de 8 bytes:

Lista ligada com dois nós.  Todos estão ocupados porque os nós livres estavam no topo do heap e, portanto, foram destruídos.

Esse exemplo ilustra como, ao dar preferência a blocos mais novos, acabamos acumulando blocos livres antigos, desperdiçando memória e levando a um crescimento desnecessário do heap. A solução é modificar a estratégia de busca, priorizando a reutilização de blocos mais antigos.

Implementando preferência por blocos antigos

Para resolver esse problema, vamos implementar uma nova abordagem no código. Primeiramente, vamos adicionar ao cabeçalho dos blocos um ponteiro para o próximo bloco, permitindo percorrermos a memória de maneira eficiente. Também criaremos um ponteiro global para o primeiro bloco, o que facilitará a busca:

typedef struct Header {<br>  struct Header *previous, *next;<br>  size_t size;<br>  bool available;<br>} Header;<br><br>Header *first = NULL;<br>Header *last = NULL;Code language: HTML, XML (xml)

Agora, faremos uma pequena refatoração. Como criamos blocos de memória com headers em duas situações diferentes, extrairemos essa lógica para uma função auxiliar que aloca e inicializa o cabeçalho (inclusive setando o campo next com NULL):

Header *header_new(Header *previous, size_t size, bool available) {<br>  Header *header = sbrk(sizeof(Header) + size);<br>  header->previous = previous;<br>  header->next = NULL;<br>  header->size = size;<br>  header->available = false;<br>  return header;<br>}Code language: HTML, XML (xml)

Com essa nova função, podemos simplificar a lógica dentro de abmalloc():

void *abmalloc(size_t size) {<br>  if (size == 0) {<br>    return NULL;<br>  }<br>  Header *header = last;<br>  while (header != NULL) {<br>    if (header->available && (header->size >= size)) {<br>      header->available = false;<br>      return header + 1;<br>    }<br>    header = header->previous;<br>  }<br>  last = header_new(last, size, false);<br>  return last + 1;<br>}Code language: HTML, XML (xml)

Agora temos acesso ao primeiro e último blocos e, dado um bloco, podemos descobrir o anterior e o posterior. Sabemos, também, que quando o ponteiro para o primeiro bloco for nulo, nenhum bloco terá sido alocado ainda. Então, neste caso, alocaremos o bloco imediatamente, e inicializaremos tanto first quanto last:

void *abmalloc(size_t size) {<br>  if (size == 0) {<br>    return NULL;<br>  }<br>  if (first == NULL) {<br>    first = last = header_new(NULL, size, false);<br>    return first + 1;<br>  }Code language: HTML, XML (xml)

Se first não é mais NULL, já existem blocos alocados, então começaremos a busca por um bloco reutilizável. Continuaremos utilizando a variável header como iterador, mas, em vez de começar pelo bloco mais recente, a pesquisa será iniciada a partir do mais antigo:

  Header *header = first;

A cada iteração, avançaremos para o próximo bloco na sequência, em vez de retroceder para o bloco anterior:

  while (header != NULL) {<br>    if (header->available && (header->size >= size)) {<br>      header->available = false;<br>      return header + 1;<br>    }<br>    header = header->next;<br>  }Code language: PHP (php)

A lógica permanece a mesma: se encontramos um bloco disponível e de tamanho suficiente, ele é retornado. Caso contrário, se nenhum bloco reutilizável for encontrado após percorrermos a lista, um novo bloco será alocado:

  last = header_new(last, size, false);<br>Code language: JavaScript (javascript)

Agora, precisamos ajustar o bloco que era o último (e que, após a alocação, passa a ser o penúltimo). Ele apontava para NULL, mas agora deve apontar para o novo bloco. Para isso, acessamos o ponteiro previous do novo último bloco e atualizamos o campo next do bloco anterior:

  last->previous->next = last;<br>  return last + 1;<br>}Code language: PHP (php)

Ajustes na Função abfree()

A função abfree() mantém basicamente a mesma estrutura, mas agora devemos tratar alguns casos de borda. Quando liberamos blocos no topo do heap, um novo bloco passa a ser o último, conforme já fazemos neste trecho:

    last = header->previous;<br>    brk(header)Code language: HTML, XML (xml)

Aqui, o ponteiro header referencia o último bloco não nulo e disponível na pilha. Temos dois cenários possíveis: no primeiro, o bloco atual possui um bloco anterior, que se tornará o novo último bloco. Nesse caso, devemos ajustar o ponteiro next desse bloco para NULL. No segundo cenário, o bloco atual não possui um bloco anterior (ou seja, ele é o primeiro e mais antigo bloco). Quando ele é liberado, a pilha fica vazia. Nesse caso, em vez de tentar atualizar um campo de um bloco inexistente, basta definirmos first como NULL, indicando que não há mais blocos alocados:

  last = header->previous;<br>  if (last != NULL) {<br>    last->next = NULL;<br>  } else {<br>    first = NULL;<br>  }<br>  brk(header);Code language: HTML, XML (xml)

Conclusão

Nossas funções abmalloc() e abfree() agora estão assim:

typedef struct Header {
  struct Header *previous, *next;
  size_t size;
  bool available;
} Header;

Header *first = NULL;
Header *last = NULL;

Header *header_new(Header *previous, size_t size, bool available) {
  Header *header = sbrk(sizeof(Header) + size);
  header->previous = previous;
  header->next = NULL;
  header->size = size;
  header->available = false;
  return header;
}

void *abmalloc(size_t size) {
  if (size == 0) {
    return NULL;
  }
  if (first == NULL) {
    first = last = header_new(NULL, size, false);
    return first + 1;
  }
  Header *header = first;
  while (header != NULL) {
    if (header->available && (header->size >= size)) {
      header->available = false;
      return header + 1;
    }
    header = header->next;
  }
  last = header_new(last, size, false);
  last->previous->next = last;
  return last + 1;
}

void abfree(void *ptr) {
  if (ptr == NULL) {
   return;
  }
  Header *header = (Header*) ptr - 1;
  if (header == last) {
    while ((header->previous != NULL) && header->previous->available) {
      header = header->previous;
    }
    last = header->previous;
    if (last != NULL) {
      last->next = NULL;
    } else {
      first = NULL;
    }
    brk(header);
  } else {
   header->available = true;
  }
 }Code language: PHP (php)

Esta mudança nos permite economizar consideravelmente mais memória. Existem, contudo, problemas a resolver ainda. Por exemplo, considere o seguinte cenário: solicitamos a alocação de um bloco de memória de 8 bytes, e abmalloc() reaproveita um boco de, digamos, 1024 bytes. Há claramente um desperdício.

Veremos como resolver isso no próximo post.

Post Revisions:

Changes:

There are no differences between the September 18, 2024 @ 18:11:13 revision and the current revision. (Maybe only post meta information was changed.)

Post a Comment

Your email is never shared. Required fields are marked *

*
*