Eu criei um alocador de arena muito simples em C e fiquei pensando em uma coisa.
Aqui está meu arquivo de cabeçalho (a propósito, se esta API não parecer correta, por favor me avise!).
#ifndef ARENA_H
#define ARENA_H
#include <stddef.h>
struct Arena;
/* Returns a new arena allocator with a capacity of SIZE */
struct Arena *arena_new(size_t size);
/* Allocates SIZE bytes of memory from ARENA aligned with a default alignment
set to `__alignof__(max_align_t)`. */
void *arena_alloc(struct Arena *arena, size_t size);
/* Allocates SIZE bytes of memory from ARENA with no alignment */
void *arena_alloc_packed(struct Arena *arena, size_t size);
/* Allocates SIZE bytes of memory from ARENA aligned on ALIGN. */
void *arena_alloc_align(struct Arena *arena, size_t size, size_t align);
/* Free all memory allocated with ARENA */
void arena_free(struct Arena *arena);
#endif /* ARENA_H */
Na implementação, arena_alloc_align
costumo uintptr_t
converter o endereço do ponteiro do bloco de memória para um tipo com o qual posso calcular o alinhamento do endereço.
void *arena_alloc_align(struct Arena *arena, size_t size, size_t align)
{
assert((align & 1) == 0 && "aligment must be a power of two");
uintptr_t curr_addr = (uintptr_t)arena->mem_block + arena->cursor;
size_t padding = 0;
if (align > 0 && !IS_ALIGNED(curr_addr, align))
padding = align - (curr_addr & (align - 1));
if(arena->cursor + padding + size > arena->capacity)
return NULL;
arena->cursor = padding + size;
return (void *)curr_addr + padding;
}
Esse tipo ( uintptr_t
) é ok para usar? Tipo, ele é portátil? Porque pelo que eu li, esse tipo é opcional , isso significa que em alguma implementação de compilador ele pode não ser implementado?
Se sim, que tipo posso usar para converter um ponteiro para um tipo que seja grande o suficiente para conter qualquer endereço de memória?
Encontrei algumas outras perguntas relacionadas, mas as pessoas não parecem dar uma alternativa que seja definida no padrão e que não seja opcional. (Posso ter perdido a resposta que estava procurando).
Isso está correto. De acordo com §7.22.1.4 do padrão C23 , o tipo de dado
uintptr_t
é opcional.Isso significa que um compilador não é obrigado a suportá-lo, então é teoricamente possível que ele não seja implementado em um determinado compilador. No entanto, não tenho conhecimento de nenhum compilador moderno que não o suporte.
Se um compilador decide não oferecer suporte
uintptr_t
a uma determinada plataforma, provavelmente é uma plataforma muito antiga ou exótica e provavelmente há uma razão técnica especial para não oferecer suporte a ela.Por exemplo, na década de 1980, era comum que as CPUs suportassem apenas aritmética de 16 bits e tivessem apenas um tamanho de registro geral de 16 bits. Mas com 16 bits, você só pode endereçar até 64 KiB de memória. Portanto, para que essas CPUs de 16 bits pudessem endereçar mais de 64 KiB de memória, essas CPUs tinham que usar um modelo de memória segmentada em vez de um modelo de memória plana (que as CPUs modernas usam), o que significa que vários registros de 16 bits tinham que ser usados para armazenar um endereço.
Para implementar o tipo de dado inteiro
uintptr_t
em uma plataforma de 16 bits com endereços de memória maiores que 16 bits, o compilador precisaria implementar um tipo de dado inteiro maior que 16 bits, apesar de tal tipo de dado não ser suportado diretamente pela CPU. O compilador poderia, por exemplo, implementar um tipo de dado de 32 bits em software, fazendo com que esse tipo de dado usasse dois registradores de CPU de 16 bits e fazendo com que operações aritméticas nesse tipo de dado de 32 bits usassem internamente várias operações aritméticas de 16 bits.Por outro lado, o compilador poderia simplesmente decidir não suportar tal tipo de dado de 32 bits, e suportar apenas tipos de dados de até 16 bits (que são suportados diretamente pela CPU). Nesse caso, o compilador não definiria o tipo de dado
uintptr_t
, e o compilador ainda estaria em conformidade com o padrão C.Este exemplo da década de 1980 talvez não seja um bom exemplo, porque o tipo de dado
uintptr_t
só foi introduzido na linguagem C no padrão C99 (que é do ano de 1999), e este padrão também introduziu o tipo de dadolong long int
, que é necessário ter uma largura de pelo menos 64 bits. No entanto, este exemplo histórico demonstra que pode haver razões técnicas pelas quais um compilador não gostaria de ser obrigado a implementar o tipo de dadouintptr_t
, o que é provavelmente o motivo pelo qual o comitê de padrões decidiu tornar este tipo de dado opcional.Se o compilador decidir não suportar o tipo de dado
uintptr_t
, então, como descrito acima, isso provavelmente significa que há uma razão técnica especial pela qual não é trivial converter um ponteiro para um inteiro. Em tais situações, é altamente improvável que você consiga resolver o problema simplesmente usando outro tipo de dado que o compilador implementa. Em vez disso, você provavelmente terá que determinar a razão pela qual o tipo de dadouintptr_t
não é suportado nesta plataforma específica e, então, agir de acordo.Por outro lado, se a única razão pela qual você quer usar o tipo de dado
uintptr_t
é para determinar o alinhamento, então usar um tipo de dado menor comounsigned int
pode ser suficiente. Mas vale a pena notar que mesmo ao usar o tipo de dadouintptr_t
, o padrão C não garante que um número par represente um endereço de memória com um alinhamento de2
(mesmo que isso provavelmente seja uma suposição geralmente segura). A única garantia que o padrão fornece é queuintptr_t
é um "tipo inteiro sem sinal" (o que significa que operações aritméticas podem ser executadas neste tipo de dado) e que converter umvoid *
valor parauintptr_t
e de volta produzirá um valor de ponteiro que compara igual ao valor do ponteiro original.Entretanto, como já foi dito acima, é improvável que tudo isso seja um problema na prática, exceto em plataformas muito antigas ou exóticas.
Sim.
Não. Sim.
Sim.
Usar
uintptr_t
.Não há uma maneira totalmente portátil de escrever um alocador de memória geral em C; ele deve usar pré-requisitos além do que o padrão C fornece, em relação à manipulação de endereços, conversões de ponteiros, aliasing de tipos e mais. Usar
uintptr_t
é um requisito muito modesto.Um alocador de memória geral deve suportar alocação de espaços para objetos de diferentes tamanhos e requisitos de alinhamento. O padrão C não fornece adequadamente maneiras totalmente portáteis de testar e gerenciar alinhamento. (Algo bruto e inadequado pode ser feito definindo o pool de memória como uma grande matriz de caracteres com um requisito de alinhamento máximo, e então os alinhamentos podem ser gerenciados como deslocamentos dentro dessa matriz. No entanto, se uma matriz for definida com um tipo de elemento de caractere, então usá-la para outros tipos, como os clientes do alocador farão, viola as regras de aliasing em C 2024 6.5.)
Algum alocador de memória estritamente limitado que lida apenas com tipos predeterminados pode ser possível em código estritamente conforme. Além disso, qualquer alocador de memória dependerá de recursos de implementação. Usar
uintptr_t
é uma escolha razoável porque é amplamente fornecido e, se não for, geralmente pode ser definido como o maior tipo inteiro sem sinal. E não há alternativa melhor.