Rust: Traços do alocador e std :: heap

Criado em 8 abr. 2016  ·  412Comentários  ·  Fonte: rust-lang/rust

📢 Este recurso tem um grupo de trabalho dedicado , por favor, encaminhe comentários e preocupações ao repo do grupo de trabalho .

Postagem original:


Proposta FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336957415
Caixas de seleção do FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336980230


Problema de rastreamento para rust-lang / rfcs # 1398 e o módulo std::heap .

  • [x] pousar struct Layout , trait Allocator e implementações padrão em alloc crate (https://github.com/rust-lang/rust/pull/42313)
  • [x] decidir onde as partes devem ficar (por exemplo, impls padrão depende de alloc crate, mas Layout / Allocator _could_ está em libcore ...) (https://github.com/rust-lang/rust/pull/42313)
  • [] fixme do código-fonte: implementações padrão de auditoria (em Layout para erros de estouro, (potencialmente mudando para overflowing_add e overflowing_mul conforme necessário).
  • [x] decidir se realloc_in_place deve ser substituído por grow_in_place e shrink_in_place ( comentário ) (https://github.com/rust-lang/rust/pull/42313)
  • [] analise os argumentos a favor / contra o tipo de erro associado (consulte o subtítulo aqui )
  • [] determinar quais são os requisitos no alinhamento fornecido para fn dealloc . (Veja a discussão sobre o alocador rfc e o alocador global rfc e o traço Alloc PR .)

    • É necessário desalocar exatamente com align que você alocou? Surgiram preocupações de que alocadores como o jemalloc não exigem isso, e é difícil imaginar um alocador que exija isso. ( mais discussão ). Parece que @ruuda e @rkruppe foram os que mais pensaram nisso até agora.

  • [] deveria AllocErr ser Error ? ( comentário )
  • [x] É necessário desalocar com o tamanho exato que você alocou? Com o negócio usable_size , podemos permitir, por exemplo, que se você alocar com (size, align) você deve desalocar com um tamanho em algum lugar na faixa de size...usable_size(size, align) . Parece que o jemalloc está totalmente ok com isso (não exige que você desaloque com um size preciso que você alocou) e isso também permitiria que Vec aproveitasse naturalmente o excesso de capacidade do jemalloc dá quando faz uma alocação. (embora fazer isso também seja um tanto ortogonal a essa decisão, estamos apenas autorizando Vec ). Até agora, @Gankro tem a maior parte dos pensamentos sobre isso. ( @alexcrichton acredita que isso foi resolvido em https://github.com/rust-lang/rust/pull/42313 devido à definição de "ajustes")
  • [] semelhante à pergunta anterior: É necessário desalocar com o alinhamento exato que você alocou? (Ver comentário de 5 de junho de 2017 )
  • [x] OSX / alloc_system tem erros em alinhamentos enormes (por exemplo, um alinhamento de 1 << 32 ) https://github.com/rust-lang/rust/issues/30170 # 43217
  • [] Layout fornecer um método fn stride(&self) ? (Veja também https://github.com/rust-lang/rfcs/issues/1397, https://github.com/rust-lang/rust/issues/17027)
  • [x] Allocator::owns como método? https://github.com/rust-lang/rust/issues/44302

Estado de std::heap após https://github.com/rust-lang/rust/pull/42313 :

pub struct Layout { /* ... */ }

impl Layout {
    pub fn new<T>() -> Self;
    pub fn for_value<T: ?Sized>(t: &T) -> Self;
    pub fn array<T>(n: usize) -> Option<Self>;
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
    pub fn align_to(&self, align: usize) -> Self;
    pub fn padding_needed_for(&self, align: usize) -> usize;
    pub fn repeat(&self, n: usize) -> Option<(Self, usize)>;
    pub fn extend(&self, next: Self) -> Option<(Self, usize)>;
    pub fn repeat_packed(&self, n: usize) -> Option<Self>;
    pub fn extend_packed(&self, next: Self) -> Option<(Self, usize)>;
}

pub enum AllocErr {
    Exhausted { request: Layout },
    Unsupported { details: &'static str },
}

impl AllocErr {
    pub fn invalid_input(details: &'static str) -> Self;
    pub fn is_memory_exhausted(&self) -> bool;
    pub fn is_request_unsupported(&self) -> bool;
    pub fn description(&self) -> &str;
}

pub struct CannotReallocInPlace;

pub struct Excess(pub *mut u8, pub usize);

pub unsafe trait Alloc {
    // required
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);

    // provided
    fn oom(&mut self, _: AllocErr) -> !;
    fn usable_size(&self, layout: &Layout) -> (usize, usize);
    unsafe fn realloc(&mut self,
                      ptr: *mut u8,
                      layout: Layout,
                      new_layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn alloc_excess(&mut self, layout: Layout) -> Result<Excess, AllocErr>;
    unsafe fn realloc_excess(&mut self,
                             ptr: *mut u8,
                             layout: Layout,
                             new_layout: Layout) -> Result<Excess, AllocErr>;
    unsafe fn grow_in_place(&mut self,
                            ptr: *mut u8,
                            layout: Layout,
                            new_layout: Layout) -> Result<(), CannotReallocInPlace>;
    unsafe fn shrink_in_place(&mut self,
                              ptr: *mut u8,
                              layout: Layout,
                              new_layout: Layout) -> Result<(), CannotReallocInPlace>;

    // convenience
    fn alloc_one<T>(&mut self) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn dealloc_one<T>(&mut self, ptr: Unique<T>)
        where Self: Sized;
    fn alloc_array<T>(&mut self, n: usize) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn realloc_array<T>(&mut self,
                               ptr: Unique<T>,
                               n_old: usize,
                               n_new: usize) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn dealloc_array<T>(&mut self, ptr: Unique<T>, n: usize) -> Result<(), AllocErr>
        where Self: Sized;
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}
B-RFC-approved B-unstable C-tracking-issue Libs-Tracked T-lang T-libs disposition-merge finished-final-comment-period

Comentários muito úteis

@alexcrichton A decisão de mudar de -> Result<*mut u8, AllocErr> para -> *mut void pode vir como uma surpresa significativa para as pessoas que seguiram o desenvolvimento original das RFCs do alocador.

Não discordo dos seus pontos de vista , mas, no entanto, parecia que um bom número de pessoas estaria disposto a viver com o "peso-pesado" de Result sobre o aumento da probabilidade de perder um nulo verifique o valor retornado.

  • Estou ignorando os problemas de eficiência de tempo de execução impostos pela ABI porque, como @alexcrichton , suponho que poderíamos lidar com eles de alguma forma por meio de truques do compilador.

Existe alguma maneira de aumentarmos a visibilidade dessa alteração tardia por conta própria?

Uma maneira (em cima da minha cabeça): Mude a assinatura agora, em um PR por conta própria, no branch master, enquanto Allocator ainda está instável. E depois veja quem reclama no PR (e quem comemora!).

  • Isso é muito pesado? Parece que é por definições menos pesado do que combinar tal mudança com estabilização ...

Todos 412 comentários

Infelizmente, eu não estava prestando atenção suficiente para mencionar isso na discussão RFC, mas acho que realloc_in_place deve ser substituído por duas funções, grow_in_place e shrink_in_place , para dois razões:

  • Não consigo pensar em um único caso de uso (exceto implementar realloc ou realloc_in_place ) onde não se sabe se o tamanho da alocação está aumentando ou diminuindo. O uso de métodos mais especializados torna um pouco mais claro o que está acontecendo.
  • Os caminhos do código para aumentar e diminuir as alocações tendem a ser radicalmente diferentes - aumentar envolve testar se os blocos adjacentes de memória estão livres e reivindicá-los, enquanto a redução envolve separar subblocos de tamanho adequado e liberá-los. Embora o custo de um branch dentro de realloc_in_place seja bem pequeno, usar grow e shrink captura melhor as tarefas distintas que um alocador precisa realizar.

Observe que eles podem ser adicionados de forma compatível com versões anteriores ao lado de realloc_in_place , mas isso restringiria quais funções seriam implementadas por padrão em termos de quais outras.

Para consistência, realloc provavelmente também gostaria de ser dividido em grow e split , mas a única vantagem de ter uma função sobrecarregável realloc que eu conheço é ser capaz de usar a opção de remapeamento de mmap , que não possui tal distinção.

Além disso, acho que as implementações padrão de realloc e realloc_in_place devem ser ligeiramente ajustadas - em vez de verificar o usable_size , realloc deve apenas tentar primeiro realloc_in_place . Por sua vez, realloc_in_place deve, por padrão, verificar o tamanho utilizável e retornar o sucesso no caso de uma pequena mudança em vez de uma falha universalmente retornada.

Isso torna mais fácil produzir uma implementação de alto desempenho de realloc : tudo o que é necessário é melhorar realloc_in_place . No entanto, o desempenho padrão de realloc não é afetado, pois a verificação em relação a usable_size ainda é realizada.

Outro problema: o documento para fn realloc_in_place diz que se retornar Ok, então é garantido que ptr agora "se encaixa" new_layout .

Para mim, isso implica que ele deve verificar se o alinhamento do endereço fornecido corresponde a qualquer restrição implícita em new_layout .

No entanto, não acho que a especificação da função fn reallocate_inplace subjacente implica que _it_ realizará tal verificação.

  • Além disso, parece razoável que qualquer cliente que mergulhar no uso de fn realloc_in_place esteja garantindo que os alinhamentos funcionem (na prática, eu suspeito que significa que o mesmo alinhamento é necessário em todos os lugares para o caso de uso dado ...)

Portanto, a implementação de fn realloc_in_place realmente ser sobrecarregada com a verificação de que o alinhamento de ptr é compatível com o de new_layout ? Provavelmente, é melhor _neste caso_ (deste método) enviar esse requisito de volta ao chamador ...

@gereeter você faz bons pontos; Vou adicioná-los à lista de verificação que estou acumulando na descrição do problema.

(neste ponto, estou aguardando o suporte de #[may_dangle] para entrar no trem para o canal beta para que eu possa usá-lo para coleções std como parte da integração do alocador)

Sou novo no Rust, então me perdoe se isso foi discutido em outro lugar.

Existe alguma ideia sobre como oferecer suporte a alocadores específicos de objeto? Alguns alocadores, como alocadores de bloco e magazine, são vinculados a um tipo específico e fazem o trabalho de construção de novos objetos, armazenando em cache os objetos construídos que foram "liberados" (em vez de realmente descartá-los), retornando objetos em cache já construídos e descartar objetos antes de liberar a memória subjacente para um alocador subjacente quando necessário.

Atualmente, esta proposta não inclui nada na linha de ObjectAllocator<T> , mas seria muito útil. Em particular, estou trabalhando em uma implementação de uma camada de cache de objeto de alocador de revista (link acima), e embora eu possa fazer com que isso envolva apenas Allocator e faço o trabalho de construir e soltar objetos no cache camada em si, seria ótimo se eu pudesse também envolver outros alocadores de objeto (como um alocador de bloco) e ser realmente uma camada de cache genérica.

Onde um tipo de alocador de objeto ou característica se encaixaria nesta proposta? Seria deixado para uma futura RFC? Algo mais?

Não acho que isso tenha sido discutido ainda.

Você poderia escrever seu próprio ObjectAllocator<T> , e então fazer impl<T: Allocator, U> ObjectAllocator<U> for T { .. } , de forma que cada alocador regular possa servir como um alocador específico de objeto para todos os objetos.

O trabalho futuro seria modificar coleções para usar sua característica para seus nós, em vez de alocadores simples (genéricos) diretamente.

@pnkfelix

(neste ponto, estou aguardando o suporte # [may_dangle] para entrar no trem para o canal beta, para que eu possa usá-lo para coleções std como parte da integração do alocador)

Eu acho que isso aconteceu?

@ Ericson2314 Sim, escrever o meu é definitivamente uma opção para fins experimentais, mas acho que haveria muito mais benefícios em ser padronizado em termos de interoperabilidade (por exemplo, pretendo também implementar um alocador de placa, mas seria bom se um usuário de terceiros do meu código pudesse usar o alocador slab de alguém _else's_ com minha camada de cache de revista). Minha pergunta é simplesmente se um traço ObjectAllocator<T> ou algo parecido vale a pena discutir. Embora pareça que seja melhor para um RFC diferente? Não estou muito familiarizado com as diretrizes de quanto pertence a um único RFC e quando as coisas pertencem a RFCs separados ...

@joshlf

Onde um tipo de alocador de objeto ou característica se encaixaria nesta proposta? Seria deixado para uma futura RFC? Algo mais?

Sim, seria outro RFC.

Não estou muito familiarizado com as diretrizes de quanto pertence a um único RFC e quando as coisas pertencem a RFCs separados ...

isso depende do escopo do próprio RFC, que é decidido pela pessoa que o escreve, e o feedback é dado por todos.

Mas, realmente, como este é um problema de rastreamento para este RFC já aceito, pensar sobre extensões e alterações de design não é realmente para este segmento; você deve abrir um novo no repositório RFCs.

@joshlf Ah, pensei que ObjectAllocator<T> era para ser uma característica. Eu quis dizer prototipar o traço, não um alocador específico. Sim, essa característica mereceria seu próprio RFC, como diz @steveklabnik .


@steveklabnik sim, agora a discussão seria melhor em outro lugar. Mas @joshlf também estava levantando a questão para não expor uma falha até então imprevista no design de API aceito, mas não implementado. Nesse sentido, ele corresponde às postagens anteriores neste tópico.

@ Ericson2314 Sim, achei que era isso que você queria dizer. Acho que estamos na mesma página :)

@steveklabnik Parece bom; Vou vasculhar minha própria implementação e enviar uma RFC se acabar parecendo uma boa ideia.

@joshlf Não tenho nenhuma razão para que os alocadores personalizados entrem no compilador ou na biblioteca padrão. Assim que esta RFC chegar, você pode publicar facilmente sua própria caixa que faz um tipo arbitrário de alocação (até mesmo um alocador completo como o jemalloc poderia ser implementado de forma personalizada!).

@alexreg Não se trata de um alocador personalizado em particular, mas sim de um traço que especifica o tipo de todos os alocadores que são paramétricos em um tipo particular. Assim, assim como a RFC 1398 define um traço ( Allocator ) que é o tipo de qualquer alocador de baixo nível, estou perguntando sobre um traço ( ObjectAllocator<T> ) que é o tipo de qualquer alocador que pode alocar / desalocar e construir / descartar objetos do tipo T .

@alexreg Veja meu ponto inicial sobre o uso de coleções de bibliotecas padrão com alocadores específicos de objetos personalizados.

Claro, mas não tenho certeza de que isso pertenceria à biblioteca padrão. Poderia facilmente entrar em outra caixa, sem perda de funcionalidade ou usabilidade.

Em 4 de janeiro de 2017, às 21h59, Joshua Liebow-Feeser [email protected] escreveu:

@alexreg https://github.com/alexreg Não se trata de um alocador customizado em particular, mas sim de uma característica que especifica o tipo de todos os alocadores que são paramétricos em um tipo particular. Assim, assim como a RFC 1398 define um trait (Allocator) que é o tipo de qualquer alocador de baixo nível, estou perguntando sobre um trait (ObjectAllocator) que é o tipo de qualquer alocador que pode alocar / desalocar e construir / descartar objetos do tipo T.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499064 ou ignore o tópico https://github.com/notifications/unsubscribe-auth/ AAEF3IhyyPhFgu1EGHr_GM_Evsr0SRzIks5rPBZGgaJpZM4IDYUN .

Eu acho que você gostaria de usar coleções de biblioteca padrão (qualquer valor alocado no heap) com um alocador personalizado arbitrário ; ou seja, não se limita a objetos específicos.

Em 4 de janeiro de 2017, às 22:01, John Ericson [email protected] escreveu:

@alexreg https://github.com/alexreg Veja meu ponto inicial sobre o uso de coleções de bibliotecas padrão com alocadores específicos de objetos personalizados.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499628 ou ignore o tópico https://github.com/notifications/unsubscribe-auth/ AAEF3CrjYIXqcv8Aqvb4VTyPcajJozICks5rPBbOgaJpZM4IDYUN .

Claro, mas não tenho certeza de que isso pertenceria à biblioteca padrão. Poderia facilmente entrar em outra caixa, sem perda de funcionalidade ou usabilidade.

Sim, mas você provavelmente deseja que alguma funcionalidade de biblioteca padrão dependa dela (como o que @ Ericson2314 sugeriu).

Eu acho que você gostaria de usar coleções de biblioteca padrão (qualquer valor alocado no heap) com um alocador personalizado arbitrário ; ou seja, não se limita a objetos específicos.

Idealmente, você gostaria de ambos - aceitar qualquer tipo de alocador. Há benefícios muito significativos em usar o cache específico de objeto; por exemplo, tanto a alocação de placas quanto o armazenamento em cache do magazine oferecem benefícios de desempenho muito significativos - dê uma olhada nos artigos que vinculei acima se você estiver curioso.

Mas o traço de alocador de objeto poderia ser simplesmente um subtítulo do traço de alocador geral. É tão simples assim, no que me diz respeito. Claro, certos tipos de alocadores podem ser mais eficientes do que alocadores de propósito geral, mas nem o compilador nem o padrão realmente precisam (ou deveriam) saber sobre isso.

Em 4 de janeiro de 2017, às 22:13, Joshua Liebow-Feeser [email protected] escreveu:

Claro, mas não tenho certeza de que isso pertenceria à biblioteca padrão. Poderia facilmente entrar em outra caixa, sem perda de funcionalidade ou usabilidade.

Sim, mas você provavelmente deseja que alguma funcionalidade de biblioteca padrão dependa dela (como o que @ Ericson2314 https://github.com/Ericson2314 sugeriu).

Acho que você gostaria de usar coleções de biblioteca padrão (qualquer valor alocado no heap) com um alocador personalizado arbitrário; ou seja, não se limita a objetos específicos.

Idealmente, você gostaria de ambos - aceitar qualquer tipo de alocador. Há benefícios muito significativos em usar o cache específico de objeto; por exemplo, tanto a alocação de placas quanto o armazenamento em cache do magazine oferecem benefícios de desempenho muito significativos - dê uma olhada nos artigos que vinculei acima se você estiver curioso.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270502231 ou ignore o tópico https://github.com/notifications/unsubscribe-auth/ AAEF3L9F9r_0T5evOtt7Es92vw6gBxR9ks5rPBl9gaJpZM4IDYUN .

Mas o traço de alocador de objeto poderia ser simplesmente um subtítulo do traço de alocador geral. É tão simples assim, no que me diz respeito. Claro, certos tipos de alocadores podem ser mais eficientes do que alocadores de propósito geral, mas nem o compilador nem o padrão realmente precisam (ou deveriam) saber sobre isso.

Ah, então o problema é que a semântica é diferente. Allocator aloca e libera blobs de bytes brutos. ObjectAllocator<T> , por outro lado, alocaria objetos já construídos e também seria responsável por descartar esses objetos (incluindo a capacidade de armazenar em cache objetos construídos que poderiam ser entregues posteriormente no processo de construção de um objeto recém-alocado , que é caro). O traço seria mais ou menos assim:

trait ObjectAllocator<T> {
    fn alloc() -> T;
    fn free(t T);
}

Isso não é compatível com Allocator , cujos métodos lidam com ponteiros brutos e não têm noção de tipo. Além disso, com Allocator s, é responsabilidade do chamador drop o objeto sendo liberado primeiro. Isso é realmente importante - saber sobre o tipo T permite que ObjectAllocator<T> faça coisas como chamar T drop método free(t) move t para free , o chamador _não pode abandonar t primeiro - ao invés disso, é a responsabilidade de ObjectAllocator<T> . Fundamentalmente, esses dois traços são incompatíveis um com o outro.

Ah certo, entendo. Achei que essa proposta já incluía algo assim, ou seja, um alocador de “nível superior” sobre o nível de bytes. Nesse caso, uma proposta perfeitamente justa!

Em 4 de janeiro de 2017, às 22:29, Joshua Liebow-Feeser [email protected] escreveu:

Mas o traço de alocador de objeto poderia ser simplesmente um subtítulo do traço de alocador geral. É tão simples assim, no que me diz respeito. Claro, certos tipos de alocadores podem ser mais eficientes do que alocadores de propósito geral, mas nem o compilador nem o padrão realmente precisam (ou deveriam) saber sobre isso.

Ah, então o problema é que a semântica é diferente. O Allocator aloca e libera blobs de bytes brutos. ObjectAllocator, por outro lado, alocaria objetos já construídos e também seria responsável por descartar esses objetos (incluindo a capacidade de armazenar em cache objetos construídos que poderiam ser entregues posteriormente no processo de construção de um objeto recém-alocado, o que é caro). O traço seria mais ou menos assim:

traço ObjectAllocator{
fn aloc () -> T;
fn livre (t T);
}
Isso não é compatível com o Allocator, cujos métodos lidam com ponteiros brutos e não têm noção de tipo. Além disso, com Allocators, é responsabilidade do chamador descartar o objeto que está sendo liberado primeiro. Isso é realmente importante - saber sobre o tipo T permite ObjectAllocatorpara fazer coisas como chamar o método drop de T, e uma vez que free (t) move t para free, o chamador não pode largar t primeiro - é o ObjectAllocatorresponsabilidade de. Fundamentalmente, esses dois traços são incompatíveis um com o outro.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270505704 ou ignore o tópico https://github.com/notifications/unsubscribe-auth/ AAEF3GViJBefuk8IWgPauPyL5tV78Fn5ks5rPB08gaJpZM4IDYUN .

@alexreg Ah sim, eu esperava isso também :) Bem - vai ter que esperar por outro RFC.

Sim, comece a RFC, tenho certeza de que obteria muito apoio! E obrigado pelo esclarecimento (eu não tinha me mantido informado sobre os detalhes dessa RFC).

Em 5 de janeiro de 2017, às 00h53, Joshua Liebow-Feeser [email protected] escreveu:

@alexreg https://github.com/alexreg Ah sim, eu também esperava :) Bem - vai ter que esperar por outro RFC.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270531535 ou ignore o tópico https://github.com/notifications/unsubscribe-auth/ AAEF3MQQeXhTliU5CBsoheBFL26Ee9WUks5rPD8RgaJpZM4IDYUN .

Uma caixa para testar alocadores personalizados seria útil.

Perdoe-me se algo óbvio está faltando, mas há uma razão para o traço Layout descrito neste RFC não implementar Copy , bem como Clone , já que é apenas POD?

Não consigo pensar em nenhum.

Desculpe por trazer isso à tona tão tarde no processo, mas ...

Pode valer a pena adicionar suporte para uma função semelhante a dealloc que não é um método, mas sim uma função? A ideia seria usar o alinhamento para poder inferir a partir de um ponteiro onde está o seu alocador pai na memória e, assim, ser capaz de liberar sem precisar de uma referência de alocador explícita.

Isso pode ser uma grande vitória para estruturas de dados que usam alocadores personalizados. Isso permitiria que eles não mantivessem uma referência ao próprio alocador, mas apenas precisariam ser paramétricos no _tipo_ do alocador para poder chamar a função dealloc correta. Por exemplo, se Box for eventualmente modificado para oferecer suporte a alocadores personalizados, ele poderá continuar sendo apenas uma única palavra (apenas o ponteiro) em vez de ter que ser expandido para duas palavras para armazenar uma referência para o alocador também.

Em uma nota relacionada, também pode ser útil oferecer suporte a uma função não-método alloc para permitir alocadores globais. Isso seria muito bem composto por uma função não-método dealloc - para alocadores globais, não haveria necessidade de fazer qualquer tipo de inferência de ponteiro para alocador, pois haveria apenas uma única instância estática do alocador para todo o programa.

@joshlf O design atual permite que você obtenha isso apenas tendo seu alocador um tipo de unidade (tamanho zero) - isto é, struct MyAlloc; que você implementa a característica Allocator .
Armazenar referências ou nada, sempre , é menos geral do que armazenar o alocador por valor.

Eu pude ver que isso é verdade para um tipo diretamente incorporado, mas e se uma estrutura de dados decidir manter uma referência em vez disso? Uma referência a um tipo de tamanho zero ocupa espaço zero? Ou seja, se eu tiver:

struct Foo()

struct Blah{
    foo: &Foo,
}

Blah tem tamanho zero?

Na verdade, mesmo que seja possível, você pode não querer que seu alocador tenha tamanho zero. Por exemplo, você pode ter um alocador com um tamanho diferente de zero que você aloca _de_, mas que tem a capacidade de liberar objetos sem saber sobre o alocador original. Isso ainda seria útil para fazer Box pegar apenas uma palavra. Você teria algo como Box::new_from_allocator que teria que tomar um alocador como argumento - e pode ser um alocador de tamanho diferente de zero - mas se o alocador suportasse a liberação sem a referência do alocador original, o Box<T> retornado Box::new_from_allocator .

Por exemplo, você pode ter um alocador com um tamanho diferente de zero do qual você aloca, mas que tem a capacidade de liberar objetos sem saber sobre o alocador original.

Lembro-me de há muito, muito tempo atrás, propor traços separados de alocador e desalocador (com tipos de associados conectando os dois) basicamente por esta razão.

O compilador pode / deve ter permissão para otimizar alocações fora com esses alocadores?

O compilador pode / deve ter permissão para otimizar alocações fora com esses alocadores?

@Zoxc O que você quer dizer?

Lembro-me de há muito, muito tempo atrás, propor traços separados de alocador e desalocador (com tipos de associados conectando os dois) basicamente por esta razão.

Para a posteridade, deixe-me esclarecer esta declaração (conversei com @ Ericson2314 sobre isso offline): A ideia é que Box poderia ser paramétrico apenas em um desalocador. Portanto, você poderia ter a seguinte implementação:

trait Allocator {
    type D: Deallocator;

    fn get_deallocator(&self) -> Self::D;
}

trait Deallocator {}

struct Box<T, D: Deallocator> {
    ptr: *mut T,
    d: D,
}

impl<T, D: Deallocator> Box<T, D> {
    fn new_from_allocator<A: Allocator>(x: T, a: A) -> Box<T, A::D> {
        ...
        Box {
            ptr: ptr,
            d: a.get_deallocator()
        }
    }
}

Desta forma, ao chamar new_from_allocator , se A::D é um tipo de tamanho zero, então o campo d de Box<T, A::D> assume o tamanho zero, e assim o o tamanho do Box<T, A::D> resultante é uma única palavra.

Existe um cronograma para quando isso vai pousar? Estou trabalhando em algumas coisas do alocador e seria bom se essas coisas estivessem lá para eu construir.

Se houver interesse, ficaria feliz em emprestar alguns ciclos para isso, mas sou relativamente novo no Rust, então isso pode criar mais trabalho para os mantenedores em termos de ter que revisar o código de um novato. Não quero pisar no pé de ninguém e não quero dar mais trabalho às pessoas.

Ok, recentemente nos encontramos para avaliar o estado dos alocadores e acho que também há boas notícias para isso! Parece que o suporte ainda não chegou à libstd para essas APIs, mas todos ainda estão felizes com elas chegando a qualquer momento!

Uma coisa que discutimos é que mudar todos os tipos de libstd pode ser um pouco prematuro devido a possíveis problemas de inferência, mas, independentemente disso, parece uma boa ideia obter o traço Allocator e o Layout digite o módulo proposto std::heap para experimentação em outro lugar no ecossistema!

@joshlf se você quiser ajudar aqui, acho que seria mais do que bem-vindo! A primeira parte provavelmente será colocar o tipo / característica básica deste RFC na biblioteca padrão e, a partir daí, podemos começar a experimentar e brincar com as coleções em libstd também.

@alexcrichton , acho que seu link está quebrado? Ele aponta para trás aqui.

Uma coisa que discutimos é que mudar todos os tipos libstd pode ser um pouco prematuro devido a possíveis problemas de inferência

Adicionar a característica é um bom primeiro passo, mas sem refatorar APIs existentes para usá-la, eles não verão muito uso. Em https://github.com/rust-lang/rust/issues/27336#issuecomment -300721558, proponho que possamos refatorar as caixas atrás da fachada imediatamente, mas adicionar wrappers newtype em std . É chato de fazer, mas nos permite fazer progressos.

@alexcrichton Qual seria o processo para obter alocadores de objeto? Meus experimentos até agora (em breve serão públicos; posso adicioná-lo ao repositório privado de GH se você estiver curioso) e a discussão aqui me levou a acreditar que haverá uma simetria quase perfeita entre os traços de alocador e o objeto traços de alocador. Por exemplo, você terá algo como (eu mudei Address para *mut u8 para simetria com *mut T de ObjectAllocator<T> ; provavelmente terminaríamos com Address<T> ou algo parecido):

unsafe trait Allocator {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);
}
unsafe trait ObjectAllocator<T> {
    unsafe fn alloc(&mut self) -> Result<*mut T, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut T);
}

Portanto, acho que experimentar os alocadores e os alocadores de objetos ao mesmo tempo pode ser útil. Não tenho certeza se este é o lugar certo para isso, ou se deveria haver outro RFC ou, pelo menos, um PR separado.

Oh, eu pretendia vincular aqui que também contém informações sobre o alocador global. @joshlf é isso que você está pensando?

Parece que @alexcrichton deseja um PR que forneça o traço Allocator e o tipo Layout , mesmo que não esteja integrado em nenhuma coleção em libstd .

Se eu entendi isso corretamente, posso colocar um PR para isso. Eu não tinha feito isso porque continuo tentando obter pelo menos integração com RawVec e Vec prototipados. (Neste ponto eu tenho RawVec pronto, mas Vec é um pouco mais desafiador devido às muitas outras estruturas que se formam a partir dele, como Drain e IntoIter etc ...)

na verdade, meu branch atual parece que pode realmente construir (e o único teste de integração com RawVec passou), então fui em frente e postei: # 42313

@hawkw perguntou:

Perdoe-me se algo óbvio está faltando, mas há uma razão para o traço Layout descrito neste RFC não implementar Copiar, bem como Clonar, já que é apenas POD?

A razão pela qual fiz Layout implementar apenas Clone e não Copy é que queria deixar em aberto a possibilidade de adicionar mais estrutura ao tipo Layout . Em particular, ainda estou interessado em tentar fazer com que Layout rastreie qualquer estrutura de tipo usada para construí-la (por exemplo, 16-array de struct { x: u8, y: [char; 215] } ), para que os alocadores tenham a opção de expor rotinas de instrumentação que relatam de quais tipos seus conteúdos atuais são compostos.

Isso quase certamente teria que ser um recurso opcional, ou seja, parece que a maré está firmemente contra forçar os desenvolvedores a usar os construtores Layout enriquecidos por tipo. portanto, qualquer instrumentação dessa forma precisaria incluir algo como uma categoria de "blocos de memória desconhecidos" para lidar com alocações que não têm as informações de tipo.

Mesmo assim, recursos como esse foram o principal motivo pelo qual não optei por fazer Layout implementar Copy ; Basicamente, imaginei que uma implementação de Copy seria uma restrição prematura no próprio Layout .

@alexcrichton @pnkfelix

Parece que @pnkfelix tem esse assunto coberto e que o PR está ganhando força, então vamos em frente. Estou olhando e fazendo comentários agora, e parece ótimo!

Atualmente, a assinatura de Allocator::oom é:

    fn oom(&mut self, _: AllocErr) -> ! {
        unsafe { ::core::intrinsics::abort() }
    }

Fui informado , porém, que o Gecko pelo menos também gosta de saber o tamanho da alocação no OOM. Podemos desejar considerar isso ao estabilizar para talvez adicionar contexto como Option<Layout> para saber por que OOM está acontecendo.

@alexcrichton
Pode valer a pena ter várias variantes oom_xxx ou uma enumeração de diferentes tipos de argumento? Existem algumas assinaturas diferentes para métodos que podem falhar (por exemplo, alloc pega um layout, realloc pega um ponteiro, um layout original e um novo layout, etc), e pode haver ser casos em que um método do tipo oom gostaria de saber sobre todos eles.

@joshlf isso é verdade, sim, mas não tenho certeza se é útil. Eu não gostaria de apenas adicionar recursos porque podemos, eles devem continuar bem motivados.


Um ponto de estabilização aqui também é "determinar quais são os requisitos no alinhamento fornecido para fn dealloc ", e a implementação atual de dealloc no Windows usa align para determinar como corretamente livre. @ruuda você pode estar interessado neste fato.

Um ponto de estabilização aqui também é "determinar quais são os requisitos no alinhamento fornecido para fn dealloc ", e a implementação atual de dealloc no Windows usa align para determinar como corretamente livre.

Sim, acho que foi assim que me deparei com isso; meu programa travou no Windows por causa disso. Como HeapAlloc não oferece nenhuma garantia de alinhamento, allocate aloca uma região maior e armazena o ponteiro original em um cabeçalho, mas como uma otimização, isso é evitado se os requisitos de alinhamento forem satisfeitos de qualquer maneira. Eu me pergunto se existe uma maneira de converter HeapAlloc em um alocador com reconhecimento de alinhamento que não requeira alinhamento no free, sem perder essa otimização.

@ruuda

Como HeapAlloc não oferece garantias de alinhamento

Ele fornece uma garantia de alinhamento mínimo de 8 bytes para 32 bits ou 16 bytes para 64 bits, mas não fornece nenhuma maneira de garantir um alinhamento maior do que isso.

O _aligned_malloc fornecido pelo CRT no Windows pode fornecer alocações de alinhamento superior, mas notavelmente deve ser emparelhado com _aligned_free , usando free é ilegal. Então, se você não sabe se uma alocação foi feita via malloc ou _aligned_malloc então você está preso no mesmo enigma que alloc_system está no Windows se você não fizer isso Não sei o alinhamento de deallocate . O CRT não fornece a função aligned_alloc padrão, que pode ser emparelhada com free , portanto, nem mesmo a Microsoft foi capaz de resolver esse problema. (Embora seja uma função C11 e a Microsoft não suporte C11, esse é um argumento fraco.)

Observe que deallocate só se preocupa com o alinhamento para saber se ele está superalinhado, o valor real em si é irrelevante. Se você quisesse um deallocate que fosse realmente independente do alinhamento, poderia simplesmente tratar todas as alocações como superalinhadas, mas desperdiçaria muita memória em pequenas alocações.

@alexcrichton escreveu :

Atualmente, a assinatura de Allocator::oom é:

    fn oom(&mut self, _: AllocErr) -> ! {
        unsafe { ::core::intrinsics::abort() }
    }

Fui informado , porém, que o Gecko pelo menos também gosta de saber o tamanho da alocação no OOM. Podemos desejar considerar isso ao estabilizar para talvez adicionar contexto como Option<Layout> para saber por que OOM está acontecendo.

O AllocErr já carrega o Layout na variante AllocErr::Exhausted . Poderíamos apenas adicionar Layout à variante AllocErr::Unsupported também, o que eu acho que seria mais simples em termos de expectativas do cliente. (Tem a desvantagem de aumentar levemente o lado do próprio AllocErr enum, mas talvez não devamos nos preocupar com isso ...)

Oh, eu suspeito que isso seja tudo o que é necessário, obrigado pela correção @pnkfelix!

Vou começar a redirecionar esse problema para o problema de rastreamento de std::heap em geral, pois será depois que https://github.com/rust-lang/rust/pull/42727 cair. Fecharei algumas outras questões relacionadas a favor disso.

Existe um problema de rastreamento para a conversão de coleções? Agora que os PRs foram fundidos, gostaria de

  • Discuta o tipo de erro associado
  • Discuta a conversão de coleções para usar qualquer alocador local (especialmente aproveitando o tipo de erro associado)

Abri https://github.com/rust-lang/rust/issues/42774 para acompanhar a integração de Alloc em coleções std. Com a discussão histórica na equipe de libs, é provável que esteja em um caminho diferente de estabilização do que uma passagem inicial do módulo std::heap .

Ao revisar questões relacionadas ao alocador, também me deparei com https://github.com/rust-lang/rust/issues/30170 que @pnkfelix há algum tempo. Parece que o alocador de sistema OSX está cheio de erros com alinhamentos altos e, ao executar esse programa com jemalloc, ele está causando falha em segmen- to durante a desalocação no Linux, pelo menos. Vale a pena considerar durante a estabilização!

Abri o nº 42794 como um lugar para discutir a questão específica de se as alocações de tamanho zero precisam corresponder ao alinhamento solicitado.

(espere, alocações de tamanho zero são ilegais em alocadores de usuários!)

Como a função alloc::heap::allocate e os amigos não estão mais no Nightly, atualizei o Servo para usar essa nova API. Isso é parte do diff:

-use alloc::heap;
+use alloc::allocator::{Alloc, Layout};
+use alloc::heap::Heap;
-        let ptr = heap::allocate(req_size as usize, FT_ALIGNMENT) as *mut c_void;
+        let layout = Layout::from_size_align(req_size as usize, FT_ALIGNMENT).unwrap();
+        let ptr = Heap.alloc(layout).unwrap() as *mut c_void;

Acho que a ergonomia não é ótima. Passamos da importação de um item para a importação de três de dois módulos diferentes.

  • Faria sentido ter um método de conveniência para allocator.alloc(Layout::from_size_align(…)) ?
  • Faria sentido disponibilizar métodos <Heap as Alloc>::_ como funções livres ou métodos inerentes? (Para ter um item a menos para importar, o traço Alloc .)

Alternativamente, o traço Alloc poderia estar no prelúdio ou é muito nicho de um caso de uso?

@SimonSapin IMO não há muito sentido em otimizar a ergonomia de uma API de baixo nível.

@SimonSapin

Acho que a ergonomia não é ótima. Passamos da importação de um item para a importação de três de dois módulos diferentes.

Tive exatamente a mesma sensação com minha base de código - é bem desajeitado agora.

Faria sentido ter um método de conveniência para allocator.alloc(Layout::from_size_align(…))?

Você quer dizer no traço Alloc , ou apenas para Heap ? Uma coisa a se considerar aqui é que agora há uma terceira condição de erro: Layout::from_size_align retorna um Option , então poderia retornar None além dos erros normais que você pode obter ao alocar .

Alternativamente, o traço Alloc poderia estar no prelúdio ou é muito nicho de um caso de uso?

IMO, não há muito sentido em otimizar a ergonomia de uma API de baixo nível.

Eu concordo que provavelmente é um nível muito baixo para colocar no prelúdio, mas eu ainda acho que há valor em otimizar a ergonomia (egoisticamente, pelo menos - isso foi uma refatoração realmente irritante 😝).

@SimonSapin você não std todos os três tipos estão disponíveis no módulo std::heap (eles deveriam estar em um módulo). Além disso, você não lidou com o caso de tamanhos excessivos antes? Ou tipos de tamanho zero?

você não lidou com OOM antes?

Quando existia, a função alloc::heap::allocate retornava um ponteiro sem Result e não deixava uma escolha no tratamento OOM. Acho que abortou o processo. Agora eu adicionei .unwrap() para causar pânico no tópico.

eles deveriam estar em um módulo

Vejo agora que heap.rs contém pub use allocator::*; . Mas quando cliquei em Alloc no impl listado na página rustdoc para Heap fui enviado para alloc::allocator::Alloc .

Quanto ao resto, não investiguei. Estou portando para um novo compilador uma grande pilha de código que foi escrita anos atrás. Acho que esses são retornos de chamada para FreeType, uma biblioteca C.

Quando existia, a função alloc :: heap :: allocate retornava um ponteiro sem um Result e não deixava uma escolha no tratamento OOM.

Isso deu a você uma escolha. O ponteiro que ele retornou pode ter sido um ponteiro nulo, o que indicaria que o alocador de heap falhou ao alocar. É por isso que estou tão feliz que mudou para Result para que as pessoas não se esqueçam de cuidar desse caso.

Bem, talvez o FreeType acabou fazendo uma verificação nula, não sei. Enfim, sim, retornar um Resultado é bom.

Dados # 30170 e # 43097, estou tentado a resolver o problema do OS X com alinhamentos ridiculamente grandes, simplesmente especificando que os usuários não podem solicitar alinhamentos> = 1 << 32 .

Uma maneira muito fácil de impor isso: Mude a interface Layout modo que align seja denotado por u32 vez de usize .

@alexcrichton , você tem alguma opinião sobre isso? Devo apenas fazer um PR que faça isso?

@pnkfelix Layout::from_size_align ainda pegaria Layout::from_size_align usize e retornaria um erro no estouro de u32 , certo?

@SimonSapin que razão há para que continue tomando usize align, se uma pré-condição estática é que não é seguro passar um valor> = 1 << 32 ?

e se a resposta for "bem, alguns alocadores podem suportar um alinhamento> = 1 << 32 ", então estamos de volta ao status quo e você pode desconsiderar minha sugestão. O objetivo da minha sugestão é basicamente um "+1" para comentários como este

Porque std::mem::align_of retorna usize

@SimonSapin ah, boa e velha API estável ... suspiro.

@pnkfelix limitar a 1 << 32 parece razoável para mim!

@rfcbot fcp merge

Ok, esse traço e seus tipos já se consolidaram por um tempo e também foram a implementação subjacente das coleções padrão desde seu início. Eu proporia começar com uma oferta inicial particularmente conservadora, ou seja, apenas estabilizar a seguinte interface:

pub struct Layout { /* ... */ }

extern {
    pub type void;
}

impl Layout {
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
}

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> *mut void;
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> *mut void;
    unsafe fn dealloc(&mut self, ptr: *mut void, layout: Layout);

    // all other methods are default and unstable
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}

Proposta original

pub struct Layout { /* ... */ }

impl Layout {
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
}

// renamed from AllocErr today
pub struct Error {
    // ...
}

impl Error {
    pub fn oom() -> Self;
}

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, Error>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);

    // all other methods are default and unstable
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}

Notavelmente:

  • Apenas estabilizando os métodos alloc , alloc_zeroed e dealloc no traço Alloc por enquanto. Acho que isso resolve o problema mais urgente que temos hoje, definir um alocador global personalizado.
  • Remova o tipo Error em favor de usar apenas ponteiros brutos.
  • Altere o tipo u8 na interface para void
  • Uma versão simplificada do tipo Layout .

Ainda há questões em aberto, como o que fazer com dealloc e alinhamento (alinhamento preciso? Encaixa? Inseguro?), Mas espero que possamos resolvê-los durante o FCP, pois provavelmente não será uma API quebra de mudança.

1 para obter algo estabilizado!

Renomeia AllocErr para Error e move a interface para ser um pouco mais conservadora.

Isso elimina a opção dos alocadores de especificar Unsupported ? Correndo o risco de insistir em algo que venho insistindo muito, acho que # 44557 ainda é um problema.

Layout

Parece que você removeu alguns dos métodos de Layout . Você quis dizer que aqueles que você deixou de fora foram realmente removidos ou apenas deixados como instáveis?

impl Error {
    pub fn oom() -> Self;
}

Este é um construtor para o que é hoje AllocErr::Exhausted ? Se sim, não deveria ter um parâmetro Layout ?

O membro da equipe @alexcrichton propôs fundir isso. A próxima etapa é revisada pelo restante das equipes marcadas:

  • [x] @BurntSushi
  • [x] @Kimundi
  • [x] @alexcrichton
  • [x] @aturon
  • [x] @cramertj
  • [x] @dtolnay
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @sfackler
  • [x] @withoutboats

Preocupações:

Assim que esses revisores chegarem a um consenso, isso entrará em seu período final para comentários. Se você identificar uma questão importante que não foi levantada em algum ponto deste processo, fale!

Consulte este documento para obter informações sobre quais comandos os membros da equipe marcados podem me dar.

Estou muito animado para estabilizar parte desse trabalho!

Uma pergunta: no tópico acima, @joshlf e @ Ericson2314 levantaram um ponto interessante sobre a possibilidade de separar os traços Alloc e Dealloc para otimizar os casos em que alloc requer alguns dados, mas dealloc não requer nenhuma informação extra, então o tipo Dealloc pode ter tamanho zero.

Esta questão já foi resolvida? Quais são as desvantagens de separar as duas características?

@joshlf

Isso elimina a opção de os alocadores especificarem Sem suporte?

Sim e não, isso significaria que tal operação não é suportada em ferrugem estável imediatamente, mas poderíamos continuar a suportá-la em ferrugem instável.

Você quis dizer que aqueles que você deixou de fora foram realmente removidos ou apenas deixados como instáveis?

De fato! Novamente, embora eu esteja apenas propondo uma área de superfície de API estável, podemos deixar todos os outros métodos como instáveis. Com o tempo, podemos continuar a estabilizar mais a funcionalidade. Acho que é melhor começar o mais conservador possível.


@SimonSapin

Este é um construtor para o que é hoje AllocErr :: Exhausted? Em caso afirmativo, não deveria ter um parâmetro Layout?

Aha, bom ponto! Eu meio que queria deixar a possibilidade de fazer Error um tipo de tamanho zero se realmente precisássemos, mas podemos, é claro, manter os métodos de obtenção de layout instáveis ​​e estabilizá-los se necessário. Ou você acha que a preservação de layout Error deve ser estabilizada na primeira passagem?


@cramertj

Eu não tinha visto pessoalmente tal questão / preocupação ainda (acho que perdi!), Mas pessoalmente não consideraria que valesse a pena. Duas características é o dobro do clichê em geral, pois agora todos teriam que digitar Alloc + Dealloc em coleções, por exemplo. Eu esperaria que esse uso especializado não informasse a interface que todos os outros usuários acabam usando, pessoalmente.

@cramertj @alexcrichton

Eu não tinha visto pessoalmente tal questão / preocupação ainda (acho que perdi!), Mas pessoalmente não consideraria que valesse a pena.

Em geral, concordo que não vale a pena com uma exceção gritante: Box . Box<T, A: Alloc> , dada a definição atual de Alloc , teria que ter pelo menos duas palavras (o ponteiro que já possui e uma referência a um Alloc no mínimo ), exceto no caso de singletons globais (que podem ser implementados como ZSTs). Uma ampliação 2x (ou mais) no espaço necessário para armazenar um tipo tão comum e fundamental é preocupante para mim.

@alexcrichton

já que agora todos teriam que digitar Alloc + Dealloc em coleções, por exemplo

Poderíamos adicionar algo assim:

trait Allocator: Alloc + Dealloc {}
impl<T> Allocator for T where T: Alloc + Dealloc {}

Uma ampliação 2x (ou mais) no espaço necessário para armazenar tal tipo comum e fundamental

Somente quando você usa um alocador personalizado que não é global de processo. std::heap::Heap (o padrão) tem tamanho zero.

Ou você acha que o erro de preservação de layout deve ser estabilizado na primeira passagem?

@alexcrichton Eu realmente não entendo por que essa primeira passagem proposta é assim. Quase não há mais do que já poderia ser feito abusando de Vec , e não o suficiente, por exemplo, para usar https://crates.io/crates/jemallocator.

O que ainda precisa ser resolvido para estabilizar a coisa toda?

Somente quando você usa um alocador personalizado que não é global de processo. std :: heap :: Heap (o padrão) tem tamanho zero.

Esse parece ser o caso de uso primário de alocadores paramétricos, não? Imagine a seguinte definição simples de árvore:

struct Node<T, A: Alloc> {
    t: T,
    left: Option<Box<Node<T, A>>>,
    right: Option<Box<Node<T, A>>>,
}

Uma árvore construída a partir daquelas com uma palavra Alloc teria um aumento de ~ 1.7x em tamanho para toda a estrutura de dados em comparação com um ZST Alloc . Isso parece muito ruim para mim, e esses tipos de aplicativos são o motivo de ter Alloc uma característica.

@cramertj

Poderíamos adicionar algo assim:

Também teremos aliases de características reais :) https://github.com/rust-lang/rust/issues/41517

@glaebhoerl Sim, mas a estabilização ainda parece um pouco distante, pois ainda não existe uma implementação. Se desabilitarmos impls manuais de Allocator acho que podemos mudar para aliases de características com compatibilidade reversa quando eles chegarem

@joshlf

Uma ampliação 2x (ou mais) no espaço necessário para armazenar um tipo tão comum e fundamental é preocupante para mim.

Eu imagino que todas as implementações hoje são apenas do tipo tamanho zero ou um ponteiro grande, certo? Não é a possível otimização que alguns tipos de tamanho de ponteiro possam ter tamanho zero? (ou algo assim?)


@cramertj

Poderíamos adicionar algo assim:

De fato! Então, pegamos uma característica de cada três . No passado, nunca tivemos uma grande experiência com essas características. Por exemplo, Box<Both> não lança para Box<OnlyOneTrait> . Tenho certeza de que poderíamos esperar que os recursos da linguagem suavizassem tudo isso, mas parece que ainda estão muito longe, na melhor das hipóteses.


@SimonSapin

O que ainda precisa ser resolvido para estabilizar a coisa toda?

Eu não sei. Eu queria começar com a menor coisa absoluta para que houvesse menos debate.

Eu imagino que todas as implementações hoje são apenas do tipo tamanho zero ou um ponteiro grande, certo? Não é a possível otimização que alguns tipos de tamanho de ponteiro possam ter tamanho zero? (ou algo assim?)

Sim, a ideia é que, dado um ponteiro para um objeto alocado de seu tipo de alocador, você pode descobrir de qual instância ele veio (por exemplo, usando metadados embutidos). Portanto, as únicas informações que você precisa para desalocar são informações de tipo, não informações de tempo de execução.

Para voltar ao alinhamento na desalocação, vejo duas maneiras de avançar:

  • Estabilize conforme proposto (com alinhamento em desalocar). Distribuir a propriedade da memória alocada manualmente seria impossível, a menos que Layout fosse incluído. Em particular, é impossível construir um Vec ou Box ou String ou outro std contêiner com um alinhamento mais rígido do que o necessário (por exemplo, porque você não não quer que o elemento em caixa estenda uma linha de cache), sem desconstruir e desalocar manualmente mais tarde (o que nem sempre é uma opção). Outro exemplo de algo que seria impossível é preencher um Vec usando operações simd e, em seguida, distribuí-lo.

  • Não exija alinhamento na desalocação e remova a otimização de HeapAlloc baseado em alloc_system . Sempre armazene o alinhamento. @alexcrichton , conforme você HeapAlloc estivesse arredondando os tamanhos de qualquer maneira.)

Em qualquer caso, é uma troca muito difícil de fazer; o impacto na memória e no desempenho dependerá muito do tipo de aplicativo, e para qual otimizar também é específico do aplicativo.

Acho que podemos realmente ser Just Fine (TM). Citando os Alloc docs :

Alguns dos métodos requerem que um layout se ajuste a um bloco de memória.
O que significa para um layout "caber" em um bloco de memória (ou
equivalentemente, para um bloco de memória "caber" em um layout) é que o
as duas seguintes condições devem ser válidas:

  1. O endereço inicial do bloco deve ser alinhado a layout.align() .

  2. O tamanho do bloco deve estar na faixa de [use_min, use_max] , onde:

    • use_min é self.usable_size(layout).0 , e

    • use_max é a capacidade que era (ou teria sido)
      retornado quando (se) o bloco foi alocado por meio de uma chamada para
      alloc_excess ou realloc_excess .

Observe que:

  • o tamanho do layout usado mais recentemente para alocar o bloco
    tem garantia de estar na faixa de [use_min, use_max] , e

  • um limite inferior em use_max pode ser aproximado com segurança por uma chamada para
    usable_size .

  • se um layout k cabe em um bloco de memória (denotado por ptr )
    atualmente alocado por meio de um alocador a , então é legal para
    use esse layout para desalocá-lo, ou seja, a.dealloc(ptr, k); .

Observe o último marcador. Se eu alocar com um layout com alinhamento a , então deveria ser legal para mim desalocar com alinhamento b < a porque um objeto que está alinhado com a também está alinhado com b e, portanto, um layout com alinhamento b se encaixa em um objeto alocado com um layout com alinhamento a (e com o mesmo tamanho).

O que isso significa é que você deve ser capaz de alocar com um alinhamento maior do que o alinhamento mínimo necessário para um tipo específico e então permitir que algum outro código seja desalocado com o alinhamento mínimo, e isso deve funcionar.

Não é a possível otimização que alguns tipos de tamanho de ponteiro possam ter tamanho zero? (ou algo assim?)

Recentemente, houve um RFC para isso e parece muito improvável que isso pudesse ser feito devido a questões de compatibilidade: https://github.com/rust-lang/rfcs/pull/2040

Por exemplo, Box<Both> não lança para Box<OnlyOneTrait> . Tenho certeza de que poderíamos esperar que os recursos da linguagem suavizassem tudo isso, mas parece que ainda estão muito longe, na melhor das hipóteses.

O upcasting de objeto de característica, por outro lado, parece incontroversamente desejável, e principalmente uma questão de esforço / largura de banda / força de vontade para implementá-lo. Houve um tópico recentemente: https://internals.rust-lang.org/t/trait-upcasting/5970

@ruuda Fui eu que escrevi aquela implementação alloc_system originalmente. alexcrichton meramente mudou durante os grandes refatores de alocador de <time period> .

A implementação atual requer que você desaloque com o mesmo alinhamento especificado com o qual você alocou um determinado bloco de memória. Independentemente do que a documentação possa reivindicar, esta é a realidade atual que todos devem respeitar até que alloc_system no Windows seja alterado.

As alocações no Windows sempre usam um múltiplo de MEMORY_ALLOCATION_ALIGNMENT (embora lembrem o tamanho que você atribuiu ao byte). MEMORY_ALLOCATION_ALIGNMENT é 8 em 32 bits e 16 em 64 bits. Para tipos superalinhados, porque o alinhamento é maior que MEMORY_ALLOCATION_ALIGNMENT , a sobrecarga causada por alloc_system é consistentemente a quantidade de alinhamento especificada, portanto, uma alocação alinhada de 64 bytes teria 64 bytes de sobrecarga.

Se decidíssemos estender esse truque superalinhado a todas as alocações (o que eliminaria a necessidade de desalocar com o mesmo alinhamento que você especificou ao alocar), então mais alocações teriam sobrecarga. Alocações cujos alinhamentos são idênticos a MEMORY_ALLOCATION_ALIGNMENT sofrerão uma sobrecarga constante de MEMORY_ALLOCATION_ALIGNMENT bytes. Alocações cujos alinhamentos são menores que MEMORY_ALLOCATION_ALIGNMENT sofrerão uma sobrecarga de MEMORY_ALLOCATION_ALIGNMENT bytes aproximadamente na metade do tempo. Se o tamanho da alocação arredondado para MEMORY_ALLOCATION_ALIGNMENT for maior ou igual ao tamanho da alocação mais o tamanho de um ponteiro, não há sobrecarga, caso contrário, há. Considerando que 99,99% das alocações não serão superalinhadas, você realmente deseja incorrer nesse tipo de sobrecarga em todas essas alocações?

@ruuda

Pessoalmente, sinto que a implementação de alloc_system hoje no Windows é um benefício maior do que ter a capacidade de abrir mão da propriedade de uma alocação para outro contêiner como Vec . AFAIK, embora não haja dados para medir o impacto de sempre preencher com o alinhamento e não exigir um alinhamento na desalocação.

@joshlf

Eu acho que o comentário está errado, como apontado alloc_system no Windows depende do mesmo alinhamento sendo passado para desalocação como foi passado para alocação.

Considerando que 99,99% das alocações não serão superalinhadas, você realmente deseja incorrer nesse tipo de sobrecarga em todas essas alocações?

Depende do aplicativo se a sobrecarga é significativa e se deve otimizar a memória ou o desempenho. Minha suspeita é que para a maioria dos aplicativos também está bom, mas uma pequena minoria se preocupa profundamente com a memória e eles realmente não podem pagar esses bytes extras. E outra pequena minoria precisa de controle sobre o alinhamento, e realmente precisa disso.

@alexcrichton

Acho que o comentário está errado, como apontado alloc_system no Windows depende do mesmo alinhamento sendo passado para desalocação como foi passado para alocação.

Isso não implica que alloc_system no Windows não implementa corretamente o traço Alloc (e, portanto, talvez devamos mudar os requisitos do traço Alloc )?


@ retep998

Se estou lendo seu comentário corretamente, essa sobrecarga de alinhamento não está presente para todas as alocações, independentemente de precisarmos ser capazes de desalocar com um alinhamento diferente? Ou seja, se eu alocar 64 bytes com alinhamento de 64 bytes e também desalocar com alinhamento de 64 bytes, a sobrecarga que você descreveu ainda estará presente. Portanto, não é um recurso de ser capaz de desalocar com alinhamentos diferentes, mas sim um recurso de solicitar alinhamentos maiores do que o normal.

@joshlf O overhead causado por alloc_system atualmente é devido à solicitação de alinhamentos maiores que o normal. Se o seu alinhamento for menor ou igual a MEMORY_ALLOCATION_ALIGNMENT , então não há sobrecarga causada por alloc_system .

No entanto, se mudássemos a implementação para permitir a desalocação com alinhamentos diferentes, a sobrecarga se aplicaria a quase todas as alocações, independentemente do alinhamento.

Ah, entendo; faz sentido.

Qual é o significado de implementar o Alloc para Heap e & Heap? Em que casos o usuário usaria um desses impls em vez do outro?

Esta é a primeira API de biblioteca padrão na qual *mut u8 significaria "ponteiro para qualquer coisa"? Existe String :: from_raw_parts, mas esse realmente significa um ponteiro para bytes. Não sou fã de *mut u8 significa "ponteiro para qualquer coisa" - até mesmo C se sai melhor. Quais são algumas outras opções? Talvez um ponteiro para o tipo opaco seja mais significativo.

@rfcbot concern * mut u8

@dtolnay Alloc for Heap é uma espécie de "padrão" e Alloc for &Heap é como Write for &T onde o traço requer &mut self mas a implementação não. Notavelmente, isso significa que tipos como Heap e System são thread-safe e não precisam ser sincronizados durante a alocação.

Mais importante, porém, o uso de #[global_allocator] requer que o estático ao qual está anexado, que tem o tipo T , tenha Alloc for &T . (também conhecido como todos os alocadores globais devem ser thread-safe)

Por *mut u8 , acho que *mut () pode ser interessante, mas pessoalmente não me sinto muito compelido a "entender direito" aqui per se.

A principal vantagem de *mut u8 é que é muito conveniente usar .offset com deslocamentos de bytes.

Por *mut u8 , acho que *mut () pode ser interessante, mas pessoalmente não me sinto muito compelido a "entender direito" aqui per se.

Se usarmos *mut u8 em uma interface estável, não estaremos nos travando? Em outras palavras, assim que estabilizarmos isso, não teremos a chance de "acertar" no futuro.

Além disso, *mut () parece um pouco perigoso para mim, caso façamos uma otimização como a RFC 2040 no futuro.

A principal vantagem de *mut u8 é que é muito conveniente usar .offset com offsets de bytes.

É verdade, mas você poderia facilmente fazer let ptr = (foo as *mut u8) e então seguir em frente. Isso não parece uma motivação suficiente para ficar com *mut u8 na API se houver alternativas atraentes (que, para ser justo, não tenho certeza se existem).

Além disso, * mut () parece um pouco perigoso para mim, caso façamos uma otimização como a RFC 2040 no futuro.

Essa otimização provavelmente nunca acontecerá - quebraria muitos códigos existentes. Mesmo se tivesse, seria aplicado a &() e &mut () , não a *mut () .

Se o RFC 1861 estivesse perto de ser implementado / estabilizado, sugiro usá-lo:

extern { pub type void; }

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut void, Error>;
    unsafe fn dealloc(&mut self, ptr: *mut void, layout: Layout);
    // ...
}

Provavelmente é muito longe, certo?

@joshlf Eu pensei ter visto um DynSized .

Isso funcionará para objetos do tipo struct hack? Digamos que eu tenha um Node<T> assim:

struct Node<T> {
   size: u32,
   data: T,
   // followed by `size` bytes
}

e um tipo de valor:

struct V {
  a: u32,
  b: bool,
}

Agora, quero alocar Node<V> com uma string de tamanho 7 em uma única alocação. Idealmente, eu quero fazer uma alocação de tamanho 16, alinhar 4 e encaixar tudo nele: 4 para u32 , 5 para V e 7 para os bytes da string. Isso funciona porque o último membro de V tem alinhamento 1 e os bytes da string também têm alinhamento 1.

Observe que isso não é permitido em C / C ++ se os tipos forem compostos como acima, pois a gravação no armazenamento compactado é um comportamento indefinido. Acho que é uma lacuna no padrão C / C ++ que infelizmente não pode ser corrigida. Posso explicar por que isso está quebrado, mas vamos nos concentrar em Rust. Isso pode funcionar? :-)

Com relação ao tamanho e alinhamento da estrutura Node<V> si, você está praticamente por conta do compilador Rust. É UB (comportamento indefinido) alocar com qualquer tamanho ou alinhamento menor do que o exigido pelo Rust, pois o Rust pode fazer otimizações com base na suposição de que qualquer objeto Node<V> - na pilha, no heap, atrás de uma referência, etc. - tem um tamanho e alinhamento correspondentes ao que é esperado em tempo de compilação.

Na prática, parece que a resposta é infelizmente não: executei este programa e descobri que, pelo menos no Rust Playground, Node<V> tem um tamanho de 12 e um alinhamento de 4, o que significa que quaisquer objetos após o Node<V> deve ser compensado em pelo menos 12 bytes. Parece que o deslocamento do campo data.b dentro de Node<V> é de 8 bytes, o que implica que os bytes 9-11 são preenchimento final. Infelizmente, embora esses bytes de preenchimento "não sejam usados" em algum sentido, o compilador ainda os trata como parte de Node<V> e se reserva o direito de fazer o que quiser com eles (o mais importante, incluindo escrever a eles quando você atribui Node<V> , o que implica que se você tentar guardar dados extras lá, eles podem ser sobrescritos).

(observe, aliás: você não pode tratar um tipo como compactado que o compilador Rust não pensa que está compactado. No entanto, você _pode_ dizer ao compilador Rust que algo está compactado, o que mudará o layout do tipo (removendo preenchimento), usando repr(packed) )

No entanto, com relação ao layout de um objeto após o outro sem que ambos sejam parte do mesmo tipo Rust, estou quase 100% certo de que isso é válido - afinal, é o que Vec faz. Você pode usar os métodos do tipo Layout para calcular dinamicamente quanto espaço é necessário para a alocação total:

let node_layout = Layout::new::<Node<V>>();
// NOTE: This is only valid if the node_layout.align() is at least as large as mem::align_of_val("a")!
// NOTE: I'm assuming that the alignment of all strings is the same (since str is unsized, you can't do mem::align_of::<str>())
let padding = node_layout.padding_needed_for(mem::align_of_val("a"));
let total_size = node_layout.size() + padding + 7;
let total_layout = Layout::from_size_align(total_size, node_layout.align()).unwrap();

Algo assim funcionaria?

#[repr(C)]
struct Node<T> {
   size: u32,
   data: T,
   bytes: [u8; 0],
}

… Então aloque com um tamanho maior e use slice::from_raw_parts_mut(node.bytes.as_mut_ptr(), size) ?

Obrigado @joshlf pela resposta detalhada! O TLDR para meu caso de uso é que posso obter um Node<V> de tamanho 16, mas somente se V for repr(packed) . Caso contrário, o melhor que posso fazer é o tamanho 19 (12 + 7).

@SimonSapin não tenho certeza; Eu vou tentar.

Ainda não entendi esse tópico, mas ainda estou decidido a não estabilizar nada. Ainda não fizemos progresso na implementação dos problemas difíceis:

  1. Coleções alocador-polimórficas

    • nem mesmo uma caixa não inchada!

  2. Coleções falíveis

Eu acho que o design dos traços fundamentais afetarão as soluções daqueles: Eu tive pouco tempo para Rust para os últimos meses, mas argumentaram este, às vezes. Eu duvido que terei tempo para apresentar meu caso aqui também, então eu só posso esperar que primeiro, pelo menos, escrevamos uma solução completa para todos eles: alguém me prove que estou errado que é impossível ser rigoroso (forçar o uso correto), flexível , e ergonômico com as características atuais. Ou simplesmente termine de marcar as caixas na parte superior.

Re: comentário de @ Ericson2314

Acho que uma questão relevante relacionada ao conflito entre essa perspectiva e o desejo de @alexcrichton de estabilizar algo é: quanto benefício obtemos ao estabilizar uma interface mínima? Em particular, muito poucos consumidores chamarão métodos Alloc diretamente (até mesmo a maioria das coleções provavelmente usará Box ou algum outro contêiner semelhante), então a verdadeira questão é: o que estabilizar compra para usuários que o farão não está chamando Alloc métodos diretamente? Honestamente, o único caso de uso sério em que posso pensar é que abre um caminho para coleções polimórficas de alocador (que provavelmente serão usadas por um conjunto muito mais amplo de usuários), mas parece que está bloqueado em # 27336, que está longe de ser sendo resolvido. Talvez haja outros grandes casos de uso que estou perdendo, mas com base nessa análise rápida, estou inclinado a me afastar da estabilização por ter apenas benefícios marginais ao custo de nos prender a um design que podemos mais tarde considerar subótimo .

@joshlf permite que as pessoas definam e usem seus próprios alocadores globais.

Hmmm bom ponto. seria possível estabilizar especificando o alocador global sem estabilizar Alloc ? Ou seja, o código que implementa Alloc teria que ser instável, mas provavelmente seria encapsulado em sua própria caixa, e o mecanismo para marcar esse alocador como o alocador global seria ele próprio estável. Ou estou entendendo mal como estável / instável e o compilador estável / compilador noturno interagem?

Ah @joshlf, lembre-se de que # 27336 é uma distração, conforme https://github.com/rust-lang/rust/issues/42774#issuecomment -317279035. Tenho certeza de que teremos outros problemas - problemas com as características que existem, e é por isso que quero trabalhar para começar a trabalhar nisso agora. É muito mais fácil discutir esses problemas, uma vez que eles cheguem para que todos vejam, do que debater os futuros preditos após o # 27336.

@joshlf Mas você não pode compilar a caixa que define o alocador global com um compilador estável.

@sfackler Ah sim, aí está aquele mal-entendido que eu temia: P

Acho o nome Excess(ptr, usize) um pouco confuso porque usize não é excess em tamanho da alocação solicitada (como no tamanho extra alocado), mas total tamanho da alocação.

IMO Total , Real , Usable , ou qualquer nome que transmita que o tamanho é o tamanho total ou o tamanho real da alocação é melhor do que "excesso", que eu acho enganoso. O mesmo se aplica aos métodos _excess .

Eu concordo com @gnzlbg acima, acho que uma tupla simples (ptr, usize) seria adequada.

Observe que Excess não deve ser estabilizado na primeira passagem, no entanto

Postado este tópico para discussão no reddit, que tem algumas pessoas preocupadas: https://www.reddit.com/r/rust/comments/78dabn/custom_allocators_are_on_the_verge_of_being/

Após uma discussão mais aprofundada com @ rust-lang / libs hoje, gostaria de fazer alguns ajustes na proposta de estabilização, que podem ser resumidos com:

  • Adicione alloc_zeroed ao conjunto de métodos estabilizados, caso contrário, tendo a mesma assinatura de alloc .
  • Altere *mut u8 para *mut void na API usando o suporte extern { type void; } , resolvendo a preocupação de @dtolnay e fornecendo um caminho a seguir para unificar c_void todo o ecossistema.
  • Altere o tipo de retorno de alloc para *mut void , removendo o Result e o Error

Talvez o mais contencioso seja o último ponto, então quero elaborá-lo também. Isso saiu da discussão com a equipe de libs hoje e girou especificamente em torno de como (a) a interface baseada em Result tem uma ABI menos eficiente do que uma de retorno de ponteiro e (b) quase nenhum alocador de "produção" hoje fornecer a capacidade de aprender qualquer coisa mais do que "isso apenas OOM". Para desempenho, podemos principalmente cobri-lo com inlining e tal, mas continua sendo que Error é uma carga útil extra difícil de remover nas camadas mais baixas.

O pensamento para retornar cargas úteis de erros é que os alocadores podem fornecer introspecção específica da implementação para aprender por que uma alocação falhou e, caso contrário, quase todos os consumidores precisam apenas saber se a alocação foi bem-sucedida ou falhou. Além disso, pretende-se que seja uma API de nível muito baixo que, na verdade, não é chamada com frequência (em vez disso, as APIs digitadas que encerram as coisas bem devem ser chamadas). Nesse sentido, não é fundamental termos a API mais utilizável e ergonômica para esse local, mas é mais importante habilitar casos de uso sem sacrificar o desempenho.

A principal vantagem de *mut u8 é que é muito conveniente usar .offset com deslocamentos de bytes.

Na reunião de libs, também sugerimos impl *mut void { fn offset } que não entra em conflito com o offset definido para T: Sized . Também pode ser byte_offset .

1 por usar *mut void e byte_offset . Haverá um problema com a estabilização do recurso de tipos externos ou podemos contornar esse problema porque apenas a definição é instável (e liballoc pode fazer coisas instáveis ​​internamente) e não o uso (por exemplo, let a: *mut void = ... isn é instável)?

Sim, não precisamos bloquear a estabilização de tipo externo. Mesmo se o suporte de tipo externo for excluído, o void que definimos para isso sempre pode ser o pior caso de tipo mágico.

Houve alguma discussão adicional na reunião de libs sobre se Alloc e Dealloc deveriam ser traços separados ou não?

Não tocamos nisso especificamente, mas geralmente achamos que não deveríamos nos desviar da técnica anterior, a menos que tenhamos um motivo particularmente convincente para fazê-lo. Em particular, o conceito de alocador do C ++ não tem uma divisão semelhante.

Não tenho certeza de que seja uma comparação adequada neste caso. Em C ++, tudo é explicitamente liberado, então não há equivalente a Box que precise armazenar uma cópia de (ou uma referência a) seu próprio alocador. Isso é o que causa a explosão de grande porte para nós.

std::unique_ptr é genérico em um "Deleter" .

@joshlf unique_ptr é o equivalente a Box , vector é o equivalente a Vec , unordered_map é o equivalente a HashMap , etc.

@cramertj Ah, interessante, eu só estava olhando os tipos de coleções. Parece que isso pode ser algo a fazer então. Sempre podemos adicioná-lo mais tarde por meio de implantes de cobertor, mas provavelmente seria mais limpo para evitar isso.

A abordagem de implante de cobertor pode ser mais limpa, na verdade:

pub trait Dealloc {
    fn dealloc(&self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

Uma característica a menos com que se preocupar na maioria dos casos de uso.

  • Altere o tipo de retorno de alocação para * mut void, removendo o Resultado e o Erro

Talvez o mais contencioso seja o último ponto, então quero elaborá-lo também. Isso saiu da discussão com a equipe de libs hoje e girou especificamente em torno de como (a) a interface baseada em resultados tem uma ABI menos eficiente do que uma de retorno de ponteiro e (b) quase nenhum alocador de "produção" hoje fornece a capacidade de aprender nada mais do que "isso é apenas OOM". Para desempenho, podemos principalmente cobri-lo com inlining e tal, mas continua sendo que o Error é uma carga extra difícil de remover nas camadas mais baixas.

Estou preocupado com o fato de que isso tornaria muito fácil usar o ponteiro retornado sem verificar se há nulo. Parece que a sobrecarga também poderia ser removida sem adicionar esse risco, retornando Result<NonZeroPtr<void>, AllocErr> e tornando AllocErr tamanho zero?

( NonZeroPtr é uma fusão de ptr::Shared e ptr::Unique conforme proposto em https://github.com/rust-lang/rust/issues/27730#issuecomment-316236397.)

@SimonSapin algo como Result<NonZeroPtr<void>, AllocErr> requer três tipos de estabilização, todos eles novos e alguns dos quais historicamente definham um pouco para a estabilização. Algo como void nem sequer é necessário e é bom ter (na minha opinião).

Concordo que é "fácil de usar sem verificar se há nulo", mas esta é, novamente, uma API de nível muito baixo que não se destina a ser muito usada, então não acho que devemos otimizar para ergonomia do chamador.

As pessoas também podem construir abstrações de nível superior, como alloc_one sobre o nível inferior alloc que podem ter tipos de retorno mais complexos, como Result<NonZeroPtr<void>, AllocErr> .

Eu concordo que AllocErr não seria útil na prática, mas que tal apenas Option<NonZeroPtr<void>> ? APIs que são impossíveis de usar indevidamente acidentalmente, sem sobrecarga, é uma das coisas que diferenciam o Rust do C, e retornar aos ponteiros nulos no estilo C parece um retrocesso para mim. Dizer que é “uma API de nível muito baixo que não se destina a ser muito usada” é como dizer que não devemos nos preocupar com a segurança da memória em arquiteturas de microcontroladores incomuns porque elas são de nível muito baixo e não são muito usadas.

Cada interação com o alocador envolve código inseguro, independentemente do tipo de retorno desta função. APIs de alocação de baixo nível podem ser mal utilizadas se o tipo de retorno for Option<NonZeroPtr<void>> ou *mut void .

Alloc::alloc em particular é a API de baixo nível e não se destina a ser muito usada. Métodos como Alloc::alloc_one<T> ou Alloc::alloc_array<T> são as alternativas que seriam mais usadas e teriam um tipo de retorno "mais agradável".

A stateful AllocError não vale a pena, mas um tipo de tamanho zero que implementos de erro e tem um Display de allocation failure é bom ter. Se formos pela rota NonZeroPtr<void> , considero Result<NonZeroPtr<void>, AllocError> preferível a Option<NonZeroPtr<void>> .

Por que a pressa para estabilizar :( !! Result<NonZeroPtr<void>, AllocErr> é indiscutivelmente mais agradável para os clientes usarem. Dizer que esta é uma "API de nível muito baixo" que não precisa ser boa é apenas deprimente não ambicioso. Código em todos os níveis deve ser o mais seguro e sustentável possível; código obscuro que não é constantemente editado (e, portanto, visualizado nas memórias de curto prazo das pessoas) ainda mais!

Além disso, se quisermos ter coleções polimórficas de alocação escritas pelo usuário, o que certamente espero, essa é uma quantidade aberta de código bastante complexo usando alocadores diretamente.

Re-desalocação, operacionalmente, quase certamente queremos referenciar / clonar o aloator apenas uma vez por coleção baseada em árvore. Isso significa passar o alocador para cada caixa de alocador personalizada que está sendo destruída. Mas, é um problema aberto sobre a melhor forma de fazer isso no Rust sem tipos lineares. Ao contrário do meu comentário anterior, eu estaria bem com algum código inseguro em implementações de coleções para isso, porque o caso de uso ideal altera a implementação de Box , não a implementação de características de alocador e desalocador de divisão. Ou seja, podemos fazer um progresso estabilizável sem bloquear a linearidade.

@sfackler Acho que precisamos de alguns tipos associados conectando o desalocador ao alocador; pode não ser possível adaptá-los.

@ Ericson2314 Há uma "corrida" para estabilizar porque as pessoas querem usar alocadores para coisas reais no mundo real. Este não é um projeto de ciências.

Para que esse tipo associado seria usado?

O pessoal @sfackler ainda pode fixar um / e o tipo de pessoa que se preocupa com esse tipo de recurso avançado deve se sentir confortável fazendo isso. [Se o problema for ferrugem instável versus ferrugem instável, esse é um problema diferente que precisa de uma correção de política.] Assar APIs ruins, ao contrário, nos prejudica para sempre , a menos que queiramos dividir o ecossistema com um novo 2.0 std.

Os tipos associados relacionariam o desalocador ao alocador. Cada um precisa saber sobre o outro para que isso funcione. [Ainda há o problema de usar o (des) alocador errado do tipo certo, mas aceito que ninguém propôs remotamente uma solução para isso.]

Se as pessoas podem apenas fixar em uma noite, por que temos compilações estáveis? O conjunto de pessoas que estão interagindo diretamente com as APIs do alocador é muito menor do que as pessoas que querem tirar vantagem dessas APIs, por exemplo, substituindo o alocador global.

Você pode escrever algum código que mostre por que um desalocador precisa saber o tipo de seu alocador associado? Por que a API de alocador do C ++ não precisa de um mapeamento semelhante?

Se as pessoas podem apenas fixar em uma noite, por que temos compilações estáveis?

Para indicar a estabilidade da linguagem. O código que você escreve contra essa versão das coisas nunca será quebrado. Em um compilador mais recente. Você marca uma noite quando precisa de algo tão ruim que não vale a pena esperar pela iteração final do recurso considerado de qualidade digna dessa garantia.

O conjunto de pessoas que estão interagindo diretamente com as APIs do alocador é muito menor do que as pessoas que querem tirar vantagem dessas APIs, por exemplo, substituindo o alocador global.

Aha! Isso seria para tirar o jemalloc da árvore, etc? Ninguém propôs estabilizar os terríveis hacks que permitem escolher o alocador global, apenas a estática do heap em si? Ou eu li a proposta errada?

Os terríveis hacks que permitem a escolha do alocador global devem ser estabilizados, o que é metade do que nos permite tirar o jemalloc da árvore. Essa questão é a outra metade.

#[global_allocator] estabilização de atributo: https://github.com/rust-lang/rust/issues/27389#issuecomment -336955367

caramba

@ Ericson2314 Qual você acha que seria uma maneira não horrível de selecionar o alocador global?

(Respondido em https://github.com/rust-lang/rust/issues/27389#issuecomment-342285805)

A proposta foi alterada para usar * mut void.

@rfcbot resolvido * mut u8

@rfcbot revisado

Depois de alguma discussão no IRC, estou aprovando isso com o entendimento de que _não_ pretendemos estabilizar um Box genérico em Alloc , mas em vez disso em alguma característica Dealloc com um cobertor implante apropriado, como sugerido por aqui . Por favor, deixe-me saber se eu entendi mal a intenção.

@cramertj Só para esclarecer, é possível adicionar aquele cobertor impl depois do fato e não quebrar a Alloc definição que estamos estabilizando aqui?

Como especificaremos Dealloc para um determinado Alloc ? Eu imagino algo assim?

pub unsafe trait Alloc {
    type Dealloc: Dealloc = Self;
    ...
}

Acho que isso nos coloca em um território espinhoso WRT https://github.com/rust-lang/rust/issues/29661.

Sim, não acho que haja uma maneira de ter a adição de Dealloc compatível com versões anteriores com as definições existentes de Alloc (que não têm esse tipo associado) sem ter um padrão.

Se você quiser obter automaticamente o desalocador correspondente a um alocador, precisará de mais do que apenas um tipo associado, mas de uma função para produzir um valor de desalocador.

Mas, isso pode ser tratado no futuro com essas coisas sendo anexadas a um subtítulo separado de Alloc eu acho.

@sfackler não tenho certeza se entendi. Você pode escrever a assinatura de Box::new sob o seu design?

Isso é ignorar a sintaxe de posicionamento e tudo mais, mas uma maneira de fazer isso seria

pub struct Box<T, D>(NonZeroPtr<T>, D);

impl<T, D> Box<T, D>
where
    D: Dealloc
{
    fn new<A>(alloc: A, value: T) -> Box<T, D>
    where
        A: Alloc<Dealloc = D>
    {
        let ptr = alloc.alloc_one().unwrap_or_else(|_| alloc.oom());
        ptr::write(&value, ptr);
        let deallocator = alloc.deallocator();
        Box(ptr, deallocator)
    }
}

Notavelmente, precisamos realmente ser capazes de produzir uma instância do desalocador, não apenas saber seu tipo. Você também pode parametrizar Box sobre Alloc e armazenar A::Dealloc , o que pode ajudar na inferência de tipo. Podemos fazer isso funcionar após essa estabilização movendo Dealloc e deallocator para uma característica separada:

pub trait SplitAlloc: Alloc {
    type Dealloc;

    fn deallocator(&self) -> Self::Dealloc;
}

Mas como seria o impl de Drop ?

impl<T, D> Drop for Box<T, D>
where
    D: Dealloc
{
    fn drop(&mut self) {
        unsafe {
            ptr::drop_in_place(self.0);
            self.1.dealloc_one(self.0);
        }
    }
}

Mas supondo que estabilizemos Alloc primeiro, nem todos os Alloc s implementarão Dealloc , certo? E eu pensei que a especialização em impl ainda estava longe? Em outras palavras, em teoria, você gostaria de fazer algo como o seguinte, mas acho que ainda não funciona?

impl<T, D> Drop for Box<T, D> where D: Dealloc { ... }
impl<T, A> Drop for Box<T, A> where A: Alloc { ... }

Se qualquer coisa, teríamos um

default impl<T> SplitAlloc for T
where
    T: Alloc { ... }

Mas não acho que isso seja realmente necessário. Os casos de uso de alocadores personalizados e alocadores globais são distintos o suficiente para que eu não suponha que haja uma tonelada de sobreposição entre eles.

Suponho que isso poderia funcionar. Parece muito mais limpo para mim, no entanto, ter apenas Dealloc cara para que possamos ter uma interface mais simples. Imagino que poderíamos ter uma interface bastante simples e incontroversa que não exigiria nenhuma mudança no código existente que já implementa Alloc :

unsafe trait Dealloc {
    fn dealloc(&mut self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

unsafe trait Alloc {
    type Dealloc: Dealloc = &mut Self;
    fn deallocator(&mut self) -> Self::Dealloc { self }
    ...
}

Achei que os padrões de tipo associados eram problemáticos?

Um Dealloc que é uma referência mutável para o alocador não parece tão útil - você só pode alocar uma coisa de cada vez, certo?

Achei que os padrões de tipo associados eram problemáticos?

Oh, eu acho que os padrões de tipo associado estão distantes o suficiente para que não possamos confiar neles.

Ainda assim, poderíamos ter o mais simples:

unsafe trait Dealloc {
    fn dealloc(&mut self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

unsafe trait Alloc {
    type Dealloc: Dealloc;
    fn deallocator(&mut self) -> Self::Dealloc;
    ...
}

e apenas requer que o implementador escreva um pouco de boilerplate.

Um Dealloc que é uma referência mutável ao alocador não parece tão útil - você só pode alocar uma coisa de cada vez, certo?

Sim, bom ponto. Provavelmente um ponto discutível, dado seu outro comentário.

deallocator levar self , &self ou &mut self ?

Provavelmente &mut self para ser consistente com os outros métodos.

Existem alocadores que prefeririam assumir self por valor para que não precisassem, por exemplo, clonar o estado?

O problema em tomar self por valor é que isso impede a obtenção de Dealloc e então continuar a alocar.

Estou pensando em um alocador "onehot" hipotético, embora não saiba o quanto ele é real.

Esse alocador pode existir, mas tomar self por valor exigiria que _todos_ os alocadores funcionassem dessa maneira e impediria qualquer alocador que permitisse a alocação após deallocator ter sido chamado.

Eu ainda gostaria de ver parte disso implementado e usado em coleções antes de pensarmos em estabilizá-lo.

Você acha que https://github.com/rust-lang/rust/issues/27336 ou os pontos discutidos em https://github.com/rust-lang/rust/issues/32838#issuecomment -339066870 nos permitirão avançar nas coleções?

Estou preocupado com o impacto da abordagem do alias de tipo na legibilidade da documentação. Uma maneira (muito detalhada) de permitir o progresso seria agrupar os tipos:

pub struct Vec<T>(alloc::Vec<T, Heap>);

impl<T> Vec<T> {
    // forwarding impls for everything
}

Eu sei que é uma dor, mas parece que as mudanças que estamos discutindo aqui são grandes o suficiente para que, se decidirmos avançar com os traços de alocação / desalocação dividida, deveríamos experimentá-los primeiro em std e depois em novo FCP.

Qual é o cronograma de espera para que essas coisas sejam implementadas?

O método grow_in_place não retorna nenhum tipo de excesso de capacidade. Atualmente, chama usable_size com um layout, estende a alocação para _pelo menos_ se ajustar a esse layout, mas se a alocação for estendida além desse layout, os usuários não terão como saber.

Estou tendo dificuldade em entender a vantagem dos métodos alloc e realloc sobre alloc_excess e realloc_excess .

Um alocador precisa encontrar um bloco de memória adequado para realizar uma alocação: isso requer o conhecimento do tamanho do bloco de memória. Se o alocador retorna um ponteiro ou a tupla "ponteiro e tamanho do bloco de memória" não faz nenhuma diferença mensurável de desempenho.

Portanto, alloc e realloc apenas aumentam a superfície da API e parecem encorajar a escrita de código com menos desempenho. Por que os temos na API? Qual é a vantagem deles?


EDITAR: ou em outras palavras: todas as funções de alocação potencial na API devem retornar Excess , o que basicamente remove a necessidade de todos os métodos _excess .

O excesso só é interessante para casos de uso que envolvem arrays que podem crescer. Não é útil ou relevante para Box ou BTreeMap , por exemplo. Pode haver algum custo em calcular qual é o excesso, e certamente há uma API mais complexa, então não me parece que um código que não se preocupa com o excesso de capacidade deva ser forçado a pagar por isso.

Pode haver algum custo em calcular qual é o excesso

Você pode dar um exemplo? Eu não sei, e não posso imaginar, um alocador que é capaz de alocar memória, mas que não sabe quanta memória está realmente alocando (que é o que Excess é: quantidade real de memória alocada; devemos renomeá-lo).

O único Alloc ator comumente usado onde isso pode ser um pouco controverso é POSIX malloc , que embora sempre calcule Excess internamente, não o expõe como parte de seu C API. No entanto, retornar o tamanho solicitado como Excess está ok, portátil, simples, não incorre em nenhum custo e é o que todo mundo usando POSIX malloc já está assumindo de qualquer maneira.

jemalloc e basicamente qualquer outro Alloc ator por aí fornecem APIs que retornam Excess sem incorrer em quaisquer custos, portanto, para esses alocadores, retornar Excess é zero custo também.

Pode haver algum custo em calcular qual é o excesso, e certamente há uma API mais complexa, então não me parece que um código que não se preocupa com o excesso de capacidade deva ser forçado a pagar por isso.

No momento, todo mundo já está pagando o preço do atributo alocador, tendo duas APIs para alocar memória. E embora se possa construir uma API Excess -less em cima de uma Excess -full one`, o oposto não é verdadeiro. Então eu me pergunto por que não é feito assim:

  • Alloc métodos de trait sempre retornam Excess
  • adicione um traço ExcessLessAlloc que simplesmente remove os métodos Excess de Alloc para todos os usuários que 1) se importam o suficiente para usar Alloc mas 2) não preocupa-se com a quantidade real de memória atualmente sendo alocada (parece um nicho para mim, mas ainda acho que é bom ter essa API)
  • se um dia alguém descobrir uma maneira de implementar Alloc ators com atores para métodos Excess -less, podemos sempre fornecer uma implementação personalizada de ExcessLessAlloc para isso.

FWIW Acabei de parar neste tópico de novo porque não consigo implementar o que quero além de Alloc . Eu mencionei que falta grow_in_place_excess antes, mas fiquei preso novamente porque também falta alloc_zeroed_excess (e quem sabe o que mais).

Eu ficaria mais confortável se a estabilização aqui se concentrasse em estabilizar uma API Excess -full primeiro. Mesmo que seu API não seja o mais ergonômico para todos os usos, tal API permitiria pelo menos todos os usos, o que é uma condição necessária para mostrar que o design não é defeituoso.

Você pode dar um exemplo? Eu não sei, e não posso imaginar, um alocador que é capaz de alocar memória, mas que não sabe quanta memória está realmente alocando (que é o que Excess é: quantidade real de memória alocada; devemos renomeá-lo).

A maioria dos alocadores hoje usa classes de tamanho, onde cada classe de tamanho aloca apenas objetos de um determinado tamanho fixo, e as solicitações de alocação que não se ajustam a uma classe de tamanho particular são arredondadas para a classe de menor tamanho que cabem dentro. Neste esquema, é comum fazer coisas como ter um array de objetos de classe de tamanho e então fazer classes[size / SIZE_QUANTUM].alloc() . Nesse mundo, descobrir qual classe de tamanho é usada requer instruções extras: por exemplo, let excess = classes[size / SIZE_QUANTUM].size . Pode não ser muito, mas o desempenho de alocadores de alto desempenho (como jemalloc) é medido em ciclos de clock únicos, portanto, pode representar uma sobrecarga significativa, especialmente se esse tamanho acabar sendo passado por uma cadeia de retornos de função.

Você pode dar um exemplo?

Pelo menos saindo de seu PR para alloc_jemalloc, alloc_excess está muito claramente executando mais código do que alloc : https://github.com/rust-lang/rust/pull/45514/files.

Neste esquema, é comum fazer coisas como ter um array de objetos de classe de tamanho e então fazer classes [size / SIZE_QUANTUM] .alloc (). Nesse mundo, descobrir qual classe de tamanho é usada requer instruções extras: por exemplo, let excess = classes [size / SIZE_QUANTUM] .size

Então deixe-me ver se estou seguindo corretamente:

// This happens in both cases:
let size_class = classes[size / SIZE_QUANTUM];
let ptr = size_class.alloc(); 
// This would happen only if you need to return the size:
let size = size_class.size;
return (ptr, size);

É isso?


Pelo menos saindo do seu PR para o alloc_jemalloc, o alloc_excess está claramente executando mais código do que o alloc

Esse PR foi uma correção de bug (não uma correção de desempenho), há muitas coisas erradas com o estado atual de nossa camada de jemalloc em termos de desempenho, mas como esse PR, pelo menos retorna o que deveria:

  • nallocx é uma função const no sentido GCC, ou seja, uma função pura verdadeira. Isso significa que não tem efeitos colaterais, seus resultados dependem apenas de seus argumentos, não acessa nenhum estado global, seus argumentos não são ponteiros (então a função não pode acessar o estado global através deles), e para programas C / C ++ LLVM pode usar isto informações para eliminar a chamada se o resultado não for usado. Atualmente, o AFAIK Rust não pode marcar as funções FFI C como const fn ou semelhantes. Portanto, esta é a primeira coisa que pode ser corrigida e que tornaria realloc_excess custo zero para aqueles que não usam o excesso, desde que o inlining e as otimizações funcionem corretamente.
  • nallocx é sempre calculado para alocações alinhadas dentro de mallocx , ou seja, todo o código já o está compilando, mas mallocx joga seu resultado fora, então aqui estamos realmente o computando duas vezes , e em alguns casos nallocx é quase tão caro quanto mallocx ... Eu tenho um fork do jemallocator que tem alguns benchmarks para coisas como esta em seus branches, mas isso deve ser corrigido antes de jemalloc, fornecendo uma API que não joga isso fora. Esta correção, no entanto, afeta apenas aqueles que estão usando atualmente Excess .
  • e então é o problema de que estamos calculando os sinalizadores de alinhamento duas vezes, mas isso é algo que o LLVM pode otimizar do nosso lado (e fácil de corrigir).

Então, sim, parece mais código, mas esse código extra é um código que estamos chamando duas vezes, porque na primeira vez que o chamamos, jogamos os resultados fora. Não é impossível consertar, mas ainda não achei tempo para fazer isso.


EDITAR: @sfackler consegui liberar algum tempo para isso hoje e consegui fazer alloc_excess "grátis" em relação a alloc no caminho lento de jemallocs e tenho apenas uma sobrecarga de ~ 1ns em caminho rápido de jemallocs. Eu realmente não analisei o caminho rápido em muitos detalhes, mas pode ser possível melhorar ainda mais. Os detalhes estão aqui: https://github.com/jemalloc/jemalloc/issues/1074#issuecomment -345040339

É isso?

Sim.

Portanto, esta é a primeira coisa que pode ser corrigida e que tornaria realloc_excess custo zero para aqueles que não usam o excedente, desde que o inlining e as otimizações funcionem corretamente.

Quando usado como o alocador global, nada disso pode ser embutido.

Mesmo que seu API não seja o mais ergonômico para todos os usos, tal API permitiria pelo menos todos os usos, o que é uma condição necessária para mostrar que o design não é defeituoso.

Há literalmente código zero no Github que chama alloc_excess . Se esse é um recurso tão importante, por que ninguém nunca o usou? As APIs de alocação do C ++ não fornecem acesso à capacidade excedente. Parece incrivelmente simples adicionar / estabilizar esses recursos no futuro de uma forma compatível com versões anteriores se houver evidências reais de que eles melhoram o desempenho e se alguém realmente se importa o suficiente para usá-los.

Quando usado como o alocador global, nada disso pode ser embutido.

Então esse é um problema que devemos tentar resolver, pelo menos para compilações LTO, porque alocadores globais como jemalloc dependem disso: nallocx é a forma como é _por design_, e a primeira recomendação os desenvolvedores de jemalloc nos fizeram em relação ao desempenho de alloc_excess é que devemos ter essas chamadas alinhadas e devemos propagar os atributos C adequadamente, de modo que o compilador remova as chamadas nallocx dos sites de chamadas que não use Excess , como os compiladores C e C ++ fazem.

Mesmo se não pudermos fazer isso, a API Excess ainda pode ter custo zero corrigindo a API jemalloc (eu tenho uma implementação inicial de tal patch em meu rust-lang / garfo jemalloc). Poderíamos manter essa API nós mesmos ou tentar colocá-la no upstream, mas para que isso aconteça no upstream, devemos fazer um bom caso sobre por que essas outras linguagens podem realizar essas otimizações e o Rust não. Ou devemos ter outro argumento, como esta nova API é significativamente mais rápida do que mallocx + nallocx para aqueles usuários que precisam de Excess .

Se esse é um recurso tão importante, por que ninguém nunca o usou?

Esta é uma boa pergunta. std::Vec é a criança-pôster para usar a API Excess , mas atualmente não a usa, e todos os meus comentários anteriores afirmando "isso e aquilo estão faltando em Excess API "estava tentando fazer Vec usá-lo. A API Excess :

Não consigo saber porque ninguém está usando essa API. Mas dado que nem mesmo a biblioteca std pode usá-lo para a estrutura de dados para a qual é mais adequada ( Vec ), se eu tivesse que adivinhar, diria que o principal motivo é que esta API está quebrada no momento.

Se eu tivesse que adivinhar ainda mais, eu diria que nem mesmo aqueles que projetaram esta API a usaram, principalmente porque nenhuma coleção std usa (que é onde eu espero que esta API seja testada inicialmente) , e também porque usar _excess e Excess todos os lugares para significar usable_size / allocation_size é extremamente confuso / irritante para programar.

Isso provavelmente ocorre porque mais trabalho foi colocado nas APIs Excess -less e, quando você tem duas APIs, é difícil mantê-las sincronizadas, é difícil para os usuários descobrirem ambas e saberem quais usar, e, finalmente, é difícil para os usuários preferir a conveniência a fazer a coisa certa.

Ou, em outras palavras, se eu tiver duas APIs concorrentes e dedicar 100% do trabalho para melhorar uma e 0% do trabalho para melhorar a outra, não é surpreendente chegar à conclusão de que uma delas está significativamente na prática melhor que o outro.

Pelo que eu posso dizer, essas são as duas únicas chamadas para nallocx fora dos testes de jemalloc no Github:

https://github.com/facebook/folly/blob/f2925b23df8d85ebca72d62a69f1282528c086de/folly/detail/ThreadLocalDetail.cpp#L182
https://github.com/louishust/mysql5.6.14_tokudb/blob/4897660dee3e8e340a1e6c8c597f3b2b7420654a/storage/tokudb/ft-index/ftcxx/malloc_utils.hpp#L91

Nenhum deles se parece com a API alloc_excess atual, mas são usados ​​de forma autônoma para calcular um tamanho de alocação antes de ser feito.

Apache Arrow analisou o uso de nallocx em sua implementação, mas descobriu que as coisas não funcionaram bem:

https://issues.apache.org/jira/browse/ARROW-464

Estas são basicamente as únicas referências a nallocx que posso encontrar. Por que é importante que a implementação inicial de APIs de alocadores suporte um recurso tão obscuro?

Pelo que eu posso dizer, essas são as únicas duas chamadas para nallocx fora dos testes de jemalloc no Github:

Do topo da minha cabeça, eu sei que pelo menos o tipo de vetor do Facebook está usando-o através da implementação de malloc do Facebook ( política de crescimento de malloc e fbvector ; isso é uma grande parte dos vetores de C ++ no Facebook usam isso) e também que Chapel o usou para melhorar o desempenho de seu tipo String ( aqui e o problema de rastreamento ). Então, talvez hoje não tenha sido o melhor dia do Github?

Por que é importante que a implementação inicial de APIs de alocadores suporte um recurso tão obscuro?

A implementação inicial de uma API de alocador não precisa oferecer suporte a esse recurso.

Mas um bom suporte para esse recurso deve bloquear a estabilização de tal API.

Por que deveria bloquear a estabilização se ele pode ser adicionado com compatibilidade reversa posteriormente?

Por que deveria bloquear a estabilização se ele pode ser adicionado com compatibilidade reversa posteriormente?

Porque para mim, pelo menos, significa que apenas metade do espaço de design foi suficientemente explorado.

Você espera que as partes relacionadas ao excesso da API sejam afetadas pelo design da funcionalidade relacionada ao excesso? Admito que só acompanhei essa discussão com indiferença, mas me parece improvável.

Se não podemos fazer esta API:

fn alloc(...) -> (*mut u8, usize) { 
   // worst case system API:
   let ptr = malloc(...);
   let excess = malloc_excess(...);
   (ptr, excess)
}
let (ptr, _) = alloc(...); // drop the excess

tão eficiente quanto este:

fn alloc(...) -> *mut u8 { 
   // worst case system API:
   malloc(...)
}
let ptr = alloc(...);

então temos problemas maiores.

Você espera que as partes relacionadas ao excesso da API sejam afetadas pelo design da funcionalidade relacionada ao excesso?

Então, sim, espero que uma boa API de excesso tenha um efeito enorme no design da funcionalidade relacionada a não excesso: ela a removeria completamente.

Isso evitaria a situação atual de ter duas APIs fora de sincronia e nas quais a excess-api tem menos funcionalidade do que a excess-less. Embora seja possível construir uma API excess-less sobre uma excess-full, o oposto não é verdadeiro.

Aqueles que querem abandonar Excess devem simplesmente abandoná-lo.

Para esclarecer, se houvesse alguma maneira de adicionar um método alloc_excess após o fato de uma forma compatível com versões anteriores, então você estaria bem com isso? (mas é claro, estabilizar sem alloc_excess significa que adicioná-lo mais tarde seria uma alteração significativa; estou apenas perguntando, então entendo seu raciocínio)

@joshlf É muito simples fazer isso.

: bell: Agora está entrando em seu período de comentário final , conforme a revisão acima . :Sino:

Aqueles que querem abandonar o Excesso devem simplesmente abandoná-lo.

Alternativamente, o 0,01% de pessoas que se preocupam com o excesso de capacidade podem usar outro método.

@sfackler Isto é o que ganho por tirar uma folga de duas semanas da ferrugem - esqueci o método impls padrão :)

Alternativamente, o 0,01% de pessoas que se preocupam com o excesso de capacidade podem usar outro método.

Onde você está conseguindo esse número?

Todas as minhas estruturas de dados Rust são planas na memória. A capacidade de fazer isso é a única razão pela qual uso o Rust; se eu pudesse encaixotar tudo, estaria usando um idioma diferente. Então eu não me importo com Excess a 0.01% do tempo, eu me importo com isso o tempo todo.

Eu entendo que este é um domínio específico e que em outros domínios as pessoas nunca se importariam com Excess , mas duvido que apenas 0,01% dos usuários do Rust se importem com isso (quero dizer, muitas pessoas usam Vec e String , que são as estruturas de dados posteriores de Excess ).

Estou obtendo esse número pelo fato de que há aproximadamente 4 coisas no total que usam nallocx, em comparação com o conjunto de coisas que usam malloc.

@gnzlbg

Você está sugerindo que, se fizéssemos "certo" desde o início, teríamos apenas fn alloc(layout) -> (ptr, excess) e nenhum fn alloc(layout) -> ptr ? Isso parece longe de ser óbvio para mim. Mesmo se o excesso estiver disponível, parece natural ter a última API para os casos de uso em que o excesso não importa (por exemplo, a maioria das estruturas de árvore), mesmo se for implementado como alloc_excess(layout).0 .

@rkruppe

Isso parece longe de ser óbvio para mim. Mesmo se o excesso estiver disponível, parece natural ter a última API para os casos de uso onde o excesso não importa (por exemplo, a maioria das estruturas de árvore), mesmo se for implementado como alloc_excess (layout) .0.

Atualmente, a API excess-full é implementada em cima da API excess-less. A implementação de Alloc para um alocador sem excesso requer que o usuário forneça os métodos alloc e dealloc .

No entanto, se eu quiser implementar Alloc para um alocador cheio em excesso, preciso fornecer mais métodos (pelo menos alloc_excess , mas isso aumenta se entrarmos em realloc_excess , alloc_zeroed_excess , grow_in_place_excess , ...).

Se fôssemos fazer o contrário, isto é, implementar a API excess-less como uma sutileza em cima da excess-full, então implementar alloc_excess e dealloc é suficiente para dar suporte ambos os tipos de alocadores.

Os usuários que não se importam ou não podem retornar ou consultar o excesso podem apenas retornar o tamanho de entrada ou layout (o que é um pequeno inconveniente), mas os usuários que podem lidar e querem lidar com o excesso não precisam implementar quaisquer outros métodos.


@sfackler

Estou obtendo esse número pelo fato de que há aproximadamente 4 coisas no total que usam nallocx, em comparação com o conjunto de coisas que usam malloc.

Dados estes fatos sobre o uso de _excess no ecossistema Rust:

  • 0 coisas no total usam _excess no ecossistema da ferrugem
  • 0 coisas em uso total _excess na biblioteca rust std
  • nem mesmo Vec e String podem usar a API _excess adequadamente na biblioteca enferrujada std
  • a API _excess é instável, fora de sincronia com a API excess-less, cheia de erros até muito recentemente (nem mesmo retornou excess ), ...

    e dados estes fatos sobre o uso de _excess em outros idiomas:

  • A API do jemalloc não é nativamente suportada por programas C ou C ++ devido à compatibilidade com versões anteriores

  • Os programas C e C ++ que desejam usar o excesso de API do jemalloc precisam sair de seu caminho para usá-lo:

    • optando por sair do alocador do sistema e entrar em jemalloc (ou tcmalloc)

    • reimplemente a biblioteca std de sua linguagem (no caso de C ++, implemente uma biblioteca std incompatível)

    • escrever toda a pilha sobre esta biblioteca std incompatível

  • algumas comunidades (o firefox usa, o Facebook reimplementa as coleções na biblioteca padrão C ++ para poder usá-lo, ...) ainda se esforçam para usá-lo.

Esses dois argumentos parecem plausíveis para mim:

  • A API excess em std não é utilizável, portanto, a biblioteca std não pode usá-la, portanto ninguém pode, e é por isso que ela não é usada nenhuma vez no ecossistema Rust .
  • Mesmo que C e C ++ tornem quase impossível o uso desta API, grandes projetos com mão de obra fazem um grande esforço para usá-la, portanto, pelo menos uma comunidade potencialmente pequena de pessoas se preocupa _muito_ com ela.

Seu argumento:

  • Ninguém usa a API _excess , portanto, apenas 0,01% das pessoas se importam com ela.

não.

@alexcrichton A decisão de mudar de -> Result<*mut u8, AllocErr> para -> *mut void pode vir como uma surpresa significativa para as pessoas que seguiram o desenvolvimento original das RFCs do alocador.

Não discordo dos seus pontos de vista , mas, no entanto, parecia que um bom número de pessoas estaria disposto a viver com o "peso-pesado" de Result sobre o aumento da probabilidade de perder um nulo verifique o valor retornado.

  • Estou ignorando os problemas de eficiência de tempo de execução impostos pela ABI porque, como @alexcrichton , suponho que poderíamos lidar com eles de alguma forma por meio de truques do compilador.

Existe alguma maneira de aumentarmos a visibilidade dessa alteração tardia por conta própria?

Uma maneira (em cima da minha cabeça): Mude a assinatura agora, em um PR por conta própria, no branch master, enquanto Allocator ainda está instável. E depois veja quem reclama no PR (e quem comemora!).

  • Isso é muito pesado? Parece que é por definições menos pesado do que combinar tal mudança com estabilização ...

Sobre o assunto de retornar *mut void ou Result<*mut void, AllocErr> : É possível que devamos estar revisando a ideia de características de alocador de "alto nível" e "baixo nível" separadas, conforme discutido na tomada II do RFC do Allocator .

(Obviamente, se eu tivesse uma objeção séria ao valor de retorno de *mut void , então eu a registraria como uma preocupação por meio do fcpbot. Mas, neste ponto, estou bastante confiante no julgamento da equipe de libs, talvez em alguma parte devido ao cansaço sobre esta saga de alocador.)

@pnkfelix

A decisão de mudar de -> Result<*mut u8, AllocErr> para -> *mut void pode vir como uma surpresa significativa para as pessoas que seguiram o desenvolvimento original das RFCs do alocador.

O último implica que, como discutido, o único erro que queremos expressar é OOM. Assim, um meio-termo ligeiramente mais leve que ainda tem o benefício de proteção contra falha acidental na verificação de erros é -> Option<*mut void> .

@gnzlbg

O excesso de API em std não é utilizável, portanto, a biblioteca std não pode usá-la, portanto ninguém pode, e é por isso que ela não é usada nenhuma vez no ecossistema Rust.

Então vá consertar.

@pnkfelix

Sobre o assunto de retornar mut void ou retornar Resultado < mut void, AllocErr>: É possível que devamos estar revisando a ideia de características de alocador de "alto nível" e "baixo nível" separadas, conforme discutido na parte II do Allocator RFC.

Essas foram basicamente nossas idéias, exceto que a API de alto nível estaria em Alloc si como alloc_one , alloc_array etc. Podemos até mesmo permitir que elas se desenvolvam primeiro no ecossistema como extensão características para ver em quais APIs as pessoas convergem.

@pnkfelix

A razão pela qual fiz o Layout implementar apenas Clonar e não Copiar é que eu queria deixar em aberto a possibilidade de adicionar mais estrutura ao tipo de Layout. Em particular, ainda estou interessado em tentar fazer com que o Layout tente rastrear qualquer estrutura de tipo usada para construí-lo (por exemplo, 16-array de struct {x: u8, y: [char; 215]}), para que os alocadores tenham a opção de expor rotinas de instrumentação que relatam de quais tipos seus conteúdos atuais são compostos.

Isso foi experimentado em algum lugar?

@sfackler Já fiz a maior parte disso, e tudo pode ser feito com a API duplicada (sem excesso + _excess métodos). Eu ficaria bem em ter duas APIs e não ter uma API _excess agora.

A única coisa que ainda me preocupa um pouco é que para implementar um alocador agora é necessário implementar alloc + dealloc , mas alloc_excess + dealloc também deve funcionar bem. Seria possível dar a alloc uma implementação padrão em termos de alloc_excess mais tarde ou isso não é possível ou é uma alteração importante? Na prática, a maioria dos alocadores vai implementar a maioria dos métodos de qualquer maneira, então isso não é um grande problema, mas mais como um desejo.


jemallocator implementa Alloc duas vezes (para Jemalloc e &Jemalloc ), onde a implementação de Jemalloc para alguns method é justa a (&*self).method(...) que encaminha a chamada do método para a implementação &Jemalloc . Isso significa que deve-se manter as duas implementações de Alloc para Jemalloc sincronizadas manualmente. Se obter comportamentos diferentes para as implementações de &/_ pode ser trágico ou não, não sei.


Achei muito difícil descobrir o que as pessoas estão realmente fazendo com o traço Alloc na prática. Os únicos projetos que descobri que o estão usando vão continuar usando todas as noites de qualquer maneira (servo, redox) e estão usando-o apenas para alterar o alocador global. Preocupa-me muito não ter encontrado nenhum projeto que o utilize como parâmetro de tipo de coleção (talvez eu só tenha tido azar e existem alguns?). Eu estava procurando por exemplos de implementação de SmallVec e ArrayVec em cima de um tipo Vec (já que std::Vec não tem um Alloc type parâmetro ainda), e também queria saber como a clonagem entre esses tipos ( Vec s com um Alloc ator diferente) funcionaria (o mesmo provavelmente se aplica à clonagem Box es com Alloc s diferentes). Existem exemplos de como essas implementações ficariam em algum lugar?

Os únicos projetos que descobri que estão usando vão continuar usando todas as noites (servo, redox)

Pelo que vale a pena, o Servo está tentando sair dos recursos instáveis ​​sempre que possível: https://github.com/servo/servo/issues/5286

Esse também é um problema do ovo e da galinha. Muitos projetos não usam Alloc ainda porque ainda é instável.

Não está muito claro para mim por que deveríamos ter um complemento completo de APIs _excess em primeiro lugar. Eles existiam originalmente para espelhar a API experimental * allocm do jemalloc, mas foram removidos na versão 4.0 há vários anos em favor de não duplicar toda a superfície da API. Parece que podemos seguir o exemplo deles?

Seria possível dar a alocação uma implementação padrão em termos de alloc_excess posteriormente ou isso não é possível ou é uma alteração significativa?

Podemos adicionar uma implementação padrão de alloc em termos de alloc_excess , mas alloc_excess precisará ter uma implementação padrão em termos de alloc . Tudo funciona bem se você implementar um ou ambos, mas se você não implementar nenhum dos dois, seu código será compilado, mas infinitamente recursivo. Isso já aconteceu antes (talvez por Rand ?), E poderíamos ter uma maneira de dizer que você precisa implementar pelo menos uma dessas funções, mas não nos importamos qual.

Preocupa-me muito não ter encontrado nenhum projeto que o utilize como parâmetro de tipo de coleção (talvez eu só tenha tido azar e existem alguns?).

Não conheço ninguém que esteja fazendo isso.

Os únicos projetos que descobri que estão usando vão continuar usando todas as noites (servo, redox)

Uma grande coisa que impede que isso avance é que as coleções stdlib ainda não suportam alocadores paramétricos. Isso praticamente exclui a maioria das outras caixas também, já que a maioria das coleções externas usa as internas sob o capô ( Box , Vec , etc).

Os únicos projetos que descobri que estão usando vão continuar usando todas as noites (servo, redox)

Uma grande coisa que impede que isso avance é que as coleções stdlib ainda não suportam alocadores paramétricos. Isso praticamente exclui a maioria das outras caixas, já que a maioria das coleções externas usa as internas sob o capô (Box, Vec, etc).

Isso se aplica a mim - eu tenho um kernel de brinquedo e, se pudesse, estaria usando Vec<T, A> , mas, em vez disso, preciso ter uma fachada de alocador global interior mutável, o que é nojento.

@remexre como a parametrização de suas estruturas de dados vai evitar um estado global com mutabilidade interior?

Ainda haverá um estado global interior mutável, suponho, mas parece muito mais seguro ter uma configuração em que o alocador global seja inutilizável até que a memória seja totalmente mapeada do que ter uma função set_allocator global.


EDIT : Acabei de perceber que não respondi à pergunta. No momento, tenho algo como:

struct BumpAllocator{ ... }
struct RealAllocator{ ... }
struct LinkedAllocator<A: 'static + AreaAllocator> {
    head: Mutex<Option<Cons<A>>>,
}
#[global_allocator]
static KERNEL_ALLOCATOR: LinkedAllocator<&'static mut (AreaAllocator + Send + Sync)> =
    LinkedAllocator::new();

onde AreaAllocator é uma característica que me permite (em tempo de execução) verificar se os alocadores não estão acidentalmente "sobrepostos" (em termos dos intervalos de endereço em que eles se alocam). BumpAllocator só é usado muito cedo, como espaço temporário ao mapear o restante da memória para criar RealAllocator s.

Idealmente, eu gostaria de ter um Mutex<Option<RealAllocator>> (ou um invólucro que o torne "somente inserção") seja o único alocador e ter tudo alocado desde o início seja parametrizado pelo inicializador BumpAllocator . Isso também me permite garantir que o BumpAllocator não seja usado após a inicialização, já que as coisas que aloco não sobreviveriam a ele.

@sfackler

Não está muito claro para mim por que deveríamos ter um complemento completo de APIs _excess em primeiro lugar. Eles existiam originalmente para espelhar a API experimental * allocm do jemalloc, mas foram removidos na versão 4.0 há vários anos em favor de não duplicar toda a superfície da API. Parece que podemos seguir o exemplo deles?

Atualmente shrink_in_place chama xallocx que retorna o tamanho real da alocação. Como shrink_in_place_excess não existe, ele joga fora esse tamanho e os usuários devem chamar nallocx para recalculá-lo, cujo custo realmente depende do tamanho da alocação.

Portanto, pelo menos algumas funções de alocação de jemalloc que já estamos usando estão retornando o tamanho utilizável, mas a API atual não nos permite usá-lo.

@remexre

Quando eu estava trabalhando no meu kernel de brinquedo, evitar o alocador global para garantir que nenhuma alocação acontecesse até que um alocador fosse configurado era uma meta minha também. Fico feliz em saber que não sou o único!

Não gosto da palavra Heap para o alocador global padrão. Por que não Default ?

Outro ponto de esclarecimento: RFC 1974 coloca tudo isso em std::alloc mas está atualmente em std::heap . Qual local está sendo proposto para estabilização?

@jethrogb "Heap" é um termo bastante canônico para "aquela coisa malloc lhe dá indicações para" - quais são suas preocupações com o termo?

@sfackler

"aquela coisa malloc te dá dicas para"

Exceto em minha mente que System é isso.

Ah, claro. Global é outro nome então talvez? Uma vez que você usa #[global_allocator] para selecioná-lo.

Pode haver vários alocadores de heap (por exemplo, libc e jemalloc com prefixo). Que tal renomear std::heap::Heap para std::heap::Default e #[global_allocator] para #[default_allocator] ?

O fato de que é o que você obtém se não especificar o contrário (presumivelmente quando, por exemplo, Vec ganha um parâmetro / campo de tipo extra para o alocador) é mais importante do que o fato de não ter "por -instances "estado (ou instâncias realmente).

O período de comentários final agora está completo.

Em relação ao FCP, acho que o subconjunto de API que foi proposto para estabilização é de uso muito limitado. Por exemplo, ele não suporta a caixa jemallocator .

De que maneira? jemallocator pode ter que sinalizar alguns dos impls de métodos instáveis ​​por trás de um sinalizador de recurso, mas é isso.

Se jemallocator em Rust estável não pode implementar, por exemplo Alloc::realloc chamando je_rallocx mas precisa confiar no padrão aloc + copiar + desalocar impl, então não é uma substituição aceitável para o IMO da biblioteca padrão alloc_jemalloc crate.

Claro, você pode conseguir algo para compilar, mas não é uma coisa particularmente útil.

Por quê? C ++ não tem nenhum conceito de realloc em sua API de alocador e isso não parece ter prejudicado a linguagem. Obviamente não é o ideal, mas não entendo por que seria inaceitável.

As coleções C ++ geralmente não usam realloc porque os construtores de movimento C ++ podem executar código arbitrário, não porque realloc não é útil.

E a comparação não é com C ++, é com a biblioteca padrão Rust atual com suporte a jemalloc integrado. Alternar para um alocador fora do padrão com apenas este subconjunto de Alloc API seria uma regressão.

E realloc é um exemplo. jemallocator atualmente também implementa alloc_zeroed , alloc_excess , usable_size , grow_in_place , etc.

aloc_zeroed é proposto para ser estabilizado. Até onde eu posso dizer (procure no próximo tópico), existem literalmente zero usos de alloc_excess . Você poderia mostrar algum código que irá regredir se voltar a uma implementação padrão.

De forma mais geral, porém, não vejo por que isso é um argumento contra a estabilização de uma parte dessas APIs. Se não quiser usar o jemallocator, você pode continuar sem usá-lo.

Layout::array<T>() poderia ser transformado em fn constante?

Pode entrar em pânico, então não no momento.

Pode entrar em pânico, então não no momento.

Entendo ... eu aceitaria const fn Layout::array_elem<T>() que seria um equivalente sem pânico a Layout::<T>::repeat(1).0 .

@mzabaluev Acho que o que você está descrevendo é equivalente a Layout::new<T>() . No momento, ele pode entrar em pânico, mas é apenas porque foi implementado usando Layout::from_size_align e, em seguida, .unwrap() , e espero que pudesse ser feito de forma diferente.

@joshlf Acho que esta estrutura tem o tamanho de 5, enquanto os elementos de uma matriz são colocados a cada 8 bytes devido ao alinhamento:

struct Foo {
    bar: u32,
    baz: u8
}

Não tenho certeza se um array de Foo incluiria o preenchimento do último elemento para o cálculo do tamanho, mas essa é minha forte expectativa.

Em Rust, o tamanho de um objeto é sempre um múltiplo de seu alinhamento, de modo que o endereço do elemento n th de um array seja sempre array_base_pointer + n * size_of<T>() . Portanto, o tamanho de um objeto em uma matriz é sempre igual ao tamanho daquele objeto sozinho. Consulte a página Rustonomicon em repr (Rust) para obter mais detalhes.

OK, acontece que uma estrutura é preenchida até seu alinhamento, mas AFAIK, esta não é uma garantia estável, exceto em #[repr(C)] .
De qualquer forma, fazer Layout::new a const fn também seria bem-vindo.

Este é o comportamento documentado (e portanto garantido) de uma função estável:

https://doc.rust-lang.org/std/mem/fn.size_of.html

Retorna o tamanho de um tipo em bytes.

Mais especificamente, este é o deslocamento em bytes entre elementos sucessivos em uma matriz com esse tipo de item, incluindo preenchimento de alinhamento. Assim, para qualquer tipo T e comprimento n , [T; n] tem um tamanho de n * size_of::<T>() .

Obrigado. Acabei de perceber que qualquer const fn que multiplique o resultado de Layout::new entraria em pânico por sua vez (a menos que seja feito com saturating_mul ou algo parecido), então estou de volta à estaca zero. Continuando com uma pergunta sobre pânico no problema de rastreamento const fn.

A macro panic!() não é atualmente suportada em expressões constantes, mas pânicos da aritmética verificada são gerados pelo compilador e não são afetados por essa limitação:

error[E0080]: constant evaluation error
 --> a.rs:1:16
  |
1 | const A: i32 = i32::max_value() * 2;
  |                ^^^^^^^^^^^^^^^^^^^^ attempt to multiply with overflow

error: aborting due to previous error

Isso está relacionado a Alloc::realloc mas não à estabilização da interface mínima ( realloc não faz parte dela):

Atualmente, como Vec::reserve/double chamam RawVec::reserve/double que chamam Alloc::realloc , o impl padrão de Alloc::realloc copia elementos vetoriais mortos (na faixa [len(), capacity()) ) . No caso absurdo de um enorme vetor vazio que deseja inserir capacity() + 1 elementos e, portanto, realocar, o custo de tocar toda aquela memória não é insignificante.

Em teoria, se a implementação padrão de Alloc::realloc também tomaria um intervalo "bytes_used", ela poderia apenas copiar a parte relevante na realocação. Na prática, pelo menos jemalloc substitui Alloc::realloc impl default com uma chamada para rallocx . Se fazer uma dança alloc / dealloc copiar apenas a memória relevante é mais rápido ou mais lento do que uma chamada rallocx provavelmente dependerá de muitas coisas ( rallocx gerencia para expandir o bloco no local? quanta memória desnecessária rallocx copiará? etc.).

https://github.com/QuiltOS/rust/tree/allocator-error Comecei a demonstrar como acho que o tipo de erro associado resolve nossas coleções e problemas de tratamento de erros fazendo a própria generalização. Em particular, observe como nos módulos eu mudo isso

  • Sempre reutilize a implementação Result<T, A::Err> para a implementação T
  • Nunca unwrap ou qualquer outra coisa parcial
  • Não oom(e) fora de AbortAdapter .

Isso significa que as alterações que estou fazendo são bastante seguras e também totalmente estúpidas! Trabalhar tanto com o retorno quanto com a anulação do erro não deve exigir esforço extra para manter invariáveis ​​mentais --- o verificador de tipo faz todo o trabalho.

Eu me lembro --- acho que na RFC de @Gankro ? ou o segmento pré-rfc --- Gecko / Servo pessoas dizendo que era bom não ter a falibilidade das coleções fazendo parte de seu tipo. Bem, eu posso adicionar #[repr(transparent)] a AbortAdapter para que as coleções possam ser transmutadas com segurança entre Foo<T, A> e Foo<T, AbortAdapter<A>> (dentro de embalagens seguras), permitindo que uma alterne para frente e para trás sem duplicar todos os métodos. [Para back-compat, as coleções da biblioteca padrão precisarão ser duplicadas em qualquer evento, mas os métodos do usuário não precisam ser tão Result<T, !> são atualmente muito fáceis de trabalhar.]

Infelizmente, o código não completa a verificação de tipo porque alterar os parâmetros de tipo de um item de idioma (caixa) confunde o compilador (surpresa!). A confirmação da caixa que causa ICE é a última --- tudo antes de ser bom. @eddyb corrigiu ferrugem em # 47043!

edit @joshlf Fui informado de sua https://github.com/rust-lang/rust/pull/45272 e incorporei isso aqui. Obrigado!

A memória persistente (por exemplo, http://pmem.io ) é o próximo grande sucesso, e o Rust precisa ser posicionado para funcionar bem com ele.

Tenho trabalhado recentemente em um wrapper Rust para um alocador de memória persistente (especificamente, libpmemcto). Quaisquer que sejam as decisões tomadas em relação à estabilização desta API, ela precisa: -

  • Ser capaz de suportar um wrapper de desempenho em torno de um alocador de memória persistente como libpmemcto;
  • Ser capaz de especificar (parametrizar) os tipos de coleção por alocador (no momento, é necessário duplicar Box, Rc, Arc, etc)
  • Ser capaz de clonar dados entre alocadores
  • Ser capaz de suportar estruturas armazenadas em memória persistente com campos que são reinicializados na instanciação de um pool de memória persistente, ou seja, algumas estruturas de memória persistente precisam ter campos que são armazenados apenas temporariamente no heap. Meus casos de uso atuais são uma referência ao pool de memória persistente usado para alocação e dados transitórios usados ​​para bloqueios.

Como um aparte, o desenvolvimento pmem.io (Intel's PMDK) faz uso pesado de um alocador jemalloc modificado por baixo das cobertas - então parece prudente usar jemalloc como um exemplo de consumidor de API.

Seria possível reduzir o escopo disso para cobrir apenas GlobalAllocator s primeiro, até ganharmos mais experiência com o uso de Alloc atores em coleções?

IIUC isso já atenderia às necessidades de servo e nos permitiria fazer experiências com a parametrização de contêineres em paralelo. No futuro, podemos mover as coleções para usar GlobalAllocator ou apenas adicionar um cobertor impl de Alloc para GlobalAllocator para que possam ser usados ​​para todas as coleções.

Pensamentos?

@gnzlbg Para que o #[global_allocator] seja útil (além de selecionar heap::System ), o traço Alloc precisa ser estável também, para que possa ser implementado por grades como https: / /crates.io/crates/jemallocator. Não há nenhum tipo ou característica chamada GlobalAllocator no momento. Você está propondo alguma nova API?

não há nenhum tipo ou característica chamada GlobalAllocator no momento. Você está propondo alguma nova API?

O que sugeri é renomear a API "mínima" que @alexcrichton sugeriu estabilizar aqui de Alloc para GlobalAllocator para representar apenas alocadores globais, e deixar a porta aberta para coleções serem parametrizadas por um diferente traço alocador no futuro (o que não significa que não podemos parametrizá-los pelo traço GlobalAllocator ).

IIUC servo atualmente só precisa ser capaz de alternar o alocador global (ao contrário de também ser capaz de parametrizar algumas coleções por um alocador). Portanto, talvez em vez de tentar estabilizar uma solução que deva ser preparada para o futuro para ambos os casos de uso, possamos resolver apenas o problema do alocador global agora e descobrir como parametrizar coleções por alocadores mais tarde.

Não sei se isso faz sentido.

O servo IIUC atualmente só precisa ser capaz de alternar o alocador global (em oposição a também ser capaz de parametrizar algumas coleções por um alocador).

Isso está correto, mas:

  • Se uma característica e seu método são estáveis ​​para que possam ser implementados, então também podem ser chamados diretamente sem passar por std::heap::Heap . Portanto, não é apenas um traço de alocador global, é um traço para alocadores (mesmo se acabarmos fazendo um diferente para coleções genéricas sobre alocadores) e GlobalAllocator não é um nome particularmente bom.
  • A caixa do jemallocator atualmente implementa alloc_excess , realloc , realloc_excess , usable_size , grow_in_place , e shrink_in_place que não são parte da API mínima proposta. Eles podem ser mais eficientes do que o impl padrão, portanto, removê-los seria uma regressão de desempenho.

Ambos os pontos fazem sentido. Eu apenas pensei que a única maneira de acelerar significativamente a estabilização desse recurso era eliminar a dependência de ele também ser uma boa característica para parametrizar coleções sobre ele.

[Seria bom se o Servo pudesse ser como (estável | caixa oficial de mozilla), e a carga pudesse reforçar isso, para remover um pouco de pressão aqui.]

@ Ericson2314 servo não é o único projeto que deseja usar essas APIs.

@ Ericson2314 Não entendo o que isso significa, você poderia reformular?

Para contextualizar: o servo atualmente usa vários recursos instáveis ​​(incluindo #[global_allocator] ), mas estamos tentando nos afastar disso lentamente (seja atualizando para um compilador que estabilizou alguns recursos ou encontrando alternativas estáveis. ) Isso é monitorado em https://github.com/servo/servo/issues/5286. Portanto, estabilizar #[global_allocator] seria bom, mas não está bloqueando o trabalho do Servo.

O Firefox se baseia no fato de que Rust std padroniza para o alocador do sistema ao compilar um cdylib , e que mozjemalloc que acaba sendo vinculado ao mesmo binário define símbolos como malloc e free aquela "sombra" (não conheço a terminologia do vinculador adequada) aqueles da libc. Portanto, as alocações do código Rust no Firefox acabam usando mozjemalloc. (Isso é no Unix, não sei como funciona no Windows.) Isso funciona, mas parece frágil para mim. O Firefox usa Rust estável, e eu gostaria que ele usasse #[global_allocator] para selecionar explicitamente mozjemalloc para tornar a configuração inteira mais robusta.

@SimonSapin quanto mais brinco com alocadores e coleções, mais tendo a pensar que não queremos parametrizar as coleções em Alloc ainda, porque, dependendo do alocador, uma coleção pode querer oferecer um API diferente, a complexidade de algumas operações mudam, alguns detalhes da coleção realmente dependem do alocador, etc.

Portanto, gostaria de sugerir uma maneira pela qual podemos progredir aqui.

Etapa 1: alocador de heap

Poderíamos nos restringir a princípio para tentar permitir que os usuários selecionem o alocador para o heap (ou o sistema / plataforma / global / alocador de armazenamento gratuito, ou como você preferir nomeá-lo) no Rust estável.

A única coisa que inicialmente parametrizamos por ele é Box , que só precisa alocar ( new ) e desalocar ( drop ) memória.

Esta característica de alocador pode inicialmente ter a API que @alexcrichton propôs (ou um pouco estendida), e esta característica de alocador pode, à noite, ainda ter uma API ligeiramente estendida para suportar as coleções std:: .

Assim que estivermos lá, os usuários que desejam migrar para o stable poderão fazê-lo, mas podem obter um impacto no desempenho, por causa da API instável.

Etapa 2: alocador de heap sem impacto no desempenho

Nesse ponto, podemos reavaliar os usuários que não podem mudar para estável devido a um impacto no desempenho e decidir como estender essa API e estabilizá-la.

Etapas 3 a N: suporte a alocadores personalizados em coleções std .

Primeiro, isso é difícil, então pode nunca acontecer, e acho que nunca acontecer não é uma coisa ruim.

Quando quero parametrizar uma coleção com um alocador personalizado, tenho um problema de desempenho ou de usabilidade.

Se eu tiver um problema de usabilidade, normalmente quero uma API de coleção diferente que explore recursos do meu alocador personalizado, como, por exemplo, meu SliceDeque crate. Parametrizar uma coleção por um alocador personalizado não vai me ajudar aqui.

Se eu tiver um problema de desempenho, ainda será muito difícil para um alocador personalizado me ajudar. Vou considerar Vec nas próximas seções, porque é a coleção que reimplementei com mais freqüência.

Reduza o número de chamadas do alocador do sistema (Otimização de vetores pequenos)

Se eu quiser alocar alguns elementos dentro do objeto Vec para reduzir o número de chamadas para o alocador do sistema, hoje utilizo apenas SmallVec<[T; M]> . No entanto, um SmallVec não é um Vec :

  • mover Vec é O (1) no número de elementos, mas mover SmallVec<[T; M]> é O (N) para N <M e O (1) depois,

  • ponteiros para os elementos Vec são invalidados em movimento se len() <= M mas não de outra forma, ou seja, se len() <= M operações como into_iter precisam mover os elementos para o o próprio objeto iterador, em vez de apenas pegar ponteiros.

Poderíamos fazer Vec genérico em vez de um alocador para oferecer suporte a isso? Tudo é possível, mas acho que os custos mais importantes são:

  • fazer isso torna a implementação de Vec mais complexa, o que pode afetar os usuários que não usam este recurso
  • a documentação de Vec se tornaria mais complexa, porque o comportamento de algumas operações dependeria do alocador.

Acho que esses custos não são desprezíveis.

Faça uso de padrões de alocação

O fator de crescimento de Vec é adaptado a um alocador específico. Em std , podemos adaptá-lo aos comuns jemalloc / malloc / ... mas se você estiver usando um alocador personalizado, é provável que o fator de crescimento que escolhemos por padrão, não será o melhor para seu caso de uso. Todo alocador deve ser capaz de especificar um fator de crescimento para padrões de alocação do tipo vec? Não sei, mas minha intuição me diz: provavelmente não.

Explorar recursos extras de seu alocador de sistema

Por exemplo, um alocador supercomprometido está disponível na maioria dos destinos de Camada 1 e Camada 2. Em sistemas semelhantes ao Linux e Macos, o alocador de heap faz overcommit por padrão, enquanto a API do Windows expõe VirtualAlloc que pode ser usado para reservar memória (por exemplo, em Vec::reserve/with_capacity ) e comprometer memória em push .

Atualmente, o traço Alloc não expõe uma maneira de implementar tal alocador no Windows, porque não separa os conceitos de confirmação e reserva de memória (no Linux, um alocador não supercomprometido pode ser hackeado tocando apenas uma vez em cada página). Ele também não expõe uma maneira para um alocador declarar se ele over-commits ou não por padrão em alloc .

Ou seja, precisaríamos estender a API Alloc para suportar isso para Vec , e isso seria IMO para um pequeno ganho. Porque quando você tem tal alocador, Vec semântica muda novamente:

  • Vec não precisa crescer nunca mais, então operações como reserve fazem pouco sentido
  • push é O(1) vez de O(1) amortizado.
  • iteradores para objetos ativos nunca são invalidados, desde que o objeto esteja ativo, o que permite algumas otimizações

Explore mais recursos extras de seu alocador de sistema

Alguns alocadores de sistema como cudaMalloc / cudaMemcpy / ... diferenciam entre memória fixada e não fixada, permitem que você aloque memória em espaços de endereço separados (então precisaríamos de um tipo de Ponteiro associado no Traço Alloc), ...

Mas usar isso em coleções como Vec altera novamente a semântica de algumas operações de maneiras sutis, como se a indexação de um vetor invoca repentinamente um comportamento indefinido ou não, dependendo se você faz isso a partir de um kernel GPU ou do host.

Empacotando

Acho que tentar criar uma API Alloc que possa ser usada para parametrizar todas as coleções (ou apenas Vec ) é difícil, provavelmente muito difícil.

Talvez depois de obtermos os alocadores globais / sistema / plataforma / pilha / loja livre corretos e Box , possamos repensar as coleções. Talvez possamos reutilizar Alloc , talvez precisemos de um VecAlloc, VecDequeAlloc , HashMapAlloc`, ... ou talvez apenas digamos, "quer saber, se você realmente precisa disso , basta copiar e colar a coleção padrão em uma caixa e moldá-la ao seu alocador ". Talvez a melhor solução seja apenas tornar isso mais fácil, tendo coleções std em sua própria caixa (ou caixas) no berçário e usando apenas recursos estáveis, talvez implementados como um conjunto de blocos de construção.

De qualquer forma, acho que tentar resolver todos esses problemas aqui de uma vez e tentar chegar a uma Alloc característica que seja boa para tudo é muito difícil. Estamos na etapa 0. Acho que a melhor maneira de chegar rapidamente às etapas 1 e 2 é deixar as coleções fora de cena até que cheguemos lá.

Assim que estivermos lá, os usuários que desejam migrar para o stable poderão fazê-lo, mas podem obter um impacto no desempenho, por causa da API instável.

A escolha de um alocador personalizado geralmente é para melhorar o desempenho, portanto, não sei a quem essa estabilização inicial serviria.

A escolha de um alocador personalizado geralmente é para melhorar o desempenho, portanto, não sei a quem essa estabilização inicial serviria.

Todo mundo? Pelo menos agora. Muitos dos métodos que você reclama que estão faltando na proposta de estabilização inicial ( alloc_excess , por exemplo), são AFAIK ainda não usados ​​por nada na biblioteca padrão. Ou isso mudou recentemente?

Vec (e outros usuários de RawVec ) usam realloc em push

@SimonSapin

A caixa jemallocator implementa atualmente alloc_excess, realloc, realloc_excess, usable_size, grow_in_place e shrink_in_place

A partir desses métodos, AFAIK realloc , grow_in_place e shrink_in_place são usados, mas grow_in_place é apenas um invólucro ingênuo sobre shrink_in_place para jemalloc em pelo menos se implementamos o implícito instável padrão de grow_in_place em termos de shrink_in_place no traço Alloc , que o reduz a dois métodos: realloc e shrink_in_place .

Escolher um alocador personalizado geralmente significa melhorar o desempenho,

Embora isso seja verdade, você pode obter mais desempenho usando um alocador mais adequado sem esses métodos do que um alocador ruim que os tenha.

O principal caso de uso do IIUC para o servo era usar o jemalloc do Firefox em vez de ter um segundo jemalloc por perto, certo?

Mesmo se adicionarmos realloc e shrink_in_place ao traço Alloc em uma estabilização inicial, isso apenas atrasaria as reclamações de desempenho.

Por exemplo, no momento em que adicionamos qualquer API instável ao traço Alloc que acaba sendo usado pelas coleções std , você não seria capaz de obter o mesmo desempenho no estável do que faria ser capaz de entrar todas as noites. Ou seja, se adicionarmos realloc_excess e shrink_in_place_excess ao traço de alocação e fizermos Vec / String / ... usá-los, isso estabilizamos realloc e shrink_in_place não o teriam ajudado nem um pouco.

O principal caso de uso do IIUC para o servo era usar o jemalloc do Firefox em vez de ter um segundo jemalloc por perto, certo?

Embora compartilhem algum código, Firefox e Servo são dois projetos / aplicativos separados.

O Firefox usa mozjemalloc, que é um fork de uma versão antiga do jemalloc com vários recursos adicionados. Eu acho que algum código unsafe FFI depende da correção e integridade do mozjemalloc sendo usado pelo Rust std.

Servo usa jemalloc, que por acaso é o padrão de Rust para executáveis ​​no momento, mas há planos para alterar esse padrão para o alocador do sistema. O servo também tem algum código de relatório de uso de memória unsafe que depende da robustez do jemalloc sendo usado de fato. (Passando Vec::as_ptr() para je_malloc_usable_size .)

Servo usa jemalloc, que por acaso é o padrão de Rust para executáveis ​​no momento, mas há planos para alterar esse padrão para o alocador do sistema.

Seria bom saber se os alocadores de sistema nos sistemas que servo-alvos fornecem realloc e shrink_to_fit APIs otimizados como o jemalloc? realloc (e calloc ) são muito comuns, mas shrink_to_fit ( xallocx ) é AFAIK específico para jemalloc . Talvez a melhor solução seja estabilizar realloc e alloc_zeroed ( calloc ) na implementação inicial e deixar shrink_to_fit para depois. Isso deve permitir que o servo funcione com alocadores de sistema na maioria das plataformas sem problemas de desempenho.

O servo também tem algum código de relatório de uso de memória não seguro que depende da robustez do jemalloc sendo usado. (Passando Vec :: as_ptr () para je_malloc_usable_size.)

Como você sabe, o jemallocator crate tem APIs para isso. Espero que caixas semelhantes à caixa jemallocator apareçam para outros alocadores que oferecem APIs semelhantes à medida que a história do alocador global começa a se estabilizar. Não pensei se essas APIs pertencem ao traço Alloc .

Eu não acho que malloc_usable_size precisa estar no traço Alloc . Usar #[global_allocator] para ter certeza de qual alocador é usado por Vec<T> e separadamente usar uma função da caixa jemallocator é bom.

@SimonSapin uma vez que a característica Alloc se torne estável, provavelmente teremos uma caixa como jemallocator para Linux malloc e Windows. Essas caixas podem ter um recurso extra para implementar as partes que podem da API instável Alloc (como, por exemplo, usable_size em cima de malloc_usable_size ) e algumas outras coisas que não fazem parte da API Alloc , como relatórios de memória no topo de mallinfo . Assim que houver caixas utilizáveis ​​para os sistemas que o servo tem como alvo, seria mais fácil saber quais partes da característica Alloc priorizar a estabilização, e provavelmente descobriremos APIs mais recentes que devem ser pelo menos experimentadas por alguns alocadores.

@gnzlbg Estou um pouco cético em relação às coisas em https://github.com/rust-lang/rust/issues/32838#issuecomment -358267292. Deixando de lado todas as coisas específicas do sistema, não é difícil generalizar coleções para aloc - eu fiz isso. Tentar incorporar isso parece um desafio separado.

@SimonSapin O firefox não tem uma política de ferrugem instável? Acho que estava ficando confuso: Firefox e Servo querem isso, mas se for o caso de uso do Firefox, isso aumentaria a pressão para estabilizar.

@sfackler Veja isso ^. Eu estava tentando fazer uma distinção entre projetos que precisam e querem essa estabilidade, mas o Servo está do outro lado dessa divisão.

Tenho um projeto que quer isso e exige que seja estável. Não há nada de particularmente mágico no Servo ou no Firefox como consumidores do Rust.

@ Ericson2314 Correto, o Firefox usa estável: https://wiki.mozilla.org/Rust_Update_Policy_for_Firefox. Como eu expliquei, há uma solução de trabalho hoje, então este não é um bloqueador real para nada. Seria melhor / mais robusto usar #[global_allocator] , só isso.

O servo usa alguns recursos instáveis, mas como mencionamos, estamos tentando mudar isso.

FWIW, alocadores paramétricos são muito úteis para implementar alocadores. Muito da contabilidade menos sensível ao desempenho torna-se muito mais fácil se você pode usar várias estruturas de dados internamente e parametrizá-las por algum alocador mais simples (como bsalloc ). Atualmente, a única maneira de fazer isso em um ambiente std é ter uma compilação de duas fases em que a primeira fase é usada para definir seu alocador mais simples como o alocador global e a segunda fase é usada para compilar o alocador maior e mais complicado . No no-std, não há como fazer isso.

@ Ericson2314

Deixando de lado todas as coisas específicas do sistema, não é difícil generalizar coleções para aloc - eu fiz isso. Tentar incorporar isso parece um desafio separado.

Você tem uma implementação de ArrayVec ou SmallVec em cima de Vec + alocadores personalizados que eu possa examinar? Esse foi o primeiro ponto que mencionei, e isso não é específico do sistema. Indiscutivelmente, esses seriam os dois alocadores mais simples imagináveis, um é apenas um array bruto como armazenamento e o outro pode ser construído em cima do primeiro adicionando um fallback ao Heap quando o array ficar sem capacidade. A principal diferença é que esses alocadores não são "globais", mas cada um dos Vec s tem seu próprio alocador independente de todos os outros, e esses alocadores têm estado.

Além disso, não estou argumentando para nunca fazer isso. Estou apenas afirmando que isso é muito difícil: C ++ vem tentando há 30 anos com sucesso apenas parcial: os alocadores de GPU e os alocadores de GC funcionam devido aos tipos de ponteiro genérico, mas implementando ArrayVec e SmallVec em cima de Vec não resulta em uma abstração de custo zero no terreno do C ++ ( P0843r1 discute alguns dos problemas de ArrayVec em detalhes).

Então, eu apenas preferiria que buscássemos isso depois de estabilizar as peças que oferecem algo útil, desde que não tornem a busca por alocadores de coleção personalizados no futuro.


Conversei um pouco com @SimonSapin no IRC e se realloc e alloc_zeroed , então o Rust no Firefox (que só usa Rust estável) seria capaz de usar mozjemalloc como um alocador global no Rust estável sem a necessidade de qualquer hacks extra. Como mencionado por @SimonSapin , o Firefox atualmente tem uma solução viável para isso hoje, então, embora isso seja bom, não parece ser de alta prioridade.

Ainda assim, poderíamos começar por aí e, assim que chegarmos lá, mover servo para #[global_allocator] estável sem perda de desempenho.


@joshlf

FWIW, alocadores paramétricos são muito úteis para implementar alocadores.

Você poderia elaborar um pouco mais sobre o que você quer dizer? Existe uma razão pela qual você não pode parametrizar seus alocadores personalizados com o traço Alloc ? Ou seu próprio traço de alocador personalizado e apenas implementar o traço Alloc nos alocadores finais (esses dois traços não precisam ser necessariamente iguais)?

Não entendo de onde vem o caso de uso de "SmallVec = Vec + alocador especial". Não é algo que eu tenha visto muito mencionado antes (nem no Rust nem em outros contextos), justamente porque tem muitos problemas sérios. Quando penso em "melhorar o desempenho com um alocador especializado", não é isso que penso.

Olhando para a API Layout , eu estava me perguntando sobre as diferenças no tratamento de erros entre from_size_align e align_to , onde o primeiro retorna None em caso de erro , enquanto o último entra em pânico (!).

Não seria mais útil e consistente adicionar um LayoutErr enum informativo e adequadamente definido e retornar um Result<Layout, LayoutErr> em ambos os casos (e talvez usá-lo para as outras funções que atualmente retornam um Option também)?

@rkruppe

Não entendo de onde vem o caso de uso de "SmallVec = Vec + alocador especial". Não é algo que eu tenha visto muito mencionado antes (nem no Rust nem em outros contextos), justamente porque tem muitos problemas sérios. Quando penso em "melhorar o desempenho com um alocador especializado", não é isso que penso.

Existem duas maneiras independentes de usar alocadores em Rust e C ++: o alocador do sistema, usado por todas as alocações por padrão, e como um argumento de tipo para uma coleção parametrizada por algum traço de alocador, como uma forma de criar um objeto daquela coleção particular que usa um alocador específico (que pode ser o alocador do sistema ou não).

O que se segue concentra-se apenas neste segundo caso de uso: usar uma coleção e um tipo de alocador para criar um objeto dessa coleção que usa um alocador específico.

Na minha experiência com C ++, parametrizar uma coleção com um alocador atende a dois casos de uso:

  • melhorar o desempenho de um objeto de coleção, fazendo com que a coleção use um alocador personalizado direcionado a um padrão de alocação específico e / ou
  • adicionar um novo recurso a uma coleção, permitindo que ela faça algo que não podia fazer antes.

Adicionando novos recursos às coleções

Este é o caso de uso de alocadores que vejo em bases de código C ++ 99% das vezes. O fato de que adicionar um novo recurso a uma coleção melhora o desempenho é, na minha opinião, coincidência. Em particular, nenhum dos alocadores a seguir melhora o desempenho visando um padrão de alocação. Eles fazem isso adicionando recursos que, em alguns casos, como @ Ericson2314 menciona, podem ser considerados "específicos do sistema". Estes são alguns exemplos:

  • empilhar alocadores para fazer pequenas otimizações de buffer (consulte o artigo stack_alloc de Howard Hinnant). Eles permitem que você use std::vector ou flat_{map,set,multimap,...} e, ao passar um alocador personalizado, você adiciona em uma pequena otimização de buffer com ( SmallVec ) ou sem ( ArrayVec ) pilha cair para trás. Isso permite, por exemplo, colocar uma coleção com seus elementos na pilha ou memória estática (onde, de outra forma, teria usado o heap).

  • arquiteturas de memória segmentada (como destinos x86 de ponteiro largo de 16 bits e GPGPUs). Por exemplo, C ++ 17 Parallel STL foi, durante C ++ 14, a Especificação Técnica Paralela. Sua biblioteca precursora do mesmo autor é a biblioteca Thrust da NVIDIA, que inclui alocadores para permitir que classes de contêineres usem memória GPGPU (por exemplo, thrust :: device_malloc_allocator ) ou memória fixada (por exemplo, thrust :: pinned_allocator ; memória fixada permite uma transferência mais rápida entre o dispositivo host em alguns casos).

  • alocadores para resolver problemas relacionados ao paralelismo, como falso compartilhamento (por exemplo, Intel Thread Building Blocks cache_aligned_allocator ) ou requisitos de alinhamento excessivo de tipos SIMD (por exemplo, aligned_allocator do Eigen3).

  • memória compartilhada entre processos: Boost.Interprocess tem alocadores que alocam a memória da coleção usando recursos de memória compartilhada entre processos do SO (por exemplo, como a memória compartilhada do System V). Isso permite usar diretamente um contêiner std para gerenciar a memória usada para a comunicação entre diferentes processos.

  • coleta de lixo: a biblioteca de alocação de memória adiada de Herb Sutter usa um tipo de ponteiro definido pelo usuário para implementar alocadores que coletam memória de lixo. De forma que, por exemplo, quando um vetor cresce, o antigo pedaço de memória é mantido vivo até que todos os ponteiros para aquela memória tenham sido destruídos, evitando a invalidação do iterador.

  • alocadores instrumentados: o blsma_testallocator da Biblioteca de Software da Bloomberg permite que você registre padrões de alocação / desalocação de memória (e C ++ - construção / destruição de objeto específico) dos objetos onde você o usa. Você não sabe se Vec alocado após reserve ? Conecte esse alocador e eles dirão se isso acontecer. Alguns desses alocadores permitem que você os nomeie, para que possa usá-los em vários objetos e obter logs informando qual objeto está fazendo o quê.

Esses são os tipos de alocadores que vejo com mais frequência à solta em C ++. Como mencionei antes, o fato de melhorarem o desempenho em alguns casos, é, na minha opinião, uma coincidência. O importante é que nenhum deles tenta atingir um determinado padrão de alocação.

Objetivando padrões de alocação para melhorar o desempenho.

AFAIK, não há alocadores C ++ amplamente usados ​​que façam isso e vou explicar porque acho que isso acontece em um segundo. As seguintes bibliotecas se destinam a este caso de uso:

No entanto, essas bibliotecas não fornecem realmente um único alocador para um caso de uso específico. Em vez disso, eles fornecem blocos de construção de alocadores que você pode usar para construir alocadores personalizados direcionados ao padrão de alocação específico em uma parte específica de um aplicativo.

O conselho geral que recordo dos meus dias de C ++ era apenas "não usá-los" (eles são o último recurso) porque:

  • combinar o desempenho do alocador de sistema é muito difícil, vencê-lo é muito, muito difícil,
  • as chances de o padrão de alocação de memória do aplicativo de outra pessoa corresponder ao seu são mínimas, então você realmente precisa saber seu padrão de alocação e saber quais blocos de construção de alocador você precisa para combiná-lo
  • eles não são portáteis porque diferentes fornecedores têm diferentes implementações de biblioteca padrão C ++ que usam diferentes padrões de alocação; os fornecedores normalmente direcionam sua implementação para seus alocadores de sistema. Ou seja, uma solução feita sob medida para um fornecedor pode ter um desempenho horrível (pior do que o alocador do sistema) em outro.
  • existem muitas alternativas que você pode esgotar antes de tentar usá-las: usar uma coleção diferente, reservar memória, ... A maioria das alternativas é de menor esforço e pode gerar ganhos maiores.

Isso não significa que as bibliotecas para esse caso de uso não sejam úteis. Eles são, e é por isso que bibliotecas como foonathan / memory estão florescendo. Mas pelo menos na minha experiência eles são muito menos usados ​​do que os alocadores que "adicionam recursos extras" porque para entregar uma vitória você deve vencer o alocador do sistema, o que requer mais tempo do que a maioria dos usuários estão dispostos a investir (Stackoverflow está cheio de perguntas do tipo "Usei Boost.Pool e meu desempenho piorou, o que posso fazer? Não uso Boost.Pool.").

Empacotando

IMO Eu acho ótimo que o modelo de alocador C ++, embora longe de ser perfeito, suporte ambos os casos de uso, e eu acho que se as coleções std de Rust devem ser parametrizadas por alocadores, eles deveriam suportar ambos os casos de uso também, porque pelo menos em Os alocadores C ++ para ambos os casos mostraram-se úteis.

Eu só acho que este problema é ligeiramente ortogonal para ser capaz de personalizar o alocador global / sistema / plataforma / padrão / heap / free-store de um aplicativo específico, e que tentar resolver os dois problemas ao mesmo tempo pode atrasar a solução de um deles desnecessariamente.

O que alguns usuários desejam fazer com uma coleção parametrizada por um alocador pode ser muito diferente do que alguns outros usuários desejam fazer. Se @rkruppe começar "combinando padrões de alocação" e eu começar "evitando o falso compartilhamento" ou "usando uma pequena otimização de buffer com fallback de heap", será difícil primeiro entender as necessidades um do outro e, segundo, chegar em uma solução que funciona para ambos.

@gnzlbg Obrigado pelo artigo abrangente. A maior parte não aborda minha pergunta original e eu discordo de algumas delas, mas é bom que seja bem explicado para que não falemos nada.

Minha pergunta era especificamente sobre este aplicativo:

empilhar alocadores para fazer pequenas otimizações de buffer (consulte o artigo stack_alloc de Howard Hinnant). Eles permitem que você use std :: vector ou flat_ {map, set, multimap, ...} e, passando um alocador personalizado, você adiciona uma pequena otimização de buffer com (SmallVec) ou sem (ArrayVec) fallback de heap. Isso permite, por exemplo, colocar uma coleção com seus elementos na pilha ou memória estática (onde, de outra forma, teria usado o heap).

Lendo sobre stack_alloc, eu percebo agora como isso pode funcionar. Não é o que as pessoas geralmente querem dizer com SmallVec (onde o buffer é armazenado embutido na coleção), e é por isso que perdi essa opção, mas evita o problema de ter que atualizar ponteiros quando a coleção se move (e também torna esses movimentos mais baratos ) Observe também que short_alloc permite que várias coleções compartilhem um arena , o que o torna ainda mais diferente dos tipos típicos de SmallVec. É mais parecido com um alocador linear / bump-pointer com fallback elegante para a alocação de heap ao ficar sem muito espaço.

Não concordo que este tipo de alocador e cache_aligned_allocator estejam fundamentalmente adicionando novos recursos. Eles são usados ​​de forma diferente e, dependendo da sua definição de "padrão de alocação", podem não otimizar para um padrão de alocação específico. No entanto, eles certamente otimizam para casos de uso específicos e não têm diferenças comportamentais significativas de alocadores de heap de uso geral.

No entanto, concordo que casos de uso como a alocação de memória adiada de Sutter, que mudam substancialmente o que um "ponteiro" significa, são um aplicativo separado que pode precisar de um design separado se quisermos suportá-lo.

Lendo sobre stack_alloc, eu percebo agora como isso pode funcionar. Não é o que as pessoas geralmente querem dizer com SmallVec (onde o buffer é armazenado embutido na coleção), e é por isso que perdi essa opção, mas evita o problema de ter que atualizar ponteiros quando a coleção se move (e também torna esses movimentos mais baratos )

Mencionei stack_alloc porque é o único alocador com "um papel", mas foi lançado em 2009 e precede o C ++ 11 (C ++ 03 não suportava alocadores com estado em coleções).

A forma como isso funciona em C ++ 11 (que oferece suporte a alocadores com estado), em poucas palavras, é:

  • std :: vector armazena um objeto Allocator dentro dele, assim como Rust RawVec faz .
  • a interface do Allocator :: propagate_on_container_move_assignment (POCMA de agora em diante) que os alocadores definidos pelo usuário podem personalizar; esta propriedade é true por padrão. Se esta propriedade for false , na atribuição de movimentação, o alocador não pode ser propagado, portanto, uma coleção é exigida pelo padrão para mover cada um de seus elementos para o novo armazenamento manualmente.

Portanto, quando um vetor com o alocador do sistema é movido, primeiro o armazenamento para o novo vetor na pilha é alocado, então o alocador é movido (que tem tamanho zero) e então os 3 ponteiros são movidos, que ainda são válidos. Esses movimentos são O(1) .

OTOHO, quando um vetor com um alocador POCMA == true é movido, primeiro o armazenamento para o novo vetor na pilha é alocado e inicializado com um vetor vazio, então a coleção antiga é drain ed para o novo, de modo que o antigo fique vazio e o novo cheio. Isso move cada elemento da coleção individualmente, usando seus operadores de atribuição de movimentação. Esta etapa é O(N) e corrige ponteiros internos dos elementos. Finalmente, a coleção original agora vazia é descartada. Observe que isso parece um clone, mas não é porque os próprios elementos não são clonados, mas movidos em C ++.

Isso faz sentido?

O principal problema com essa abordagem em C ++ são:

  • a política de crescimento do vetor é definida pela implementação
  • a API do alocador não tem métodos _excess
  • a combinação dos dois problemas acima significa que se você sabe que seu vetor pode conter no máximo 9 elementos, você não pode ter um alocador de pilha que pode conter 9 elementos, porque seu vetor pode tentar crescer quando tem 8 com um fator de crescimento de 1,5, portanto, você precisa pessimizar e alocar espaço para 18 elementos.
  • a complexidade da operação do vetor muda dependendo das propriedades do alocador (POCMA é apenas uma das muitas propriedades que a API do alocador C ++ possui; escrever alocadores C ++ não é trivial). Isso torna a especificação da API do vetor uma dor, porque às vezes copiar ou mover elementos entre diferentes alocadores do mesmo tipo tem custos extras, que mudam a complexidade das operações. Isso também torna a leitura das especificações uma grande dor. Muitas fontes online de documentação, como cppreference, colocam o caso geral à frente e os detalhes obscuros de quais mudanças se uma propriedade de alocador for verdadeira ou falsa em minúsculas letras minúsculas para evitar incomodar 99% dos usuários com elas.

Há muitas pessoas trabalhando para melhorar a API de alocador do C ++ para corrigir esses problemas, por exemplo, adicionando métodos _excess e garantindo que as coleções em conformidade com o padrão os usem.

Eu discordo que este tipo de alocador e cache_aligned_allocator estão fundamentalmente adicionando novos recursos.

Talvez o que eu quis dizer é que eles permitem que você use coleções std em situações ou para tipos para os quais você não poderia usá-los antes. Por exemplo, em C ++ você não pode colocar os elementos de um vetor no segmento de memória estática de seu binário sem algo como um alocador de pilha (ainda que você possa escrever sua própria coleção que faça isso). OTOH, o padrão C ++ não suporta tipos superalinhados como tipos SIMD, e se você tentar alocar um heap com new você invocará um comportamento indefinido (você precisa usar posix_memalign ou similar) . Usar o objeto normalmente manifesta o comportamento indefinido por meio de um segfault (*). Coisas como aligned_allocator permitem que você aloque esses tipos em heap e até mesmo os coloque em coleções std, sem invocar um comportamento indefinido, usando um alocador diferente. Claro que o novo alocador terá diferentes padrões de alocação (esses alocadores basicamente superalinham toda a memória, aliás ...), mas as pessoas os usam para fazer algo que não podiam antes.

Obviamente, Rust não é C ++. E C ++ tem problemas que Rust não tem (e vice-versa). Um alocador que adiciona um novo recurso em C ++ pode ser desnecessário no Rust, que, por exemplo, não tem problemas com tipos SIMD.

(*) Os usuários do Eigen3 sofrem com isso profundamente, pois para evitar comportamento indefinido ao usar containers C ++ e STL, você precisa proteger os containers contra tipos de SIMD, ou tipos que contêm tipos de SIMD ( documentos Eigen3 ) e também precisa se proteger de sempre usando new em seus tipos, sobrecarregando o operador new para eles ( mais documentos Eigen3 ).

@gnzlbg obrigado, também fiquei confuso com o exemplo do smallvec. Isso exigiria tipos não móveis e algum tipo de alloca em Rust --- duas RFCs em revisão e depois mais trabalho de acompanhamento --- então não tenho escrúpulos em apontar isso agora. A estratégia existente da smallvec de sempre usar todo o espaço de pilha de que você precisa parece boa por enquanto.

Também concordo com @rkruppe que, em sua lista revisada, os novos recursos do alocador não precisam ser conhecidos pela coleção que usa o alocador. Às vezes, o Collection<Allocator> completo tem novas propriedades (existindo inteiramente na memória fixada, digamos), mas isso é apenas uma consequência natural do uso do alocador.

A única exceção que vejo aqui são os alocadores que alocam apenas um único tamanho / tipo (os da NVidia fazem isso, assim como os alocadores de bloco). Poderíamos ter um traço ObjAlloc<T> separado que é implementado em alocadores normais: impl<A: Alloc, T> ObjAlloc<T> for A . Então, as coleções usariam limites ObjAlloc se precisassem apenas alocar alguns itens. Mas, eu me sinto um pouco bobo, mesmo trazendo isso à tona, uma vez que deveria ser possível fazer para trás de forma compatível mais tarde.

Isso faz sentido?

Claro, mas não é realmente relevante para Rust, já que não temos construtores de movimento. Portanto, um alocador (móvel) que contém diretamente a memória para a qual distribui ponteiros não é possível, ponto final.

Por exemplo, em C ++ você não pode colocar os elementos de um vetor no segmento de memória estática de seu binário sem algo como um alocador de pilha (ainda que você possa escrever sua própria coleção que faça isso).

Esta não é uma mudança de comportamento. Existem muitos motivos válidos para controlar onde as coleções obtêm sua memória, mas todos eles relacionados a "externalidades" como desempenho, scripts de linker, controle sobre o layout de memória de todo o programa, etc.

Coisas como align_allocator permitem que você aloque em heap esses tipos e até mesmo os coloque em coleções std, sem invocar um comportamento indefinido, usando um alocador diferente.

É por isso que mencionei especificamente o cache_aligned_allocator de TBB e não o align_allocator de Eigen. cache_aligned_allocator não parece garantir nenhum alinhamento específico em sua documentação (apenas diz que é "tipicamente" 128 bytes), e mesmo que o fizesse normalmente não seria usado para este propósito (porque seu alinhamento é provavelmente muito grande para o comum Tipos SIMD e muito pequenos para coisas como DMA alinhado a página). Seu propósito, como você afirma, é evitar o falso compartilhamento.

@gnzlbg

FWIW, alocadores paramétricos são muito úteis para implementar alocadores.

Você poderia elaborar um pouco mais sobre o que você quer dizer? Existe uma razão pela qual você não pode parametrizar seus alocadores personalizados com o traço Alloc? Ou seu próprio traço de alocador personalizado e apenas implementar o traço de alocação nos alocadores finais (esses dois traços não precisam ser necessariamente iguais)?

Acho que não fui claro; deixe-me tentar explicar melhor. Digamos que estou implementando um alocador que pretendo usar:

  • Como o alocador global
  • Em um ambiente sem padrões

E digamos que eu gostaria de usar Vec sob o capô para implementar esse alocador. Não posso simplesmente usar Vec diretamente como existe hoje porque

  • Se eu for o alocador global, usá-lo apenas introduzirá uma dependência recursiva em mim
  • Se estou em um ambiente sem padrão, não existe Vec como existe hoje

Portanto, o que eu preciso é ser capaz de usar um Vec que está parametrizado em outro alocador que eu uso internamente para contabilidade interna simples. Este é o objetivo do bsalloc (e a fonte do nome - ele é usado para inicializar outros alocadores).

Em elfmalloc, ainda somos capazes de ser um alocador global:

  • Ao nos compilar, compilar estaticamente o jemalloc como o alocador global
  • Produz um arquivo de objeto compartilhado que pode ser carregado dinamicamente por outros programas

Note-se que, neste caso, é importante que nós não compilar-nos usando o alocador sistema como o alocador global, porque então, uma vez carregado, nós re-introduzir a dependência recursiva porque, naquele momento, somos o alocador sistema.

Mas não funciona quando:

  • Alguém quer nos usar como o alocador global em Rust da maneira "oficial" (em vez de criar um arquivo de objeto compartilhado primeiro)
  • Estamos em um ambiente sem padrões

OTOH, o padrão C ++ não suporta tipos superalinhados como tipos SIMD, e se você tentar alocar em heap um com novo, você invocará um comportamento indefinido (você precisa usar posix_memalign ou similar).

Como nosso traço Alloc atual leva o alinhamento como parâmetro, presumo que essa classe de problema (o problema "Não consigo trabalhar sem um alinhamento diferente") desaparece para nós.

@gnzlbg - um artigo abrangente (obrigado), mas nenhum dos casos de uso cobre a memória persistente *.

Este caso de uso deve ser considerado. Em particular, influencia fortemente o que a coisa certa a fazer é: -

  • Que mais de um alocador está em uso e, especialmente, quando usado esse alocador para memória persistente, ele nunca seria o alocador do sistema ; (na verdade, pode haver vários alocadores de memória persistente)
  • O custo de 'reimplementar' as coleções padrão é alto e leva a códigos incompatíveis com bibliotecas de terceiros.
  • A vida útil do alocador não é necessariamente 'static .
  • Os objetos armazenados na memória persistente precisam de um estado adicional que deve ser preenchido a partir do heap, ou seja, precisam do estado reinicializado. Isso é particularmente verdadeiro para mutexes e similares. O que antes era descartável não é mais descartado.

A Rust tem uma excelente oportunidade de aproveitar a iniciativa aqui e torná-la uma plataforma de primeira classe para o que irá substituir HDs, SSDs e até mesmo armazenamento conectado a PCI.

* Não é uma surpresa, realmente, porque até recentemente tem sido um pouco especial. Agora é amplamente compatível com Linux, FreeBSD e Windows.

@raphaelcohn

Este não é realmente o lugar para trabalhar a memória persistente. A sua não é a única escola de pensamento em relação à interface com a memória persistente - por exemplo, pode ser que a abordagem predominante seja simplesmente tratá-la como um disco mais rápido, por motivos de integridade de dados.

Se você tiver um caso de uso para usar memória persistente dessa forma, provavelmente seria melhor fazê-lo em outro lugar primeiro. Crie um protótipo, apresente algumas mudanças mais concretas na interface do alocador e, de preferência, faça com que essas mudanças valham o impacto que teriam no caso médio.

@rpjohnst

Discordo. Este é exatamente o tipo de lugar ao qual pertence. Quero evitar que seja tomada uma decisão que crie um design que seja o resultado de um foco muito estreito e da busca por evidências.

O atual Intel PMDK - que é onde se concentra um grande esforço para suporte de espaço de usuário de baixo nível - se aproxima muito mais como memória regular alocada com ponteiros - memória que é semelhante àquela via mmap , digamos. Na verdade, se alguém deseja trabalhar com memória persistente no Linux, então, acredito que é praticamente sua única escala no momento. Em essência, um dos kits de ferramentas mais avançados para usá-lo - o predominante, se preferir - o trata como memória alocada.

Quanto a prototipá-lo - bem, isso é exatamente o que eu disse que fiz: -

Tenho trabalhado recentemente em um wrapper Rust para um alocador de memória persistente (especificamente, libpmemcto).

(Você pode usar uma versão dos primeiros dias da minha caixa em https://crates.io/crates/nvml . Há muito mais experimentação no controle de origem no módulo cto_pool ).

Meu protótipo é construído em mente com o que é necessário para substituir um mecanismo de armazenamento de dados em um sistema de grande escala do mundo real. Uma mentalidade semelhante está por trás de muitos dos meus projetos de código aberto. Eu descobri ao longo de muitos anos as melhores bibliotecas, assim como os melhores padrões, aqueles que derivam _de_ uso real.

Nada como tentar ajustar um alocador do mundo real à interface atual. Francamente, a experiência de usar a interface Alloc e, em seguida, copiar Vec inteiro e ajustá-la foi dolorosa. Muitos lugares assumem que alocadores não são passados, por exemplo, Vec::new() .

Ao fazer isso, fiz algumas observações em meu comentário original sobre o que seria exigido de um alocador e o que seria exigido de um usuário de tal alocador. Eu acho que eles são muito válidos em um tópico de discussão sobre uma interface de alocador.

A boa notícia é que seus primeiros três pontos de https://github.com/rust-lang/rust/issues/32838#issuecomment -358940992 são compartilhados por outros casos de uso.

Só queria acrescentar que não adicionei memória não volátil à lista
porque a lista listou casos de uso de alocadores que parametrizam contêineres em
o mundo C ++ que são "amplamente" usados, pelo menos na minha experiência (aqueles
alocadores que mencionei são principalmente de bibliotecas muito populares usadas por muitos).
Embora eu saiba dos esforços do Intel SDK (algumas de suas bibliotecas
target C ++) Eu pessoalmente não conheço nenhum projeto que os use (eles têm
um alocador que pode ser usado com std :: vector? Eu não sei). Isso não
significa que eles não são usados ​​nem importantes. Eu estaria interessado em saber
sobre isso, mas o ponto principal da minha postagem foi que parametrizar
alocadores por contêineres é muito complexo, e devemos tentar fazer
progresso com alocadores de sistema sem fechar nenhuma porta para contêineres
(mas devemos resolver isso mais tarde).

No domingo, 21 de janeiro de 2018 às 17:36, John Ericson [email protected] escreveu:

A boa notícia são os seus primeiros 3 pontos de # 32838 (comentário)
https://github.com/rust-lang/rust/issues/32838#issuecomment-358940992
são compartilhados por outros casos de uso.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-359261305 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AA3Npk95PZBZcm7tknNp_Cqrs_3T1UkEks5tM2ekgaJpZM4IDYUN
.

Tentei ler a maior parte do que já foi escrito, então isso pode já estar aqui e nesse caso me desculpe se perdi, mas aqui vai:

Algo bastante comum para jogos (em C / C ++) é o uso de "alocação por frame scratch". Isso significa que há um alocador linear / bump que é usado para alocações que estão ativas por um certo período de tempo (em um quadro de jogo) e então "destruído".

Destruído, neste caso, significa que você redefiniu o alocador de volta à sua posição inicial. Não há "destruição" de objetos, pois esses objetos devem ser do tipo POD (portanto, nenhum destruidor está sendo executado)

Eu me pergunto se algo assim vai se encaixar no design do alocador atual em Rust?

(editar: deve haver NENHUMA destruição de objetos)

@emoon

Algo bastante comum para jogos (em C / C ++) é o uso de "alocação por frame scratch". Isso significa que há um alocador linear / bump que é usado para alocações que estão ativas por um certo período de tempo (em um quadro de jogo) e então "destruído".

Destruído, neste caso, significa que você redefiniu o alocador de volta à sua posição inicial. Há "destruição" de objetos, pois esses objetos devem ser do tipo POD (portanto, nenhum destruidor está sendo executado)

Deve ser factível. No topo da minha cabeça, você precisaria de um objeto para a arena em si e outro objeto que é uma alça por quadro na arena. Então, você poderia implementar Alloc para esse identificador, e assumindo que estava usando wrappers seguros de alto nível para alocação (por exemplo, imagine que Box se torna paramétrico em Alloc ), o os tempos de vida garantiriam que todos os objetos alocados fossem descartados antes que o identificador por quadro fosse descartado. Observe que dealloc ainda seria chamado para cada objeto, mas se dealloc fosse autônomo, toda a lógica de soltar e desalocar poderia ser totalmente ou quase totalmente otimizada.

Você também pode usar um tipo de ponteiro inteligente personalizado que não implemente Drop , o que tornaria muitas coisas mais fáceis em outros lugares.

Obrigado! Eu cometi um erro de digitação em minha postagem original. É para dizer que não destruição de objetos.

Para as pessoas que não são especialistas em alocadores e não podem seguir este segmento, qual é o consenso atual: planejamos oferecer suporte a alocadores personalizados para os tipos de coleção stdlib?

@alexreg Não tenho certeza de qual é o plano final, mas confirmamos 0 dificuldades técnicas em fazê-lo. OTOH, não temos uma boa maneira de expor isso em std porque as variáveis ​​de tipo padrão são suspeitas, mas não tenho nenhum problema em torná-lo apenas alloc por agora, então nós pode progredir no lado da lib sem impedimentos.

@ Ericson2314 Ok, bom saber. As variáveis ​​de tipo padrão já foram implementadas? Ou talvez na fase de RFC? Como você disse, se eles estiverem restritos apenas a coisas relacionadas a aloc / std::heap , deve estar tudo bem.

Eu realmente acho que AllocErr deve ser Error. Seria mais consistente com outros módulos (por exemplo, io).

impl Error for AllocError provavelmente faz sentido e não faz mal, mas eu pessoalmente considero o traço Error inútil.

Eu estava olhando para a função Layout :: from_size_align hoje, e a limitação " align não deve exceder 2 ^ 31 (ou seja, 1 << 31 )", não fazia sentido para mim. E git blame apontou para # 30170.

Devo dizer que foi uma mensagem de commit bastante enganosa, falando sobre align caber em um u32, o que é apenas incidental, quando a coisa real sendo "consertada" (mais contornada) é um mau comportamento do alocador do sistema.

O que me leva a esta observação: O item "OSX / alloc_system tem erros em alinhamentos enormes" aqui não deve ser verificado. Embora o problema direto tenha sido resolvido, não acho que a correção seja certa a longo prazo: porque um alocador de sistema se comporta mal não deve impedir a implementação de um alocador que se comporta. E a limitação arbitrária de Layout :: from_size_align faz isso.

@glandium É útil solicitar alinhamento para um múltiplo de 4 gigbytes ou mais?

Posso imaginar casos em que se queira ter uma alocação de 4GiB alinhada a 4GiB, o que não é possível atualmente, mas dificilmente mais. Mas não acho que limitações arbitrárias devam ser adicionadas apenas porque não pensamos nessas razões agora.

Posso imaginar casos em que se queira ter uma alocação de 4GiB alinhada a 4GiB

Quais são esses casos?

Posso imaginar casos em que se queira ter uma alocação de 4GiB alinhada a 4GiB

Quais são esses casos?

Concretamente, acabei de adicionar suporte para alinhamentos arbitrariamente grandes em mmap-alloc fim de oferecer suporte à alocação de blocos grandes e alinhados de memória para uso em elfmalloc . A ideia é fazer com que a placa de memória seja alinhada ao seu tamanho de forma que, dado um ponteiro para um objeto alocado a partir dessa placa, você simplesmente precise mascarar os bits baixos para encontrar a placa que o contém. Atualmente, não usamos placas com 4 GB de tamanho (para objetos tão grandes, vamos diretamente para o mmap), mas não há razão para que não possamos, e eu poderia imaginar totalmente um aplicativo com grandes requisitos de RAM que quisesse isso (isto é, se alocou objetos de vários GB com freqüência suficiente para não aceitar a sobrecarga do mmap).

Aqui está um possível caso de uso para um alinhamento> 4GiB: alinhamento com um limite de página grande. Já existem plataformas que suportam páginas> 4 GiB. Este documento da IBM diz "o processador POWER5 + oferece suporte a quatro tamanhos de página de memória virtual: 4 KB, 64 KB, 16 MB e 16 GB". Mesmo o x86-64 não está longe: "páginas enormes" normalmente têm 2 MiB, mas também suporta 1 GiB.

Todas as funções não digitadas no traço Alloc estão lidando com *mut u8 . O que significa que eles poderiam receber ou retornar ponteiros nulos, e o inferno iria explodir. Eles deveriam usar NonNull vez disso?

Há muitos indicadores de que eles poderiam retornar, dos quais todo o inferno
se soltar.
No domingo, 4 de março de 2018 às 3h56, Mike Hommey [email protected] escreveu:

Todas as funções não digitadas no traço Alloc estão lidando com * mut u8.
O que significa que eles poderiam pegar ou retornar ponteiros nulos, e todo o inferno
se soltar. Eles deveriam usar NonNull em vez disso?

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370223269 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABY2UR2dRxDtdACeRUh_djM-DExRuLxiks5ta9aFgaJpZM4IDYUN
.

Uma razão mais convincente para usar NonNull é que isso permitiria os Result s atualmente retornados de Alloc métodos (ou Options , se mudarmos para aquele em futuro) seja menor.

Uma razão mais convincente para usar NonNull é que isso permitiria que os resultados atualmente retornados dos métodos Alloc (ou Opções, se mudarmos para isso no futuro) sejam menores.

Eu não acho que seria porque AllocErr tem duas variantes.

Existem muitos indícios de que eles poderiam retornar, dos quais todo o inferno se soltaria.

Mas um ponteiro nulo é claramente mais errado do que qualquer outro ponteiro.

Gosto de pensar que o sistema de tipo de ferrugem ajuda com armas de fogo e é usado para codificar invariantes. A documentação para alloc diz claramente "Se este método retornar um Ok(addr) , então o endereço retornado será um endereço não nulo", mas seu tipo de retorno não. Como as coisas estão, Ok(malloc(layout.size())) seria uma implementação válida, quando claramente não é.

Observe, também há notas sobre o tamanho de Layout precisa ser diferente de zero, então eu também diria que deve codificar isso como um diferente de zero.

Não é porque todas essas funções são inerentemente inseguras que não devemos ter alguma prevenção de footgun.

De todos os erros possíveis ao usar (editar: e implementar) alocadores, passar um ponteiro nulo é um dos mais fáceis de rastrear (você sempre obtém um segfault limpo na desreferência, pelo menos se você tiver um MMU e não o fez coisas muito estranhas com ele), e geralmente um dos mais triviais para consertar também. É verdade que interfaces inseguras podem tentar prevenir canhões, mas este canhão parece desproporcionalmente pequeno (em comparação com os outros erros possíveis e com a verbosidade de codificar esta invariante no sistema de tipos).

Além disso, parece provável que as implementações do alocador usariam apenas o construtor não verificado de NonNull "para desempenho": já que em um alocador correto nunca retornaria nulo de qualquer maneira, ele iria querer pular o NonNell::new(...).unwrap() . Nesse caso, você não terá nenhuma prevenção tangível de footgun, apenas mais clichês. (Os benefícios do tamanho de Result , se reais, ainda podem ser uma razão convincente para isso.)

implementações de alocador usariam apenas o construtor não verificado de NonNull

A questão é menos ajudar na implementação do alocador do que ajudar seus usuários. Se MyVec contém NonNull<T> e Heap.alloc() já retorna NonNull , aquela chamada a menos marcada ou não-segura que preciso fazer.

Observe que os ponteiros não são apenas tipos de retorno, eles também são tipos de entrada para, por exemplo, dealloc e realloc . Essas funções devem ser protegidas contra a possibilidade de sua entrada ser nula ou não? A documentação tenderia a dizer não, mas o sistema de tipos tenderia a dizer sim.

De forma bastante semelhante com layout.size (). As funções de alocação devem lidar com o tamanho solicitado sendo 0 de alguma forma, ou não?

(Os benefícios do tamanho do Resultado, se reais, ainda podem ser uma razão convincente para isso.)

Duvido que haja benefícios de tamanho, mas com algo como o # 48741, haveria benefícios do codegen.

Se continuarmos com esse princípio de ser mais flexível para os usuários da API, os ponteiros devem ser NonNull em tipos de retorno, mas não em argumentos. (Isso não significa que esses argumentos devam ser verificados em tempo de execução.)

Acho que a abordagem da lei de Postel é a errada a se tomar aqui. Existe algum
caso em que passar um ponteiro nulo para um método Alloc é válido? Se não,
então essa flexibilidade é basicamente apenas dar à espingarda um pouco mais
gatilho sensível.

Em 5 de março de 2018, às 8h, "Simon Sapin" [email protected] escreveu:

Se continuarmos esse princípio de ser mais flexível para os usuários da API,
os ponteiros devem ser NonNull em tipos de retorno, mas não em argumentos. (Este
não significa que esses argumentos devam ser verificados em tempo de execução.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370327018 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AA_2L8zrOLyUv5mUc_kiiXOAn1f60k9Uks5tbOJ0gaJpZM4IDYUN
.

A questão é menos ajudar na implementação do alocador do que ajudar seus usuários. Se MyVec contém um NonNulle Heap.alloc () já retorna um NonNull, aquela chamada a menos marcada ou insegura-desmarcada que preciso fazer.

Ah, isso faz sentido. Não conserta a espingarda, mas centraliza a responsabilidade por ela.

Observe que os ponteiros não são apenas tipos de retorno, eles também são tipos de entrada para, por exemplo, desalocar e realocar. Essas funções devem ser protegidas contra a possibilidade de sua entrada ser nula ou não? A documentação tenderia a dizer não, mas o sistema de tipos tenderia a dizer sim.

Existe algum caso em que passar um ponteiro nulo para um método Alloc é válido? Se não, essa flexibilidade é basicamente dar à metralhadora um gatilho um pouco mais sensível.

O usuário absolutamente tem que ler a documentação e manter as invariáveis ​​em mente. Muitas invariantes não podem ser aplicadas via sistema de tipos - se pudessem, a função não seria insegura para começar. Portanto, esta é apenas uma questão de saber se colocar NonNull em qualquer interface ajudará os usuários

  • lembrando-os de ler a documentação e pensar sobre as invariáveis
  • oferecendo conveniência (valor de retorno de @SimonSapin point wrt aloc)
  • dando alguma vantagem material (por exemplo, otimizações de layout)

Não vejo nenhuma vantagem forte em transformar, por exemplo, o argumento de dealloc em NonNull . Vejo quase duas classes de uso dessa API:

  1. Uso relativamente trivial, onde você chama alloc , armazena o ponteiro retornado em algum lugar, e depois de um tempo passa o ponteiro armazenado para dealloc .
  2. Cenários complicados envolvendo FFI, muita aritmética de ponteiro, etc., onde há uma lógica significativa envolvida em garantir que você passe a coisa certa para dealloc no final.

Pegar NonNull aqui basicamente ajuda apenas o primeiro tipo de caso de uso, porque aqueles vão armazenar NonNull em algum lugar agradável e apenas passá-lo para NonNull inalterado. Teoricamente, isso poderia evitar alguns erros de digitação (passando foo quando você quis dizer bar ) se você estiver fazendo malabarismos com vários ponteiros e apenas um deles for NonNull , mas isso não parece muito comum ou importante. A desvantagem de dealloc pegar um ponteiro bruto (assumindo que alloc retorne NonNull que @SimonSapin me convenceu que deveria acontecer) seria que requer um as_ptr em a chamada dealloc, que é potencialmente irritante, mas não afeta a segurança de qualquer maneira.

O segundo tipo de caso de uso não é ajudado porque provavelmente não pode continuar usando NonNull ao longo de todo o processo, então teria que recriar manualmente um NonNull do ponteiro bruto que obteve por qualquer meio. Como argumentei antes, isso provavelmente se tornaria uma afirmação / unsafe não verificada em vez de uma verificação de tempo de execução real, portanto, nenhuma pistola é evitada.

Isso não quer dizer que eu seja a favor de dealloc aceitar um indicador bruto. Eu simplesmente não vejo nenhuma das vantagens reivindicadas em relação às armas de fogo. A consistência dos tipos provavelmente vence por padrão.

Sinto muito, mas li isso como "Muitas invariantes não podem ser aplicadas através do sistema de tipos ... portanto, nem vamos tentar". Não deixe o perfeito ser inimigo do bom!

Acho que é mais sobre as compensações entre as garantias fornecidas por NonNull e a ergonomia perdida por ter que fazer a transição entre NonNull e indicadores brutos. Eu não tenho uma opinião particularmente forte de qualquer maneira - nenhum dos lados parece irracional.

@cramertj Sim, mas eu realmente não acredito na premissa desse tipo de argumento. As pessoas dizem que Alloc é para casos de uso obscuros, ocultos e em grande parte inseguros. Bem, em um código obscuro e difícil de ler, eu gostaria de ter o máximo de segurança possível --- precisamente porque eles são tão raramente tocados que provavelmente o autor original não estará por perto. Por outro lado, se o código está sendo lido anos depois, estrague a egonomia. Na verdade, é contraproducente. O código deve se esforçar para ser muito explícito, para que um leitor desconhecido possa descobrir melhor o que está acontecendo. Menos ruído <invariantes mais claros.

O segundo tipo de caso de uso não é ajudado porque provavelmente não pode continuar usando NonNull ao longo de todo o processo, então teria que recriar manualmente um NonNull do ponteiro bruto que obteve por qualquer meio.

Isso é simplesmente uma falha de coordenação, não uma inevitabilidade técnica. Claro, agora muitas APIs inseguras podem usar ponteiros brutos. Portanto, algo deve liderar a mudança para uma interface superior usando NonNull ou outros invólucros. Então, outro código pode seguir o exemplo mais facilmente. Não vejo razão para recorrer constantemente a indicadores brutos não informativos e difíceis de ler em código inexplorado, totalmente Rust e inseguro.

Oi!

Só quero dizer que, como autor / mantenedor de um alocador customizado Rust, sou a favor de NonNull . Basicamente, por todas as razões que já foram apresentadas neste tópico.

Além disso, gostaria de salientar que @glandium é o mantenedor do fork do jemalloc do firefox e tem muita experiência em hackear alocadores também.

Eu poderia escrever muito mais respondendo a @ Ericson2314 e outros, mas está rapidamente se tornando um debate muito distanciado e filosófico, então estou segurança de NonNull neste tipo de API (há outros benefícios, é claro). Isso não quer dizer que não haja benefícios de segurança, mas, como @cramertj disse, há compensações e acho que o lado "pró" é exagerado. Independentemente disso, eu já disse que inclino a usar NonNull em vários lugares por outras razões - pela razão que @SimonSapin cedeu em alloc , em dealloc para consistência. Portanto, vamos fazer isso e não sair pela tangente.

Se houver alguns NonNull casos de uso com os quais todos estão envolvidos, isso é um grande começo.

Provavelmente vamos querer atualizar Unique e amigos para usar NonNull vez de NonZero para manter o atrito pelo menos dentro de liballoc baixo. Mas isso parece algo que já estamos fazendo em um nível de abstração sobre o alocador, e não consigo pensar em nenhuma razão para não fazer isso também no nível do alocador. Parece uma mudança razoável para mim.

(Já existem duas implementações do traço From que convertem com segurança entre Unique<T> e NonNull<T> .)

Considerando que preciso de algo muito parecido com a API de alocador em ferrugem estável, extraí o código do repositório de ferrugem e o coloquei em uma caixa separada:

Isso / poderia / ser usado para iterar em alterações experimentais na API, mas, a partir de agora, é uma cópia simples do que está no repositório de ferrugem.

[Fora do tópico] Sim, seria bom se std pudesse usar caixas de código estáveis ​​como essa, para que possamos experimentar interfaces instáveis ​​em código estável. Esta é uma das razões pelas quais gosto de ter std uma fachada.

std poderia depender de uma cópia de uma caixa de crates.io, mas se o seu programa também depender da mesma caixa, não "pareceria" a mesma caixa / tipos / características para enferrujar de qualquer maneira, então eu não não vejo como isso ajudaria. De qualquer forma, independentemente da fachada, tornar os recursos instáveis ​​indisponíveis no canal estável é uma escolha muito deliberada, não um acidente.

Parece que temos algum acordo sobre o uso de NonNull. Qual é o caminho a seguir para que isso realmente aconteça? apenas um PR fazendo isso? um RFC?

De forma independente, estive observando alguns assemblies gerados a partir de coisas do Boxing, e os caminhos de erro são bastante grandes. Algo deve ser feito sobre isso?

Exemplos:

pub fn bar() -> Box<[u8]> {
    vec![0; 42].into_boxed_slice()
}

pub fn qux() -> Box<[u8]> {
    Box::new([0; 42])
}

compila para:

example::bar:
  sub rsp, 56
  lea rdx, [rsp + 8]
  mov edi, 42
  mov esi, 1
  call __rust_alloc_zeroed<strong i="11">@PLT</strong>
  test rax, rax
  je .LBB1_1
  mov edx, 42
  add rsp, 56
  ret
.LBB1_1:
  mov rax, qword ptr [rsp + 8]
  movups xmm0, xmmword ptr [rsp + 16]
  movaps xmmword ptr [rsp + 32], xmm0
  mov qword ptr [rsp + 8], rax
  movaps xmm0, xmmword ptr [rsp + 32]
  movups xmmword ptr [rsp + 16], xmm0
  lea rdi, [rsp + 8]
  call __rust_oom<strong i="12">@PLT</strong>
  ud2

example::qux:
  sub rsp, 104
  xorps xmm0, xmm0
  movups xmmword ptr [rsp + 58], xmm0
  movaps xmmword ptr [rsp + 48], xmm0
  movaps xmmword ptr [rsp + 32], xmm0
  lea rdx, [rsp + 8]
  mov edi, 42
  mov esi, 1
  call __rust_alloc<strong i="13">@PLT</strong>
  test rax, rax
  je .LBB2_1
  movups xmm0, xmmword ptr [rsp + 58]
  movups xmmword ptr [rax + 26], xmm0
  movaps xmm0, xmmword ptr [rsp + 32]
  movaps xmm1, xmmword ptr [rsp + 48]
  movups xmmword ptr [rax + 16], xmm1
  movups xmmword ptr [rax], xmm0
  mov edx, 42
  add rsp, 104
  ret
.LBB2_1:
  movups xmm0, xmmword ptr [rsp + 16]
  movaps xmmword ptr [rsp + 80], xmm0
  movaps xmm0, xmmword ptr [rsp + 80]
  movups xmmword ptr [rsp + 16], xmm0
  lea rdi, [rsp + 8]
  call __rust_oom<strong i="14">@PLT</strong>
  ud2

Essa é uma grande quantidade de código para adicionar a qualquer lugar criando caixas. Compare com 1,19, que não tinha a API de alocador:

example::bar:
  push rax
  mov edi, 42
  mov esi, 1
  call __rust_allocate_zeroed<strong i="18">@PLT</strong>
  test rax, rax
  je .LBB1_2
  mov edx, 42
  pop rcx
  ret
.LBB1_2:
  call alloc::oom::oom<strong i="19">@PLT</strong>

example::qux:
  sub rsp, 56
  xorps xmm0, xmm0
  movups xmmword ptr [rsp + 26], xmm0
  movaps xmmword ptr [rsp + 16], xmm0
  movaps xmmword ptr [rsp], xmm0
  mov edi, 42
  mov esi, 1
  call __rust_allocate<strong i="20">@PLT</strong>
  test rax, rax
  je .LBB2_2
  movups xmm0, xmmword ptr [rsp + 26]
  movups xmmword ptr [rax + 26], xmm0
  movaps xmm0, xmmword ptr [rsp]
  movaps xmm1, xmmword ptr [rsp + 16]
  movups xmmword ptr [rax + 16], xmm1
  movups xmmword ptr [rax], xmm0
  mov edx, 42
  add rsp, 56
  ret
.LBB2_2:
  call alloc::oom::oom<strong i="21">@PLT</strong>

Se isso for realmente significativo, é realmente irritante. No entanto, talvez o LLVM otimize isso para programas maiores?

Há 1439 chamadas para __rust_oom no Firefox todas as noites. No entanto, o Firefox não usa alocador de ferrugem, então recebemos chamadas diretas para malloc / calloc, seguidas por uma verificação nula que salta para o código de preparação oom, que geralmente é dois movq e um lea, preenchendo o AllocErr e obtendo seu endereço para passá-lo para __rust__oom . Esse é o melhor cenário, essencialmente, mas ainda são 20 bytes de código de máquina para os dois movq e o lea.

Se eu olhar para ripgrep, existem 85, e todos eles estão em funções _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn idênticas. Todos eles têm 16 bytes de comprimento. Existem 685 chamadas para essas funções de wrapper, a maioria das quais são precedidas por código semelhante ao que colei em https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485.

@nox estava olhando hoje para habilitar o passe mergefunc llvm, eu me pergunto se isso faz alguma diferença aqui.

mergefunc aparentemente não se livra das múltiplas funções _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn idênticas (tentadas com -C passes=mergefunc em RUSTFLAGS ).

Mas o que faz uma grande diferença é o LTO, que é realmente o que faz o Firefox chamar malloc diretamente, deixando a criação de AllocErr à direita antes de chamar __rust_oom . Isso também torna a criação do Layout desnecessária antes de chamar o alocador, deixando-o assim ao preencher o AllocErr .

Isso me faz pensar que as funções de alocação, exceto __rust_oom , provavelmente devem ser marcadas inline.

BTW, tendo olhado o código gerado para o Firefox, estou pensando que seria idealmente desejável usar moz_xmalloc vez de malloc . Isso não é possível sem uma combinação das características de alocador e ser capaz de substituir o alocador de heap global, mas traz a possível necessidade de um tipo de erro personalizado para a característica de alocador: moz_xmalloc é infalível e nunca retorna no caso de fracasso. IOW, ele lida com o próprio OOM, e o código de ferrugem não precisaria chamar __rust_oom nesse caso. O que tornaria desejável que as funções de alocador retornassem opcionalmente ! vez de AllocErr .

Discutimos fazer AllocErr uma estrutura de tamanho zero, o que também pode ajudar aqui. Com o ponteiro também feito em NonNull , todo o valor de retorno pode ter o tamanho do ponteiro.

https://github.com/rust-lang/rust/pull/49669 faz uma série de alterações nessas APIs, com o objetivo de estabilizar um subconjunto que cobre alocadores globais. Rastreamento de problema para esse subconjunto: https://github.com/rust-lang/rust/issues/49668. Em particular, um novo traço GlobalAlloc é introduzido.

Este PR nos permitirá fazer coisas como Vec::new_with_alloc(alloc) where alloc: Alloc breve?

@alexreg não

@sfackler Hmm, por que não? O que precisamos antes de fazer isso? Eu realmente não entendo o ponto deste PR de outra forma, a menos que seja simplesmente para alterar o alocador global.

@alexreg

Eu realmente não entendo o ponto deste PR de outra forma, a menos que seja simplesmente para alterar o alocador global.

Acho que é simplesmente para alterar o alocador global.

@alexreg Se você quer dizer estável, há uma série de questões de design não resolvidas que não estamos prontos para estabilizar. No Nightly, isso é suportado por RawVec e provavelmente pode ser adicionado como #[unstable] para Vec para qualquer um que queira trabalhar nisso.

E sim, como mencionado no PR, seu objetivo é permitir a alteração do alocador global, ou alocação (por exemplo, em um tipo de coleção personalizado) sem absolver Vec::with_capacity .

FWIW, a caixa allocator_api mencionada em https://github.com/rust-lang/rust/issues/32838#issuecomment -376793369 tem RawVec<T, A> e Box<T, A> no mestre ramo (ainda não lançado). Estou pensando nisso como uma incubadora para a aparência de coleções genéricas sobre o tipo de alocação (além do fato de que eu preciso de um tipo Box<T, A> para ferrugem estável). Eu não comecei a portar vec.rs para adicionar Vec<T, A> ainda, mas PRs são bem-vindos. vec.rs é grande.

Observarei que os "problemas" do codegen mencionados em https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485 devem ter desaparecido com as alterações em # 49669.

Agora, com um pouco mais de reflexão sobre o uso do traço Alloc para ajudar a implementar um alocador em camadas, há duas coisas que acho que seriam úteis (pelo menos para mim):

  • como mencionado anteriormente, opcionalmente sendo capaz de especificar um tipo AllocErr . Isso pode ser útil para torná-lo ! ou, agora que AllocErr está vazio, opcionalmente, fazer com que ele transmita mais informações do que "falhou".
  • sendo opcionalmente capaz de especificar um tipo diferente de Layout . Imagine que você tenha duas camadas de alocadores: uma para alocações de páginas e outra para regiões maiores. O último pode contar com o primeiro, mas se ambos tomarem o mesmo tipo Layout , então ambas as camadas precisam fazer sua própria validação: no nível mais baixo, esse tamanho e alinhamento são múltiplos do tamanho da página, e o nível superior, que o tamanho e o alinhamento correspondem aos requisitos das regiões maiores. Mas essas verificações são redundantes. Com tipos Layout , a validação poderia ser delegada à criação Layout vez de no próprio alocador, e as conversões entre os tipos Layout permitiriam pular as verificações redundantes.

@cramertj @SimonSapin @glandium Ok, obrigado pelo esclarecimento. Posso apenas enviar um PR para alguns dos outros tipos principais de coleções. É melhor fazer isso em seu alocador-api repo / crate, @glandium ou rust master?

@alexreg considerando a quantidade de alterações de interrupção para o traço Alloc em # 49669, é provavelmente melhor esperar que ele se fundir primeiro.

@glandium Bastante justo. Isso não parece muito longe de pousar. Acabei de notar o https://github.com/pnkfelix/collections-prime repo também ... o que é isso em relação ao seu?

Eu acrescentaria mais uma questão em aberto:

  • Alloc::oom pode entrar em pânico? Atualmente os documentos dizem que este método deve abortar o processo. Isso tem implicações para o código que usa alocadores, uma vez que eles devem ser projetados para lidar com o desenrolamento adequadamente sem vazar memória.

Acho que devemos permitir o pânico, pois uma falha em um alocador local não significa necessariamente que o alocador global também falhará. No pior caso, o alocador global oom será chamado, o que irá abortar o processo (fazer o contrário quebraria o código existente).

@alexreg Não é. Parece ser apenas uma cópia simples do que está em std / aloc / coleções. Bem, uma cópia de dois anos dele. Minha caixa é muito mais limitada em escopo (a versão publicada tem apenas o traço Alloc algumas semanas atrás, o branch master tem apenas RawVec e Box no topo que), e um dos meus objetivos é mantê-lo construindo com ferrugem estável.

@glandium Ok, nesse caso provavelmente faz sentido para mim esperar até que o PR chegue, então crie um PR contra o rust master e marque você, para que você saiba quando ele for mesclado com o master (e poderá então mesclá-lo com sua caixa) , justo?

@alexreg faz sentido. Você / poderia / começar a trabalhar nisso agora, mas isso provavelmente induziria a alguma agitação em sua extremidade se / quando a apresentação de bicicletas mudasse as coisas nesse PR.

@glandium Eu tenho outras coisas para me manter ocupado com Rust por enquanto, mas estarei nisso quando o PR for aprovado. Será ótimo obter alocação / coleções de heap genérico de alocador tanto noturno quanto estável em breve. :-)

O Alloc :: oom pode entrar em pânico? Atualmente os documentos dizem que este método deve abortar o processo. Isso tem implicações para o código que usa alocadores, uma vez que eles devem ser projetados para lidar com o desenrolamento adequadamente sem vazar memória.

@Amanieu Este RFC foi mesclado: https://github.com/rust-lang/rfcs/pull/2116 Os documentos e a implementação podem não ter sido atualizados ainda.

Há uma alteração na API para a qual estou pensando em enviar um PR:

Divida a característica Alloc em duas partes: "implementação" e "ajudantes". O primeiro seria funções como alloc , dealloc , realloc , etc. e o último, alloc_one , dealloc_one , alloc_array , etc. Embora haja alguns benefícios hipotéticos em poder ter uma implementação personalizada para o último, está longe de ser a necessidade mais comum, e quando você precisa implementar wrappers genéricos (que descobri ser incrivelmente comuns, até o ponto em que comecei a escrever uma derivação personalizada para isso), você ainda precisa implementar todos eles porque o wrappee pode estar personalizando-os.

OTOH, se um implementador do traço Alloc tentar fazer coisas extravagantes, por exemplo, alloc_one , eles não garantem que dealloc_one será chamado para essa alocação. Há múltiplas razões para isto:

  • Os ajudantes não são usados ​​de forma consistente. Apenas um exemplo, raw_vec usa uma mistura de alloc_array , alloc / alloc_zeroed , mas usa apenas dealloc .
  • Mesmo com o uso consistente de, por exemplo, alloc_array / dealloc_array , ainda é possível converter com segurança Vec em Box , que então usaria dealloc .
  • Então, há algumas partes da API que simplesmente não existem (nenhuma versão zerada de alloc_one / alloc_array )

Portanto, embora haja casos de uso reais para a especialização de, por exemplo, alloc_one (e, na verdade, eu realmente preciso do mozjemalloc), é melhor usar um alocador especializado.

Na verdade, é pior do que isso, no repositório de ferrugem, há exatamente um uso de alloc_array e nenhum uso de alloc_one , dealloc_one , realloc_array , dealloc_array . Nem mesmo a sintaxe da caixa usa alloc_one , ela usa exchange_malloc , que usa size e align . Portanto, essas funções são mais convenientes para os clientes do que para os implementadores.

Com algo como impl<A: Alloc> AllocHelpers for A (ou AllocExt , qualquer que seja o nome escolhido), ainda teríamos a conveniência dessas funções para os clientes, embora não permitíssemos que implementadores atirassem no próprio pé se pensassem eles fariam coisas fantásticas substituindo-os (e tornando mais fácil para as pessoas que implementam alocadores de proxy).

Há uma alteração na API para a qual estou pensando em enviar um PR para

Fez isso em # 50436

@glandium

(e para falar a verdade, eu realmente preciso de mozjemalloc),

Você poderia elaborar este caso de uso?

mozjemalloc tem um alocador de base que vaza propositalmente. Exceto por um tipo de objeto, onde mantém uma lista livre. Posso fazer isso sobrepondo alocadores em vez de fazer truques com alloc_one .

É necessário desalocar com o alinhamento exato que você alocou?

Apenas para reforçar que a resposta a esta pergunta é SIM , tenho esta linda citação da própria Microsoft :

align_alloc () provavelmente nunca será implementado, como C11 especificou de uma forma que é incompatível com nossa implementação (ou seja, que free () deve ser capaz de lidar com alocações altamente alinhadas)

Usar o alocador do sistema no Windows sempre exigirá saber o alinhamento ao desalocar, a fim de desalocar corretamente as alocações altamente alinhadas, portanto, podemos apenas marcar essa questão como resolvida?

Usar o alocador do sistema no Windows sempre exigirá saber o alinhamento ao desalocar, a fim de desalocar corretamente as alocações altamente alinhadas, portanto, podemos apenas marcar essa questão como resolvida?

É uma pena, mas é assim que está. Vamos desistir dos vetores superalinhados então. :confuso:

Vamos desistir dos vetores superalinhados então

Por quê? Você só precisa de Vec<T, OverAlignedAlloc<U16>> que aloca e desaloca com superalinhamento.

Por quê? Você só precisa de Vec<T, OverAlignedAlloc<U16>> que aloca e desaloca com superalinhamento.

Eu deveria ter sido mais específico. Eu quis dizer mover vetores superalinhados para uma API fora do seu controle, ou seja, uma que recebe Vec<T> e não Vec<T, OverAlignedAlloc<U16>> . (Por exemplo CString::new() .)

Você deve preferir usar

#[repr(align(16))]
struct OverAligned16<T>(T);

e então Vec<OverAligned16<T>> .

Você deve preferir usar

Depende. Suponha que você queira usar AVX intrínseco (256 bits de largura, requisito de alinhamento de 32 bytes) em um vetor de f32 s:

  • Vec<T, OverAlignedAlloc<U32>> resolve o problema, pode-se usar os intrínsecos AVX diretamente nos elementos do vetor (em particular, cargas de memória alinhadas), e o vetor ainda é desfeito em uma fatia &[f32] tornando-o ergonômico de usar.
  • Vec<OverAligned32<f32>> não resolve realmente o problema. Cada f32 ocupa 32 bytes de espaço devido ao requisito de alinhamento. O preenchimento introduzido evita o uso direto de operações AVX, pois os f32 s não estão mais na memória contínua. E eu pessoalmente acho o erro de &[OverAligned32<f32>] um pouco tedioso de lidar.

Para um único elemento em Box , Box<T, OverAligned<U32>> vs Box<OverAligned32<T>> , ambas as abordagens são mais equivalentes, e a segunda abordagem pode de fato ser preferível. Em qualquer caso, é bom ter as duas opções.

Postado estas alterações escritas no traço Alloc: https://internals.rust-lang.org/t/pre-rfc-changing-the-alloc-trait/7487

A postagem de rastreamento no topo desta edição está terrivelmente desatualizada (foi editada pela última vez em 2016). Precisamos de uma lista atualizada de preocupações ativas para continuar a discussão produtivamente.

A discussão também se beneficiaria significativamente de um documento de design atualizado, contendo as questões atuais não resolvidas e a justificativa para as decisões de design.

Existem vários tópicos de diffs de "o que está atualmente implementado nas noites" a "o que foi proposto no Alloc RFC original" gerando milhares de comentários em diferentes canais (repo rfc, problema de rastreamento de idioma de ferrugem, alocação global RFC, postagens internas, muitos enormes PRs, etc.), e o que está sendo estabilizado na RFC GlobalAlloc não se parece muito com o que foi proposto na RFC original.

Isso é algo que precisamos de qualquer maneira para terminar de atualizar os documentos e a referência, e seria útil nas discussões atuais também.

Acho que antes mesmo de pensarmos em estabilizar o traço Alloc , devemos primeiro tentar implementar o suporte a alocador em todas as coleções de biblioteca padrão. Isso deve nos dar alguma experiência sobre como esse traço será usado na prática.

Acho que antes mesmo de pensarmos em estabilizar o traço Alloc , devemos primeiro tentar implementar o suporte a alocador em todas as coleções de biblioteca padrão. Isso deve nos dar alguma experiência sobre como esse traço será usado na prática.

Sim absolutamente. Especialmente Box , já que ainda não sabemos como evitar que Box<T, A> ocupe duas palavras.

Sim absolutamente. Principalmente o Box, já que ainda não sabemos como evitar que o Boxpegue duas palavras.

Não acho que devemos nos preocupar com o tamanho de Box<T, A> para a implementação inicial, mas isso é algo que pode ser adicionado posteriormente de forma compatível com versões anteriores, adicionando uma característica DeAlloc que só suporta desalocação.

Exemplo:

trait DeAlloc {
    fn dealloc(&mut self, ptr: NonNull<Opaque>, layout: Layout);
}

trait Alloc {
    // In addition to the existing trait items
    type DeAlloc: DeAlloc = Self;
    fn into_dealloc(self) -> Self::DeAlloc {
        self
    }
}

impl<T: Alloc> DeAlloc for T {
    fn dealloc(&mut self, ptr: NonNull<Opaque>, layout: Layout) {
        Alloc::dealloc(self, ptr, layout);
    }
}

Acho que antes mesmo de pensarmos em estabilizar o traço Alloc, devemos primeiro tentar implementar o suporte a alocador em todas as coleções de biblioteca padrão. Isso deve nos dar alguma experiência sobre como esse traço será usado na prática.

Acho que @ Ericson2314 está trabalhando nisso, por https://github.com/rust-lang/rust/issues/42774. Seria bom receber uma atualização dele.

Não acho que devemos nos preocupar com o tamanho de Box<T, A> para a implementação inicial, mas isso é algo que pode ser adicionado posteriormente de forma compatível com versões anteriores, adicionando uma característica DeAlloc que só suporta desalocação.

Essa é uma abordagem, mas não está claro para mim se é definitivamente a melhor. Tem as desvantagens distintas, por exemplo, que a) só funciona quando um ponteiro -> pesquisa de alocador é possível (isso não é verdade, por exemplo, na maioria dos alocadores de arena) e, b) adiciona uma sobrecarga significativa a dealloc (ou seja, para fazer a pesquisa inversa). Pode acabar sendo o caso que a melhor solução para este problema seja um efeito de propósito mais geral ou sistema de contexto como esta proposta ou esta proposta . Ou talvez algo totalmente diferente. Portanto, não acho que devemos supor que isso será fácil de resolver de uma maneira compatível com as versões anteriores da encarnação atual do traço Alloc .

@joshlf Considerando o fato de que Box<T, A> só tem acesso a si mesmo quando é descartado, esta é a melhor coisa que podemos fazer apenas com código seguro. Tal padrão pode ser útil para alocadores tipo arena que têm um não operacional dealloc e apenas memória livre quando o alocador é descartado.

Para sistemas mais complicados em que o alocador pertence a um contêiner (por exemplo, LinkedList ) e gerenciam várias alocações, espero que Box não seja usado internamente. Em vez disso, os LinkedList internos usarão ponteiros brutos que são alocados e liberados com a instância Alloc que está contida no objeto LinkedList . Isso evitará dobrar o tamanho de cada ponteiro.

Considerando o fato de que Box<T, A> só tem acesso a si mesmo quando é descartado, esta é a melhor coisa que podemos fazer apenas com código seguro. Tal padrão pode ser útil para alocadores tipo arena que têm um não operacional dealloc e apenas memória livre quando o alocador é descartado.

Certo, mas Box não sabe que dealloc é autônomo.

Para sistemas mais complicados em que o alocador pertence a um contêiner (por exemplo, LinkedList ) e gerencia várias alocações, espero que o Box não seja usado internamente. Em vez disso, os LinkedList internos usarão ponteiros brutos que são alocados e liberados com a instância Alloc que está contida no objeto LinkedList . Isso evitará dobrar o tamanho de cada ponteiro.

Eu acho que seria realmente uma vergonha exigir que as pessoas usem código inseguro para escrever qualquer coleção. Se o objetivo é tornar todas as coleções (provavelmente incluindo aquelas fora da biblioteca padrão) opcionalmente paramétricas em um alocador, e Box não é um alocador-paramétrico, então um autor de coleções não deve usar Box todo ou usar código inseguro (e tenha em mente que lembrar de sempre liberar coisas é um dos tipos mais comuns de insegurança de memória em C e C ++, portanto, é difícil acertar um código inseguro). Isso parece uma barganha infeliz.

Certo, mas Box não sabe que desalocar não funciona.

Por que não adaptaria o que C ++ unique_ptr faz?
Isto é: para armazenar o ponteiro para o alocador se ele for "com estado", e não o armazenar se o alocador for "sem estado"
(por exemplo, envoltório global em torno de malloc ou mmap ).
Isso exigiria a divisão do treinamento de Alloc atual em duas características: StatefulAlloc e StatelessAlloc .
Eu percebo que é muito rude e deselegante (e provavelmente alguém já o propôs em discussões anteriores).
Apesar de sua deselegância, esta solução é simples e compatível com versões anteriores (sem penalidades de desempenho).

Eu acho que seria realmente uma vergonha exigir que as pessoas usem código inseguro para escrever qualquer coleção. Se o objetivo é tornar todas as coleções (presumivelmente incluindo aquelas fora da biblioteca padrão) opcionalmente paramétricas em um alocador, e Box não é alocador-paramétrico, então um autor de coleções não deve usar Box de forma alguma ou usar código inseguro (e tenha em mente que lembrar de sempre liberar coisas é um dos tipos mais comuns de insegurança de memória em C e C ++, portanto, é difícil acertar um código inseguro nisso). Isso parece uma barganha infeliz.

Temo que uma implementação de efeito ou sistema de contexto que possa permitir a escrita de contêineres baseados em nós como listas, árvores, etc, de maneira segura pode levar muito tempo (se for possível em princípio).
Não vi nenhum artigo ou linguagem acadêmica que resolva esse problema (por favor, corrija-me se esse tipo de trabalho realmente existir).

Portanto, recorrer a unsafe na implementação de contêineres baseados em nó pode ser um mal necessário, pelo menos em uma perspectiva de curto prazo.

@eucpp Observe que unique_ptr não armazena um alocador - ele armazena um Deleter :

Deleter deve ser FunctionObject ou referência lvalue para um FunctionObject ou referência lvalue para função, que pode ser chamada com um argumento do tipo unique_ptr:: ponteiro`

Eu vejo isso como aproximadamente equivalente a fornecermos as características de divisão Alloc e Dealloc .

@cramertj Sim, você está certo. Ainda assim, duas características são necessárias - com estado e sem estado Dealloc .

Um Dealloc ZST não seria suficiente?

Na terça, 12 de junho de 2018 às 15h08, Evgeniy Moiseenko [email protected]
escrevi:

@cramertj https://github.com/cramertj Sim, você está certo. Ainda assim, dois
características são necessárias - Dealloc com estado e sem estado.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-396716689 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AEAJtWkpF0ofVc18NwbfV45G4QY6SCFBks5t8B_AgaJpZM4IDYUN
.

Um Dealloc ZST não seria suficiente?

@remexre , suponho que sim :)

Eu não sabia que o compilador de ferrugem suporta ZST fora da caixa.
Em C ++, isso exigiria pelo menos alguns truques em torno da otimização de base vazia.
Sou muito novo na Rust, sinto muito por alguns erros óbvios.

Não acho que precisamos de características separadas para stateful vs stateless.

Com Box aumentado com um parâmetro de tipo A , ele conteria um valor de A diretamente, não uma referência ou ponteiro para A . Esse tipo pode ter tamanho zero para um (des) alocador sem estado. Ou A si pode ser algo como uma referência ou identificador para um alocador stateful que pode ser compartilhado entre vários objetos alocados. Então, em vez de impl Alloc for MyAllocator , você pode querer fazer algo como impl<'r> Alloc for &'r MyAllocator

A propósito, um Box que sabe apenas como desalocar e não como alocar não implementaria Clone .

@SimonSapin eu esperaria que Clone ing exigisse a especificação de um alocador novamente, da mesma forma que criar um novo Box (isto é, não seria feito usando o Clone trait).

@cramertj Não seria inconsistente em comparação com Vec e outros contêineres que implementam Clone ?
Quais são as desvantagens de armazenar a instância de Alloc dentro de Box vez de Dealloc ?
Então Box pode implementar Clone bem como clone_with_alloc .

Não creio que as características de divisão realmente afetem o Clone de uma maneira enorme - o impl seria apenas impl<T, A> Clone for Box<T, A> where A: Alloc + Dealloc + Clone { ... } .

@sfackler Eu não me oporia a esse impl, mas também esperaria ter um clone_into ou algo que usa um alocador fornecido.

Faria sentido um método alloc_copy para Alloc ? Isso poderia ser usado para fornecer implementações memcpy mais rápidas ( Copy/Clone ) para grandes alocações, por exemplo, fazendo cópias na gravação de clones de páginas.

Isso seria muito legal e trivial para fornecer uma implementação padrão.

O que estaria usando essa função alloc_copy ? impl Clone for Box<T, A> ?

Sim, idem para Vec .

Tendo examinado isso um pouco mais, parece que abordagens para criar páginas copy-on-write dentro do mesmo intervalo de processo entre hacky e impossível, pelo menos se você quiser fazer isso em mais de um nível de profundidade. Portanto, alloc_copy não seria um grande benefício.

Em vez disso, uma saída de emergência mais geral que permita futuras travessuras da memória virtual pode ser útil. Ou seja, se uma alocação for grande, apoiada pelo mmap de qualquer maneira e sem estado, então o alocador pode prometer não se dar conta das mudanças futuras na alocação. O usuário pode então mover essa memória para um tubo, desmapear ou coisas semelhantes.
Alternativamente, poderia haver um alocador mmap-all-the-things burro e uma função de transferência try.

Em vez disso, uma saída de emergência mais geral que permite a memória virtual futura

Os alocadores de memória (malloc, jemalloc, ...) geralmente não permitem que você roube qualquer tipo de memória deles, e eles geralmente não permitem que você consulte ou altere as propriedades da memória que eles possuem. Então, o que essa saída de emergência geral tem a ver com alocadores de memória?

Além disso, o suporte à memória virtual difere muito entre as plataformas, tanto que o uso eficaz da memória virtual geralmente requer algoritmos diferentes por plataforma, muitas vezes com garantias completamente diferentes. Já vi algumas abstrações portáteis sobre a memória virtual, mas ainda não vi nenhuma que não fosse aleijada a ponto de ser inútil em algumas situações devido à sua "portabilidade".

Você está certo. Qualquer caso de uso desse tipo (eu estava pensando principalmente em otimizações específicas de plataforma) provavelmente é mais bem atendido usando um alocador personalizado em primeiro lugar.

Alguma opinião sobre a API Composable Allocator descrita por Andrei Alexandrescu em sua apresentação CppCon? O vídeo está disponível no YouTube aqui: https://www.youtube.com/watch?v=LIb3L4vKZ7U (ele começa a descrever seu projeto proposto por volta das 26:00, mas a palestra é divertida o suficiente, você pode preferir assistir) .

Parece que a conclusão inevitável de tudo isso é que as bibliotecas de coleções devem ser alocações WRT genéricas e o próprio programador do aplicativo deve ser capaz de compor alocadores e coleções livremente no canteiro de obras.

Alguma opinião sobre a API Composable Allocator descrita por Andrei Alexandrescu em sua apresentação CppCon?

A API Alloc atual permite escrever alocadores combináveis ​​(por exemplo, MyAlloc<Other: Alloc> ) e você pode usar traços e especialização para alcançar praticamente tudo o que é alcançado na conversa de Andreis. No entanto, além da "ideia" de que alguém deveria ser capaz de fazer isso, praticamente nada da palestra de Andrei pode se aplicar a Rust, uma vez que a forma como Andrei constrói a API se baseia em genéricos irrestritos + SFINAE / estático desde o início e no sistema de genéricos de Rust é completamente diferente daquele.

Eu gostaria de propor estabilizar o resto dos métodos Layout . Eles já são úteis com a API de alocador global atual.

Esses são todos os métodos que você quer dizer?

  • pub fn align_to(&self, align: usize) -> Layout
  • pub fn padding_needed_for(&self, align: usize) -> usize
  • pub fn repeat(&self, n: usize) -> Result<(Layout, usize), LayoutErr>
  • pub fn extend(&self, next: Layout) -> Result<(Layout, usize), LayoutErr>
  • pub fn repeat_packed(&self, n: usize) -> Result<Layout, LayoutErr>
  • pub fn extend_packed(&self, next: Layout) -> Result<(Layout, usize), LayoutErr>
  • pub fn array<T>(n: usize) -> Result<Layout, LayoutErr>

@gnzlbg Sim.

@Amanieu Parece ok para mim, mas esse problema já é enorme. Considere apresentar uma questão separada (ou até mesmo um PR de estabilização) para que possamos FCP separadamente?

de alocadores e tempos de vida :

  1. (para impls de alocador): mover um valor de alocador não deve invalidar seus blocos de memória pendentes.

    Todos os clientes podem assumir isso em seu código.

    Portanto, se um cliente aloca um bloco de um alocador (chame-o de a1) e, em seguida, a1 se move para um novo local (por exemplo, vialet a2 = a1;), então continua válido para o cliente desalocar aquele bloco via a2.

Isso implica que um alocador deve ser Unpin ?

Boa pegada!

Como o traço Alloc ainda é instável, acho que ainda podemos mudar as regras se quisermos e modificar esta parte do RFC. Mas é realmente algo para se manter em mente.

@gnzlbg Sim, estou ciente das enormes diferenças nos sistemas genéricos e que nem tudo que ele detalha pode ser implementado da mesma maneira no Rust. Tenho trabalhado na biblioteca intermitentemente desde a postagem, e estou fazendo um bom progresso.

Isso implica que um Alocador _deve_ ser Unpin ?

Não é. Unpin é sobre o comportamento de um tipo quando envolvido em Pin , não há nenhuma conexão particular com esta API.

Mas não pode Unpin ser usado para impor a restrição mencionada?

Outra pergunta a respeito de dealloc_array : Por que a função retorna Result ? Na implementação atual, isso pode falhar em dois casos:

  • n é zero
  • estouro de capacidade para n * size_of::<T>()

Para o primeiro, temos dois casos (como na documentação, o implementador pode escolher entre eles):

  • A alocação retorna Ok no zerado n => dealloc_array também deve retornar Ok .
  • A alocação retorna Err zerado n => não há nenhum ponteiro que pode ser passado para dealloc_array .

O segundo é garantido pela seguinte restrição de segurança:

o layout de [T; n] deve caber nesse bloco de memória.

Isso significa que devemos chamar dealloc_array com o mesmo n da alocação. Se um array com n elementos puder ser alocado, n é válido para T . Caso contrário, a alocação teria falhado.

Edit: Com relação ao último ponto: Mesmo que usable_size retorne um valor maior do que n * size_of::<T>() , isso ainda é válido. Caso contrário, a implementação viola esta restrição de característica:

O tamanho do bloco deve estar na faixa de [use_min, use_max] , onde:

  • [...]
  • use_max é a capacidade que foi (ou teria sido) retornada quando (se) o bloco foi alocado por meio de uma chamada para alloc_excess ou realloc_excess .

Isso só é válido, já que o traço requer unsafe impl .

Para o primeiro, temos dois casos (como na documentação, o implementador pode escolher entre eles):

  • A alocação retorna Ok zerado n

Onde você conseguiu essa informação?

Todos os métodos Alloc::alloc_ nos documentos especificam que o comportamento de alocações de tamanho zero é indefinido em sua cláusula de "Segurança".

Documentos de core::alloc::Alloc (destacadas as partes relevantes):

Uma nota sobre tipos de tamanho zero e layouts de tamanho zero: muitos métodos no traço Alloc afirmam que as solicitações de alocação devem ter tamanho diferente de zero, caso contrário, pode ocorrer um comportamento indefinido.

  • No entanto, alguns métodos de alocação de nível superior ( alloc_one , alloc_array ) são bem definidos em tipos de tamanho zero e podem opcionalmente suportá-los : é deixado ao implementador decidir se deve retornar Err , ou para retornar Ok com algum ponteiro.
  • Se uma implementação Alloc escolher retornar Ok neste caso (isto é, o ponteiro denota um bloco inacessível de tamanho zero) então esse ponteiro retornado deve ser considerado "atualmente alocado". Em tal alocador, todos os métodos que usam ponteiros atualmente alocados como entradas devem aceitar esses ponteiros de tamanho zero, sem causar comportamento indefinido.

  • Em outras palavras, se um ponteiro de tamanho zero pode fluir para fora de um alocador, esse alocador deve, da mesma forma, aceitar esse ponteiro voltando para seus métodos de desalocação e realocação .

Portanto, uma das condições de erro de dealloc_array é definitivamente suspeita:

/// # Safety
///
/// * the layout of `[T; n]` must *fit* that block of memory.
///
/// # Errors
///
/// Returning `Err` indicates that either `[T; n]` or the given
/// memory block does not meet allocator's size or alignment
/// constraints.

Se [T; N] não atender ao tamanho do alocador ou às restrições de alinhamento, então AFAICT ele não caberá no bloco de memória da alocação e o comportamento será indefinido (de acordo com a cláusula de segurança).

A outra condição de erro é "Sempre retorna Err em estouro aritmético." o que é bastante genérico. É difícil dizer se é uma condição de erro útil. Para cada implementação de Alloc trait, pode-se ser capaz de chegar a um diferente que possa fazer alguma aritmética que, em teoria, poderia envolver, então so


Documentos de core::alloc::Alloc (destacadas as partes relevantes):

De fato. Acho estranho que tantos métodos (por exemplo, Alloc::alloc ) declarem que alocações de tamanho zero são um comportamento indefinido, mas então apenas fornecemos Alloc::alloc_array(0) com um comportamento definido pela implementação. Em certo sentido, Alloc::alloc_array(0) é um teste decisivo para verificar se um alocador suporta alocações de tamanho zero ou não.

Se [T; N] não atender ao tamanho do alocador ou às restrições de alinhamento, então AFAICT ele não caberá no bloco de memória da alocação e o comportamento será indefinido (de acordo com a cláusula de segurança).

Sim, acho que essa condição de erro pode ser descartada, pois é redundante. Precisamos da cláusula de segurança ou de uma condição de erro, mas não de ambas.

A outra condição de erro é "Sempre retorna Err em estouro aritmético." o que é bastante genérico. É difícil dizer se é uma condição de erro útil.

IMO, é protegido pela mesma cláusula de segurança acima; se a capacidade de [T; N] estourar, não cabe aquele bloco de memória para desalocar. Talvez @pnkfelix pudesse elaborar sobre isso?

Em certo sentido, Alloc::alloc_array(1) é um teste decisivo para verificar se um alocador suporta alocações de tamanho zero ou não.

Você quis dizer Alloc::alloc_array(0) ?

IMO, é protegido pela mesma cláusula de segurança acima; se a capacidade de [T; N] estourar, não _apta_ aquele bloco de memória para desalocar.

Observe que essa característica pode ser implementada por usuários para seus próprios alocadores personalizados e que esses usuários podem substituir as implementações padrão desses métodos. Portanto, ao considerar se isso deve retornar Err para estouro aritmético ou não, deve-se não apenas focar no que a implementação padrão atual do método padrão faz, mas também considerar o que pode fazer sentido para os usuários que os implementam para outros alocadores.

Você quis dizer Alloc::alloc_array(0) ?

Sim, desculpe.

Observe que essa característica pode ser implementada por usuários para seus próprios alocadores personalizados e que esses usuários podem substituir as implementações padrão desses métodos. Portanto, ao considerar se isso deve retornar Err para estouro aritmético ou não, deve-se não apenas focar no que a implementação padrão atual do método padrão faz, mas também considerar o que pode fazer sentido para os usuários que os implementam para outros alocadores.

Entendo, mas a implementação de Alloc requer unsafe impl e os implementadores devem seguir as regras de segurança mencionadas em https://github.com/rust-lang/rust/issues/32838#issuecomment -467093527 .

Cada API apontada aqui para um problema de rastreamento é o traço Alloc ou está relacionado ao traço Alloc . @ rust-lang / libs, você acha que é útil mantê-lo aberto além de https://github.com/rust-lang/rust/issues/42774?

Pergunta simples de fundo: Qual é a motivação por trás da flexibilidade com ZSTs? Parece-me que, dado que sabemos em tempo de compilação que um tipo é um ZST, podemos otimizar completamente tanto a alocação (para retornar um valor constante) quanto a desalocação. Dado isso, parece-me que devemos dizer um dos seguintes:

  • Sempre depende do implementador oferecer suporte a ZSTs, e eles não podem retornar Err para ZSTs
  • É sempre UB alocar ZSTs, e é responsabilidade do chamador causar curto-circuito neste caso
  • Existe algum tipo de método alloc_inner que os chamadores implementam e um método alloc com uma implementação padrão que faz o curto-circuito; alloc deve suportar ZSTs, mas alloc_inner NÃO pode ser chamado para um ZST (isso é apenas para que possamos adicionar a lógica de curto-circuito em um único lugar - na definição do traço - em ordem para salvar alguns implementadores de boilerplate)

Existe um motivo para a necessidade da flexibilidade que temos com a API atual?

Existe um motivo para a necessidade da flexibilidade que temos com a API atual?

É uma troca. Indiscutivelmente, a característica Alloc é usada com mais frequência do que implementada, então pode fazer sentido tornar o uso de Alloc o mais fácil possível, fornecendo suporte integrado para ZSTs.

Isso significaria que os implementadores da característica Alloc precisarão cuidar disso, mas o mais importante para mim é que aqueles que estão tentando evoluir a característica Alloc precisarão manter os ZSTs em mente em cada alteração de API. Também complica os documentos da API, explicando como os ZSTs são (ou poderiam ser, se fosse "definida pela implementação") manipulados.

Os alocadores C ++ seguem essa abordagem, em que o alocador tenta resolver muitos problemas diferentes. Isso não apenas os tornou mais difíceis de implementar e evoluir, mas também mais difíceis para os usuários realmente usarem devido à forma como todos esses problemas interagem na API.

Acho que lidar com ZSTs e alocar / desalocar memória bruta são dois problemas ortogonais e diferentes e, portanto, devemos manter a API Alloc trait simples, simplesmente não manipulando-os.

Os usuários do Alloc como o libstd precisarão lidar com ZSTs, por exemplo, em cada coleção. Esse é definitivamente um problema que vale a pena resolver, mas não acho que o traço Alloc seja o lugar para isso. Eu esperaria que um utilitário para resolver esse problema aparecesse dentro da libstd por necessidade e, quando isso acontecer, talvez possamos tentar RFC esse utilitário e expô-lo em std :: heap.

Tudo isso parece razoável.

Acho que lidar com ZSTs e alocar / desalocar memória bruta são dois problemas ortogonais e diferentes e, portanto, devemos manter a API Alloc trait simples, simplesmente não manipulando-os.

Isso não implica que devemos ter a API explicitamente para não lidar com ZSTs em vez de ser definida pela implementação? IMO, um erro "sem suporte" não é muito útil em tempo de execução, uma vez que a grande maioria dos chamadores não será capaz de definir um caminho de fallback e, portanto, terá que assumir que os ZSTs não têm suporte de qualquer maneira. Parece mais limpo apenas para simplificar a API e declarar que eles _nunca_ são suportados.

A especialização seria usada por alloc usuários para lidar com ZST? Ou apenas cheques de if size_of::<T>() == 0 ?

A especialização seria usada por alloc usuários para lidar com ZST? Ou apenas cheques de if size_of::<T>() == 0 ?

O último deve ser suficiente; os caminhos de código apropriados seriam removidos trivialmente em tempo de compilação.

Isso não implica que devemos ter a API explicitamente para não lidar com ZSTs em vez de ser definida pela implementação?

Para mim, uma restrição importante é que se proibirmos as alocações de tamanho zero, os métodos Alloc devem ser capazes de assumir que Layout passado a eles não é de tamanho zero.

Existem várias maneiras de fazer isso. Um seria adicionar outra cláusula Safety a todos os métodos Alloc , afirmando que se Layout tiver tamanho zero, o comportamento é indefinido.

Alternativamente, poderíamos banir Layout s de tamanho zero, então Alloc não precisa dizer nada sobre alocações de tamanho zero, já que não podem acontecer com segurança, mas fazer isso teria algumas desvantagens.

Por exemplo, alguns tipos como HashMap acumulam Layout de múltiplos Layout s, e enquanto o Layout final pode não ser de tamanho zero, o os intermediários podem ser (por exemplo, em HashSet ). Portanto, esses tipos precisariam usar "outra coisa" (por exemplo, um tipo LayoutBuilder ) para construir seus Layout s finais e pagar por um cheque "diferente de zero" (ou usar um método _unchecked ) ao converter para Layout .

A especialização seria usada por usuários alocados para lidar com ZST? Ou apenas se size_of ::() == 0 verificações?

Ainda não podemos nos especializar em ZSTs. No momento, todo código usa size_of::<T>() == 0 .

Existem várias maneiras de fazer isso. Uma seria adicionar outra cláusula Safety a todos os métodos Alloc , declarando que se Layout tiver tamanho zero, o comportamento é indefinido.

Seria interessante considerar se há maneiras de tornar isso uma garantia de tempo de compilação, mas mesmo debug_assert que o layout não tem tamanho zero deve ser suficiente para detectar 99% dos bugs.

Não prestei atenção às discussões sobre alocadores, sinto muito por isso. Mas há muito desejo que o alocador tivesse acesso ao tipo de valor que está alocando. Pode haver designs de alocadores que poderiam usá-lo.

Então provavelmente teríamos os mesmos problemas que C ++ e sua API de alocador.

Mas há muito desejo que o alocador tivesse acesso ao tipo de valor que está alocando. T

Para que você precisa disso?

@gnzblg @brson Hoje eu tive um caso de uso potencial para saber _algo_ sobre o tipo de valor que está sendo alocado.

Estou trabalhando em um alocador global que pode ser alternado entre três alocadores subjacentes - um thread local, um global com bloqueios e um para co-rotinas usarem - a ideia é que posso restringir uma co-rotina que representa uma conexão de rede a um máximo quantidade de uso de memória dinâmica (na ausência de ser capaz de controlar alocadores em coleções, especialmente em código de terceiros) *.

Seria útil saber se estou alocando um valor que pode se mover entre os threads (por exemplo, Arc) versus um que não vai. Ou pode não ser. Mas é um cenário possível. No momento, o alocador global tem um switch que o usuário usa para dizer a partir de qual alocador fazer as alocações (não necessário para realocação ou livre; podemos apenas verificar o endereço da memória para isso).

* [Ele também me permite usar a memória local NUMA sempre que possível, sem qualquer bloqueio e, com um modelo de thread 1 núcleo 1, para limitar o uso total de memória].

@raphaelcohn

Estou trabalhando em um alocador global

Não acho que nada disso se aplicaria (ou poderia) ao traço GlobalAlloc , e o traço Alloc já possui métodos genéricos que podem fazer uso de informações de tipo (por exemplo, alloc_array<T>(1) aloca um único T , onde T é o tipo real, de modo que o alocador pode levar o tipo em consideração ao realizar a alocação). Acho que seria mais útil para os propósitos desta discussão ver realmente os alocadores de implementação de código que fazem uso de informações de tipo. Não ouvi nenhum bom argumento sobre por que esses métodos precisam ser parte de algum traço de alocador genérico, ao invés de apenas ser parte da API do alocador, ou algum outro traço de alocador.

Acho que também seria muito interessante saber quais dos tipos parametrizados por Alloc você pretende combinar com alocadores que usam informações de tipo e qual você espera que seja o resultado.

AFAICT, o único tipo interessante para isso seria Box porque aloca T diretamente. Praticamente todos os outros tipos em std nunca alocam um T , mas algum tipo interno privado sobre o qual seu alocador não pode saber nada. Por exemplo, Rc e Arc poderiam alocar (InternalRefCounts, T) , List / BTreeSet / etc. alocar tipos de nós internos, Vec / Deque / ... aloca arrays de T s, mas não T s eles próprios, etc.

Para Box e Vec , poderíamos adicionar de maneiras compatíveis com versões anteriores um traço BoxAlloc e um ArrayAlloc com impls de cobertor para Alloc que os alocadores poderiam especialize-se para sequestrar como eles se comportam, se houver necessidade de atacar esses problemas de forma genérica. Mas há uma razão pela qual fornecer seus próprios tipos MyAllocBox e MyAllocVec que conspiram com seu alocador para explorar informações de tipo não é uma solução viável?

Como agora temos um repositório dedicado para os alocadores WG , e a lista no OP está desatualizada, esta questão pode ser encerrada para manter as discussões e o rastreamento desse recurso em um só lugar?

Um bom ponto @TimDiekmann! Vou prosseguir e encerrar em favor dos tópicos de discussão nesse repositório.

Este ainda é o problema de rastreamento para o qual algum atributo #[unstable] aponta. Acho que não deve ser fechado até que esses recursos tenham sido estabilizados ou preteridos. (Ou podemos alterar os atributos para apontar para um problema diferente.)

Sim, recursos instáveis ​​referenciados no git master definitivamente devem ter um problema de rastreamento aberto.

Acordado. Também foi adicionado um aviso e um link para o OP.

Esta página foi útil?
0 / 5 - 0 avaliações