Rust: Rasgos del asignador y std :: heap

Creado en 8 abr. 2016  ·  412Comentarios  ·  Fuente: rust-lang/rust

📢 Esta función tiene un grupo de trabajo dedicado , por favor dirija sus comentarios y preocupaciones al repo del grupo de trabajo .

Publicación original:


Propuesta de FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336957415
Casillas de verificación FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336980230


Problema de seguimiento para rust-lang / rfcs # 1398 y el módulo std::heap .

  • [x] land struct Layout , trait Allocator , e implementaciones predeterminadas en alloc crate (https://github.com/rust-lang/rust/pull/42313)
  • [x] decide dónde deben vivir las partes (por ejemplo, las impls predeterminadas dependen de alloc caja, pero Layout / Allocator _podría_ estar en libcore ...) (https://github.com/rust-lang/rust/pull/42313)
  • [] fixme desde el código fuente: auditorías de implementaciones predeterminadas (en Layout para errores de desbordamiento, (potencialmente cambiando a overflowing_add y overflowing_mul según sea necesario).
  • [x] decide si realloc_in_place debe reemplazarse con grow_in_place y shrink_in_place ( comentario ) (https://github.com/rust-lang/rust/pull/42313)
  • [] revise los argumentos a favor / en contra del tipo de error asociado (consulte el subproceso aquí )
  • [] determinar cuáles son los requisitos en la alineación proporcionada a fn dealloc . (Consulte la discusión sobre el rfc del asignador y el rfc del asignador global y el rasgo Alloc PR ).

    • ¿Es necesario desasignar los align exactos con los que asigna? Se ha planteado la preocupación de que asignadores como jemalloc no requieran esto, y es difícil imaginar un asignador que lo requiera. ( más discusión ). @ruuda y @rkruppe parece que tienen más pensamientos hasta ahora sobre esto.

  • [] ¿Debería AllocErr ser Error lugar? ( comentario )
  • [x] ¿Es necesario desasignar con el tamaño exacto con el que asigna? Con el negocio usable_size , es posible que deseemos permitir, por ejemplo, que si asigna con (size, align) debe desasignar con un tamaño en algún lugar en el rango de size...usable_size(size, align) . Parece que jemalloc está totalmente de acuerdo con esto (no requiere que desasigne con un size preciso con el que asigne) y esto también permitiría que Vec aproveche naturalmente el exceso de capacidad jemalloc lo da cuando hace una asignación. (aunque en realidad hacer esto también es algo ortogonal a esta decisión, solo estamos habilitando Vec ). Hasta ahora, @Gankro tiene la mayoría de los pensamientos sobre esto. ( @alexcrichton cree que esto se resolvió en https://github.com/rust-lang/rust/pull/42313 debido a la definición de "ajustes")
  • [] similar a la pregunta anterior: ¿Es necesario desasignar con la alineación exacta con la que asignó? (Ver comentario del 5 de junio de 2017 )
  • [x] OSX / alloc_system tiene errores en alineaciones enormes (por ejemplo, una alineación de 1 << 32 ) https://github.com/rust-lang/rust/issues/30170 # 43217
  • [] ¿debería Layout proporcionar un método fn stride(&self) ? (Consulte también 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 después de 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

Comentario más útil

@alexcrichton La decisión de cambiar de -> Result<*mut u8, AllocErr> a -> *mut void puede ser una sorpresa significativa para las personas que siguieron el desarrollo original de las RFC de asignación.

No estoy en desacuerdo con los puntos que hace , pero, no obstante, parecía que un buen número de personas habría estado dispuesta a vivir con el "peso pesado" de Result sobre la mayor probabilidad de perder un valor nulo. comprobar el valor devuelto.

  • Estoy ignorando los problemas de eficiencia en tiempo de ejecución impuestos por la ABI porque yo, como @alexcrichton , supongo que podríamos lidiar con ellos de alguna manera a través de trucos del compilador.

¿Hay alguna forma de que podamos obtener una mayor visibilidad sobre ese cambio tardío por sí solo?

Una forma (fuera de mi cabeza): Cambie la firma ahora, en un PR por sí solo, en la rama maestra, mientras que Allocator todavía es inestable. Y luego ver quién se queja en las relaciones públicas (¡y quién celebra!).

  • ¿Es esto demasiado torpe? Parece que, según las definiciones, es menos torpe que unir tal cambio con la estabilización ...

Todos 412 comentarios

Desafortunadamente, no presté suficiente atención para mencionar esto en la discusión de RFC, pero creo que realloc_in_place debería ser reemplazado por dos funciones, grow_in_place y shrink_in_place , para dos razones:

  • No puedo pensar en un solo caso de uso (aparte de implementar realloc o realloc_in_place ) donde se desconoce si el tamaño de la asignación aumenta o disminuye. El uso de métodos más especializados deja un poco más claro lo que está sucediendo.
  • Las rutas de código para aumentar y reducir las asignaciones tienden a ser radicalmente diferentes: crecer implica probar si los bloques de memoria adyacentes están libres y reclamarlos, mientras que reducir implica tallar subbloques de tamaño adecuado y liberarlos. Si bien el costo de una sucursal dentro de realloc_in_place es bastante pequeño, usar grow y shrink captura mejor las distintas tareas que un asignador debe realizar.

Tenga en cuenta que estos se pueden agregar de manera compatible con versiones anteriores junto a realloc_in_place , pero esto limitaría qué funciones se implementarían de forma predeterminada en términos de cuáles otras.

Por consistencia, realloc probablemente también querría dividirse en grow y split , pero la única ventaja de tener una función realloc sobrecargable que yo sepa es poder usar la opción de reasignación de mmap , que no tiene tal distinción.

Además, creo que las implementaciones predeterminadas de realloc y realloc_in_place deberían ajustarse ligeramente, en lugar de compararlas con usable_size , realloc deberían intentar primero realloc_in_place . A su vez, realloc_in_place debería comprobar de forma predeterminada el tamaño utilizable y devolver el éxito en el caso de un pequeño cambio en lugar de devolver un error universalmente.

Esto facilita la producción de una implementación de alto rendimiento de realloc : todo lo que se requiere es mejorar realloc_in_place . Sin embargo, el rendimiento predeterminado de realloc no se ve afectado, ya que aún se realiza la comprobación contra usable_size .

Otro problema: el documento de fn realloc_in_place dice que si devuelve Ok, entonces uno está seguro de que ptr ahora "se ajusta" a new_layout .

Para mí, esto implica que debe verificar que la alineación de la dirección dada coincida con cualquier restricción implícita en new_layout .

Sin embargo, no creo que la especificación de la función fn reallocate_inplace subyacente implique que _it_ realizará dicha verificación.

  • Además, parece razonable que cualquier cliente que se sumerja en el uso de fn realloc_in_place se asegure de que las alineaciones funcionen (en la práctica, sospecho que significa que se requiere la misma alineación en todas partes para el caso de uso dado ...)

Entonces, ¿debería la implementación de fn realloc_in_place realmente tener la carga de verificar que la alineación del ptr dado es compatible con la de new_layout ? Probablemente sea mejor _en este caso_ (de este método) enviar ese requisito a la persona que llama ...

@gereeter haces buenos puntos; Los agregaré a la lista de verificación que estoy acumulando en la descripción del problema.

(en este punto, estoy esperando el soporte de #[may_dangle] para viajar en tren al canal beta para poder usarlo para colecciones estándar como parte de la integración del asignador)

Soy nuevo en Rust, así que perdóname si esto se ha discutido en otra parte.

¿Hay alguna idea sobre cómo admitir asignadores específicos de objetos? Algunos asignadores, como los asignadores de bloques y los revistas, están vinculados a un tipo en particular y realizan el trabajo de construir nuevos objetos, almacenar en caché los objetos construidos que se han "liberado" (en lugar de eliminarlos), devolver objetos en caché ya construidos soltar objetos antes de liberar la memoria subyacente a un asignador subyacente cuando sea necesario.

Actualmente, esta propuesta no incluye nada parecido a ObjectAllocator<T> , pero sería muy útil. En particular, estoy trabajando en una implementación de una capa de almacenamiento en caché de objetos del asignador de revistas (enlace de arriba), y aunque puedo hacer que esto solo envuelva un Allocator y haga el trabajo de construir y soltar objetos en el almacenamiento en caché capa en sí, sería genial si también pudiera envolver otros asignadores de objetos (como un asignador de bloques) y ser realmente una capa de almacenamiento en caché genérica.

¿Dónde encajaría un tipo o rasgo de asignador de objetos en esta propuesta? ¿Se dejaría para un RFC futuro? ¿Algo más?

No creo que esto se haya discutido todavía.

Puede escribir su propio ObjectAllocator<T> , y luego hacer impl<T: Allocator, U> ObjectAllocator<U> for T { .. } , de modo que cada asignador regular pueda servir como un asignador específico de objeto para todos los objetos.

El trabajo futuro sería modificar colecciones para usar su rasgo para sus nodos, en lugar de asignadores simples (genéricos) directamente.

@pnkfelix

(en este punto, estoy esperando el apoyo de # [may_dangle] para viajar en tren al canal beta para poder usarlo para colecciones estándar como parte de la integración del asignador)

¿Supongo que esto ha pasado?

@ Ericson2314 Sí, escribir el mío es definitivamente una opción para fines experimentales, pero creo que sería mucho más beneficioso que se estandarizara en términos de interoperabilidad (por ejemplo, planeo implementar también un asignador de losas, pero sería bueno si un usuario externo de mi código pudiera usar el asignador de losas de alguien _else_ con la capa de almacenamiento en caché de mi revista). Mi pregunta es simplemente si vale la pena discutir ObjectAllocator<T> rasgo

@joshlf

¿Dónde encajaría un tipo o rasgo de asignador de objetos en esta propuesta? ¿Se dejaría para un RFC futuro? ¿Algo más?

Sí, sería otro RFC.

No estoy muy familiarizado con las pautas sobre cuánto pertenece a un solo RFC y cuándo pertenecen las cosas en RFC separadas ...

eso depende del alcance del RFC en sí, que lo decide la persona que lo escribe, y luego todos dan retroalimentación.

Pero en realidad, como se trata de un problema de seguimiento para este RFC ya aceptado, pensar en extensiones y cambios de diseño no es realmente para este hilo; debe abrir uno nuevo en el repositorio de RFC.

@joshlf Ah, pensé que se suponía que ObjectAllocator<T> era un rasgo. Me refiero al prototipo del rasgo, no a un asignador específico. Sí, ese rasgo merecería su propio RFC como dice @steveklabnik .


@steveklabnik sí, ahora la discusión sería mejor en otro lugar. Pero @joshlf también planteó el problema para que no exponga una falla imprevista hasta ahora en el diseño de API aceptado pero no implementado. En ese sentido, coincide con las publicaciones anteriores de este hilo.

@ Ericson2314 Sí, pensé que eso era lo que

@steveklabnik Suena bien; Buscaré con mi propia implementación y enviaré un RFC si termina pareciendo una buena idea.

@joshlf No tengo ninguna razón por la que los asignadores personalizados irían al compilador o biblioteca estándar. Una vez que este RFC aterrice, podría publicar fácilmente su propia caja que realiza un tipo de asignación arbitraria (¡incluso un asignador completo como jemalloc podría implementarse de manera personalizada!).

@alexreg No se trata de un asignador personalizado en particular, sino de un rasgo que especifica el tipo de todos los asignadores que son paramétricos en un tipo en particular. Entonces, al igual que RFC 1398 define un rasgo ( Allocator ) que es el tipo de cualquier asignador de bajo nivel, estoy preguntando sobre un rasgo ( ObjectAllocator<T> ) que es el tipo de cualquier asignador que puede asignar / desasignar y construir / soltar objetos de tipo T .

@alexreg Vea mi punto inicial sobre el uso de colecciones de bibliotecas estándar con asignadores personalizados específicos de objetos.

Seguro, pero no estoy seguro de que pertenezca a la biblioteca estándar. Podría ir fácilmente a otra caja, sin pérdida de funcionalidad o usabilidad.

El 4 de enero de 2017, a las 21:59, Joshua Liebow-Feeser [email protected] escribió:

@alexreg https://github.com/alexreg No se trata de un asignador personalizado en particular, sino de un rasgo que especifica el tipo de todos los asignadores que son paramétricos en un tipo particular. Entonces, al igual que RFC 1398 define un rasgo (Asignador) que es el tipo de cualquier asignador de bajo nivel, estoy preguntando acerca de un rasgo (ObjectAllocator) que es el tipo de cualquier asignador que puede asignar / desasignar y construir / eliminar objetos de tipo T.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499064 , o silencie el hilo https://github.com/notifications/unsubscribe-auth/ AAEF3IhyyPhFgu1EGHr_GM_Evsr0SRzIks5rPBZGgaJpZM4IDYUN .

Creo que querrás usar colecciones de bibliotecas estándar (cualquier valor asignado al montón) con un asignador personalizado arbitrario ; es decir, no se limita a objetos específicos.

El 4 de enero de 2017, a las 22:01, John Ericson [email protected] escribió:

@alexreg https://github.com/alexreg Vea mi punto inicial sobre el uso de colecciones de bibliotecas estándar con asignadores personalizados específicos de objetos.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499628 , o silencie el hilo https://github.com/notifications/unsubscribe-auth/ AAEF3CrjYIXqcv8Aqvb4VTyPcajJozICks5rPBbOgaJpZM4IDYUN .

Seguro, pero no estoy seguro de que pertenezca a la biblioteca estándar. Podría ir fácilmente a otra caja, sin pérdida de funcionalidad o usabilidad.

Sí, pero probablemente desee que alguna funcionalidad de biblioteca estándar se base en ella (como lo que sugirió @ Ericson2314 ).

Creo que querrás usar colecciones de bibliotecas estándar (cualquier valor asignado al montón) con un asignador personalizado arbitrario ; es decir, no se limita a objetos específicos.

Idealmente, querrá ambos: aceptar cualquier tipo de asignador. Existen beneficios muy significativos al usar el almacenamiento en caché específico de objetos; por ejemplo, tanto la asignación de bloques como el almacenamiento en caché de revistas brindan beneficios de rendimiento muy significativos; si tiene curiosidad, eche un vistazo a los artículos que he vinculado anteriormente.

Pero el rasgo del asignador de objetos podría ser simplemente un sustrato del rasgo del asignador general. Es tan simple como eso, en lo que a mí respecta. Claro, ciertos tipos de asignadores pueden ser más eficientes que los asignadores de propósito general, pero ni el compilador ni el estándar realmente necesitan (o deberían) saberlo.

El 4 de enero de 2017, a las 22:13, Joshua Liebow-Feeser [email protected] escribió:

Seguro, pero no estoy seguro de que pertenezca a la biblioteca estándar. Podría ir fácilmente a otra caja, sin pérdida de funcionalidad o usabilidad.

Sí, pero probablemente desee que alguna funcionalidad de biblioteca estándar se base en ella (como lo que sugirió @ Ericson2314 https://github.com/Ericson2314 ).

Creo que querrás usar colecciones de bibliotecas estándar (cualquier valor asignado al montón) con un asignador personalizado arbitrario; es decir, no se limita a objetos específicos.

Idealmente, querrá ambos: aceptar cualquier tipo de asignador. Existen beneficios muy significativos al usar el almacenamiento en caché específico de objetos; por ejemplo, tanto la asignación de bloques como el almacenamiento en caché de revistas brindan beneficios de rendimiento muy significativos; si tiene curiosidad, eche un vistazo a los artículos que he vinculado anteriormente.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270502231 , o silencie el hilo https://github.com/notifications/unsubscribe-auth/ AAEF3L9F9r_0T5evOtt7Es92vw6gBxR9ks5rPBl9gaJpZM4IDYUN .

Pero el rasgo del asignador de objetos podría ser simplemente un sustrato del rasgo del asignador general. Es tan simple como eso, en lo que a mí respecta. Claro, ciertos tipos de asignadores pueden ser más eficientes que los asignadores de propósito general, pero ni el compilador ni el estándar realmente necesitan (o deberían) saberlo.

Ah, entonces el problema es que la semántica es diferente. Allocator asigna y libera blobs de bytes sin formato. ObjectAllocator<T> , por otro lado, asignaría objetos ya construidos y también sería responsable de eliminar estos objetos (incluida la posibilidad de almacenar en caché los objetos construidos que podrían entregarse más tarde en lugar de construir un objeto recién asignado , que es caro). El rasgo se vería así:

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

Esto no es compatible con Allocator , cuyos métodos tratan con punteros sin formato y no tienen noción de tipo. Además, con Allocator s, es responsabilidad de la persona que llama drop el objeto que se libera primero. Esto es realmente importante - saber sobre el tipo T permite a ObjectAllocator<T> hacer cosas como llamar al método T drop , y desde free(t) mueve t a free , la persona que llama _no puede_ soltar t primero, sino que es responsabilidad de ObjectAllocator<T> . Básicamente, estos dos rasgos son incompatibles entre sí.

Ah, ya veo. Pensé que esta propuesta ya incluía algo así, es decir, un asignador de "nivel superior" sobre el nivel de bytes. En ese caso, ¡una propuesta perfectamente justa!

El 4 de enero de 2017, a las 22:29, Joshua Liebow-Feeser [email protected] escribió:

Pero el rasgo del asignador de objetos podría ser simplemente un sustrato del rasgo del asignador general. Es tan simple como eso, en lo que a mí respecta. Claro, ciertos tipos de asignadores pueden ser más eficientes que los asignadores de propósito general, pero ni el compilador ni el estándar realmente necesitan (o deberían) saberlo.

Ah, entonces el problema es que la semántica es diferente. Allocator asigna y libera blobs de bytes sin formato. ObjectAllocator, por otro lado, asignaría objetos ya construidos y también sería responsable de descartar estos objetos (incluida la posibilidad de almacenar en caché los objetos construidos que podrían entregarse más tarde en lugar de construir un objeto recién asignado, lo cual es caro). El rasgo se vería así:

rasgo ObjectAllocator{
fn alloc () -> T;
fn libre (t T);
}
Esto no es compatible con Allocator, cuyos métodos tratan con punteros sin formato y no tienen noción de tipo. Además, con los asignadores, es responsabilidad de la persona que llama soltar primero el objeto que se libera. Esto es realmente importante: conocer el tipo T permite que ObjectAllocatorpara hacer cosas como llamar al método de caída de T, y dado que free (t) mueve t a libre, la persona que llama no puede eliminar t primero, sino que es el ObjectAllocatorresponsabilidad. Básicamente, estos dos rasgos son incompatibles entre sí.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270505704 , o silencie el hilo https://github.com/notifications/unsubscribe-auth/ AAEF3GViJBefuk8IWgPauPyL5tV78Fn5ks5rPB08gaJpZM4IDYUN .

@alexreg Ah, sí, yo también lo esperaba :) Bueno, habrá que esperar otro RFC.

Sí, ponga en marcha ese RFC, ¡estoy seguro de que recibirá mucho apoyo! Y gracias por la aclaración (no me había mantenido al día con los detalles de este RFC).

El 5 de enero de 2017, a las 00:53, Joshua Liebow-Feeser [email protected] escribió:

@alexreg https://github.com/alexreg Ah, sí, yo también lo esperaba :) Bueno, tendrá que esperar otro RFC.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270531535 , o silencie el hilo https://github.com/notifications/unsubscribe-auth/ AAEF3MQQeXhTliU5CBsoheBFL26Ee9WUks5rPD8RgaJpZM4IDYUN .

Sería útil una caja para probar asignadores personalizados.

Perdóname si me estoy perdiendo algo obvio, pero ¿hay alguna razón para que el rasgo Layout descrito en este RFC no implemente Copy así como Clone , ya que es solo ¿VAINA?

No puedo pensar en ninguno.

Perdón por sacar esto a colación tan tarde en el proceso, pero ...

¿Podría valer la pena agregar soporte para una función similar a dealloc que no es un método, sino una función? La idea sería usar la alineación para poder inferir de un puntero dónde está en la memoria su asignador principal y así poder liberar sin necesidad de una referencia explícita de asignador.

Esto podría ser una gran ventaja para las estructuras de datos que utilizan asignadores personalizados. Esto les permitiría no mantener una referencia al asignador en sí, sino que solo necesitarían ser paramétricos en el _tipo_ del asignador para poder llamar a la función dealloc correcta. Por ejemplo, si Box finalmente se modifica para admitir asignadores personalizados, entonces podría seguir siendo solo una palabra (solo el puntero) en lugar de tener que expandirse a dos palabras para almacenar una referencia al asignador también.

En una nota relacionada, también podría ser útil admitir una función alloc no sea del método para permitir asignadores globales. Esto se compondría muy bien con una función dealloc no sea del método: para los asignadores globales, no habría necesidad de hacer ningún tipo de inferencia de puntero a asignador, ya que solo habría una única instancia estática del asignador para todo el programa.

@joshlf El diseño actual le permite obtener eso simplemente haciendo que su asignador sea un tipo de unidad (de tamaño cero), es decir, struct MyAlloc; que luego implemente el rasgo Allocator .
Almacenar referencias o nada en absoluto, siempre , es menos general que almacenar el asignador por valor.

Pude ver que eso es cierto para un tipo directamente incorporado, pero ¿qué pasa si una estructura de datos decide mantener una referencia en su lugar? ¿Una referencia a un tipo de tamaño cero ocupa espacio cero? Es decir, si tengo:

struct Foo()

struct Blah{
    foo: &Foo,
}

¿ Blah tiene tamaño cero?

En realidad, incluso si es posible, es posible que no desee que su asignador tenga un tamaño cero. Por ejemplo, puede tener un asignador con un tamaño distinto de cero que asigna _from_, pero que tiene la capacidad de liberar objetos sin conocer el asignador original. Esto aún sería útil para hacer que Box tome solo una palabra. Tendría algo como Box::new_from_allocator que tendría que tomar un asignador como argumento, y podría ser un asignador de tamaño distinto de cero, pero si el asignador admitía la liberación sin la referencia original del asignador, el Box<T> devuelto Box::new_from_allocator .

Por ejemplo, puede tener un asignador con un tamaño distinto de cero desde el que asigna, pero que tiene la capacidad de liberar objetos sin conocer el asignador original.

Recuerdo que hace mucho, mucho tiempo, propuse factorizar los rasgos de asignador y desasignador separados (con tipos asociados que conectan los dos) básicamente por esta razón.

¿Se debe permitir al compilador optimizar las asignaciones con estos asignadores?

¿Se debe permitir al compilador optimizar las asignaciones con estos asignadores?

@Zoxc ¿A qué te refieres?

Recuerdo que hace mucho, mucho tiempo, propuse factorizar los rasgos de asignador y desasignador separados (con tipos asociados que conectan los dos) básicamente por esta razón.

Para la posteridad, permítanme aclarar esta afirmación (hablé con @ Ericson2314 al respecto sin conexión): la idea es que un Box podría ser paramétrico solo en un desasignador. Entonces podrías tener la siguiente implementación:

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()
        }
    }
}

De esta forma, al llamar a new_from_allocator , si A::D es un tipo de tamaño cero, entonces el campo d de Box<T, A::D> ocupa un tamaño cero, por lo que el el tamaño del Box<T, A::D> resultante es una sola palabra.

¿Hay una línea de tiempo para cuándo aterrizará? Estoy trabajando en algunas cosas de asignación, y sería bueno si estas cosas estuvieran ahí para que yo pudiera construir.

Si hay interés, estaría feliz de prestar algunos ciclos para esto, pero soy relativamente nuevo en Rust, por lo que podría crear más trabajo para los mantenedores en términos de tener que revisar el código de un novato. No quiero pisotear a nadie y no quiero hacer más trabajo para la gente.

Ok, nos hemos reunido recientemente

Una cosa que discutimos es que cambiar todos los tipos de libstd puede ser un poco prematuro debido a posibles problemas de inferencia, pero independientemente de eso, parece una buena idea obtener el rasgo Allocator y el Layout escriba el módulo std::heap para experimentar en otras partes del ecosistema.

@joshlf, si desea ayudar aquí, creo que sería más que bienvenido. La primera pieza probablemente aterrizará el tipo / rasgo básico de este RFC en la biblioteca estándar, y luego, desde allí, podemos comenzar a experimentar y jugar con colecciones en libstd también.

@alexcrichton ¿Creo que tu enlace está roto? Señala aquí.

Una cosa que discutimos es que cambiar todos los tipos de libstd puede ser un poco prematuro debido a posibles problemas de inferencia.

Agregar el rasgo es un buen primer paso, pero sin refactorizar las API existentes para usarlo, no verán mucho uso. En https://github.com/rust-lang/rust/issues/27336#issuecomment -300721558, propongo que podamos refactorizar las cajas detrás de la fachada de inmediato, pero agregar envoltorios de tipo nuevo en std . Es molesto hacerlo, pero nos permite avanzar.

@alexcrichton ¿Cuál sería el proceso para obtener asignadores de objetos? Mis experimentos hasta ahora (pronto serán públicos; puedo agregarte al repositorio privado de GH si tienes curiosidad) y la discusión aquí me han llevado a creer que habrá una simetría casi perfecta entre los rasgos del asignador y el objeto. rasgos del asignador. Por ejemplo, tendrás algo como (cambié Address a *mut u8 por simetría con *mut T de ObjectAllocator<T> ; probablemente terminaríamos con Address<T> o algo así):

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

Por lo tanto, creo que podría ser útil experimentar con asignadores y asignadores de objetos al mismo tiempo. Sin embargo, no estoy seguro de si este es el lugar adecuado para eso, o si debería haber otro RFC o, al menos, un PR separado.

Oh, quise vincular aquí que también tiene información sobre el asignador global. @joshlf, ¿eso es lo que estás pensando?

Parece que @alexcrichton quiere un PR que proporcione el rasgo Allocator y el tipo Layout , incluso si no está integrado en ninguna colección en libstd .

Si lo entiendo correctamente, puedo poner un PR para eso. No lo había hecho porque sigo tratando de obtener al menos la integración con RawVec y Vec prototipados. (En este punto tengo RawVec hecho, pero Vec es un poco más desafiante debido a las muchas otras estructuras que se construyen a partir de él, como Drain y IntoIter etc ...)

en realidad, mi rama actual parece que podría compilarse (y la única prueba de integración con RawVec pasó), así que seguí adelante y la publiqué: # 42313

@hawkw preguntó:

Perdóneme si me estoy perdiendo algo obvio, pero ¿hay alguna razón para que el rasgo de diseño descrito en este RFC no implemente Copiar y Clonar, ya que es solo POD?

La razón por la que hice que Layout solo implemente Clone y no Copy es que quería dejar abierta la posibilidad de agregar más estructura al tipo Layout . En particular, todavía estoy interesado en intentar que el Layout intente rastrear cualquier estructura de tipo utilizada para construirla (por ejemplo, 16-array de struct { x: u8, y: [char; 215] } ), de modo que los asignadores tengan la opción de exponer rutinas de instrumentación que informan de qué tipos se componen sus contenidos actuales.

Es casi seguro que esto debería ser una característica opcional, es decir, parece que la marea está firmemente en contra de obligar a los desarrolladores a usar los constructores Layout enriquecidos con tipos. por lo que cualquier instrumentación de este formulario debería incluir algo como una categoría de "bloques de memoria desconocidos" para manejar asignaciones que no tienen la información de tipo.

Sin embargo, características como ésta fueron la razón principal por la que no opté por hacer Layout implementar Copy ; Básicamente, pensé que una implementación de Copy sería una restricción prematura en Layout .

@alexcrichton @pnkfelix

Parece que @pnkfelix tiene esto cubierto, y las relaciones públicas están ganando terreno, así que vayamos con eso. Lo estoy revisando y haciendo comentarios ahora, ¡y se ve genial!

Actualmente, la firma de Allocator::oom es:

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

Sin embargo, me llamó la atención que a Gecko al menos le gusta saber el tamaño de la asignación también en OOM. Es posible que deseemos considerar esto al estabilizar para quizás agregar un contexto como Option<Layout> para explicar por

@alexcrichton
¿Podría valer la pena tener múltiples variantes oom_xxx o una enumeración de diferentes tipos de argumentos? Hay un par de firmas diferentes para métodos que podrían fallar (por ejemplo, alloc toma un diseño, realloc toma un puntero, un diseño original y un nuevo diseño, etc.), y puede que Hay casos en los que un método similar a oom querría conocerlos todos.

@joshlf eso es cierto, sí, pero no estoy seguro de si es útil. No quisiera simplemente agregar funciones porque podemos, deberían seguir estando bien motivados.


Un punto para la estabilización aquí es también "determinar cuáles son los requisitos en la alineación proporcionada a fn dealloc ", y la implementación actual de dealloc en Windows usa align para determinar cómo correctamente libre. @ruuda te puede interesar este hecho.

Un punto para la estabilización aquí es también "determinar cuáles son los requisitos en la alineación proporcionada a fn dealloc ", y la implementación actual de dealloc en Windows usa align para determinar cómo correctamente libre.

Sí, creo que así es como me encontré inicialmente con esto; mi programa se bloqueó en Windows debido a esto. Como HeapAlloc no ofrece garantías de alineación, allocate asigna una región más grande y almacena el puntero original en un encabezado, pero como optimización, esto se evita si los requisitos de alineación se cumplen de todos modos. Me pregunto si hay una manera de convertir HeapAlloc en un asignador consciente de la alineación que no requiera alineación en forma gratuita, sin perder esta optimización.

@ruuda

Como HeapAlloc no ofrece garantías de alineación

Proporciona una garantía de alineación mínima de 8 bytes para 32 bits o 16 bytes para 64 bits, simplemente no proporciona ninguna forma de garantizar una alineación superior a eso.

El _aligned_malloc proporcionado por el CRT en Windows puede proporcionar asignaciones de mayor alineación, pero notablemente debe emparejarse con _aligned_free , usar free es ilegal. Entonces, si no sabe si una asignación se realizó a través de malloc o _aligned_malloc entonces está atrapado en el mismo enigma en el que se encuentra alloc_system en Windows si no lo hace. No conozco la alineación de deallocate . El CRT no proporciona la función estándar aligned_alloc que se puede emparejar con free , por lo que ni siquiera Microsoft ha podido resolver este problema. (Aunque es una función C11 y Microsoft no es compatible con C11, ese es un argumento débil).

Tenga en cuenta que deallocate solo se preocupa por la alineación para saber si está sobrealineada, el valor real en sí mismo es irrelevante. Si quisiera un deallocate que fuera verdaderamente independiente de la alineación, simplemente podría tratar todas las asignaciones como sobrealineadas, pero desperdiciaría mucha memoria en asignaciones pequeñas.

@alexcrichton escribió :

Actualmente, la firma de Allocator::oom es:

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

Sin embargo, me llamó la atención que a Gecko al menos le gusta saber el tamaño de la asignación también en OOM. Es posible que deseemos considerar esto al estabilizar para quizás agregar un contexto como Option<Layout> para explicar por

El AllocErr ya lleva el Layout en la variante AllocErr::Exhausted . También podríamos agregar Layout a la variante AllocErr::Unsupported , que creo que sería la más simple en términos de expectativas del cliente. (Tiene el inconveniente de aumentar ligeramente el lado de la enumeración AllocErr sí, pero tal vez no deberíamos preocuparnos por eso ...)

Oh, sospecho que es todo lo que se necesita, ¡gracias por la corrección @pnkfelix!

Voy a comenzar a reutilizar este problema para el problema de seguimiento de std::heap en general, ya que será después de https://github.com/rust-lang/rust/pull/42727 aterriza. Cerraré algunas otras cuestiones relacionadas a favor de esto.

¿Existe algún problema de seguimiento para convertir colecciones? Ahora que los RP están fusionados, me gustaría

  • Discutir el tipo de error asociado
  • Discutir la conversión de colecciones para usar cualquier asignador local (especialmente aprovechando el tipo de error asociado)

Abrí https://github.com/rust-lang/rust/issues/42774 para rastrear la integración de Alloc en colecciones estándar. Con una discusión histórica en el equipo de bibliotecas, es probable que esté en una pista separada de estabilización que una pasada inicial del módulo std::heap .

Mientras revisaba los problemas relacionados con el asignador, también encontré https://github.com/rust-lang/rust/issues/30170 que @pnkfelix hace un tiempo. Parece que el asignador del sistema OSX tiene errores con alineaciones altas y cuando se ejecuta ese programa con jemalloc, al menos durante la desasignación en Linux, se segrega. ¡Vale la pena considerarlo durante la estabilización!

Abrí el número 42794 como un lugar para discutir la pregunta específica de si las asignaciones de tamaño cero deben coincidir con la alineación solicitada.

(¡Oh, espera, las asignaciones de tamaño cero son ilegales en los asignadores de usuarios!)

Dado que la función alloc::heap::allocate y los amigos ya no están en Nightly, he actualizado Servo para usar esta nueva API. Esto es parte de la diferencia:

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

Siento que la ergonomía no es excelente. Pasamos de importar un artículo a importar tres de dos módulos diferentes.

  • ¿Tendría sentido tener un método de conveniencia para allocator.alloc(Layout::from_size_align(…)) ?
  • ¿Tendría sentido hacer que los métodos <Heap as Alloc>::_ estén disponibles como funciones gratuitas o métodos inherentes? (Para tener un artículo menos para importar, el rasgo Alloc ).

Alternativamente, ¿podría el rasgo Alloc estar en el preludio o es un caso de uso demasiado nicho?

@SimonSapin En mi opinión, no tiene mucho sentido optimizar la ergonomía de una API de tan bajo nivel.

@SimonSapin

Siento que la ergonomía no es excelente. Pasamos de importar un artículo a importar tres de dos módulos diferentes.

Tuve exactamente la misma sensación con mi base de código: ahora es bastante torpe.

¿Tendría sentido tener un método de conveniencia para allocator.alloc(Layout::from_size_align(…))?

¿Te refieres al rasgo Alloc , o solo por Heap ? Una cosa a considerar aquí es que ahora hay una tercera condición de error: Layout::from_size_align devuelve un Option , por lo que podría devolver None además de los errores normales que puede obtener al asignar .

Alternativamente, ¿podría el rasgo Alloc estar en el preludio o es un caso de uso demasiado nicho?

En mi opinión, no tiene mucho sentido optimizar la ergonomía de una API de tan bajo nivel.

Estoy de acuerdo en que probablemente sea un nivel demasiado bajo para ponerlo en el preludio, pero sigo pensando que hay valor en optimizar la ergonomía (egoístamente, al menos, eso fue un refactor realmente molesto 😝).

@SimonSapin , ¿no std los tres tipos están disponibles en el módulo std::heap (se supone que están en un módulo). Además, ¿no ha manejado el caso de tamaños desbordados antes? ¿O tipos de tamaño cero?

¿No manejaste OOM antes?

Cuando existía, la función alloc::heap::allocate devolvía un puntero sin Result y no dejaba una opción en el manejo de OOM. Creo que abortó el proceso. Ahora agregué .unwrap() para provocar el pánico en el hilo.

se supone que deben estar en un módulo

Ahora veo que heap.rs contiene pub use allocator::*; . Pero cuando hice clic en Alloc en el impl que aparece en la página rustdoc para Heap me enviaron a alloc::allocator::Alloc .

En cuanto al resto, no lo he investigado. Estoy portando a un nuevo compilador una gran pila de código que fue escrito hace años. Creo que estas son devoluciones de llamada para FreeType, una biblioteca de C.

Cuando existía, la función alloc :: heap :: allocate devolvía un puntero sin un resultado y no dejaba una opción en el manejo de OOM.

Te dio una opción. El puntero que devolvió podría haber sido un puntero nulo que indicaría que el asignador de montón no pudo realizar la asignación. Es por eso que estoy tan contento de que haya cambiado a Result para que la gente no se olvide de manejar ese caso.

Bueno, tal vez el FreeType terminó haciendo una verificación nula, no lo sé. De todos modos, sí, devolver un resultado es bueno.

Dado el # 30170 y el # 43097, estoy tentado de resolver el problema de OS X con alineaciones ridículamente grandes simplemente especificando que los usuarios no pueden solicitar alineaciones> = 1 << 32 .

Una forma muy fácil de hacer cumplir esto: cambie la interfaz Layout modo que align se indique con u32 lugar de usize .

@alexcrichton, ¿tienes

@pnkfelix Layout::from_size_align aún tomaría usize y devolvería un error en u32 overflow, ¿verdad?

@SimonSapin ¿qué razón hay para que continúe tomando usize align, si una condición previa estática es que no es seguro pasar un valor> = 1 << 32 ?

y si la respuesta es "bueno, algunos asignadores podrían apoyar una alineación> = 1 << 32 ", entonces volvemos al status quo y puedes ignorar mi sugerencia. El punto de mi sugerencia es básicamente un "+1" a comentarios como este.

Porque std::mem::align_of devuelve usize

@SimonSapin ah, buen viejo API estable ... suspiro.

@pnkfelix ¡Me parece razonable limitar a 1 << 32 !

@rfcbot fcp fusionar

Ok, este rasgo y sus tipos se han horneado por un tiempo y también han sido la implementación subyacente de las colecciones estándar desde sus inicios. Propondría comenzar con una oferta inicial particularmente conservadora, es decir, solo estabilizar la siguiente interfaz:

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 {
    // ...
}

Propuesta 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 {
    // ...
}

Notablemente:

  • Solo estabilizando los métodos alloc , alloc_zeroed y dealloc en el rasgo Alloc por ahora. Creo que esto resuelve el problema más urgente que tenemos hoy, definir un asignador global personalizado.
  • Elimine el tipo Error en favor de usar solo punteros sin formato.
  • Cambie el tipo u8 en la interfaz a void
  • Una versión simplificada del tipo Layout .

Todavía hay preguntas abiertas como qué hacer con dealloc y la alineación (¿alineación precisa? ¿Se ajusta? ¿No estoy seguro?), Pero espero que podamos resolverlas durante FCP, ya que probablemente no será una API. cambio de ruptura.

+1 para estabilizar algo!

Cambia el nombre de AllocErr a Error y mueve la interfaz para que sea un poco más conservadora.

¿Elimina esto la opción para que los asignadores especifiquen Unsupported ? A riesgo de insistir más en algo en lo que he estado insistiendo mucho, creo que el número 44557 sigue siendo un problema.

Layout

Parece que ha eliminado algunos de los métodos de Layout . ¿Quería eliminar los que dejó fuera o simplemente dejarlos como inestables?

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

¿Es este un constructor para lo que es hoy AllocErr::Exhausted ? Si es así, ¿no debería tener un parámetro Layout ?

El miembro del equipo @alexcrichton ha propuesto fusionar esto. El siguiente paso lo revisan el resto de equipos etiquetados:

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

Preocupaciones:

Una vez que estos revisores lleguen a un consenso, entrará en su período de comentarios final. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡hable!

Consulte este documento para obtener información sobre los comandos que pueden darme los miembros del equipo etiquetados.

¡Estoy muy emocionado de poder estabilizar parte de este trabajo!

Una pregunta: en el hilo anterior, @joshlf y @ Ericson2314 plantearon un punto interesante sobre la posibilidad de separar los rasgos Alloc y Dealloc para optimizar los casos en los que alloc requiere algunos datos, pero dealloc no requiere información adicional, por lo que el tipo Dealloc puede ser de tamaño cero.

¿Se resolvió alguna vez esta pregunta? ¿Cuáles son las desventajas de separar los dos rasgos?

@joshlf

¿Elimina esto la opción de que los asignadores especifiquen No admitido?

Sí y no, significaría que dicha operación no es compatible con Rust estable de inmediato, pero podríamos seguir apoyándola en Rust inestable.

¿Quería eliminar los que dejó fuera o simplemente dejarlos como inestables?

¡En efecto! Una vez más, aunque solo propongo un área de superficie API estable, podemos dejar todos los demás métodos como inestables. Con el tiempo, podemos continuar estabilizando más funciones. Creo que es mejor empezar de la forma más conservadora posible.


@SimonSapin

¿Es este un constructor de lo que es hoy AllocErr :: Exhausted? Si es así, ¿no debería tener un parámetro de diseño?

¡Ah, buen punto! Sin embargo, quería dejar la posibilidad de hacer de Error un tipo de tamaño cero si realmente lo necesitáramos, pero, por supuesto, podemos mantener los métodos de toma de diseño inestables y estabilizarlos si es necesario. ¿O cree que el Error que conserva el diseño debería estabilizarse en la primera pasada?


@cramertj

Personalmente no había visto una pregunta / preocupación de este tipo todavía (¡creo que me la perdí!), Pero personalmente no vería que valiera la pena. Dos rasgos es el doble de lo habitual en general, ya que ahora todos tendrían que escribir Alloc + Dealloc en las colecciones, por ejemplo. Yo esperaría que un uso tan especializado no quisiera informar a la interfaz que todos los demás usuarios terminan usando personalmente.

@cramertj @alexcrichton

Personalmente no había visto una pregunta / preocupación de este tipo todavía (¡creo que me la perdí!), Pero personalmente no vería que valiera la pena.

En general, estoy de acuerdo en que no vale la pena con una excepción flagrante: Box . Box<T, A: Alloc> , dada la definición actual de Alloc , tendría que tener al menos dos palabras grandes (el puntero que ya tiene y una referencia a un Alloc como mínimo ) excepto en el caso de singleton globales (que se pueden implementar como ZST). Una explosión de 2x (o más) en el espacio requerido para almacenar un tipo tan común y fundamental me preocupa.

@alexcrichton

como ahora todos tendrían que escribir Alloc + Dealloc en colecciones, por ejemplo

Podríamos agregar algo como esto:

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

Una explosión de 2x (o más) en el espacio requerido para almacenar un tipo tan común y fundamental

Solo cuando usa un asignador personalizado que no es global de proceso. std::heap::Heap (el valor predeterminado) es de tamaño cero.

¿O cree que el error que conserva el diseño debería estabilizarse en la primera pasada?

@alexcrichton Realmente no entiendo por qué este primer paso propuesto está en absoluto. Apenas hay más de lo que ya se podría hacer abusando de Vec , y no es suficiente, por ejemplo, para usar https://crates.io/crates/jemallocator.

¿Qué queda por resolver para estabilizar todo el asunto?

Solo cuando usa un asignador personalizado que no es global de proceso. std :: heap :: Heap (el valor predeterminado) es de tamaño cero.

Ese parece ser el caso de uso principal de tener asignadores paramétricos, ¿no? Imagina la siguiente definición simple de un árbol:

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

Un árbol construido a partir de aquellos con una palabra Alloc tendría un tamaño de expansión de ~ 1.7x para toda la estructura de datos en comparación con un ZST Alloc . Eso me parece bastante malo, y este tipo de aplicaciones son el objetivo de que Alloc sea ​​un rasgo.

@cramertj

Podríamos agregar algo como esto:

También tendremos alias de rasgos reales :) https://github.com/rust-lang/rust/issues/41517

@glaebhoerl Sí, pero la estabilización todavía parece muy lejana ya que aún no hay una implementación. Si deshabilitamos las implicaciones manuales de Allocator , creo que podemos cambiar a los alias de rasgos de manera compatible con versiones anteriores cuando lleguen;)

@joshlf

Una explosión de 2x (o más) en el espacio requerido para almacenar un tipo tan común y fundamental me preocupa.

Me imagino que todas las implementaciones de hoy son solo un tipo de tamaño cero o un puntero grande, ¿verdad? ¿No es la posible optimización que algunos tipos de tamaño de puntero puedan tener un tamaño cero? (¿o algo así?)


@cramertj

Podríamos agregar algo como esto:

¡En efecto! Sin embargo, hemos llevado un rasgo a tres . En el pasado, nunca hemos tenido una gran experiencia con tales rasgos. Por ejemplo, Box<Both> no se convierte en Box<OnlyOneTrait> . Estoy seguro de que podríamos esperar a que las funciones del lenguaje suavicen todo esto, pero parece que, en el mejor de los casos, queda un largo camino por recorrer.


@SimonSapin

¿Qué queda por resolver para estabilizar todo el asunto?

No lo sé. Quería comenzar con lo más pequeño para que hubiera menos debate.

Me imagino que todas las implementaciones de hoy son solo un tipo de tamaño cero o un puntero grande, ¿verdad? ¿No es la posible optimización que algunos tipos de tamaño de puntero puedan tener un tamaño cero? (¿o algo así?)

Sí, la idea es que, dado un puntero a un objeto asignado desde su tipo de asignador, puede averiguar de qué instancia proviene (por ejemplo, usando metadatos en línea). Por lo tanto, la única información que necesita para desasignar es información de tipo, no información de tiempo de ejecución.

Para volver a la alineación en la desasignación, veo dos formas de avanzar:

  • Estabilizar según lo propuesto (con alineación en desasignar). Regalar la propiedad de la memoria asignada manualmente sería imposible a menos que se incluyera Layout . En particular, es imposible construir un Vec o Box o String u otro contenedor std con una alineación más estricta que la requerida (por ejemplo, porque no no quiero que el elemento en caja se extienda a una línea de caché), sin deconstruirlo y desasignarlo manualmente más tarde (lo cual no siempre es una opción) Otro ejemplo de algo que sería imposible es llenar un Vec usando operaciones simd y luego regalarlo.

  • No requiera alineación al desasignar, y elimine la optimización de asignación pequeña de HeapAlloc -based alloc_system de Windows. Guarde siempre la alineación. @alexcrichton , cuando HeapAlloc esté redondeando los tamaños de todos modos).

En cualquier caso, esta es una compensación muy difícil de hacer; el impacto en la memoria y el rendimiento dependerá en gran medida del tipo de aplicación, y para cuál optimizar también es específico de la aplicación.

Creo que en realidad podemos ser Just Fine (TM). Citando los documentos Alloc :

Algunos de los métodos requieren que un diseño se ajuste a un bloque de memoria.
Lo que significa que un diseño "encaje" en un bloque de memoria significa (o
equivalentemente, para que un bloque de memoria "encaje" en un diseño) es que el
Deben cumplirse las siguientes dos condiciones:

  1. La dirección de inicio del bloque debe estar alineada con layout.align() .

  2. El tamaño del bloque debe estar en el rango [use_min, use_max] , donde:

    • use_min es self.usable_size(layout).0 , y

    • use_max es la capacidad que estaba (o habría sido)
      devuelto cuando (si) el bloque se asignó mediante una llamada a
      alloc_excess o realloc_excess .

Tenga en cuenta que:

  • el tamaño del diseño utilizado más recientemente para asignar el bloque
    se garantiza que estará en el rango [use_min, use_max] , y

  • un límite inferior en use_max puede aproximarse con seguridad mediante una llamada a
    usable_size .

  • si un diseño k encaja en un bloque de memoria (indicado por ptr )
    asignado actualmente a través de un asignador a , entonces es legal
    use ese diseño para desasignarlo, es decir, a.dealloc(ptr, k); .

Tenga en cuenta la última viñeta. Si asigno con un diseño con alineación a , entonces debería ser legal para mí desasignar con alineación b < a porque un objeto que está alineado con a también está alineado con b , y por lo tanto un diseño con alineación b ajusta a un objeto asignado con un diseño con alineación a (y con el mismo tamaño).

Lo que esto significa es que debería poder asignar con una alineación que sea mayor que la alineación mínima requerida para un tipo en particular y luego permitir que algún otro código se desasigne con la alineación mínima, y ​​debería funcionar.

¿No es la posible optimización que algunos tipos de tamaño de puntero puedan tener un tamaño cero? (¿o algo así?)

Recientemente hubo un RFC para esto y parece muy poco probable que se pueda hacer debido a problemas de compatibilidad: https://github.com/rust-lang/rfcs/pull/2040

Por ejemplo, Box<Both> no se convierte en Box<OnlyOneTrait> . Estoy seguro de que podríamos esperar a que las funciones del lenguaje suavicen todo esto, pero parece que, en el mejor de los casos, queda un largo camino por recorrer.

Por otro lado, el upcasting de objetos de rasgo parece indiscutiblemente deseable y, sobre todo, una cuestión de esfuerzo / ancho de banda / fuerza de voluntad para implementarlo. Recientemente hubo un hilo: https://internals.rust-lang.org/t/trait-upcasting/5970

@ruuda Yo fui quien escribió originalmente esa implementación alloc_system . alexcrichton simplemente lo movió durante los grandes refactores de asignación de <time period> .

La implementación actual requiere que desasigne con la misma alineación especificada con la que asignó un bloque de memoria dado. Independientemente de lo que pueda afirmar la documentación, esta es la realidad actual que todo el mundo debe cumplir hasta que se cambie alloc_system en Windows.

Las asignaciones en Windows siempre usan un múltiplo de MEMORY_ALLOCATION_ALIGNMENT (aunque recuerdan el tamaño con el que las asignó al byte). MEMORY_ALLOCATION_ALIGNMENT es 8 en 32 bits y 16 en 64 bits. Para los tipos sobrealineados, debido a que la alineación es mayor que MEMORY_ALLOCATION_ALIGNMENT , la sobrecarga causada por alloc_system es consistentemente la cantidad de alineación especificada, por lo que una asignación alineada de 64 bytes tendría 64 bytes de sobrecarga.

Si decidimos extender ese truco generalizado a todas las asignaciones (lo que eliminaría el requisito de desasignar con la misma alineación que especificó al realizar la asignación), entonces más asignaciones tendrían sobrecarga. Las asignaciones cuyas alineaciones son idénticas a MEMORY_ALLOCATION_ALIGNMENT sufrirán una sobrecarga constante de MEMORY_ALLOCATION_ALIGNMENT bytes. Las asignaciones cuyas alineaciones son inferiores a MEMORY_ALLOCATION_ALIGNMENT sufrirán una sobrecarga de MEMORY_ALLOCATION_ALIGNMENT bytes aproximadamente la mitad del tiempo. Si el tamaño de la asignación redondeado a MEMORY_ALLOCATION_ALIGNMENT es mayor o igual que el tamaño de la asignación más el tamaño de un puntero, entonces no hay gastos generales; de lo contrario, los hay. Teniendo en cuenta que el 99,99% de las asignaciones no se asignarán en exceso, ¿realmente desea incurrir en ese tipo de gastos generales en todas esas asignaciones?

@ruuda

Personalmente, creo que la implementación de alloc_system hoy en Windows es un beneficio mayor que tener la capacidad de ceder la propiedad de una asignación a otro contenedor como Vec . AFAIK, aunque no hay datos para medir el impacto de siempre rellenar con la alineación y no requerir una alineación en la desasignación.

@joshlf

Creo que ese comentario es incorrecto, como se señaló, alloc_system en Windows se basa en que se pase la misma alineación a la desasignación que se pasó en la asignación.

Teniendo en cuenta que el 99,99% de las asignaciones no se asignarán en exceso, ¿realmente desea incurrir en ese tipo de gastos generales en todas esas asignaciones?

Depende de la aplicación si la sobrecarga es significativa y si se debe optimizar la memoria o el rendimiento. Mi sospecha es que para la mayoría de las aplicaciones cualquiera de las dos está bien, pero una pequeña minoría se preocupa profundamente por la memoria y realmente no pueden permitirse esos bytes adicionales. Y otra pequeña minoría necesita control sobre la alineación, y realmente lo necesita.

@alexcrichton

Creo que ese comentario es incorrecto, como se señaló alloc_system en Windows se basa en que se pase la misma alineación a la desasignación que se pasó en la asignación.

¿No implica eso que alloc_system en Windows en realidad no implementa correctamente el rasgo Alloc (y, por lo tanto, tal vez deberíamos cambiar los requisitos del rasgo Alloc )?


@ retep998

Si estoy leyendo su comentario correctamente, ¿no está presente esa sobrecarga de alineación para todas las asignaciones, independientemente de si necesitamos poder desasignar con una alineación diferente? Es decir, si asigno 64 bytes con alineación de 64 bytes y también desasigno la alineación de 64 bytes, la sobrecarga que describió todavía está presente. Por lo tanto, no es una característica de poder desasignar con diferentes alineaciones, sino una característica de solicitar alineaciones más grandes de lo normal.

@joshlf La sobrecarga causada por alloc_system actualmente se debe a la solicitud de alineaciones más grandes de lo normal. Si su alineación es menor o igual a MEMORY_ALLOCATION_ALIGNMENT , entonces no hay gastos generales causados ​​por alloc_system .

Sin embargo, si cambiamos la implementación para permitir la desasignación con diferentes alineaciones, entonces la sobrecarga se aplicaría a casi todas las asignaciones, independientemente de la alineación.

Ah, ya veo; tiene sentido.

¿Cuál es el significado de implementar Alloc tanto para Heap como para & Heap? ¿En qué casos el usuario usaría una de esas impls frente a la otra?

¿Es esta la primera API de biblioteca estándar en la que *mut u8 significaría "puntero a lo que sea"? Hay String :: from_raw_parts pero ese realmente significa puntero a bytes. No soy un fan de *mut u8 significa "puntero a lo que sea", incluso C lo hace mejor. ¿Cuáles son las otras opciones? Quizás un puntero a tipo opaco sería más significativo.

@rfcbot preocupación * mut u8

@dtolnay Alloc for Heap es una especie de "estándar" y Alloc for &Heap es como Write for &T donde el rasgo requiere &mut self pero la implementación no. En particular, eso significa que tipos como Heap y System son seguros para subprocesos y no necesitan sincronizarse al realizar la asignación.

Sin embargo, lo que es más importante, el uso de #[global_allocator] requiere que la estática a la que se adjunta, que tiene el tipo T , tenga Alloc for &T . (también conocido como todos los asignadores globales deben ser seguros para subprocesos)

Por *mut u8 creo que *mut () puede ser interesante, pero personalmente no me siento demasiado obligado a "hacer esto bien" aquí per se.

La principal ventaja de *mut u8 es que es muy conveniente utilizar .offset con compensaciones de bytes.

Por *mut u8 creo que *mut () puede ser interesante, pero personalmente no me siento demasiado obligado a "hacer esto bien" aquí per se.

Si vamos con *mut u8 en una interfaz estable, ¿no estamos encerrándonos? En otras palabras, una vez que estabilicemos esto, no tendremos la oportunidad de "hacer esto bien" en el futuro.

Además, *mut () parece un poco peligroso en caso de que alguna vez hagamos una optimización como RFC 2040 en el futuro.

La principal ventaja de *mut u8 es que es muy conveniente utilizar .offset con compensaciones de bytes.

Es cierto, pero podrías hacer fácilmente let ptr = (foo as *mut u8) y luego seguir tu camino feliz. Eso no parece ser una motivación suficiente para seguir con *mut u8 en la API si hay alternativas convincentes (que, para ser justos, no estoy seguro de que las haya).

Además, * mut () me parece un poco peligroso en caso de que alguna vez hagamos una optimización como RFC 2040 en el futuro.

Esa optimización probablemente nunca sucederá, rompería demasiado código existente. Incluso si lo hiciera, se aplicaría a &() y &mut () , no a *mut () .

Si RFC 1861 estuviera cerca de implementarse / estabilizarse, sugeriría usarlo:

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);
    // ...
}

Probablemente esté demasiado lejos, ¿verdad?

@joshlf Creí haber visto un PR abierto sobre ellos, el resto desconocido es DynSized .

¿Funcionará esto para objetos similares a struct hack? Digamos que tengo un Node<T> que se ve así:

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

y un tipo de valor:

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

Ahora quiero asignar Node<V> con una cadena de tamaño 7 en una sola asignación. Idealmente, quiero hacer una asignación de tamaño 16, alinear 4 y ajustar todo en él: 4 para u32 , 5 para V y 7 para los bytes de cadena. Esto funciona porque el último miembro de V tiene alineación 1 y los bytes de cadena también tienen alineación 1.

Tenga en cuenta que esto no está permitido en C / C ++ si los tipos están compuestos como arriba, ya que escribir en almacenamiento empaquetado es un comportamiento indefinido. Creo que este es un agujero en el estándar C / C ++ que, lamentablemente, no se puede solucionar. Puedo explicar por qué esto está roto, pero centrémonos en Rust. ¿Puede esto funcionar? :-)

Con respecto al tamaño y alineación de la estructura Node<V> sí, estás más o menos al capricho del compilador de Rust. Es UB (comportamiento indefinido) asignar con cualquier tamaño o alineación menor de lo que requiere Rust, ya que Rust puede realizar optimizaciones basadas en la suposición de que cualquier objeto Node<V> - en la pila, en el montón, detrás de una referencia, etc. - tiene un tamaño y una alineación que coinciden con lo esperado en el momento de la compilación.

En la práctica, parece que la respuesta es, lamentablemente, no: ejecuté este programa y descubrí que, al menos en el Rust Playground, Node<V> tiene un tamaño de 12 y una alineación de 4, lo que significa que cualquier objeto después el Node<V> debe estar compensado por al menos 12 bytes. Parece que el desplazamiento del campo data.b dentro del Node<V> es de 8 bytes, lo que implica que los bytes 9-11 son un relleno final. Desafortunadamente, aunque esos bytes de relleno "no se usan" en algún sentido, el compilador los trata como parte del Node<V> , y se reserva el derecho de hacer lo que quiera con ellos (lo más importante, incluyendo escribir a ellos cuando asigna un Node<V> , lo que implica que si intenta almacenar datos adicionales allí, es posible que se sobrescriban).

(nota, por cierto: no puede tratar un tipo como empaquetado que el compilador de Rust no cree que esté empaquetado. Sin embargo, _puede_ decirle al compilador de Rust que algo está empaquetado, lo que cambiará el diseño del tipo (eliminando el relleno), usando repr(packed) )

Sin embargo, con respecto a diseñar un objeto tras otro sin que ambos sean parte del mismo tipo de Rust, estoy casi 100% seguro de que esto es válido; después de todo, es lo que hace Vec . Puede utilizar los métodos del tipo Layout para calcular dinámicamente cuánto espacio se necesita para la asignación 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 como esto funcionaría?

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

... luego asigne con un tamaño más grande y use slice::from_raw_parts_mut(node.bytes.as_mut_ptr(), size) ?

¡Gracias @joshlf por la respuesta detallada! El TLDR para mi caso de uso es que puedo obtener un Node<V> de tamaño 16 pero solo si V es repr(packed) . De lo contrario, lo mejor que puedo hacer es la talla 19 (12 + 7).

@SimonSapin no estoy seguro; Intentaré.

Realmente no me he puesto al día con este hilo, pero estoy decidido a estabilizar nada todavía. Aún no hemos avanzado en la implementación de los problemas difíciles:

  1. Colecciones de asignadores polimórficos

    • ¡ni siquiera una caja no hinchada!

  2. Colecciones faliables

Creo que el diseño de los rasgos fundamentales afectará a las soluciones de los: He tenido poco tiempo para la roya en los últimos meses, pero han argumentado esto a veces. Dudo que tenga tiempo para exponer completamente mi caso aquí tampoco, así que solo puedo esperar que primero al menos redactemos una solución completa para todos esos: alguien demuestre que estoy equivocado de que es imposible ser riguroso (forzar el uso correcto), flexible y ergonómico con las características actuales. O incluso simplemente termine de marcar las casillas en la parte superior.

Re: Comentario de @ Ericson2314

Creo que una pregunta relevante relacionada con el conflicto entre esa perspectiva y el deseo de @alexcrichton de estabilizar algo es: ¿Cuánto beneficio obtenemos al estabilizar una interfaz mínima? En particular, muy pocos consumidores llamarán directamente a los métodos Alloc (incluso la mayoría de las colecciones probablemente usarán Box o algún otro contenedor similar), por lo que la pregunta real es: ¿qué significa estabilizar la compra para los usuarios que no llamar directamente a los métodos Alloc ? Honestamente, el único caso de uso serio en el que puedo pensar es que allana el camino para las colecciones polimórficas de asignadores (que probablemente serán utilizadas por un conjunto mucho más amplio de usuarios), pero parece que está bloqueado en # 27336, que está lejos de ser siendo resuelto. Tal vez haya otros grandes casos de uso que me faltan, pero según ese análisis rápido, me inclino a alejarme de la estabilización por tener solo beneficios marginales al costo de encerrarnos en un diseño que luego podríamos encontrar subóptimo .

@joshlf permite a las personas definir y utilizar sus propios asignadores globales.

Hmmm buen punto. ¿Sería posible estabilizar especificando el asignador global sin estabilizar Alloc ? Es decir, el código que implementa Alloc tendría que ser inestable, pero probablemente estaría encapsulado en su propia caja, y el mecanismo para marcar ese asignador como el asignador global sería estable. ¿O estoy entendiendo mal cómo interactúan estable / inestable y el compilador estable / compilador nocturno?

Ah @joshlf recuerda que # 27336 es una distracción, según https://github.com/rust-lang/rust/issues/42774#issuecomment -317279035. Estoy bastante seguro de que nos encontraremos con otros problemas: problemas con los rasgos tal como existen, por lo que quiero trabajar para comenzar a trabajar en eso ahora. Es mucho más fácil discutir esos problemas una vez que llegan para que todos los vean que debatir los futuros previstos después del # 27336.

@joshlf Pero no puedes compilar la caja que define el asignador global con un compilador estable.

@sfackler Ah, sí, existe ese malentendido que temía: P

Encuentro el nombre Excess(ptr, usize) un poco confuso porque el usize no es el excess en el tamaño de la asignación solicitada (como en el tamaño adicional asignado), sino el total tamaño de la asignación.

OMI Total , Real , Usable , o cualquier nombre que transmita que el tamaño es el tamaño total o el tamaño real de la asignación es mejor que "exceso", que encuentro engañoso. Lo mismo se aplica a los métodos _excess .

Estoy de acuerdo con @gnzlbg anterior, creo que una tupla simple (ptr, usize) estaría bien.

Tenga en cuenta que no se propone estabilizar Excess en la primera pasada, sin embargo

Publicó este hilo para la discusión en reddit, que tiene algunas personas con preocupaciones: https://www.reddit.com/r/rust/comments/78dabn/custom_allocators_are_on_the_verge_of_being/

Después de una mayor discusión con @ rust-lang / libs hoy, me gustaría hacer algunos ajustes a la propuesta de estabilización que se pueden resumir con:

  • Agregue alloc_zeroed al conjunto de métodos estabilizados; de lo contrario, tenga la misma firma que alloc .
  • Cambie *mut u8 a *mut void en la API usando el soporte de extern { type void; } , resolviendo la preocupación de @dtolnay y proporcionando un camino a seguir para unificar c_void todo el ecosistema.
  • Cambie el tipo de devolución de alloc a *mut void , eliminando Result y Error

Quizás el más polémico es el último punto, así que quiero desarrollarlo también. Esto surgió de la discusión con el equipo de libs hoy y específicamente giró en torno a cómo (a) la interfaz basada en Result tiene una ABI menos eficiente que una que devuelve punteros y (b) casi no hay asignadores de "producción" en la actualidad proporcionar la capacidad de aprender algo más que "esto solo OOM". Para el rendimiento, la mayoría de las veces podemos empapelarlo con líneas internas y demás, pero sigue siendo que Error es una carga útil adicional que es difícil de eliminar en las capas más bajas.

La idea para devolver cargas útiles de errores es que los asignadores pueden proporcionar una introspección específica de la implementación para saber por qué falló una asignación y, de lo contrario, casi todos los consumidores solo deberían necesitar saber si la asignación tuvo éxito o no. Además, se pretende que sea una API de muy bajo nivel que en realidad no se llama tan a menudo (en su lugar, las API escritas que envuelven bien las cosas deberían llamarse en su lugar). En ese sentido, no es primordial que tengamos la API más utilizable y ergonómica para esta ubicación, sino que es más importante habilitar casos de uso sin sacrificar el rendimiento.

La principal ventaja de *mut u8 es que es muy conveniente utilizar .offset con compensaciones de bytes.

En la reunión de libs también sugerimos impl *mut void { fn offset } que no entra en conflicto con el offset definido para T: Sized . También podría ser byte_offset .

+1 por usar *mut void y byte_offset . ¿Habrá un problema con la estabilización de la función de tipos externos, o podemos eludir ese problema porque solo la definición es inestable (y liballoc puede hacer cosas inestables internamente) y no el uso (por ejemplo, let a: *mut void = ... isn no es inestable)?

Sí, no necesitamos bloquear la estabilización de tipo externo. Incluso si se elimina el soporte de tipo externo, el void que definimos para esto siempre puede ser el peor de los casos de tipo mágico.

¿Hubo alguna otra discusión en la reunión de bibliotecas sobre si Alloc y Dealloc deberían ser rasgos separados o no?

No tocamos eso específicamente, pero en general sentimos que no deberíamos desviarnos del estado de la técnica a menos que tengamos una razón particularmente convincente para hacerlo. En particular, el concepto Allocator de C ++ no tiene una división similar.

No estoy seguro de que sea una comparación adecuada en este caso. En C ++, todo se libera explícitamente, por lo que no hay equivalente a Box que necesite almacenar una copia de (o una referencia a) su propio asignador. Eso es lo que nos causa la explosión de gran tamaño.

@joshlf unique_ptr es el equivalente de Box , vector es el equivalente de Vec , unordered_map es el equivalente de HashMap , etc.

@cramertj Ah, interesante, solo estaba mirando tipos de colecciones. Parece que eso podría ser algo que hacer entonces. Siempre podemos agregarlo más tarde a través de implicaciones generales, pero probablemente sería más limpio evitarlo.

El enfoque implícito general podría ser más limpio, en realidad:

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

Un rasgo menos del que preocuparse en la mayoría de los casos de uso.

  • Cambie el tipo de retorno de alloc a * mut void, eliminando el resultado y el error

Quizás el más polémico es el último punto, así que quiero desarrollarlo también. Esto surgió de la discusión con el equipo de bibliotecas hoy y giró específicamente en torno a cómo (a) la interfaz basada en resultados tiene una ABI menos eficiente que una que devuelve punteros y (b) casi ningún asignador de "producción" hoy brinda la capacidad de aprender algo más que "esto solo OOM'd". Para mejorar el rendimiento, en su mayoría podemos empapelarlo con líneas internas y demás, pero sigue siendo que el error es una carga útil adicional que es difícil de eliminar en las capas más bajas.

Me preocupa que esto facilite mucho el uso del puntero devuelto sin verificar si es nulo. Parece que la sobrecarga también podría eliminarse sin agregar este riesgo al devolver Result<NonZeroPtr<void>, AllocErr> y hacer que AllocErr tamaño cero?

( NonZeroPtr es una combinación de ptr::Shared y ptr::Unique como se propone en https://github.com/rust-lang/rust/issues/27730#issuecomment-316236397.)

@SimonSapin algo como Result<NonZeroPtr<void>, AllocErr> requiere que se estabilicen tres tipos, todos los cuales son nuevos y algunos de los cuales históricamente han estado languideciendo bastante por estabilización. Algo como void ni siquiera es necesario y es bueno tenerlo (en mi opinión).

Estoy de acuerdo en que es "fácil de usar sin verificar si hay nulos", pero esta es, nuevamente, una API de muy bajo nivel que no está destinada a un uso intensivo, por lo que no creo que debamos optimizar la ergonomía de las personas que llaman.

Las personas también pueden construir abstracciones de nivel superior como alloc_one sobre el nivel bajo alloc que podrían tener tipos de retorno más complejos como Result<NonZeroPtr<void>, AllocErr> .

Estoy de acuerdo en que AllocErr no sería útil en la práctica, pero ¿qué tal solo Option<NonZeroPtr<void>> ? Las API que son imposibles de usar incorrectamente accidentalmente, sin gastos generales, es una de las cosas que distinguen a Rust de C, y volver a los punteros nulos de estilo C me parece un paso atrás. Decir que es “una API de muy bajo nivel que no está pensada para un uso intensivo” es como decir que no deberíamos preocuparnos por la seguridad de la memoria en arquitecturas de microcontroladores poco comunes porque son de muy bajo nivel y no se usan mucho.

Cada interacción con el asignador implica un código inseguro independientemente del tipo de retorno de esta función. Es posible utilizar incorrectamente las API de asignación de bajo nivel si el tipo de retorno es Option<NonZeroPtr<void>> o *mut void .

Alloc::alloc en particular es la API que es de bajo nivel y no está destinada a un uso intensivo. Los métodos como Alloc::alloc_one<T> o Alloc::alloc_array<T> son las alternativas que se utilizarían más y tendrían un tipo de retorno "más agradable".

Un stateful AllocError no vale la pena, pero un tipo de tamaño cero que implementa un error y tiene un Display de allocation failure es bueno tener. Si vamos por la ruta NonZeroPtr<void> , veo Result<NonZeroPtr<void>, AllocError> como preferible a Option<NonZeroPtr<void>> .

Por qué la prisa por estabilizar :( !! Result<NonZeroPtr<void>, AllocErr> es indiscutiblemente más agradable para los clientes. Decir que esta es una "API de muy bajo nivel" que no tiene por qué ser agradable es deprimentemente poco ambicioso. El código en todos los niveles debería ser tan seguro y fácil de mantener como sea posible; código oscuro que no se edita constantemente (y por lo tanto se pagina en la memoria a corto plazo de las personas) ¡tanto más!

Además, si vamos a tener colecciones polimórficas de asignación escritas por el usuario, lo que ciertamente espero, esa es una cantidad abierta de código bastante complejo que usa asignadores directamente.

Re-desasignación, operativamente, es casi seguro que queremos hacer referencia / clonar el alloactor solo una vez por colección basada en árbol. Eso significa pasar el asignador a cada cuadro de asignador personalizado que se destruye. Pero, es un problema abierto sobre la mejor manera de hacer esto en Rust sin tipos lineales. Contrariamente a mi comentario anterior, estaría bien con algún código inseguro en implementaciones de colecciones para esto, porque el caso de uso ideal cambia la implementación de Box , no la implementación de los rasgos del asignador dividido y desasignador. Es decir, podemos lograr un progreso estabilizable sin bloquear la linealidad.

@sfackler Creo que necesitamos algunos tipos asociados que conecten el desasignador con el asignador; puede que no sea posible adaptarlos.

@ Ericson2314 Hay una "prisa" por estabilizar porque la gente quiere usar asignadores para cosas reales en el mundo real. Este no es un proyecto de ciencia.

¿Para qué se utilizaría ese tipo asociado?

La gente de marcar un / y el tipo de personas que se preocupan por este tipo de función avanzada deberían sentirse cómodas haciéndolo. [Si el problema es inestable rustc vs inestable Rust, ese es un problema diferente que necesita una solución de política.] Hornear en pésimas API, por el contrario, nos paraliza para siempre , a menos que queramos dividir el ecosistema con un nuevo estándar 2.0.

Los tipos asociados relacionarían el desasignador con el asignador. Cada uno necesita saber sobre el otro para que esto funcione. [Todavía existe el problema de usar el (des) asignador incorrecto del tipo correcto, pero acepto que nadie ha propuesto remotamente una solución para eso].

Si la gente puede simplemente anclar a un nightly, ¿por qué tenemos compilaciones estables? El conjunto de personas que interactúan directamente con las API de asignación es mucho más pequeño que las personas que desean aprovechar esas API, por ejemplo, reemplazando el asignador global.

¿Puede escribir algún código que muestre por qué un desasignador necesita saber el tipo de su asignador asociado? ¿Por qué la API de asignación de C ++ no necesita una asignación similar?

Si la gente puede simplemente anclar a un nightly, ¿por qué tenemos compilaciones estables?

Para indicar la estabilidad del idioma. El código que escriba en esta versión de las cosas nunca se romperá. En un compilador más nuevo. Fijas una noche cuando necesitas algo tan mal que no vale la pena esperar la versión final de la función que se considera de calidad digna de esa garantía.

El conjunto de personas que interactúan directamente con las API de asignación es mucho más pequeño que las personas que desean aprovechar esas API, por ejemplo, reemplazando el asignador global.

¡Ajá! ¿Esto sería para sacar a Jemalloc de un árbol, etc.? Nadie ha propuesto estabilizar los horribles hacks que permiten elegir el asignador global, ¿solo la pila estática en sí misma? ¿O leí mal la propuesta?

Se propone estabilizar los horribles hacks que permiten elegir el asignador global, que es la mitad de lo que nos permite sacar jemalloc del árbol. Este problema es la otra mitad.

#[global_allocator] estabilización de atributo: https://github.com/rust-lang/rust/issues/27389#issuecomment -336955367

¡Ay!

@ Ericson2314 ¿Cuál crees que sería una forma no horrible de seleccionar el asignador global?

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

La propuesta se ha modificado para utilizar * mut void.

@rfcbot resuelto * mut u8

@rfcbot revisado

Después de una discusión sobre IRC, estoy aprobando esto con el entendimiento de que _no_ tenemos la intención de estabilizar un Box genérico en Alloc , sino en algún Dealloc rasgo con un impl general apropiado, como lo sugiere @sfackler aquí . Por favor, avíseme si he entendido mal la intención.

@cramertj Solo para aclarar, ¿es posible agregar esa implícita general después del hecho y no romper la definición Alloc que estamos estabilizando aquí?

@joshlf sí, se vería así: https://github.com/rust-lang/rust/issues/32838#issuecomment -340959804

¿Cómo especificaremos Dealloc para un Alloc dado? ¿Me imagino algo como esto?

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

Supongo que eso nos pone en territorio espinoso WRT https://github.com/rust-lang/rust/issues/29661.

Sí, no creo que haya una manera de que la adición de Dealloc sea ​​retrocompatible con las definiciones existentes de Alloc (que no tienen ese tipo asociado) sin tener un valor predeterminado.

Si quisiera poder tomar automáticamente el desasignador correspondiente a un asignador, necesitaría más que solo un tipo asociado, sino una función para producir un valor desasignador.

Pero, esto se puede manejar en el futuro con esas cosas adjuntas a un sustrato separado de Alloc , creo.

@sfackler, no estoy seguro de entender. ¿Puedes escribir la firma de Box::new debajo de tu diseño?

Esto es ignorar la sintaxis de ubicación y todo eso, pero una forma de hacerlo sería

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

En particular, debemos ser capaces de producir una instancia del desasignador, no solo saber su tipo. También puede parametrizar Box sobre Alloc y almacenar A::Dealloc lugar, lo que podría ayudar con la inferencia de tipos. Podemos hacer que esto funcione después de esta estabilización moviendo Dealloc y deallocator a un rasgo separado:

pub trait SplitAlloc: Alloc {
    type Dealloc;

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

Pero, ¿cómo sería el implícito 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);
        }
    }
}

Pero suponiendo que primero estabilicemos Alloc , entonces no todos los Alloc s implementarán Dealloc , ¿verdad? ¿Y pensé que la especialización implícita aún estaba lejos? En otras palabras, en teoría, le gustaría hacer algo como lo siguiente, pero no creo que funcione todavía.

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

En todo caso, tendríamos un

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

Pero no creo que eso sea realmente necesario. Los casos de uso de asignadores personalizados y asignadores globales son lo suficientemente distintos como para no asumir que habría una gran superposición entre ellos.

Supongo que podría funcionar. Sin embargo, me parece mucho más limpio tener Dealloc desde el principio para que podamos tener la interfaz más simple. Imagino que podríamos tener una interfaz bastante simple y poco controvertida que no requeriría ningún cambio en el código existente que ya 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 }
    ...
}

¿Pensé que los tipos predeterminados asociados eran problemáticos?

Un Dealloc que es una referencia mutable al asignador no parece tan útil; solo puede asignar una cosa a la vez, ¿verdad?

¿Pensé que los tipos predeterminados asociados eran problemáticos?

Oh, supongo que los tipos predeterminados asociados están lo suficientemente lejos como para que no podamos confiar en ellos.

Aún así, podríamos tener lo más simple:

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;
    ...
}

y solo requiere que el implementador escriba un poco de repetición.

Un Dealloc que es una referencia mutable al asignador no parece tan útil; solo puede asignar una cosa a la vez, ¿verdad?

Sí, buen punto. Probablemente un punto discutible de todos modos dado su otro comentario.

¿Debería deallocator tomar self , &self o &mut self ?

Probablemente &mut self para ser coherente con los otros métodos.

¿Hay algún asignador que prefiera tomarse a sí mismo por valor para no tener que, por ejemplo, clonar el estado?

El problema de tomar self por valor es que impide obtener un Dealloc y luego continuar asignando.

Estoy pensando en un asignador hipotético "one-shot", aunque no sé qué tan real es eso.

Tal asignador podría existir, pero tomar self por valor requeriría que _todos_ los asignadores funcionen de esa manera, y excluiría cualquier asignador que permita la asignación después de que se haya llamado deallocator .

Todavía me gustaría ver algo de esto implementado y utilizado en colecciones antes de pensar en estabilizarlo.

¿Cree que https://github.com/rust-lang/rust/issues/27336 o los puntos discutidos en https://github.com/rust-lang/rust/issues/32838#issuecomment -339066870 nos permitirán avanzar con las colecciones?

Me preocupa el impacto del enfoque de alias de tipo en la legibilidad de la documentación. Una forma (muy detallada) de permitir el progreso sería ajustar los tipos:

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

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

Sé que es una molestia, pero parece que los cambios que estamos discutiendo aquí son lo suficientemente grandes como para que si decidimos seguir adelante con los rasgos divididos de alloc / dealloc, deberíamos probarlos primero en std y luego volver a FCP.

¿Cuál es el cronograma de espera para que se implementen estas cosas?

El método grow_in_place no devuelve ningún tipo de exceso de capacidad. Actualmente llama a usable_size con un diseño, extiende la asignación para _al menos_ ajustarse a este diseño, pero si la asignación se extiende más allá de ese diseño, los usuarios no tienen forma de saberlo.

Estoy teniendo dificultades para comprender la ventaja de los métodos alloc y realloc sobre alloc_excess y realloc_excess .

Un asignador necesita encontrar un bloque de memoria adecuado para realizar una asignación: esto requiere conocer el tamaño del bloque de memoria. Si el asignador devuelve un puntero o la tupla "puntero y tamaño del bloque de memoria", no hay diferencias de rendimiento medibles.

Entonces alloc y realloc solo aumentan la superficie de la API y parecen alentar la escritura de código de menor rendimiento. ¿Por qué los tenemos en la API? Cual es su ventaja?


EDITAR: o en otras palabras: todas las funciones de asignación potencial en la API deberían devolver Excess , lo que básicamente elimina la necesidad de todos los métodos _excess .

El exceso solo es interesante para casos de uso que involucran matrices que pueden crecer. No es útil ni relevante para Box o BTreeMap , por ejemplo. Puede haber algún costo en calcular cuál es el exceso, y ciertamente hay una API más compleja, por lo que no me parece que el código que no se preocupa por el exceso de capacidad deba ser forzado a pagar por ello.

Puede haber algún costo en calcular cuál es el exceso

¿Puede dar un ejemplo? No sé, y no puedo imaginar, un asignador que sea capaz de asignar memoria pero que no sepa cuánta memoria está asignando realmente (que es lo que Excess es: cantidad real de memoria asignada; deberíamos cambiarle el nombre).

El único Alloc uso común donde esto podría ser un poco controvertido es POSIX malloc , que aunque siempre calcula el Excess internamente, no lo expone como parte de su C API. Sin embargo, devolver el tamaño solicitado como Excess está bien, es portátil, simple, no incurre en ningún costo y es lo que todos los que usan POSIX malloc ya están asumiendo de todos modos.

jemalloc y básicamente cualquier otro Alloc proporcionar API que devuelvan el Excess sin incurrir en ningún costo, por lo que para esos asignadores, devolver el Excess es cero costo también.

Puede haber algún costo en calcular cuál es el exceso, y ciertamente hay una API más compleja, por lo que no me parece que el código que no se preocupa por el exceso de capacidad deba ser forzado a pagar por ello.

En este momento, todo el mundo ya está pagando el precio del rasgo del asignador que tiene dos API para asignar memoria. Y mientras que uno puede construir una API Excess -less encima de una Excess -full one`, lo contrario no es cierto. Entonces me pregunto por qué no se hace así:

  • Alloc métodos de rasgo siempre devuelven Excess
  • agregue un rasgo ExcessLessAlloc que simplemente elimine los métodos Excess de Alloc para todos los usuarios que 1) se preocupan lo suficiente como para usar Alloc pero 2) no preocuparse por la cantidad real de memoria que se está asignando actualmente (me parece un nicho, pero sigo pensando que es bueno tener una API de este tipo)
  • Si un día alguien descubre una manera de implementar Alloc ators con rutas rápidas para métodos Excess -less, siempre podemos proporcionarle una implementación personalizada de ExcessLessAlloc .

FWIW Acabo de aterrizar en este hilo nuevamente porque no puedo implementar lo que quiero además de Alloc . Mencioné que falta grow_in_place_excess antes, pero me quedé atascado nuevamente porque también falta alloc_zeroed_excess (y quién sabe qué más).

Me sentiría más cómodo si la estabilización aquí se enfocara primero en estabilizar una API completa de Excess . Incluso si su API no es la más ergonómica para todos los usos, dicha API al menos permitiría todos los usos, lo cual es una condición necesaria para demostrar que el diseño no tiene fallas.

¿Puede dar un ejemplo? No sé, y no puedo imaginar, un asignador que sea capaz de asignar memoria pero que no sepa cuánta memoria está asignando realmente (que es lo que Excess es: cantidad real de memoria asignada; deberíamos cambiarle el nombre).

La mayoría de los asignadores de hoy usan clases de tamaño, donde cada clase de tamaño asigna solo objetos de un tamaño fijo particular, y las solicitudes de asignación que no se ajustan a una clase de tamaño en particular se redondean a la clase de tamaño más pequeña en la que caben. En este esquema, es común hacer cosas como tener una matriz de objetos de clase de tamaño y luego hacer classes[size / SIZE_QUANTUM].alloc() . En ese mundo, averiguar qué clase de tamaño se usa requiere instrucciones adicionales: por ejemplo, let excess = classes[size / SIZE_QUANTUM].size . Puede que no sea mucho, pero el rendimiento de los asignadores de alto rendimiento (como jemalloc) se mide en ciclos de reloj único, por lo que podría representar una sobrecarga significativa, especialmente si ese tamaño termina pasando a través de una cadena de retornos de función.

¿Puede dar un ejemplo?

Al menos saliendo de su PR a alloc_jemalloc, alloc_excess claramente está ejecutando más código que alloc : https://github.com/rust-lang/rust/pull/45514/files.

En este esquema, es común hacer cosas como tener una matriz de objetos de clase de tamaño y luego hacer clases [size / SIZE_QUANTUM] .alloc (). En ese mundo, averiguar qué clase de tamaño se utiliza requiere instrucciones adicionales: por ejemplo, deje exceso = clases [tamaño / SIZE_QUANTUM] .size

Así que déjame ver si sigo correctamente:

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

¿Es asi?


Al menos saliendo de su PR a alloc_jemalloc, alloc_excess claramente está ejecutando más código que alloc

Ese PR fue una corrección de errores (no una corrección de rendimiento), hay muchas cosas mal con el estado actual de nuestra capa de jemalloc en términos de rendimiento, pero desde ese PR, al menos devuelve lo que debería:

  • nallocx es una función const en el sentido de GCC, es decir, una verdadera función pura. Esto significa que no tiene efectos secundarios, sus resultados dependen solo de sus argumentos, no accede a ningún estado global, sus argumentos no son punteros (por lo que la función no puede acceder al estado global y arrojarlos), y para programas C / C ++, LLVM puede usar esto información para eludir la llamada si no se utiliza el resultado. AFAIK Rust actualmente no puede marcar las funciones de FFI C como const fn o similar. Entonces, esto es lo primero que podría arreglarse y eso haría que realloc_excess costo cero para aquellos que no usan el exceso siempre y cuando la alineación y las optimizaciones funcionen correctamente.
  • nallocx siempre se calcula para asignaciones alineadas dentro de mallocx , es decir, todo el código ya lo está compilando, pero mallocx arroja su resultado, por lo que aquí lo estamos calculando dos veces , y en algunos casos nallocx es casi tan caro como mallocx ... Tengo una bifurcación de jemallocator que tiene algunos puntos de referencia para cosas como esta en sus ramas, pero esto debe ser arreglado en sentido ascendente por jemalloc al proporcionar una API que no descarte esto. Sin embargo, esta solución solo afecta a aquellos que actualmente usan Excess .
  • y luego está el problema de que estamos calculando las banderas de alineación dos veces, pero eso es algo que LLVM puede optimizar de nuestro lado (y es trivial de solucionar).

Así que sí, parece más código, pero este código adicional es código al que en realidad estamos llamando dos veces, porque la primera vez que lo llamamos, tiramos los resultados. No es imposible de arreglar, pero todavía no he encontrado el tiempo para hacerlo.


EDITAR: @sfackler Me las arreglé para liberar algo de tiempo para ello hoy y pude hacer alloc_excess "gratis" con respecto a alloc en la ruta lenta de jemallocs, y solo tengo una sobrecarga de ~ 1ns en vía rápida de jemallocs. Realmente no he analizado el camino rápido con mucho detalle, pero podría ser posible mejorarlo aún más. Los detalles están aquí: https://github.com/jemalloc/jemalloc/issues/1074#issuecomment -345040339

¿Es asi?

Si.

Entonces, esto es lo primero que podría arreglarse y que haría que realloc_excess tuviera un costo cero para aquellos que no usan el exceso, siempre que la alineación y las optimizaciones funcionen correctamente.

Cuando se utiliza como asignador global, nada de esto se puede insertar.

Incluso si su API no es la más ergonómica para todos los usos, dicha API al menos permitiría todos los usos, lo cual es una condición necesaria para demostrar que el diseño no tiene fallas.

Literalmente, hay código cero en Github que llama a alloc_excess . Si esta es una característica tan importante, ¿por qué nadie la ha utilizado? Las API de asignación de C ++ no proporcionan acceso al exceso de capacidad. Parece increíblemente sencillo agregar / estabilizar estas funciones en el futuro de una manera compatible con versiones anteriores si hay evidencia concreta real de que mejoran el rendimiento y si alguien realmente se preocupa lo suficiente como para usarlas.

Cuando se utiliza como asignador global, nada de esto se puede insertar.

Entonces ese es un problema que deberíamos intentar resolver, al menos para las compilaciones LTO, porque los asignadores globales como jemalloc confían en esto: nallocx es la forma en que es _por diseño_, y la primera recomendación Los desarrolladores de jemalloc nos hicieron con respecto al rendimiento de alloc_excess es que deberíamos tener esas llamadas en línea, y deberíamos propagar los atributos de C correctamente, para que el compilador elimine las llamadas nallocx de los sitios de llamadas que no use Excess , como hacen los compiladores C y C ++.

Incluso si no podemos hacer eso, la API Excess todavía se puede hacer sin costo al parchear la API jemalloc (tengo una implementación inicial de dicho parche en mi rust-lang / tenedor jemalloc). Podríamos mantener esa API nosotros mismos o intentar conectarla en sentido ascendente, pero para que llegue en sentido ascendente debemos presentar un buen caso sobre por qué estos otros lenguajes pueden realizar estas optimizaciones y Rust no. O debemos tener otro argumento, como que esta nueva API es significativamente más rápida que mallocx + nallocx para aquellos usuarios que necesitan el Excess .

Si esta es una característica tan importante, ¿por qué nadie la ha utilizado?

Buena pregunta. std::Vec es el modelo secundario para usar la API Excess , pero actualmente no la usa, y todos mis comentarios anteriores que dicen "esto y aquello faltan en el Excess API "estaba yo tratando de hacer que Vec usara. La API Excess :

No puedo saber por qué nadie está usando esta API. Pero dado que ni siquiera la biblioteca std puede usarla para la estructura de datos para la que es más adecuada ( Vec ), si tuviera que adivinar, diría que la razón principal es que esta API está actualmente rota.

Si tuviera que adivinar aún más, diría que ni siquiera aquellos que diseñaron esta API la han usado, principalmente porque ninguna colección std la usa (que es donde espero que esta API sea probada al principio) , y también porque usar _excess y Excess todas partes para significar usable_size / allocation_size es extremadamente confuso / molesto de programar.

Esto se debe probablemente a que se dedicó más trabajo a las API Excess sin

O en otras palabras, si tengo dos API que compiten y dedico el 100% del trabajo a mejorar una y el 0% del trabajo a mejorar la otra, no es sorprendente llegar a la conclusión de que una está en la práctica de manera significativa mejor que el otro.

Por lo que puedo decir, estas son las únicas dos llamadas a nallocx fuera de las pruebas de jemalloc en 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

Ninguno de ellos se parece a la API alloc_excess actual, sino que se utilizan de forma independiente para calcular un tamaño de asignación antes de realizarla.

Apache Arrow investigó el uso de nallocx en su implementación, pero descubrió que las cosas no funcionaron bien:

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

Estas son básicamente las únicas referencias a nallocx que puedo encontrar. ¿Por qué es importante que la implementación inicial de las API de asignación sea compatible con una característica tan oscura?

Por lo que puedo decir, estas son las únicas dos llamadas a nallocx fuera de las pruebas de jemalloc en Github:

Desde la parte superior de mi cabeza, sé que al menos el tipo de vector de Facebook lo está usando a través de la implementación de malloc de Facebook ( política de crecimiento de malloc y fbvector ; esa es una gran parte de los vectores de C ++ en Facebook usan esto) y también que Chapel lo usó para mejorar el rendimiento de su tipo String ( aquí y el problema de seguimiento ). Entonces, ¿quizás hoy no fue el mejor día de Github?

¿Por qué es importante que la implementación inicial de las API de asignación sea compatible con una característica tan oscura?

La implementación inicial de una API de asignador no necesita admitir esta función.

Pero un buen soporte para esta función debería bloquear la estabilización de dicha API.

¿Por qué debería bloquear la estabilización si se puede agregar de manera compatible con versiones anteriores más adelante?

¿Por qué debería bloquear la estabilización si se puede agregar de manera compatible con versiones anteriores más adelante?

Porque para mí al menos significa que solo la mitad del espacio de diseño se ha explorado lo suficiente.

¿Espera que las partes no relacionadas con el exceso de la API se vean afectadas por el diseño de la funcionalidad relacionada con el exceso? Admito que solo he seguido esa discusión a medias, pero me parece poco probable.

Si no podemos hacer 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

tan eficiente como este:

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

entonces tenemos problemas más grandes.

¿Espera que las partes no relacionadas con el exceso de la API se vean afectadas por el diseño de la funcionalidad relacionada con el exceso?

Así que sí, espero que un buen exceso de API tenga un gran efecto en el diseño de la funcionalidad relacionada sin exceso: la eliminaría por completo.

Eso evitaría la situación actual de tener dos API que no están sincronizadas y en las que el exceso de api tiene menos funcionalidad que el exceso de menos. Si bien se puede construir una API sin exceso sobre una que esté llena en exceso, lo contrario no es cierto.

Aquellos que quieran eliminar el Excess deberían simplemente dejarlo.

Para aclarar, si hubiera alguna forma de agregar un método alloc_excess después del hecho de una manera compatible con versiones anteriores, ¿estaría bien con eso? (pero, por supuesto, estabilizar sin alloc_excess significa que agregarlo más tarde sería un cambio importante; solo estoy preguntando para que entiendo su razonamiento)

@joshlf Es muy sencillo hacer eso.

: bell: Esto ahora está entrando en su período de comentarios final , según la revisión anterior . :campana:

Aquellos que quieran eliminar el Exceso deberían simplemente hacerlo.

Alternativamente, el 0.01% de las personas que se preocupan por el exceso de capacidad pueden usar otro método.

@sfackler Esto es lo que obtengo por tomar un descanso de dos semanas del óxido: me olvido de las implicaciones del método predeterminado :)

Alternativamente, el 0.01% de las personas que se preocupan por el exceso de capacidad pueden usar otro método.

¿De dónde obtiene este número?

Todas mis estructuras de datos de Rust están planas en la memoria. La capacidad de hacer eso es la única razón por la que uso Rust; si pudiera encasillar todo, estaría usando un idioma diferente. Así que no me preocupan los Excess los 0.01% del tiempo, me preocupo por eso todo el tiempo.

Entiendo que esto es específico del dominio, y que en otros dominios a la gente nunca le importaría el Excess , pero dudo que solo al 0.01% de los usuarios de Rust les importe esto (quiero decir, mucha gente usa Vec y String , que son las estructuras de datos poster-child para Excess ).

Obtengo ese número por el hecho de que hay aproximadamente 4 cosas en total que usan nallocx, en comparación con el conjunto de cosas que usan malloc.

@gnzlbg

¿Está sugiriendo que si lo hiciéramos "bien" desde el principio, tendríamos solo fn alloc(layout) -> (ptr, excess) y nada de fn alloc(layout) -> ptr ? Eso no me parece nada obvio. Incluso si el exceso está disponible, parece natural tener la última API para los casos de uso donde el exceso no importa (por ejemplo, la mayoría de las estructuras de árboles), incluso si se implementa como alloc_excess(layout).0 .

@rkruppe

Eso no me parece nada obvio. Incluso si el exceso está disponible, parece natural tener la última API para los casos de uso donde el exceso no importa (por ejemplo, la mayoría de las estructuras de árbol), incluso si está implementado como alloc_excess (diseño) .0.

Actualmente, la API en exceso se implementa sobre la de menos. La implementación de Alloc para un asignador sin exceso requiere que el usuario proporcione los métodos alloc y dealloc .

Sin embargo, si quiero implementar Alloc para un asignador lleno en exceso, necesito proporcionar más métodos (al menos alloc_excess , pero esto aumenta si ingresamos en realloc_excess , alloc_zeroed_excess , grow_in_place_excess , ...).

Si tuviéramos que hacerlo al revés, es decir, implementar la API sin exceso como una sutileza encima de la que está llena en exceso, luego implementar alloc_excess y dealloc suficiente para respaldar ambos tipos de asignadores.

Los usuarios a los que no les importa o no pueden devolver o consultar el exceso pueden simplemente devolver el tamaño de entrada o el diseño (que es un pequeño inconveniente), pero los usuarios que pueden manejar y quieren manejar el exceso no necesitan implementar más métodos.


@sfackler

Obtengo ese número por el hecho de que hay aproximadamente 4 cosas en total que usan nallocx, en comparación con el conjunto de cosas que usan malloc.

Dados estos datos sobre el uso de _excess en el ecosistema Rust:

  • 0 cosas en uso total _excess en el ecosistema del óxido
  • 0 cosas en total uso _excess en la biblioteca std de rust
  • ni siquiera Vec y String pueden usar la API _excess correctamente en la biblioteca rust std
  • la API _excess es inestable, no está sincronizada con la API sin exceso, con errores hasta hace muy poco (ni siquiera devolvió el excess en absoluto), ...

    y dados estos datos sobre el uso de _excess en otros idiomas:

  • La API de jemalloc no es compatible de forma nativa con los programas C o C ++ debido a la compatibilidad con versiones anteriores

  • Los programas C y C ++ que quieran usar el exceso de API de jemalloc deben hacer todo lo posible para usarlo al:

    • optar por salir del asignador del sistema y entrar en jemalloc (o tcmalloc)

    • volver a implementar la biblioteca estándar de su idioma (en el caso de C ++, implementar una biblioteca estándar incompatible)

    • escribe toda su pila encima de esta biblioteca estándar incompatible

  • algunas comunidades (firefox lo usa, facebook reimplementa las colecciones en la biblioteca estándar de C ++ para poder usarlo, ...) aún se desviven por usarlo.

Estos dos argumentos me parecen plausibles:

  • La API excess en std no se puede usar, por lo tanto, la biblioteca std no puede usarla, por lo tanto, nadie puede, por lo que no se usa ni una sola vez en el ecosistema Rust. .
  • Aunque C y C ++ hacen que sea casi imposible usar esta API, los grandes proyectos con mano de obra hacen todo lo posible para usarla, por lo tanto, al menos una comunidad potencialmente pequeña de personas se preocupa mucho por ella.

Tu argumento:

  • Nadie usa la API _excess , por lo tanto, solo al 0.01% de la gente le importa.

no.

@alexcrichton La decisión de cambiar de -> Result<*mut u8, AllocErr> a -> *mut void puede ser una sorpresa significativa para las personas que siguieron el desarrollo original de las RFC de asignación.

No estoy en desacuerdo con los puntos que hace , pero, no obstante, parecía que un buen número de personas habría estado dispuesta a vivir con el "peso pesado" de Result sobre la mayor probabilidad de perder un valor nulo. comprobar el valor devuelto.

  • Estoy ignorando los problemas de eficiencia en tiempo de ejecución impuestos por la ABI porque yo, como @alexcrichton , supongo que podríamos lidiar con ellos de alguna manera a través de trucos del compilador.

¿Hay alguna forma de que podamos obtener una mayor visibilidad sobre ese cambio tardío por sí solo?

Una forma (fuera de mi cabeza): Cambie la firma ahora, en un PR por sí solo, en la rama maestra, mientras que Allocator todavía es inestable. Y luego ver quién se queja en las relaciones públicas (¡y quién celebra!).

  • ¿Es esto demasiado torpe? Parece que, según las definiciones, es menos torpe que unir tal cambio con la estabilización ...

Sobre el tema de si devolver *mut void o devolver Result<*mut void, AllocErr> : Es posible que debamos revisar la idea de los rasgos de asignación de "alto nivel" y "bajo nivel" separados, como se discutió en la toma II del Allocator RFC .

(Obviamente, si tuviera una objeción seria al valor de retorno de *mut void , lo presentaría como una preocupación a través del fcpbot. Pero en este punto confío bastante en el juicio del equipo de libs, tal vez en en parte debido a la fatiga por esta saga de asignadores).

@pnkfelix

La decisión de cambiar de -> Result<*mut u8, AllocErr> a -> *mut void puede ser una sorpresa significativa para las personas que siguieron el desarrollo original de las RFC del asignador.

Esto último implica que, como se discutió, el único error que nos importa expresar es OOM. Por lo tanto, un intermedio ligeramente más liviano que aún tiene el beneficio de protección contra fallas accidentales para verificar errores es -> Option<*mut void> .

@gnzlbg

El exceso de API en std no se puede usar, por lo tanto, la biblioteca std no puede usarlo, por lo tanto, nadie puede, por lo que no se usa ni una sola vez en el ecosistema Rust.

Entonces ve a arreglarlo.

@pnkfelix

Sobre el tema de si devolver mut void o devolver Result < mut void, AllocErr>: Es posible que debamos estar revisando la idea de rasgos de asignación separados de "alto nivel" y "bajo nivel", como se analiza en la toma II del Allocator RFC.

Esos fueron básicamente nuestros pensamientos, excepto que la API de alto nivel estaría en Alloc sí misma como alloc_one , alloc_array etc. Incluso podemos dejar que se desarrollen primero en el ecosistema como extensión rasgos para ver en qué API convergen las personas.

@pnkfelix

La razón por la que hice que Layout solo implementara Clonar y no Copiar es que quería dejar abierta la posibilidad de agregar más estructura al tipo de Diseño. En particular, todavía estoy interesado en intentar que Layout intente rastrear cualquier estructura de tipo utilizada para construirlo (por ejemplo, matriz de 16 de struct {x: u8, y: [char; 215]}), para que los asignadores tuvieran la opción de exponer rutinas de instrumentación que informen de qué tipos se componen sus contenidos actuales.

¿Se ha experimentado con esto en alguna parte?

@sfackler Ya he hecho la mayor parte, y todo se puede hacer con la API duplicada (sin exceso + _excess métodos). Estaría bien con tener dos API y no tener una _excess API completa en este momento.

Lo único que todavía me preocupa un poco es que para implementar un asignador en este momento, es necesario implementar alloc + dealloc , pero alloc_excess + dealloc también debería funcionar. ¿Sería posible darle a alloc una implementación predeterminada en términos de alloc_excess más tarde o no es posible o es un cambio importante? En la práctica, la mayoría de los asignadores implementarán la mayoría de los métodos de todos modos, así que esto no es un gran problema, sino más bien un deseo.


jemallocator implementa Alloc dos veces (para Jemalloc y &Jemalloc ), donde la implementación Jemalloc para algunos method es solo un (&*self).method(...) que reenvía la llamada al método a la implementación &Jemalloc . Esto significa que uno debe mantener manualmente ambas implementaciones de Alloc por Jemalloc sincronizadas. Si obtener diferentes comportamientos para las implementaciones de &/_ puede ser trágico o no, no lo sé.


Me ha resultado muy difícil averiguar qué está haciendo la gente con el rasgo Alloc en la práctica. Los únicos proyectos que he encontrado que lo están usando se seguirán usando todas las noches de todos modos (servo, redox), y solo lo están usando para cambiar el asignador global. Me preocupa mucho que no haya podido encontrar ningún proyecto que lo use como parámetro de tipo de colección (¿tal vez tuve mala suerte y hay algunos?). En particular, estaba buscando ejemplos de implementación de SmallVec y ArrayVec sobre un tipo Vec -like (ya que std::Vec no tiene un Alloc type parámetro todavía), y también me preguntaba cómo funcionaría la clonación entre estos tipos ( Vec s con un Alloc ator diferente) (lo mismo probablemente se aplica a la clonación Box es con Alloc s diferentes). ¿Hay ejemplos de cómo se verían estas implementaciones en algún lugar?

Los únicos proyectos que he encontrado que lo están usando se seguirán usando todas las noches de todos modos (servo, redox)

Por lo que vale, Servo está tratando de alejarse de las características inestables siempre que sea posible: https://github.com/servo/servo/issues/5286

Este también es un problema del huevo y la gallina. Muchos proyectos aún no usan Alloc porque todavía es inestable.

No tengo muy claro por qué deberíamos tener un complemento completo de _ API en exceso en primer lugar. Originalmente existían para reflejar la API allocm experimental * de jemalloc, pero se eliminaron en 4.0 hace varios años a favor de no duplicar toda su superficie API. ¿Parece que podríamos seguir su ejemplo?

¿Sería posible darle a alloc una implementación predeterminada en términos de alloc_excess más tarde o no es posible o es un cambio importante?

Podemos agregar una implementación predeterminada de alloc en términos de alloc_excess , pero alloc_excess deberá tener una implementación predeterminada en términos de alloc . Todo funciona bien si implementa uno o ambos, pero si no implementa ninguno, su código se compilará pero se repetirá infinitamente. Esto ha surgido antes (¿tal vez por Rand ?), Y podríamos tener alguna forma de decir que necesita implementar al menos una de esas funciones, pero no nos importa cuál.

Me preocupa mucho que no haya podido encontrar ningún proyecto que lo use como parámetro de tipo de colección (¿tal vez tuve mala suerte y hay algunos?).

No conozco a nadie que esté haciendo eso.

Los únicos proyectos que he encontrado que lo están usando se seguirán usando todas las noches de todos modos (servo, redox)

Una cosa importante que impide que esto avance es que las colecciones stdlib aún no admiten asignadores paramétricos. Eso también excluye la mayoría de las otras cajas, ya que la mayoría de las colecciones externas usan las internas debajo del capó ( Box , Vec , etc.).

Los únicos proyectos que he encontrado que lo están usando se seguirán usando todas las noches de todos modos (servo, redox)

Una cosa importante que impide que esto avance es que las colecciones stdlib aún no admiten asignadores paramétricos. Eso también excluye la mayoría de las otras cajas, ya que la mayoría de las colecciones externas usan las internas debajo del capó (Box, Vec, etc.).

Esto se aplica a mí: tengo un kernel de juguete, y si pudiera, estaría usando Vec<T, A> , pero en su lugar tengo que tener una fachada de asignador global mutable interior, que es asqueroso.

@remexre ¿cómo va a parametrizar sus estructuras de datos para evitar el estado global con mutabilidad interior?

Todavía habrá un estado global mutable interior, supongo, pero se siente mucho más seguro tener una configuración en la que el asignador global no se pueda usar hasta que la memoria esté completamente mapeada que tener una función set_allocator global.


EDITAR : Me acabo de dar cuenta de que no respondí la pregunta. Ahora mismo, tengo 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();

donde AreaAllocator es un rasgo que me permite (en tiempo de ejecución) verificar que los asignadores no se "superponen" accidentalmente (en términos de los rangos de direcciones en los que se asignan). BumpAllocator solo se usa muy temprano, como espacio temporal cuando se mapea el resto de la memoria para crear los RealAllocator s.

Idealmente, me gustaría tener un Mutex<Option<RealAllocator>> (o un contenedor que lo haga "solo de inserción") sea el único asignador, y que todo lo asignado desde el principio sea parametrizado por el inicio temprano BumpAllocator . Esto también me permitiría asegurarme de que BumpAllocator no se use después del inicio temprano, ya que las cosas que asigno no podrían sobrevivirlo.

@sfackler

No tengo muy claro por qué deberíamos tener un complemento completo de _ API en exceso en primer lugar. Originalmente existían para reflejar la API allocm experimental * de jemalloc, pero se eliminaron en 4.0 hace varios años a favor de no duplicar toda su superficie API. ¿Parece que podríamos seguir su ejemplo?

Actualmente shrink_in_place llama a xallocx que devuelve el tamaño de asignación real. Debido a que shrink_in_place_excess no existe, descarta este tamaño y los usuarios deben llamar a nallocx para volver a calcularlo, cuyo costo realmente depende del tamaño de la asignación.

Entonces, al menos algunas funciones de asignación de jemalloc que ya estamos usando nos devuelven el tamaño utilizable, pero la API actual no nos permite usarlo.

@remexre

Cuando estaba trabajando en mi kernel de juguete, evitar el asignador global para asegurarme de que no ocurriera ninguna asignación hasta que se configuró un asignador era uno de mis objetivos también. ¡Me alegra saber que no soy el indicado!

No me gusta la palabra Heap para el asignador global predeterminado. ¿Por qué no Default ?

Otro punto de aclaración: RFC 1974 pone todo esto en std::alloc pero actualmente está en std::heap . ¿Qué lugar se propone para la estabilización?

@jethrogb "Heap" es un término bastante canónico para "esa cosa a la que malloc le da indicaciones". ¿Cuáles son sus preocupaciones con el término?

@sfackler

"esa cosa malloc te da consejos"

Excepto que en mi opinión eso es lo que es System .

Ah, claro. Global es otro nombre, entonces tal vez? Ya que usa #[global_allocator] para seleccionarlo.

Puede haber varios asignadores de montón (por ejemplo, libc y jemalloc con prefijo). ¿Qué tal cambiar el nombre de std::heap::Heap a std::heap::Default y #[global_allocator] a #[default_allocator] ?

El hecho de que es lo que obtiene si no especifica lo contrario (presumiblemente cuando, por ejemplo, Vec gana un parámetro / campo de tipo adicional para el asignador) es más importante que el hecho de que no tiene "por -instancias "estado (o instancias en realidad).

El período de comentarios final ahora está completo.

Con respecto a FCP, creo que el subconjunto de API propuesto para la estabilización tiene un uso muy limitado. Por ejemplo, no es compatible con la caja jemallocator .

¿En qué manera? Es posible que jemallocator tenga que señalar algunas de las implicaciones de los métodos inestables detrás de una marca de características, pero eso es todo.

Si jemallocator en Rust estable no puede implementar, por ejemplo, Alloc::realloc llamando a je_rallocx pero necesita depender de la implícita alloc + copy + dealloc predeterminada, entonces no es un reemplazo aceptable para el IMO alloc_jemalloc caja estándar de la biblioteca.

Claro, podría obtener algo para compilar, pero no es algo particularmente útil.

¿Por qué? C ++ no tiene ningún concepto de reasignación en absoluto en su API de asignación y eso no parece haber paralizado el lenguaje. Obviamente no es ideal, pero no entiendo por qué sería inaceptable.

Las colecciones de C ++ generalmente no usan realloc porque los constructores de movimientos de C ++ pueden ejecutar código arbitrario, no porque realloc no sea útil.

Y la comparación no es con C ++, es con la biblioteca estándar actual de Rust con soporte jemalloc incorporado. Cambiar ay fuera del asignador estándar con solo este subconjunto de Alloc API sería una regresión.

Y realloc es un ejemplo. jemallocator actualmente también implementa alloc_zeroed , alloc_excess , usable_size , grow_in_place , etc.

Se propone la estabilización de alloc_zeroed. Por lo que puedo decir (busque el hilo superior), hay literalmente cero usos de alloc_excess en existencia. ¿Podría mostrar algún código que retroceda si vuelve a una implementación predeterminada?

Sin embargo, de manera más general, no veo por qué este es un argumento en contra de la estabilización de una parte de estas API. Si no desea utilizar jemallocator, puede seguir sin utilizarlo.

¿Se podría convertir Layout::array<T>() una constante fn?

Puede entrar en pánico, así que no en este momento.

Puede entrar en pánico, así que no en este momento.

Ya veo ... Me conformaría con const fn Layout::array_elem<T>() que sería un equivalente sin pánico de Layout::<T>::repeat(1).0 .

@mzabaluev Creo que lo que estás describiendo es equivalente a Layout::new<T>() . Actualmente puede entrar en pánico, pero eso es solo porque se implementó usando Layout::from_size_align y luego .unwrap() , y espero que se pueda hacer de manera diferente.

@joshlf Creo que esta estructura tiene el tamaño de 5, mientras que como elementos de una matriz, estos se colocan cada 8 bytes debido a la alineación:

struct Foo {
    bar: u32,
    baz: u8
}

No estoy seguro de que una matriz de Foo incluya el relleno del último elemento para su cálculo de tamaño, pero esa es mi gran expectativa.

En Rust, el tamaño de un objeto es siempre un múltiplo de su alineación, por lo que la dirección del elemento n th de una matriz es siempre array_base_pointer + n * size_of<T>() . Entonces, el tamaño de un objeto en una matriz es siempre el mismo que el tamaño de ese objeto por sí solo. Consulte la página de Rustonomicon en repr (Rust) para obtener más detalles.

Bien, resulta que una estructura se rellena a su alineación, pero AFAIK, esto no es una garantía estable excepto en #[repr(C)] .
De todos modos, hacer Layout::new a const fn también sería bienvenido.

Este es el comportamiento documentado (y así garantizado) de una función estable:

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

Devuelve el tamaño de un tipo en bytes.

Más específicamente, este es el desplazamiento en bytes entre elementos sucesivos en una matriz con ese tipo de elemento, incluido el relleno de alineación. Por lo tanto, para cualquier tipo T y longitud n , [T; n] tiene un tamaño de n * size_of::<T>() .

Gracias. Me acabo de dar cuenta de que cualquier constante fn que multiplique el resultado de Layout::new sería intrínsecamente aterradora a su vez (a menos que se haga con saturating_mul o algo así), así que vuelvo al punto de partida. Continuando con una pregunta sobre pánicos en el problema de seguimiento constante fn.

La macro panic!() actualmente no se admite en expresiones constantes, pero el compilador genera pánicos de aritmética comprobada y no se ve afectada por esa limitación:

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

Esto está relacionado con Alloc::realloc pero no con la estabilización de la interfaz mínima ( realloc no forma parte de ella):

Actualmente, debido a que Vec::reserve/double llaman RawVec::reserve/double que llaman Alloc::realloc , la implícita predeterminada de Alloc::realloc copia elementos vectoriales muertos (en el rango [len(), capacity()) ) . En el caso absurdo de un enorme vector vacío que quiere insertar capacity() + 1 elementos y así reasignar, el costo de tocar toda esa memoria no es insignificante.

En teoría, si la implementación predeterminada Alloc::realloc también tomaría un rango "bytes_used", podría simplemente copiar la parte relevante en la reasignación. En la práctica, al menos jemalloc anula Alloc::realloc impl por defecto con una llamada a rallocx . Si hacer un baile de alloc / dealloc copiando solo la memoria relevante es más rápido o más lento que una llamada rallocx probablemente dependerá de muchas cosas (¿ rallocx maneja expandir el bloque en su lugar? ¿Cuánta memoria innecesaria se copiará rallocx ? etc.).

https://github.com/QuiltOS/rust/tree/allocator-error Comencé a demostrar cómo creo que el tipo de error asociado resuelve nuestras colecciones y los problemas de manejo de errores haciendo la generalización en sí. En particular, observe cómo en los módulos cambio que yo

  • Reutilice siempre la implementación Result<T, A::Err> para la implementación T
  • Nunca unwrap o cualquier otra cosa parcial
  • No oom(e) fuera de AbortAdapter .

Esto significa que los cambios que estoy haciendo son bastante seguros y también bastante insensatos. Trabajar tanto con la devolución de errores como con la cancelación de errores no debería requerir un esfuerzo adicional para mantener invariantes mentales: el verificador de tipos hace todo el trabajo.

Lo recuerdo --- ¿Creo que en el RFC de @Gankro ? o el hilo pre-rfc --- Gecko / Servo personas que dicen que era bueno no tener la falibilidad de las colecciones como parte de su tipo. Bueno, puedo agregar #[repr(transparent)] a AbortAdapter para que las colecciones se puedan transmutar de forma segura entre Foo<T, A> y Foo<T, AbortAdapter<A>> (dentro de envoltorios seguros), lo que permite que uno pueda libremente cambiar de un lado a otro sin duplicar todos los métodos. [Para la compatibilidad con versiones anteriores, las colecciones de bibliotecas estándar deberán duplicarse en cualquier caso, pero los métodos de usuario no tienen que ser tan fáciles de trabajar con Result<T, !> estos días.

Desafortunadamente, el código no comprueba completamente el tipo porque cambiar los parámetros de tipo de un elemento lang (cuadro) confunde al compilador (¡sorpresa!). La confirmación del cuadro que causa ICE es la última, todo lo anterior es bueno. @eddyb arregló rustc en # 47043!

editar @joshlf Me informaron de su https://github.com/rust-lang/rust/pull/45272 , y lo incorporé aquí. ¡Gracias!

La memoria persistente (por ejemplo, http://pmem.io ) es la próxima gran novedad, y Rust necesita posicionarse para funcionar bien con ella.

Recientemente he estado trabajando en un contenedor de Rust para un asignador de memoria persistente (específicamente, libpmemcto). Independientemente de las decisiones que se tomen con respecto a la estabilización de esta API, es necesario que: -

  • Ser capaz de admitir un contenedor de rendimiento alrededor de un asignador de memoria persistente como libpmemcto;
  • Ser capaz de especificar (parametrizar) tipos de colección por asignador (en este momento, es necesario duplicar Box, Rc, Arc, etc.)
  • Poder clonar datos entre asignadores
  • Ser capaz de admitir tener estructuras almacenadas en memoria persistente con campos que se reinicializan en la instanciación de un grupo de memoria persistente, es decir, algunas estructuras de memoria persistente deben tener campos que solo se almacenan temporalmente en el montón. Mis casos de uso actuales son una referencia al grupo de memoria persistente que se usa para la asignación y los datos transitorios que se usan para los bloqueos.

Por otro lado, el desarrollo de pmem.io (PMDK de Intel) hace un uso intensivo de un asignador jemalloc modificado debajo de las cubiertas, por lo que parece prudente que usar jemalloc como un consumidor de API de ejemplo sería prudente.

¿Sería posible reducir el alcance de esto para cubrir solo GlobalAllocator s primero hasta que obtengamos más experiencia con el uso de Alloc ators en colecciones?

IIUC esto ya cubriría las necesidades de servo y nos permitiría experimentar con la parametrización de contenedores en paralelo. En el futuro, podemos mover colecciones para usar GlobalAllocator lugar o simplemente agregar una implícita general de Alloc por GlobalAllocator para que se puedan usar para todas las colecciones.

Pensamientos

@gnzlbg Para que el #[global_allocator] sea ​​útil (más allá de seleccionar heap::System ), el rasgo Alloc debe ser estable, para que pueda ser implementado por cajas como https: / /crates.io/crates/jemallocator. No hay ningún tipo o rasgo llamado GlobalAllocator en este momento, ¿estás proponiendo alguna API nueva?

Aquí no hay ningún tipo o rasgo llamado GlobalAllocator en este momento, ¿está proponiendo alguna nueva API?

Lo que sugerí es cambiar el nombre de la API "mínima" que @alexcrichton sugirió estabilizar aquí de Alloc a GlobalAllocator para representar solo asignadores globales y dejar la puerta abierta para que las colecciones sean parametrizadas por un rasgo del asignador en el futuro (lo que no significa que no podamos parametrizarlos por el rasgo GlobalAllocator ).

IIUC servo actualmente solo necesita poder cambiar el asignador global (en lugar de poder también parametrizar algunas colecciones mediante un asignador). Entonces, tal vez en lugar de intentar estabilizar una solución que debería estar preparada para el futuro para ambos casos de uso, podemos abordar ahora solo el problema del asignador global y descubrir cómo parametrizar las colecciones por asignadores más adelante.

No sé si eso tiene sentido.

El servo IIUC actualmente solo necesita poder cambiar el asignador global (en lugar de poder también parametrizar algunas colecciones mediante un asignador).

Eso es correcto, pero:

  • Si un rasgo y su método son estables para que pueda implementarse, entonces también se puede llamar directamente sin pasar por std::heap::Heap . Por lo tanto, no es solo un asignador global de rasgos, es un rasgo para los asignadores (incluso si terminamos haciendo uno diferente para las colecciones genéricas sobre los asignadores) y GlobalAllocator no es un nombre particularmente bueno.
  • La caja jemallocator actualmente implementa alloc_excess , realloc , realloc_excess , usable_size , grow_in_place y shrink_in_place que no son parte de la API mínima propuesta. Estos pueden ser más eficientes que el impl predeterminado, por lo que eliminarlos sería una regresión del rendimiento.

Ambos puntos tienen sentido. Pensé que la única forma de acelerar significativamente la estabilización de esta función era eliminar la dependencia de ella, que también es un buen rasgo para parametrizar colecciones sobre ella.

[Sería bueno si Servo pudiera ser como (estable | caja oficial de mozilla), y cargo pudiera hacer cumplir esto, para eliminar un poco de presión aquí.]

@ Ericson2314 servo no es el único proyecto que quiere utilizar estas API.

@ Ericson2314 No entiendo lo que esto significa, ¿podrías reformular?

Para el contexto: Servo actualmente usa una serie de características inestables (incluido #[global_allocator] ), pero estamos tratando de alejarnos lentamente de eso (ya sea actualizando a un compilador que haya estabilizado algunas características o encontrando alternativas estables. ) Esto se rastrea en https://github.com/servo/servo/issues/5286. Entonces, estabilizar #[global_allocator] sería bueno, pero no bloquea ningún trabajo de Servo.

Firefox se basa en el hecho de que Rust std por defecto es el asignador del sistema al compilar un cdylib , y ese mozjemalloc que termina vinculado al mismo binario define símbolos como malloc y free esa "sombra" (no conozco la terminología apropiada del enlazador) los de libc. Entonces, las asignaciones del código de Rust en Firefox terminan usando mozjemalloc. (Esto es en Unix, no sé cómo funciona en Windows). Esto funciona, pero me parece frágil. Firefox usa Rust estable, y me gustaría usar #[global_allocator] para seleccionar explícitamente mozjemalloc para que toda la configuración sea más robusta.

@SimonSapin cuanto más juego con asignadores y colecciones, más tiendo a pensar que no queremos parametrizar las colecciones por Alloc todavía, porque dependiendo del asignador, una colección puede querer ofrecer un API diferente, la complejidad de algunas operaciones cambia, algunos detalles de la colección realmente dependen del asignador, etc.

Así que me gustaría sugerir una forma en la que podemos avanzar aquí.

Paso 1: asignador de montón

Al principio, podríamos restringirnos para tratar de permitir que los usuarios seleccionen el asignador para el montón (o el asignador del sistema / plataforma / global / tienda libre, o como prefiera nombrarlo) en Rust estable.

Lo único que inicialmente parametrizamos es Box , que solo necesita asignar ( new ) y desasignar ( drop ) memoria.

Este rasgo del asignador podría tener inicialmente la API que propuso @alexcrichton (o algo extendido), y este rasgo del asignador podría, todas las noches, tener una API ligeramente extendida para admitir las colecciones std:: .

Una vez que estemos allí, los usuarios que quieran migrar a estable podrán hacerlo, pero podrían tener un impacto en el rendimiento debido a la API inestable.

Paso 2: asignador de montón sin impacto en el rendimiento

En ese momento, podemos reevaluar a los usuarios que no pueden pasar a estable debido a un impacto en el rendimiento y decidir cómo extender esta API y estabilizarla.

Pasos 3 a N: compatibilidad con asignadores personalizados en colecciones std .

Primero, esto es difícil, por lo que puede que nunca suceda, y creo que nunca sucederá no es algo malo.

Cuando quiero parametrizar una colección con un asignador personalizado, tengo un problema de rendimiento o un problema de usabilidad.

Si tengo un problema de usabilidad, normalmente quiero una API de colección diferente que explote las características de mi asignador personalizado, como por ejemplo lo hace mi SliceDeque crate. Parametrizar una colección mediante un asignador personalizado no me ayudará aquí.

Si tengo un problema de rendimiento, aún sería muy difícil para un asignador personalizado ayudarme. Voy a considerar Vec en las siguientes secciones, porque es la colección que he reimplementado con más frecuencia.

Reducir el número de llamadas al asignador del sistema (optimización de vector pequeño)

Si quiero asignar algunos elementos dentro del objeto Vec para reducir la cantidad de llamadas al asignador del sistema, hoy solo uso SmallVec<[T; M]> . Sin embargo, un SmallVec no es un Vec :

  • mover un Vec es O (1) en el número de elementos, pero mover un SmallVec<[T; M]> es O (N) para N <M y O (1) después,

  • los punteros a los elementos Vec se invalidan en movimiento si len() <= M pero no de otra manera, es decir, si len() <= M operaciones como into_iter necesitan mover los elementos al objeto iterador en sí mismo, en lugar de simplemente tomar punteros.

¿Podríamos hacer Vec genérico sobre un asignador para respaldar esto? Todo es posible, pero creo que los costos más importantes son:

  • hacerlo hace que la implementación de Vec más compleja, lo que podría afectar a los usuarios que no usan esta función
  • la documentación de Vec se volvería más compleja, porque el comportamiento de algunas operaciones dependería del asignador.

Creo que estos costos no son insignificantes.

Hacer uso de patrones de asignación

El factor de crecimiento de un Vec se adapta a un asignador en particular. En std podemos adaptarlo a los comunes jemalloc / malloc / ... pero si está utilizando un asignador personalizado, es probable que el factor de crecimiento que elegimos de forma predeterminada, no será el mejor para su caso de uso. ¿Debería cada asignador poder especificar un factor de crecimiento para patrones de asignación de tipo vec? No lo sé, pero mi instinto me dice: probablemente no.

Aproveche las funciones adicionales de su asignador de sistemas

Por ejemplo, un asignador que se compromete en exceso está disponible en la mayoría de los objetivos de Nivel 1 y Nivel 2. En sistemas tipo Linux y Macos, el asignador de montón se compromete en exceso de forma predeterminada, mientras que la API de Windows expone VirtualAlloc que se puede usar para reservar memoria (por ejemplo, en Vec::reserve/with_capacity ) y confirmar memoria en push .

Actualmente, el rasgo Alloc no expone una forma de implementar un asignador de este tipo en Windows, porque no separa los conceptos de compromiso y reserva de memoria (en Linux, un asignador sin compromiso excesivo puede ser pirateado con solo tocar cada página una vez). Tampoco expone una forma para que un asignador indique si se compromete en exceso o no de forma predeterminada en alloc .

Es decir, necesitaríamos extender la API Alloc para admitir esto por Vec , y eso sería IMO para una pequeña ganancia. Porque cuando tienes un asignador de este tipo, la semántica de Vec cambia nuevamente:

  • Vec no necesita crecer nunca más, por lo que operaciones como reserve tienen poco sentido
  • push es O(1) lugar de O(1) amortizados.
  • los iteradores de los objetos en vivo nunca se invalidan mientras el objeto esté vivo, lo que permite algunas optimizaciones

Aproveche más funciones adicionales de su asignador de sistema

Algunos asignadores de sistema como cudaMalloc / cudaMemcpy / ... diferencian entre memoria fija y no fija, le permiten asignar memoria en espacios de direcciones disjuntos (por lo que necesitaríamos un tipo de puntero asociado en el Alloc rasgo), ...

Pero usarlos en colecciones como Vec vuelve a cambiar la semántica de algunas operaciones de manera sutil, como si la indexación de un vector invoca repentinamente un comportamiento indefinido o no, dependiendo de si lo hace desde un kernel de GPU o desde el host.

Terminando

Creo que intentar crear una API de Alloc que pueda usarse para parametrizar todas las colecciones (o solo Vec ) es difícil, probablemente demasiado.

Tal vez después de que obtengamos los asignadores globales / system / platform / heap / free-store correctos, y Box , podamos repensar las colecciones. Tal vez podamos reutilizar Alloc , tal vez necesitemos un VecAlloc, VecDequeAlloc , HashMapAlloc`, ... o tal vez simplemente digamos, "ya sabes qué, si realmente necesitas esto , simplemente copie y pegue la colección estándar en una caja y amolde a su asignador " Quizás la mejor solución sea simplemente hacer esto más fácil, teniendo colecciones estándar en su propia caja (o cajas) en el vivero y usando solo características estables, tal vez implementadas como un conjunto de bloques de construcción.

De todos modos, creo que tratar de abordar todos estos problemas aquí a la vez y tratar de encontrar un rasgo Alloc que sea bueno para todo es demasiado difícil. Estamos en el paso 0. Creo que la mejor manera de llegar rápidamente al paso 1 y al paso 2 es dejar las colecciones fuera de la imagen hasta que estemos allí.

Una vez que estemos allí, los usuarios que quieran migrar a estable podrán hacerlo, pero podrían tener un impacto en el rendimiento debido a la API inestable.

Elegir un asignador personalizado generalmente se trata de mejorar el rendimiento, por lo que no sé a quién serviría esta estabilización inicial.

Elegir un asignador personalizado generalmente se trata de mejorar el rendimiento, por lo que no sé a quién serviría esta estabilización inicial.

¿Todos? Al menos ahora mismo. La mayoría de algunos de los métodos de los que se queja faltan en la propuesta de estabilización inicial ( alloc_excess , por ejemplo), AFAIK no los utiliza todavía en la biblioteca estándar. ¿O esto cambió recientemente?

Vec (y otros usuarios de RawVec ) usan realloc en push

@SimonSapin

La caja jemallocator actualmente implementa alloc_excess, realloc, realloc_excess, usable_size, grow_in_place y shrink_in_place

A partir de estos métodos, se utilizan AFAIK realloc , grow_in_place y shrink_in_place , pero grow_in_place es solo una envoltura ingenua sobre shrink_in_place para jemalloc en al menos si implementamos la implícita inestable predeterminada de grow_in_place en términos de shrink_in_place en el rasgo Alloc , eso lo reduce a dos métodos: realloc y shrink_in_place .

Elegir un asignador personalizado generalmente se trata de mejorar el rendimiento,

Si bien esto es cierto, puede obtener más rendimiento al usar un asignador más adecuado sin estos métodos que un asignador malo que los tenga.

IIUC, el caso de uso principal para servo fue usar Firefox jemalloc en lugar de tener un segundo jemalloc, ¿verdad?

Incluso si agregamos realloc y shrink_in_place al rasgo Alloc en una estabilización inicial, eso solo retrasaría las quejas de rendimiento.

Por ejemplo, en el momento en que agreguemos una API inestable al rasgo Alloc que termina siendo utilizado por las colecciones std , no podrá obtener el mismo rendimiento en estable que lo haría poder seguir todas las noches. Es decir, si agregamos realloc_excess y shrink_in_place_excess al rasgo de asignación y hacemos que Vec / String / ... los usemos, estabilizamos realloc y shrink_in_place no te habrían ayudado en nada.

IIUC, el caso de uso principal para servo fue usar Firefox jemalloc en lugar de tener un segundo jemalloc, ¿verdad?

Aunque comparten algo de código, Firefox y Servo son dos proyectos / aplicaciones separados.

Firefox usa mozjemalloc, que es una bifurcación de una versión antigua de jemalloc con un montón de características agregadas. Creo que algunos códigos unsafe FFI se basan en la corrección y solidez de mozjemalloc que utiliza Rust std.

Servo usa jemalloc, que es el valor predeterminado de Rust para los ejecutables en este momento, pero hay planes para cambiar ese valor predeterminado al asignador del sistema. Servo también tiene un código de informe de uso de memoria unsafe que depende de la solidez del uso de jemalloc. (Pasando Vec::as_ptr() a je_malloc_usable_size .)

Servo usa jemalloc, que es el valor predeterminado de Rust para los ejecutables en este momento, pero hay planes para cambiar ese valor predeterminado al asignador del sistema.

Sería bueno saber si los asignadores del sistema en los sistemas que los servo objetivos proporcionan realloc y shrink_to_fit API optimizadas como lo hace jemalloc. realloc (y calloc ) son muy comunes, pero shrink_to_fit ( xallocx ) es AFAIK específico de jemalloc . Quizás la mejor solución sería estabilizar realloc y alloc_zeroed ( calloc ) en la implementación inicial, y dejar shrink_to_fit para más adelante. Eso debería permitir que el servo funcione con los asignadores de sistema en la mayoría de las plataformas sin problemas de rendimiento.

Servo también tiene un código de informe de uso de memoria inseguro que depende de la solidez del uso de jemalloc. (Pasando Vec :: as_ptr () a je_malloc_usable_size.)

Como sabe, la caja jemallocator tiene API para esto. Espero que aparezcan cajas similares a la caja jemallocator para otros asignadores que ofrecen API similares a medida que la historia del asignador global comienza a estabilizarse. No he pensado en si estas API pertenecen en absoluto al rasgo Alloc .

No creo que malloc_usable_size deba estar en el rasgo Alloc . Usar #[global_allocator] para estar seguro de qué asignador usa Vec<T> y usar por separado una función de la caja jemallocator está bien.

@SimonSapin una vez que el rasgo Alloc se estabilice, probablemente tendremos una caja como jemallocator para Linux malloc y Windows. Estas cajas podrían tener una función adicional para implementar las partes que puedan de la API Alloc inestable (como, por ejemplo, usable_size encima de malloc_usable_size ) y algunas otras cosas que no forman parte de la API Alloc , como los informes de memoria además de mallinfo . Una vez que haya cajas utilizables para los sistemas que apuntan a los servos, sería más fácil saber qué partes del rasgo Alloc priorizar la estabilización, y probablemente encontraremos API más nuevas con las que al menos deberíamos experimentar para algunos asignadores.

@gnzlbg Soy un poco escéptico de las cosas en https://github.com/rust-lang/rust/issues/32838#issuecomment -358267292. Dejando de lado todas esas cosas específicas del sistema, no es difícil generalizar colecciones para alloc, lo he hecho. Intentar incorporar eso parece un desafío aparte.

@SimonSapin ¿

@sfackler Mira eso ^. Estaba tratando de hacer una distinción entre proyectos que necesitan y quieren este estable, pero Servo está al otro lado de esa división.

Tengo un proyecto que quiere esto y requiere que sea estable. No hay nada particularmente mágico en Servo o Firefox como consumidores de Rust.

@ Ericson2314 Correcto, Firefox usa estable: https://wiki.mozilla.org/Rust_Update_Policy_for_Firefox. Como expliqué, aunque hoy hay una solución que funciona, esto no es un verdadero bloqueador para nada. Sería mejor / más robusto usar #[global_allocator] , eso es todo.

Servo usa algunas características inestables, pero como se mencionó, estamos tratando de cambiar eso.

FWIW, los asignadores paramétricos son muy útiles para implementar asignadores. Gran parte de la contabilidad menos sensible al rendimiento se vuelve mucho más fácil si puede usar varias estructuras de datos internamente y parametrizarlas con un asignador más simple (como bsalloc ). Actualmente, la única forma de hacerlo en un entorno estándar es tener una compilación de dos fases en la que la primera fase se utiliza para configurar su asignador más simple como el asignador global y la segunda fase se usa para compilar el asignador más grande y complicado. . En no-std, no hay forma de hacerlo.

@ Ericson2314

Dejando de lado todas esas cosas específicas del sistema, no es difícil generalizar colecciones para alloc, lo he hecho. Intentar incorporar eso parece un desafío aparte.

¿Tiene una implementación de ArrayVec o SmallVec además de Vec + asignadores personalizados que pueda ver? Ese fue el primer punto que mencioné, y no es específico del sistema en absoluto. Podría decirse que serían los dos asignadores más simples imaginables, uno es solo una matriz sin procesar como almacenamiento, y el otro se puede construir sobre el primero agregando una reserva al Heap una vez que la matriz se agota su capacidad. La principal diferencia es que esos asignadores no son "globales", pero cada uno de los Vec s tiene su propio asignador independiente de todos los demás, y estos asignadores tienen estado.

Además, no estoy argumentando que nunca haga esto. Solo digo que esto es muy difícil: C ++ ha estado intentando durante 30 años con solo un éxito parcial: los asignadores de GPU y los asignadores de GC funcionan debido a los tipos de punteros genéricos, pero implementando ArrayVec y SmallVec encima de Vec no da como resultado una abstracción de costo cero en la tierra C ++ ( P0843r1 analiza algunos de los problemas de ArrayVec en detalle).

Así que preferiría que siguiéramos con esto después de estabilizar las piezas que brindan algo útil, siempre y cuando no permitan la búsqueda de asignadores de colecciones personalizados en el futuro.


Hablé un poco con @SimonSapin en IRC y si extendiéramos la propuesta de estabilización inicial con realloc y alloc_zeroed , entonces Rust en Firefox (que solo usa Rust estable) podría usar mozjemalloc como asignador global en Rust estable sin necesidad de hacks adicionales. Como lo mencionó @SimonSapin , Firefox actualmente tiene una solución viable para esto hoy, por lo que si bien esto sería bueno, no parece tener una prioridad muy alta.

Aún así, podríamos comenzar allí, y una vez que estemos allí, mover servo a #[global_allocator] estables sin una pérdida de rendimiento.


@joshlf

FWIW, los asignadores paramétricos son muy útiles para implementar asignadores.

¿Podrías explicar un poco más a qué te refieres? ¿Hay alguna razón por la que no pueda parametrizar sus asignadores personalizados con el rasgo Alloc ? ¿O su propio rasgo de asignador personalizado y simplemente implementar el rasgo Alloc en los asignadores finales (estos dos rasgos no necesariamente tienen que ser iguales)?

No entiendo de dónde viene el caso de uso de "SmallVec = Vec + asignador especial". No es algo que haya visto mencionado mucho antes (ni en Rust ni en otros contextos), precisamente porque tiene muchos problemas serios. Cuando pienso en "mejorar el rendimiento con un asignador especializado", no es en absoluto lo que pienso.

Mirando la API Layout , me preguntaba acerca de las diferencias en el manejo de errores entre from_size_align y align_to , donde el primero devuelve None en caso de error , mientras que el último entra en pánico (!).

¿No sería más útil y coherente agregar una enumeración LayoutErr adecuadamente definida e informativa y devolver una Result<Layout, LayoutErr> en ambos casos (y quizás usarla para las otras funciones que actualmente devuelven una Option también)?

@rkruppe

No entiendo de dónde viene el caso de uso de "SmallVec = Vec + asignador especial". No es algo que haya visto mencionado mucho antes (ni en Rust ni en otros contextos), precisamente porque tiene muchos problemas serios. Cuando pienso en "mejorar el rendimiento con un asignador especializado", no es en absoluto lo que pienso.

Hay dos formas independientes de usar asignadores en Rust y C ++: el asignador del sistema, utilizado por todas las asignaciones de forma predeterminada, y como un argumento de tipo para una colección parametrizada por algún rasgo del asignador, como una forma de crear un objeto de esa colección en particular que usa un asignador particular (que puede ser el asignador del sistema o no).

Lo que sigue se enfoca solo en este segundo caso de uso: usar una colección y un tipo de asignador para crear un objeto de esa colección que usa un asignador particular.

En mi experiencia con C ++, parametrizar una colección con un asignador sirve para dos casos de uso:

  • mejorar el rendimiento de un objeto de colección haciendo que la colección utilice un asignador personalizado dirigido a un patrón de asignación específico, y / o
  • agregue una nueva característica a una colección que le permita hacer algo que antes no podía hacer.

Agregar nuevas funciones a las colecciones

Este es el caso de uso de asignadores que veo en las bases de código C ++ el 99% del tiempo. El hecho de que agregar una nueva característica a una colección mejore el rendimiento es, en mi opinión, una coincidencia. En particular, ninguno de los siguientes asignadores mejora el rendimiento al apuntar a un patrón de asignación. Lo hacen agregando características que, en algunos casos, como menciona @ Ericson2314 , pueden considerarse "específicas del sistema". Estos son algunos ejemplos:

  • asignadores de pila para realizar pequeñas optimizaciones de búfer (consulte el artículo stack_alloc de Howard Hinnant). Le permiten usar std::vector o flat_{map,set,multimap,...} y, al pasarle un asignador personalizado, agrega una pequeña optimización de búfer con ( SmallVec ) o sin ( ArrayVec ) montón retroceder. Esto permite, por ejemplo, poner una colección con sus elementos en la pila o en la memoria estática (donde de otra manera habría usado el montón).

  • arquitecturas de memoria segmentadas (como destinos x86 de puntero ancho de 16 bits y GPGPU). Por ejemplo, C ++ 17 Parallel STL fue, durante C ++ 14, la Especificación técnica paralela. Su biblioteca precursora del mismo autor es la biblioteca Thrust de NVIDIA, que incluye asignadores para permitir que las clases de contenedores usen memoria GPGPU (por ejemplo, thrust :: device_malloc_allocator ) o memoria fija (por ejemplo, thrust :: pinned_allocator ; la memoria fija permite una transferencia más rápida entre el dispositivo host en algunos casos).

  • asignadores para resolver problemas relacionados con el paralelismo, como el uso compartido falso (por ejemplo, Intel Thread Building Blocks cache_aligned_allocator ) o los requisitos de sobrealineación de los tipos SIMD (por ejemplo, aligned_allocator Eigen3).

  • Memoria compartida entre procesos: Boost.Interprocess tiene asignadores que asignan la memoria de la colección utilizando las funciones de memoria compartida entre procesos del sistema operativo (por ejemplo, como la memoria compartida System V). Esto permite usar directamente un contenedor estándar para administrar la memoria utilizada para comunicarse entre diferentes procesos.

  • recolección de basura: la biblioteca de asignación de memoria diferida de Herb Sutter utiliza un tipo de puntero definido por el usuario para implementar asignadores que recolectan memoria de basura. De modo que, por ejemplo, cuando un vector crece, la vieja porción de memoria se mantiene viva hasta que todos los punteros a esa memoria se hayan destruido, evitando la invalidación del iterador.

  • Asignadores instrumentados: el blsma_testallocator de la biblioteca de software de Bloomberg le permite registrar patrones de asignación / desasignación de memoria (y de construcción / destrucción de objetos específicos de C ++) de los objetos donde lo usa. ¿No sabe si un Vec asigna después de reserve ? Conecte un asignador de este tipo y le dirán si sucede. Algunos de estos asignadores le permiten nombrarlos, para que pueda usarlos en varios objetos y obtener registros que indiquen qué objeto está haciendo qué.

Estos son los tipos de asignadores que veo con más frecuencia en la naturaleza en C ++. Como mencioné antes, el hecho de que mejoren el rendimiento en algunos casos, es, en mi opinión, una coincidencia. La parte importante es que ninguno de ellos intenta apuntar a un patrón de asignación en particular.

Orientación a patrones de asignación para mejorar el rendimiento.

AFAIK, no hay asignadores de C ++ ampliamente utilizados que hagan esto y explicaré por qué creo que esto es en un segundo. Las siguientes bibliotecas apuntan a este caso de uso:

Sin embargo, estas bibliotecas no proporcionan realmente un único asignador para un caso de uso particular. En su lugar, proporcionan bloques de construcción de asignadores que puede utilizar para crear asignadores personalizados dirigidos a un patrón de asignación particular en una parte particular de una aplicación.

El consejo general que recuerdo de mis días en C ++ era simplemente "no usarlos" (son el último recurso) porque:

  • igualar el rendimiento del asignador del sistema es muy difícil, superarlo es muy, muy difícil,
  • las posibilidades de que el patrón de asignación de memoria de la aplicación de otra persona coincida con el suyo son escasas, por lo que realmente necesita conocer su patrón de asignación y saber qué bloques de construcción de asignadores necesita para igualarlo
  • no son portátiles porque diferentes proveedores tienen diferentes implementaciones de bibliotecas estándar de C ++ que utilizan diferentes patrones de asignación; los proveedores normalmente dirigen su implementación a sus asignadores de sistema. Es decir, una solución adaptada a un proveedor podría funcionar horriblemente (peor que el asignador del sistema) en otro.
  • hay muchas alternativas que uno puede agotar antes de intentar usar estas: usar una colección diferente, reservar memoria, ... La mayoría de las alternativas son de menor esfuerzo y pueden generar ganancias más grandes.

Esto no significa que las bibliotecas para este caso de uso no sean útiles. Lo son, razón por la cual las bibliotecas como foonathan / memory están floreciendo. Pero al menos, en mi experiencia, se usan mucho menos en la naturaleza que los asignadores que "agregan características adicionales" porque para obtener una ganancia debe vencer al asignador del sistema, que requiere más tiempo del que la mayoría de los usuarios están dispuestos a invertir (Stackoverflow está lleno de preguntas del tipo "Usé Boost.Pool y mi rendimiento empeoró, ¿qué puedo hacer? No usar Boost.Pool.").

Terminando

En mi opinión, creo que es genial que el modelo de asignador de C ++, aunque lejos de ser perfecto, admita ambos casos de uso, y creo que si los asignadores deben parametrizar las colecciones estándar de Rust, también deberían admitir ambos casos de uso, porque al menos en Los asignadores de C ++ para ambos casos han resultado ser útiles.

Creo que este problema es ligeramente ortogonal a la posibilidad de personalizar el asignador global / system / platform / default / heap / free-store de una aplicación en particular, y que intentar resolver ambos problemas al mismo tiempo podría retrasar la solución para uno de ellos innecesariamente.

Lo que algunos usuarios quieren hacer con una colección parametrizada por un asignador puede ser muy diferente de lo que quieren hacer otros usuarios. Si @rkruppe comienza con "patrones de asignación coincidentes" y comienzo con "evitar el uso compartido falso" o "usar una pequeña optimización de búfer con respaldo de pila", será difícil, en primer lugar, comprender las necesidades de los demás y, en segundo lugar, llegar en una solución que funcione para ambos.

@gnzlbg Gracias por la redacción completa. La mayor parte no aborda mi pregunta original y no estoy de acuerdo con parte de ella, pero es bueno tenerla enunciada para que no hablemos entre nosotros.

Mi pregunta fue específicamente sobre esta aplicación:

asignadores de pila para realizar pequeñas optimizaciones de búfer (consulte el artículo stack_alloc de Howard Hinnant). Le permiten usar std :: vector o flat_ {map, set, multimap, ...} y al pasarle un asignador personalizado, agrega una pequeña optimización de búfer con (SmallVec) o sin (ArrayVec) retroceso del montón. Esto permite, por ejemplo, poner una colección con sus elementos en la pila o en la memoria estática (donde de otra manera habría usado el montón).

Al leer sobre stack_alloc, ahora me doy cuenta de cómo puede funcionar. No es lo que la gente suele decir con SmallVec (donde el búfer se almacena en línea en la colección), razón por la cual me perdí esa opción, pero evita el problema de tener que actualizar los punteros cuando la colección se mueve (y también hace que esos movimientos sean más baratos ). También tenga en cuenta que short_alloc permite que múltiples colecciones compartan un arena , lo que lo hace aún más diferente a los tipos típicos de SmallVec. Es más como un asignador lineal / de puntero de golpe con un elegante respaldo a la asignación de montón cuando se queda sin espacio asignado.

No estoy de acuerdo con que este tipo de asignador y cache_aligned_allocator estén fundamentalmente añadiendo nuevas funciones. Se utilizan de forma diferente y, según su definición de "patrón de asignación", es posible que no se optimicen para un patrón de asignación específico. Sin embargo, ciertamente se optimizan para casos de uso específicos y no tienen diferencias de comportamiento significativas con respecto a los asignadores de almacenamiento dinámico de propósito general.

Sin embargo, estoy de acuerdo en que casos de uso como la asignación de memoria diferida de Sutter, que cambian sustancialmente lo que incluso significa un "puntero", son una aplicación separada que puede necesitar un diseño separado si queremos admitirla.

Al leer sobre stack_alloc, ahora me doy cuenta de cómo puede funcionar. No es lo que la gente suele decir con SmallVec (donde el búfer se almacena en línea en la colección), razón por la cual me perdí esa opción, pero evita el problema de tener que actualizar los punteros cuando la colección se mueve (y también hace que esos movimientos sean más baratos ).

Mencioné stack_alloc porque es el único asignador de este tipo con "un documento", pero se lanzó en 2009 y precede a C ++ 11 (C ++ 03 no admitía asignadores con estado en colecciones).

La forma en que esto funciona en C ++ 11 (que admite asignadores con estado), en pocas palabras, es:

  • tiendas std :: vector un Allocator objeto en su interior al igual que Rust RawVec lo hace .
  • la interfaz de Allocator tiene una propiedad oscura llamada Allocator :: propagate_on_container_move_assignment (POCMA de ahora en adelante) que los asignadores definidos por el usuario pueden personalizar; esta propiedad es true por defecto. Si esta propiedad es false , en la asignación de movimiento, el asignador no se puede propagar, por lo que el estándar requiere una colección para mover cada uno de sus elementos al nuevo almacenamiento manualmente.

Entonces, cuando se mueve un vector con el asignador del sistema, primero se asigna el almacenamiento para el nuevo vector en la pila, luego se mueve el asignador (que es de tamaño cero) y luego se mueven los 3 punteros, que siguen siendo válidos. Tales movimientos son O(1) .

OTOHO, cuando se mueve un vector con un asignador POCMA == true , primero se asigna el almacenamiento para el nuevo vector en la pila y se inicializa con un vector vacío, luego la colección anterior es drain ed en el uno nuevo, de modo que el viejo esté vacío y el nuevo lleno. Esto mueve cada elemento de la colección individualmente, usando sus operadores de asignación de movimiento. Este paso es O(N) y corrige punteros internos de los elementos. Finalmente, se elimina la colección original ahora vacía. Tenga en cuenta que esto parece un clon, pero no se debe a que los elementos en sí no se clonen, sino que se muevan en C ++.

¿Tiene sentido?

El principal problema con este enfoque en C ++ es que:

  • la política de crecimiento vectorial está definida por la implementación
  • la API del asignador no tiene métodos _excess
  • la combinación de los dos problemas anteriores significa que si sabe que su vector puede contener como máximo 9 elementos, no puede tener un asignador de pila que pueda contener 9 elementos, porque su vector podría intentar crecer cuando tenga 8 con un factor de crecimiento de 1,5, por lo que debe pesimizar y asignar espacio para 18 elementos.
  • la complejidad de la operación vectorial cambia dependiendo de las propiedades del asignador (POCMA es solo una de las muchas propiedades que tiene la API de asignador de C ++; escribir asignadores de C ++ no es trivial). Esto dificulta la especificación de la API de vector porque a veces copiar o mover elementos entre diferentes asignadores del mismo tipo tiene costos adicionales, que cambian la complejidad de las operaciones. También hace que leer las especificaciones sea un gran dolor. Muchas fuentes de documentación en línea como cppreference ponen el caso general al principio y los oscuros detalles de lo que cambia si una propiedad del asignador es verdadera o falsa en minúsculas para evitar molestar al 99% de los usuarios con ellas.

Hay mucha gente trabajando para mejorar la API de asignación de C ++ para solucionar estos problemas, por ejemplo, agregando métodos _excess y garantizando que las colecciones conformes estándar los usen.

No estoy de acuerdo con que este tipo de asignador y cache_aligned_allocator estén fundamentalmente agregando nuevas funciones.

Quizás lo que quise decir es que te permiten usar colecciones estándar en situaciones o para tipos para los que antes no las podías usar. Por ejemplo, en C ++ no puede colocar los elementos de un vector en el segmento de memoria estática de su binario sin algo como un asignador de pila (sin embargo, puede escribir su propia colección que lo haga). OTOH, el estándar C ++ no admite tipos sobrealineados como los tipos SIMD, y si intenta asignar uno con new , invocará un comportamiento indefinido (debe usar posix_memalign o similar) . El uso del objeto suele manifestar el comportamiento indefinido a través de un defecto de segmento (*). Cosas como aligned_allocator permiten asignar estos tipos en montón, e incluso ponerlos en colecciones estándar, sin invocar un comportamiento indefinido, utilizando un asignador diferente. Seguro que el nuevo asignador tendrá diferentes patrones de asignación (estos asignadores básicamente sobre-alinean toda la memoria por cierto ...), pero para lo que la gente los usa es para poder hacer algo que no podían hacer antes.

Obviamente, Rust no es C ++. Y C ++ tiene problemas que Rust no tiene (y viceversa). Un asignador que agrega una nueva característica en C ++ puede ser innecesario en Rust, que, por ejemplo, no tiene ningún problema con los tipos de SIMD.

(*) Los usuarios de Eigen3 sufren esto profundamente, porque para evitar un comportamiento indefinido al usar contenedores C ++ y STL, debe proteger los contenedores contra tipos SIMD, o tipos que contienen tipos SIMD ( documentos Eigen3 ) y también debe protegerse de alguna vez usando new en sus tipos sobrecargando el operador new para ellos ( más documentos de Eigen3 ).

@gnzlbg gracias, también me confundió el ejemplo de smallvec. Eso requeriría tipos no movibles y algún tipo de asignación en Rust --- dos RFC en revisión y luego más trabajo de seguimiento --- así que no tengo reparos en apostar por eso por ahora. La estrategia de smallvec existente de usar siempre todo el espacio de pila que necesitará parece estar bien por ahora.

También estoy de acuerdo con @rkruppe en que en su lista revisada, las nuevas capacidades del asignador no necesitan ser conocidas por la colección que usa el asignador. A veces, el Collection<Allocator> completo tiene nuevas propiedades (que existen por completo en la memoria fija, digamos), pero eso es solo una consecuencia natural de usar el asignador.

La única excepción que veo aquí son los asignadores que solo asignan un tamaño / tipo único (los de NVidia hacen esto, al igual que los asignadores de bloques). Podríamos tener un rasgo ObjAlloc<T> separado que se implementa de manera general para los asignadores normales: impl<A: Alloc, T> ObjAlloc<T> for A . Luego, las colecciones usarían los límites de ObjAlloc si solo necesitaran asignar algunos elementos. Pero, me siento un poco tonto incluso al mencionar esto, ya que debería ser posible de manera compatible con versiones anteriores.

¿Tiene sentido?

Claro, pero no es realmente relevante para Rust ya que no tenemos constructores de movimientos. Entonces, un asignador (móvil) que contenga directamente la memoria a la que entrega punteros simplemente no es posible, punto.

Por ejemplo, en C ++ no puede colocar los elementos de un vector en el segmento de memoria estática de su binario sin algo como un asignador de pila (sin embargo, puede escribir su propia colección que lo haga).

Este no es un cambio de comportamiento. Hay muchas razones válidas para controlar de dónde obtienen su memoria las colecciones, pero todas están relacionadas con "externalidades" como el rendimiento, los scripts del enlazador, el control sobre el diseño de la memoria de todo el programa, etc.

Cosas como alineado_allocator le permiten asignar en pila estos tipos, e incluso ponerlos en colecciones estándar, sin invocar un comportamiento indefinido, utilizando un asignador diferente.

Es por eso que mencioné específicamente el cache_aligned_allocator de TBB y no el align_allocator de Eigen. cache_aligned_allocator no parece garantizar ninguna alineación específica en su documentación (solo dice que es "típicamente" de 128 bytes), e incluso si lo hiciera, generalmente no se usaría para este propósito (porque su alineación es probablemente demasiado grande para los Tipos SIMD y demasiado pequeño para cosas como DMA alineado con páginas). Su propósito, como usted afirma, es evitar el intercambio falso.

@gnzlbg

FWIW, los asignadores paramétricos son muy útiles para implementar asignadores.

¿Podrías explicar un poco más a qué te refieres? ¿Hay alguna razón por la que no pueda parametrizar sus asignadores personalizados con el rasgo Alloc? ¿O su propio rasgo de asignador personalizado y simplemente implementar el rasgo de Alloc en los asignadores finales (estos dos rasgos no necesariamente tienen que ser iguales)?

Creo que no estaba claro; déjame intentar explicarte mejor. Digamos que estoy implementando un asignador que espero usar:

  • Como asignador global
  • En un entorno sin std

Y digamos que me gustaría usar Vec bajo el capó para implementar este asignador. No puedo usar Vec directamente como existe hoy porque

  • Si soy el asignador global, usarlo solo introducirá una dependencia recursiva de mí mismo
  • Si estoy en un entorno sin estándar, no hay Vec como existe hoy

Por lo tanto, lo que necesito es poder usar un Vec que esté parametrizado en otro asignador que uso internamente para la contabilidad interna simple. Este es el objetivo de bsalloc (y la fuente del nombre; se usa para iniciar otros asignadores).

En elfmalloc, todavía podemos ser un asignador global al:

  • Al compilarnos a nosotros mismos, compilamos estáticamente jemalloc como asignador global
  • Producir un archivo de objeto compartido que otros programas pueden cargar dinámicamente

Nótese que en este caso, es importante que no compilar nosotros mismos mediante el asignador de sistema que el asignador global, porque entonces, una vez cargado, nos gustaría volver a introducir la dependencia recursiva, ya que, en ese momento, somos el asignador de sistema.

Pero no funciona cuando:

  • Alguien quiere usarnos como asignador global en Rust de la manera "oficial" (en lugar de crear primero un archivo de objeto compartido)
  • Estamos en un entorno sin ETS

OTOH, el estándar C ++ no admite tipos sobrealineados como los tipos SIMD, y si intenta asignar uno con nuevo, invocará un comportamiento indefinido (debe usar posix_memalign o similar).

Dado que nuestro rasgo actual Alloc toma la alineación como parámetro, asumo que esta clase de problema (el problema "No puedo trabajar sin una alineación diferente") desaparece para nosotros.

@gnzlbg : un

Este caso de uso debe considerarse. En particular, influye fuertemente en lo que es correcto hacer: -

  • Que más de un asignador está en uso, y especialmente, cuando se usa ese asignador es para memoria persistente, nunca sería el asignador del sistema ; (de hecho, podría haber múltiples asignadores de memoria persistente)
  • El costo de 'volver a implementar' las colecciones estándar es alto y conduce a un código incompatible con bibliotecas de terceros.
  • La vida útil del asignador no es necesariamente 'static .
  • Que los objetos almacenados en la memoria persistente necesitan un estado adicional que se debe completar desde el montón, es decir, necesitan reinicializar el estado. Esto es particularmente cierto para los mutex y similares. Lo que antes era desechable ya no se desecha.

Rust tiene una excelente oportunidad de tomar la iniciativa aquí y convertirla en una plataforma de primera clase para lo que reemplazará a los discos duros, SSD e incluso al almacenamiento conectado a PCI.

* No es una sorpresa, de verdad, porque hasta hace muy poco ha sido un poco especial. Ahora es ampliamente compatible con Linux, FreeBSD y Windows.

@raphaelcohn

Este no es realmente el lugar para trabajar la memoria persistente. La suya no es la única escuela de pensamiento con respecto a la interfaz de la memoria persistente; por ejemplo, puede resultar que el enfoque predominante sea simplemente tratarlo como un disco más rápido, por razones de integridad de los datos.

Si tiene un caso de uso para usar la memoria persistente de esta manera, probablemente sería mejor presentar ese caso en otro lugar de alguna manera primero. Prototipéelo, proponga algunos cambios más concretos en la interfaz del asignador e idealmente defina que esos cambios valen el impacto que tendrían en el caso promedio.

@rpjohnst

Estoy en desacuerdo. Este es exactamente el tipo de lugar al que pertenece. Quiero evitar que se tome una decisión que cree un diseño que sea el resultado de un enfoque y una búsqueda de evidencia demasiado estrechos.

El Intel PMDK actual, que es donde se concentra una gran cantidad de esfuerzo para el soporte de espacio de usuario de bajo nivel, se acerca mucho más a la memoria asignada y regular con punteros, una memoria similar a la de mmap , por ejemplo. De hecho, si uno quiere trabajar con memoria persistente en Linux, creo que es prácticamente su único puerto de escala en este momento. En esencia, uno de los conjuntos de herramientas más avanzados para usarlo, el que prevalece, lo trata como memoria asignada.

En cuanto a la creación de un prototipo, bueno, eso es exactamente lo que dije que hice:

Recientemente he estado trabajando en un contenedor de Rust para un asignador de memoria persistente (específicamente, libpmemcto).

(Puede usar una versión anterior de mi caja en https://crates.io/crates/nvml . Hay mucha más experimentación en el control de código fuente en el módulo cto_pool ).

Mi prototipo está diseñado pensando en lo que se necesita para reemplazar un motor de almacenamiento de datos en un sistema a gran escala del mundo real. Una mentalidad similar está detrás de muchos de mis proyectos de código abierto. Durante muchos años he encontrado las mejores bibliotecas, al igual que los mejores estándares, que se derivan _del_ uso real.

Nada como intentar ajustar un asignador del mundo real a la interfaz actual. Francamente, la experiencia de usar la interfaz Alloc , luego copiar la totalidad de Vec y luego ajustarla, fue dolorosa. En muchos lugares se asume que los asignadores no se pasan, por ejemplo, Vec::new() .

Al hacerlo, hice algunas observaciones en mi comentario original sobre lo que se requeriría de un asignador y lo que se requeriría de un usuario de dicho asignador. Creo que esos son muy válidos en un hilo de discusión sobre una interfaz de asignador.

La buena noticia es que sus primeros 3 puntos de https://github.com/rust-lang/rust/issues/32838#issuecomment -358940992 son compartidos por otros casos de uso.

Solo quería agregar que no agregué memoria no volátil a la lista
porque la lista enumera los casos de uso de asignadores que parametrizan contenedores en
el mundo de C ++ que se utilizan "ampliamente", al menos en mi experiencia (aquellos
Los aloctores que mencioné son en su mayoría de bibliotecas muy populares utilizadas por muchos).
Aunque conozco los esfuerzos del Intel SDK (algunas de sus bibliotecas
target C ++) No conozco personalmente ningún proyecto que los use (¿tienen
un asignador que se puede utilizar con std :: vector? No lo sé). Esto no
significa que no se utilizan ni son importantes. Me interesaría saber
sobre estos, pero el punto principal de mi publicación fue que parametrizar
asignadores por contenedores es muy complejo, y deberíamos intentar hacer
progresar con los repartidores del sistema sin cerrar puertas para contenedores
(pero deberíamos abordar eso más tarde).

El domingo 21 de enero de 2018 a las 17:36, John Ericson [email protected] escribió:

La buena noticia son sus primeras 3 viñetas de # 32838 (comentario)
https://github.com/rust-lang/rust/issues/32838#issuecomment-358940992
son compartidos por otros casos de uso.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-359261305 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AA3Npk95PZBZcm7tknNp_Cqrs_3T1UkEks5tM2ekgaJpZM4IDYUN
.

Traté de leer la mayor parte de lo que ya se ha escrito, así que puede que esto ya esté aquí y, en ese caso, lo siento si me lo perdí, pero aquí va:

Algo que es bastante común para los juegos (en C / C ++) es usar "asignación de scratch por fotograma". un marco de juego) y luego "destruido".

Destruido en este caso, lo que significa que restablece el asignador a su posición inicial. No hay "destrucción" de objetos en absoluto, ya que estos objetos deben ser de tipo POD (por lo tanto, no se están ejecutando destructores)

Me pregunto si algo como esto encajará con el diseño actual del asignador en Rust.

(editar: Debería haber NO destrucción de objetos)

@emoon

Algo que es bastante común para los juegos (en C / C ++) es usar "asignación de scratch por fotograma". un marco de juego) y luego "destruido".

Destruido en este caso, lo que significa que restablece el asignador a su posición inicial. Hay "destrucción" de objetos en absoluto, ya que estos objetos deben ser del tipo POD (por lo tanto, no se están ejecutando destructores)

Debería ser factible. En la parte superior de mi cabeza, necesitarías un objeto para la arena en sí y otro objeto que sea un asa por cuadro en la arena. Luego, podría implementar Alloc para ese identificador, y asumiendo que estaba usando envoltorios seguros de alto nivel para la asignación (por ejemplo, imagine que Box vuelve paramétrico en Alloc ), el las duraciones garantizarían que todos los objetos asignados se eliminen antes de que se elimine el identificador por cuadro. Tenga en cuenta que dealloc todavía se llamaría para cada objeto, pero si dealloc no fuera operativo, entonces toda la lógica de soltar y desasignar podría estar completa o casi optimizada.

También puede usar un tipo de puntero inteligente personalizado que no implemente Drop , lo que facilitaría muchas cosas en otros lugares.

¡Gracias! Cometí un error tipográfico en mi publicación original. Es decir que no hay destrucción de objetos.

Para las personas que no son expertas en asignadores y no pueden seguir este hilo, ¿cuál es el consenso actual? ¿Planeamos admitir asignadores personalizados para los tipos de colección stdlib?

@alexreg No estoy seguro de cuál es el plan final, pero hay 0 dificultades técnicas confirmadas para hacerlo. OTOH, no tenemos una buena manera de exponer eso en std porque las variables de tipo predeterminadas son sospechosas, pero no tengo ningún problema con convertirlo en algo exclusivo de alloc por ahora, así que puede progresar en el lado lib sin obstáculos.

@ Ericson2314 Está bien, es bueno escucharlo. ¿Están implementadas las variables de tipo predeterminado? ¿O quizás en la etapa RFC? Como dices, si solo están restringidos a cosas relacionadas con alloc / std::heap , todo debería estar bien.

Realmente creo que AllocErr debería ser Error. Sería más consistente con otros módulos (por ejemplo, io).

impl Error for AllocError probablemente tenga sentido y no duele, pero personalmente he encontrado que el rasgo Error es inútil.

Estaba mirando la función Layout :: from_size_align hoy, y la limitación " align no debe exceder 2 ^ 31 (es decir, 1 << 31 )", no tenía sentido para mí. Y git blame señaló # 30170.

Debo decir que fue un mensaje de confirmación bastante engañoso, hablando de align encajando en un u32, lo cual es solo incidental, cuando lo que realmente se está "arreglando" (más trabajado) es un asignador de sistema que se está comportando mal.

Lo que me lleva a esta nota: el elemento "OSX / alloc_system tiene errores en alineaciones enormes" aquí no debe marcarse. Si bien se ha abordado el problema directo, no creo que la solución sea adecuada a largo plazo: el hecho de que un asignador de sistema se comporte mal no debería impedir la implementación de un asignador que se comporte. Y la limitación arbitraria en Layout :: from_size_align hace eso.

@glandium ¿Es útil solicitar la alineación a un múltiplo de 4 gigbytes o más?

Puedo imaginar casos en los que uno quiera tener una asignación de 4GiB alineada a 4GiB, lo que no es posible actualmente, pero apenas más. Pero no creo que se deban agregar limitaciones arbitrarias solo porque no pensamos en tales razones ahora.

Puedo imaginar casos en los que uno quiera tener una asignación de 4GiB alineada a 4GiB

¿Cuáles son esos casos?

Puedo imaginar casos en los que uno quiera tener una asignación de 4GiB alineada a 4GiB

¿Cuáles son esos casos?

Concretamente, acabo de agregar soporte para alineaciones arbitrariamente grandes en mmap-alloc con el fin de admitir la asignación de grandes bloques de memoria alineados para su uso en elfmalloc . La idea es alinear el bloque de memoria con su tamaño de modo que, dado un puntero a un objeto asignado desde ese bloque, solo necesita enmascarar los bits bajos para encontrar el bloque contenedor. Actualmente no usamos placas de 4 GB de tamaño (para objetos tan grandes, vamos directamente a mmap), pero no hay ninguna razón por la que no podamos, y podría imaginarme una aplicación con grandes requisitos de RAM que quisiera hacer eso (es decir, si asignó objetos de varios GB con la suficiente frecuencia como para no querer aceptar la sobrecarga de mmap).

Aquí hay un posible caso de uso para la alineación> 4GiB: alineación a un límite de página grande. Ya existen plataformas que admiten páginas> 4 GiB. Este documento de IBM dice que "el procesador POWER5 + admite cuatro tamaños de página de memoria virtual: 4 KB, 64 KB, 16 MB y 16 GB". Incluso x86-64 no está lejos: las "páginas enormes" suelen tener 2 MiB, pero también admiten 1 GiB.

Todas las funciones no escritas en el rasgo Alloc tratan con *mut u8 . Lo que significa que podrían aceptar o devolver punteros nulos, y se desataría el infierno. ¿Deberían usar NonNull lugar?

Hay muchos indicios de que podrían regresar de los cuales todo el infierno
liberarse con fuerza.
El domingo, 4 de marzo de 2018 a las 3:56 a.m., Mike Hommey [email protected] escribió:

Todas las funciones no escritas en el rasgo Alloc tratan con * mut u8.
Lo que significa que podrían aceptar o devolver punteros nulos, y todo el infierno
liberarse con fuerza. ¿Deberían usar NonNull en su lugar?

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370223269 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABY2UR2dRxDtdACeRUh_djM-DExRuLxiks5ta9aFgaJpZM4IDYUN
.

Una razón más convincente para usar NonNull es que permitiría los Result s actualmente devueltos de los métodos Alloc (o Options , si cambiamos a eso en el futuro) para ser más pequeño.

Una razón más convincente para usar NonNull es que permitiría que los resultados devueltos actualmente por los métodos de Alloc (u Opciones, si cambiamos a eso en el futuro) sean más pequeños.

No creo que sea así porque AllocErr tiene dos variantes.

Hay muchos indicadores de que podrían regresar y de los cuales se desataría el infierno.

Pero un puntero nulo es claramente más incorrecto que cualquier otro puntero.

Me gusta pensar que el sistema de tipo óxido ayuda con las pistolas y se usa para codificar invariantes. La documentación para alloc dice claramente "Si este método devuelve un Ok(addr) , entonces la dirección devuelta será una dirección no nula", pero su tipo de retorno no. Tal como están las cosas, Ok(malloc(layout.size())) sería una implementación válida, cuando claramente no lo es.

Tenga en cuenta que también hay notas sobre el tamaño de Layout que debe ser distinto de cero, por lo que también diría que debería codificarlo como un valor distinto de cero.

No es porque todas esas funciones sean intrínsecamente inseguras que no deberíamos tener alguna prevención de pistolas.

De todos los posibles errores al usar (editar: e implementar) asignadores, pasar un puntero nulo es uno de los más fáciles de rastrear (siempre obtienes un error de segmentación limpio en la desreferenciación, al menos si tienes una MMU y no la hiciste cosas muy extrañas con él), y generalmente una de las más triviales de arreglar también. Es cierto que las interfaces inseguras pueden intentar prevenir pistolas, pero esta pistola parece desproporcionadamente pequeña (en comparación con los otros errores posibles y con la verbosidad de codificar este invariante en el sistema de tipos).

Además, parece probable que las implementaciones de asignadores solo usen el constructor sin marcar de NonNull "para rendimiento": dado que en un asignador correcto nunca devolvería nulo de todos modos, querría omitir el NonNell::new(...).unwrap() . En ese caso, no obtendrá ninguna prevención tangible de pistolas, solo más repetición. (Los beneficios de tamaño Result , si son reales, pueden ser una razón convincente para ello).

Las implementaciones de asignadores solo usarían el constructor no verificado de NonNull

El punto es menos ayudar a la implementación del asignador que ayudar a sus usuarios. Si MyVec contiene un NonNull<T> y Heap.alloc() ya devuelve un NonNull , esa llamada menos marcada o insegura-no marcada que necesito hacer.

Tenga en cuenta que los punteros no son solo tipos de retorno, también son tipos de entrada, por ejemplo, dealloc y realloc . ¿Se supone que esas funciones deben endurecerse para que su entrada sea posiblemente nula o no? La documentación tendería a decir que no, pero el sistema de tipos tendería a decir que sí.

De manera muy similar con layout.size (). ¿Se supone que las funciones de asignación manejan el tamaño solicitado siendo 0 de alguna manera, o no?

(Los beneficios del tamaño del resultado, si son reales, aún pueden ser una razón convincente para ello).

Dudo que haya beneficios de tamaño, pero con algo como # 48741, habría beneficios de codegen.

Si continuamos con ese principio de ser más flexibles para los usuarios de la API, los punteros deberían ser NonNull en los tipos de retorno pero no en los argumentos. (Esto no significa que esos argumentos deban verificarse como nulos en tiempo de ejecución).

Creo que el enfoque de la ley de Postel no es el adecuado para adoptar aquí. Hay alguna
caso en el que pasar un puntero nulo a un método Alloc es válido? Si no,
entonces esa flexibilidad es básicamente darle a la pistola un poco más
gatillo sensible.

El 5 de marzo de 2018 a las 8:00 a. M., "Simon Sapin" [email protected] escribió:

Si continuamos con ese principio de ser más flexibles para los usuarios de la API,
los punteros deben ser NonNull en los tipos de retorno pero no en los argumentos. (Esta
no significa que esos argumentos deban verificarse como nulos en tiempo de ejecución).

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370327018 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AA_2L8zrOLyUv5mUc_kiiXOAn1f60k9Uks5tbOJ0gaJpZM4IDYUN
.

El punto es menos ayudar a la implementación del asignador que ayudar a sus usuarios. Si MyVec contiene un NonNully Heap.alloc () ya devuelve un NonNull, esa llamada menos comprobada o insegura-no comprobada que necesito hacer.

Ah, esto tiene sentido. No arregla el revólver, pero centraliza la responsabilidad.

Tenga en cuenta que los punteros no son solo tipos de retorno, también son tipos de entrada, por ejemplo, dealloc y realloc. ¿Se supone que esas funciones deben endurecerse para que su entrada sea posiblemente nula o no? La documentación tendería a decir que no, pero el sistema de tipos tendería a decir que sí.

¿Existe algún caso en el que sea válido pasar un puntero nulo a un método Alloc? Si no es así, entonces esa flexibilidad es básicamente darle a la pistola un gatillo un poco más sensible.

El usuario tiene que leer absolutamente la documentación y tener en cuenta las invariantes. Muchos invariantes no se pueden imponer a través del sistema de tipos; si pudieran, la función no sería insegura para empezar. Así que esto es solo una cuestión de si poner NonNull en una interfaz dada realmente ayudará a los usuarios al

  • recordarles que lean los documentos y piensen en las invariantes
  • oferta de conveniencia (valor de retorno de punto WRT alloc @SimonSapin 's)
  • dar alguna ventaja material (por ejemplo, optimizaciones de diseño)

No veo ninguna gran ventaja en hacer, por ejemplo, el argumento de dealloc en NonNull . Veo aproximadamente dos clases de usos de esta API:

  1. Uso relativamente trivial, donde llama a alloc , almacena el puntero devuelto en algún lugar y, después de un tiempo, pasa el puntero almacenado a dealloc .
  2. Escenarios complicados que involucran FFI, mucha aritmética de punteros, etc. donde hay una lógica significativa involucrada para garantizar que pase lo correcto a dealloc al final.

Tomar NonNull aquí básicamente solo ayuda al primer tipo de caso de uso, porque esos almacenarán el NonNull en algún lugar agradable y simplemente lo pasarán a NonNull sin alterar. En teoría, podría evitar algunos errores tipográficos (pasando foo cuando se refería a bar ) si está haciendo malabarismos con varios punteros y solo uno de ellos es NonNull , pero esto no parece demasiado común o importante. La desventaja de que dealloc tome un puntero sin procesar (asumiendo que alloc devuelve NonNull que @SimonSapin me ha convencido de que debería suceder) sería que requiere un as_ptr en la llamada dealloc, que es potencialmente molesta pero no afecta la seguridad de ninguna manera.

El segundo tipo de caso de uso no se ayuda porque probablemente no pueda seguir usando NonNull durante todo el proceso, por lo que tendría que volver a crear manualmente un NonNull partir del puntero sin procesar que obtuvo por cualquier medio. Como dije anteriormente, esto probablemente se convertiría en una aserción / unsafe sin verificar en lugar de una verificación del tiempo de ejecución real, por lo que no se evitan los pistolas.

Esto no quiere decir que esté a favor de que dealloc tome un puntero sin formato. Simplemente no veo ninguna de las ventajas declaradas con pistolas. La consistencia de los tipos probablemente solo gana por defecto.

Lo siento, pero leí esto como "Muchos invariantes no se pueden aplicar a través del sistema de tipos ... por lo tanto, ni siquiera lo intentemos". ¡No dejes que lo perfecto sea enemigo de lo bueno!

Creo que se trata más de las compensaciones entre las garantías proporcionadas por NonNull y la ergonomía perdida por tener que cambiar de un lado a otro entre NonNull y punteros en bruto. No tengo una opinión particularmente fuerte de ninguna manera, ninguna de las partes parece irrazonable.

@cramertj Sí, pero realmente no compro la premisa de ese tipo de argumento. La gente dice que Alloc es para casos de uso oscuros, ocultos y en gran parte inseguros. Bueno, en un código oscuro y difícil de leer, me gustaría tener la mayor seguridad posible, precisamente porque rara vez se tocan, es probable que el autor original no esté presente. Por el contrario, si el código se lee años después, arruine la egonomía. En todo caso, es contraproducente. El código debe esforzarse por ser muy explícito para que un lector no familiarizado pueda descubrir mejor lo que está pasando. Menos ruido <invariantes más claros.

El segundo tipo de caso de uso no se ayuda porque probablemente no pueda seguir usando NonNull durante todo el proceso, por lo que tendría que volver a crear manualmente un NonNull desde el puntero sin procesar que obtuvo. por cualquier medio.

Esto es simplemente un fallo de coordinación, no una inevitabilidad técnica. Claro, en este momento muchas API inseguras pueden usar punteros sin procesar. Entonces, algo tiene que marcar el camino para cambiar a una interfaz superior usando NonNull u otros envoltorios. Entonces, otro código puede seguir su ejemplo más fácilmente. No veo ninguna razón para recurrir constantemente a punteros en bruto poco informativos y difíciles de leer en código no seguro, totalmente oxidado y totalmente nuevo.

¡Hola!

Solo quiero decir que, como autor / mantenedor de un asignador personalizado de Rust, estoy a favor de NonNull . Básicamente por todas las razones que ya se han expuesto en este hilo.

Además, me gustaría señalar que @glandium es el

Podría escribir mucho más respondiendo a @ Ericson2314 y otros, pero rápidamente se está convirtiendo en un debate muy distante y filosófico, así que lo voy a abreviar aquí. Estaba discutiendo en contra de lo que creo que es una exageración de los beneficios de seguridad de NonNull en este tipo de API (hay otros beneficios, por supuesto). Eso no quiere decir que no haya beneficios de seguridad, pero como dijo @cramertj , hay compensaciones y creo que el lado "pro" está exagerado. Independientemente, ya he dicho que me inclino por usar NonNull en varios lugares por otras razones, por la razón que @SimonSapin dio alloc , en dealloc por coherencia. Así que hagamos eso y no nos vayamos por la tangente.

Si hay algunos casos de uso de NonNull con los que todos están de acuerdo, es un gran comienzo.

Probablemente queramos actualizar Unique y amigos para usar NonNull lugar de NonZero para mantener la fricción al menos dentro de liballoc baja. Pero esto parece algo que ya estamos haciendo de todos modos en un nivel de abstracción sobre el asignador, y no puedo pensar en ninguna razón para no hacer esto también en el nivel del asignador. Me parece un cambio razonable.

(Ya hay dos implementaciones del rasgo From que se convierten de forma segura entre Unique<T> y NonNull<T> .)

Teniendo en cuenta que necesito algo muy parecido a la API del asignador en estable rust, extraje el código del repositorio de rust y lo puse en una caja separada:

Esto / podría / usarse para iterar sobre cambios experimentales en la API, pero a partir de ahora, es una copia simple de lo que está en el repositorio de rust.

[Fuera de tema] Sí, sería bueno si std pudiera usar cajas de código estable de árbol como esa, para que podamos experimentar con interfaces inestables en código estable. Esta es una de las razones por las que me gusta tener std una fachada.

std podría depender de una copia de una caja de crates.io, pero si su programa también depende de la misma caja, no "se vería" como la misma caja / tipos / rasgos que rustc de todos modos, así que no No veo cómo ayudaría. De todos modos, independientemente de la fachada, hacer que las características inestables no estén disponibles en el canal estable es una elección muy deliberada, no un accidente.

Parece que tenemos algún acuerdo con el uso de NonNull. ¿Cuál es el camino a seguir para que esto suceda realmente? sólo un PR haciéndolo? un RFC?

Sin relación alguna, he estado viendo algunos ensamblajes generados a partir de cosas de Boxing, y las rutas de error son bastante grandes. ¿Debería hacerse algo al respecto?

Ejemplos:

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

Esa es una cantidad bastante grande de código para agregar a cualquier lugar creando cajas. Compare con 1.19, que no tenía la api del asignador:

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>

Si esto es realmente significativo, entonces es realmente molesto. Sin embargo, ¿quizás LLVM optimiza esto para programas más grandes?

Hay 1439 llamadas a __rust_oom en la última versión de Firefox todas las noches. Sin embargo, Firefox no usa el asignador de rust, por lo que recibimos llamadas directas a malloc / calloc, seguidas de una verificación nula de que salta al código de preparación de oom, que generalmente es dos movq y un lea, llenando el AllocErr y obteniendo su dirección para pasarlo a __rust__oom . Ese es el mejor de los casos, esencialmente, pero siguen siendo 20 bytes de código de máquina para los dos movq y el lea.

Si miro ripgrep, hay 85, y todos están en funciones _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn idénticas. Todos ellos tienen 16 bytes de longitud. Hay 685 llamadas a esas funciones contenedoras, la mayoría de las cuales están precedidas por un código similar al que pegué en https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485.

@nox estaba buscando hoy habilitar el pase mergefunc llvm, me pregunto si eso hace alguna diferencia aquí.

mergefunc aparentemente no elimina las múltiples funciones idénticas _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn (probadas con -C passes=mergefunc en RUSTFLAGS ).

Pero lo que marca una gran diferencia es LTO, que en realidad es lo que hace que Firefox llame directamente a malloc, dejando la creación de AllocErr a la derecha antes de llamar a __rust_oom . Eso también hace que la creación del Layout innecesaria antes de llamar al asignador, dejándolo así al llenar el AllocErr .

Esto me hace pensar que las funciones de asignación, excepto __rust_oom , probablemente deberían estar marcadas en línea.

Por cierto, habiendo mirado el código generado para Firefox, creo que idealmente sería deseable usar moz_xmalloc lugar de malloc . Esto no es posible sin una combinación de los rasgos del Allocator y la posibilidad de reemplazar el asignador del montón global, pero trae la posible necesidad de un tipo de error personalizado para el rasgo del Allocator: moz_xmalloc es infalible y nunca regresa en caso de fracaso. IOW, maneja OOM por sí mismo, y el código rust no necesitaría llamar a __rust_oom en ese caso. Lo que haría deseable que las funciones de asignación devuelvan opcionalmente ! lugar de AllocErr .

Hemos hablado de hacer de AllocErr una estructura de tamaño cero, lo que también podría ayudar aquí. Con el puntero también hecho NonNull , el valor de retorno completo podría tener el tamaño de un puntero.

https://github.com/rust-lang/rust/pull/49669 realiza una serie de cambios en estas API, con el objetivo de estabilizar un subconjunto que cubra los asignadores globales. Problema de seguimiento para ese subconjunto: https://github.com/rust-lang/rust/issues/49668. En particular, se introduce un nuevo rasgo GlobalAlloc .

¿Este RP nos permitirá hacer cosas como Vec::new_with_alloc(alloc) donde alloc: Alloc pronto?

@alexreg no

@sfackler Hmm, ¿por qué no? ¿Qué necesitamos antes de poder hacer eso? Realmente no entiendo el punto de este PR de otra manera, a menos que sea simplemente para cambiar el asignador global.

@alexreg

Realmente no entiendo el punto de este PR de otra manera, a menos que sea simplemente para cambiar el asignador global.

Creo que es simplemente para cambiar el asignador global.

@alexreg Si te refieres a estable, hay una serie de preguntas de diseño sin resolver que no estamos listos para estabilizar. En Nightly, esto es compatible con RawVec y probablemente esté bien agregar como #[unstable] por Vec para cualquiera que tenga ganas de trabajar en eso.

Y sí, como se menciona en el PR, su objetivo es permitir cambiar el asignador global, o asignar (por ejemplo, en un tipo de colección personalizado) sin obtener Vec::with_capacity .

FWIW, la caja allocator_api mencionada en https://github.com/rust-lang/rust/issues/32838#issuecomment -376793369 tiene RawVec<T, A> y Box<T, A> en el maestro branch (aún no lanzado). Estoy pensando en ello como una incubadora de cómo se verían las colecciones genéricas sobre el tipo de asignación (más el hecho de que necesito un tipo Box<T, A> para el óxido estable). Todavía no he comenzado a portar vec.rs para agregar Vec<T, A> , pero los RP son bienvenidos. vec.rs es grande.

Notaré que los "problemas" de codegen mencionados en https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485 deberían desaparecer con los cambios en # 49669.

Ahora, con un poco más de pensamiento sobre el uso del rasgo Alloc para ayudar a implementar un asignador en capas, hay dos cosas que creo que serían útiles (al menos para mí):

  • como se mencionó anteriormente, opcionalmente se puede especificar un tipo AllocErr . Esto puede ser útil para convertirlo en ! , o, ahora que AllocErr está vacío, para que opcionalmente transmita más información que "fallida".
  • opcionalmente, poder especificar un tipo Layout . Imagine que tiene dos capas de asignadores: una para las asignaciones de páginas y otra para las regiones más grandes. El último puede confiar en el primero, pero si ambos toman el mismo tipo Layout , entonces ambas capas deben hacer su propia validación: en el nivel más bajo, ese tamaño y alineación es un múltiplo del tamaño de la página, y en el nivel superior, ese tamaño y alineación coinciden con los requisitos de las regiones más grandes. Pero esos controles son redundantes. Con los tipos especializados Layout , la validación podría delegarse a la creación Layout lugar de al asignador en sí, y las conversiones entre los tipos Layout permitirían omitir las comprobaciones redundantes.

@cramertj @SimonSapin @glandium Está bien, gracias por aclarar. Puedo enviar un PR para algunos de los otros tipos principales de colecciones. ¿Es mejor hacer esto contra su repositorio / caja de allocator-api, @glandium o rust master?

@alexreg considerando la cantidad de cambios importantes en el rasgo Alloc en # 49669, probablemente sea mejor esperar a que se fusione primero.

@glandium Bastante justo. Eso no parece estar muy lejos de aterrizar. Acabo de notar el https://github.com/pnkfelix/collections-prime repositorio también ... ¿qué es eso en relación con el tuyo?

Agregaría una pregunta más abierta:

  • ¿Se permite que Alloc::oom en pánico? Actualmente, los documentos dicen que este método debe abortar el proceso. Esto tiene implicaciones para el código que usa asignadores, ya que luego deben diseñarse para manejar el desenrollado correctamente sin perder memoria.

Creo que deberíamos permitir el pánico ya que una falla en un asignador local no significa necesariamente que el asignador global también fallará. En el peor de los casos, se llamará a oom del asignador global, lo que interrumpirá el proceso (de lo contrario, se rompería el código existente).

@alexreg No lo es. Simplemente parece ser una copia simple de lo que hay en std / alloc / collections. Bueno, una copia de dos años. Mi caja tiene un alcance mucho más limitado (la versión publicada solo tiene el rasgo Alloc desde hace unas semanas, la rama maestra solo tiene RawVec y Box además de eso), y uno de mis objetivos es mantenerlo construido con óxido estable.

@glandium De acuerdo, en ese caso probablemente tenga sentido para mí esperar hasta que aterrice el PR, luego crear un PR contra rust master y etiquetarlo, para que sepa cuándo se fusiona en master (y luego puede fusionarlo en su caja) , ¿justa?

@alexreg tiene sentido. Podrías empezar a trabajar en ello ahora, pero eso probablemente provocaría un poco de abandono por tu parte si / cuando el abandono de bicicletas cambia las cosas en ese PR.

@glandium Tengo otras cosas para mantenerme ocupado con Rust por ahora, pero estaré al tanto cuando se apruebe ese PR. Será genial obtener asignaciones / colecciones de montón genérico de asignador tanto de noche como de forma estable pronto. :-)

¿Alloc :: oom puede entrar en pánico? Actualmente, los documentos dicen que este método debe abortar el proceso. Esto tiene implicaciones para el código que usa asignadores, ya que luego deben diseñarse para manejar el desenrollado correctamente sin perder memoria.

@Amanieu Este RFC se fusionó: https://github.com/rust-lang/rfcs/pull/2116 Es posible que los documentos y la implementación aún no se hayan actualizado.

Hay un cambio en la API para el que estoy considerando enviar un PR:

Divida el rasgo Alloc en dos partes: "implementación" y "ayudantes". Las primeras serían funciones como alloc , dealloc , realloc , etc. y las últimas, alloc_one , dealloc_one , alloc_array , etc. Si bien existen algunos beneficios hipotéticos de poder tener una implementación personalizada para este último, está lejos de ser la necesidad más común, y cuando necesita implementar envoltorios genéricos (que he encontrado que son increíblemente comunes, hasta el punto en que comencé a escribir una derivación personalizada para eso), aún necesita implementarlos todos porque el wrappee podría estar personalizándolos.

OTOH, si un implementador del rasgo Alloc intenta hacer cosas elegantes en, por ejemplo, alloc_one , no se garantiza que se llamará dealloc_one para esa asignación. Hay multiples razones para esto:

  • Los ayudantes no se utilizan de forma coherente. Solo un ejemplo, raw_vec usa una combinación de alloc_array , alloc / alloc_zeroed , pero solo usa dealloc .
  • Incluso con el uso constante de, por ejemplo, alloc_array / dealloc_array , aún se puede convertir de forma segura un Vec en un Box , que luego usaría dealloc .
  • Luego, hay algunas partes de la API que simplemente no existen (no hay una versión cero de alloc_one / alloc_array )

Entonces, aunque existen casos de uso reales para la especialización de, por ejemplo, alloc_one (y de hecho, tengo una gran necesidad de mozjemalloc), es mejor usar un asignador especializado en su lugar.

En realidad, es peor que eso, en el repositorio de óxido, hay exactamente un uso de alloc_array , y no se usa alloc_one , dealloc_one , realloc_array , dealloc_array . Ni siquiera la sintaxis de caja usa alloc_one , usa exchange_malloc , que toma size y align . Por lo tanto, esas funciones están pensadas más como una conveniencia para los clientes que para los implementadores.

Con algo como impl<A: Alloc> AllocHelpers for A (o AllocExt , sea cual sea el nombre que se elija), todavía tendríamos la conveniencia de esas funciones para los clientes, sin permitir que los implementadores se disparen en el pie si pensaran harían cosas sofisticadas al anularlas (y facilitar a las personas que implementan asignadores de proxy).

Hay un cambio en la API por el que estoy considerando enviar un PR

Lo hizo en # 50436

@glandio

(y de hecho, tengo tanta necesidad de mozjemalloc),

¿Podrías desarrollar este caso de uso?

mozjemalloc tiene un asignador de base que filtra deliberadamente. Excepto por un tipo de objetos, donde mantiene una lista libre. Puedo hacer eso colocando asignadores en capas en lugar de hacer trucos con alloc_one .

¿Es necesario desasignar con la alineación exacta con la que asignó?

Solo para reforzar que la respuesta a esta pregunta es , tengo esta hermosa cita de los propios Microsoft :

align_alloc () probablemente nunca se implementará, ya que C11 lo especificó de una manera que es incompatible con nuestra implementación (es decir, que free () debe poder manejar asignaciones altamente alineadas)

El uso del asignador del sistema en Windows siempre requerirá conocer la alineación al desasignar para desasignar correctamente asignaciones altamente alineadas, así que, ¿podemos marcar esa pregunta como resuelta?

El uso del asignador del sistema en Windows siempre requerirá conocer la alineación al desasignar para desasignar correctamente asignaciones altamente alineadas, así que, ¿podemos marcar esa pregunta como resuelta?

Es una pena, pero así es. Entonces, renunciemos a los vectores sobreignados. :confuso:

Renunciemos a los vectores sobrealineados entonces

¿Cómo? Solo necesita Vec<T, OverAlignedAlloc<U16>> que asigne y desasigne con sobrealineación.

¿Cómo? Solo necesita Vec<T, OverAlignedAlloc<U16>> que asigne y desasigne con sobrealineación.

Debí haber sido más especifico. Me refería a mover vectores sobrealineados a una API fuera de su control, es decir, una que toma Vec<T> y no Vec<T, OverAlignedAlloc<U16>> . (Por ejemplo CString::new() .)

Deberías usar

#[repr(align(16))]
struct OverAligned16<T>(T);

y luego Vec<OverAligned16<T>> .

Deberías usar

Eso depende. Suponga que desea utilizar intrínsecos AVX (requisito de alineación de 32 bytes de ancho de 256 bits) en un vector de f32 s:

  • Vec<T, OverAlignedAlloc<U32>> resuelve el problema, se pueden usar intrínsecos AVX directamente en los elementos del vector (en particular, cargas de memoria alineadas), y el vector aún se desarma en un segmento &[f32] lo que lo hace ergonómico de usar.
  • Vec<OverAligned32<f32>> no resuelve realmente el problema. Cada f32 ocupa 32 bytes de espacio debido al requisito de alineación. El relleno introducido evita el uso directo de operaciones AVX ya que los f32 s ya no están en la memoria continua. Y personalmente encuentro el derecho a &[OverAligned32<f32>] un poco tedioso de tratar.

Para un solo elemento en un Box , Box<T, OverAligned<U32>> vs Box<OverAligned32<T>> , ambos enfoques son más equivalentes, y el segundo enfoque podría ser preferible. En cualquier caso, es bueno tener ambas opciones.

Publicado este wrt cambios en el rasgo de Alloc: https://internals.rust-lang.org/t/pre-rfc-changing-the-alloc-trait/7487

La publicación de seguimiento en la parte superior de este número está horriblemente desactualizada (se editó por última vez en 2016). Necesitamos una lista actualizada de preocupaciones activas para continuar la discusión de manera productiva.

La discusión también se beneficiaría significativamente de un documento de diseño actualizado, que contenga las preguntas sin resolver actuales y la justificación de las decisiones de diseño.

Hay varios hilos de diferencias desde "lo que se implementa actualmente en la noche" hasta "lo que se propuso en el RFC de Alloc original" que generan miles de comentarios en diferentes canales (repositorio rfc, problema de seguimiento de rust-lang, RFC de asignación global, publicaciones internas, muchos enormes PR, etc.), y lo que se está estabilizando en el RFC GlobalAlloc no se parece mucho a lo que se propuso en el RFC original.

Esto es algo que necesitamos de todos modos para terminar de actualizar los documentos y la referencia, y también sería útil en las discusiones actuales.

Creo que antes de siquiera pensar en estabilizar el rasgo Alloc , primero deberíamos intentar implementar el soporte del asignador en todas las colecciones de bibliotecas estándar. Esto debería darnos algo de experiencia sobre cómo se utilizará este rasgo en la práctica.

Creo que antes de siquiera pensar en estabilizar el rasgo Alloc , primero deberíamos intentar implementar el soporte del asignador en todas las colecciones de bibliotecas estándar. Esto debería darnos algo de experiencia sobre cómo se utilizará este rasgo en la práctica.

Si, absolutamente. Especialmente Box , ya que aún no sabemos cómo evitar que Box<T, A> ocupe dos palabras.

Si, absolutamente. Especialmente Box, ya que todavía no sabemos cómo evitar tener Boxtoma dos palabras.

No creo que debamos preocuparnos por el tamaño de Box<T, A> para la implementación inicial, pero esto es algo que se puede agregar más adelante de una manera compatible con versiones anteriores agregando un rasgo DeAlloc que solo admite desasignación.

Ejemplo:

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

Creo que antes de siquiera pensar en estabilizar el rasgo de Alloc, primero deberíamos intentar implementar el soporte del asignador en todas las colecciones de bibliotecas estándar. Esto debería darnos algo de experiencia sobre cómo se utilizará este rasgo en la práctica.

Creo que @ Ericson2314 ha estado trabajando en esto, según https://github.com/rust-lang/rust/issues/42774. Sería bueno recibir una actualización de él.

No creo que debamos preocuparnos por el tamaño de Box<T, A> para la implementación inicial, pero esto es algo que se puede agregar más adelante de una manera compatible con versiones anteriores agregando un rasgo DeAlloc que solo admite desasignación.

Ese es un enfoque, pero no me queda claro si definitivamente es el mejor. Tiene las distintas desventajas, por ejemplo, que a) solo funciona cuando un puntero -> búsqueda de asignador es posible (esto no es cierto para, por ejemplo, la mayoría de los asignadores de arena) y, b) agrega una sobrecarga significativa a dealloc (es decir, para realizar la búsqueda inversa). Puede terminar siendo el caso de que la mejor solución a este problema sea un efecto de propósito más general o un sistema de contexto como esta propuesta o esta propuesta . O quizás algo completamente diferente. Así que no creo que debamos asumir que esto será fácil de resolver de una manera que sea compatible con versiones anteriores con la encarnación actual del rasgo Alloc .

@joshlf Teniendo en cuenta el hecho de que Box<T, A> solo tiene acceso a sí mismo cuando se elimina, esto es lo mejor que podemos hacer solo con código seguro. Tal patrón podría ser útil para asignadores de tipo arena que tienen un dealloc sin operación y solo memoria libre cuando se elimina el asignador.

Para sistemas más complicados donde el asignador es propiedad de un contenedor (por ejemplo, LinkedList ) y administra múltiples asignaciones, espero que Box no se use internamente. En su lugar, los internos de LinkedList usarán punteros sin procesar que se asignan y liberan con la instancia Alloc que está contenida en el objeto LinkedList . Esto evitará duplicar el tamaño de cada puntero.

Teniendo en cuenta el hecho de que Box<T, A> solo tiene acceso a sí mismo cuando se elimina, esto es lo mejor que podemos hacer solo con código seguro. Tal patrón podría ser útil para asignadores de tipo arena que tienen un dealloc sin operación y solo memoria libre cuando se elimina el asignador.

Correcto, pero Box no sabe que dealloc no es operativo.

Para sistemas más complicados donde el asignador es propiedad de un contenedor (por ejemplo, LinkedList ) y administra varias asignaciones, espero que Box no se use internamente. En su lugar, los internos de LinkedList usarán punteros sin procesar que se asignan y liberan con la instancia Alloc que está contenida en el objeto LinkedList . Esto evitará duplicar el tamaño de cada puntero.

Creo que sería realmente una lástima exigir a las personas que utilicen un código inseguro para escribir colecciones. Si el objetivo es hacer que todas las colecciones (presumiblemente incluidas aquellas fuera de la biblioteca estándar) sean opcionalmente paramétricas en un asignador, y Box no es asignador-paramétrico, entonces un autor de colecciones no debe usar Box en absoluto o use código inseguro (y tenga en cuenta que recordar siempre las cosas gratis es uno de los tipos más comunes de inseguridad de la memoria en C y C ++, por lo que es difícil de corregir el código inseguro). Eso parece un trato desafortunado.

Correcto, pero Box no sabe que dealloc no es una operación.

¿Por qué no adaptar lo que hace C ++ unique_ptr ?
Es decir: almacenar el puntero al asignador si tiene "estado" y no almacenarlo si el asignador es "sin estado"
(por ejemplo, envoltorio global alrededor de malloc o mmap ).
Esto requeriría dividir el seguimiento actual Alloc en dos rasgos: StatefulAlloc y StatelessAlloc .
Me doy cuenta de que es un tema muy grosero y poco elegante (y probablemente alguien ya lo haya propuesto en discusiones anteriores).
A pesar de su falta de elegancia, esta solución es simple y compatible con versiones anteriores (sin penalizaciones de rendimiento).

Creo que sería realmente una lástima exigir a las personas que utilicen un código inseguro para escribir colecciones. Si el objetivo es hacer que todas las colecciones (presumiblemente incluidas aquellas fuera de la biblioteca estándar) sean opcionalmente paramétricas en un asignador, y Box no es un asignador paramétrico, entonces un autor de colecciones no debe usar Box en absoluto o usar código inseguro (y Tenga en cuenta que recordar siempre liberar cosas es uno de los tipos más comunes de inseguridad de la memoria en C y C ++, por lo que es difícil de corregir el código inseguro en eso). Eso parece un trato desafortunado.

Me temo que una implementación de un sistema de efecto o contexto que podría permitirle a uno escribir contenedores basados ​​en nodos como listas, árboles, etc. de manera segura podría llevar demasiado tiempo (si es posible en principio).
No vi ningún artículo o lenguajes académicos que aborden este problema (por favor, corríjanme si tales trabajos realmente existen).

Entonces, recurrir a unsafe en la implementación de contenedores basados ​​en nodos podría ser un mal necesario, al menos en una perspectiva a corto plazo.

@eucpp Tenga en cuenta que unique_ptr no almacena un asignador; almacena un Deleter :

El eliminador debe ser FunctionObject o una referencia lvalue a un objeto FunctionObject o una referencia lvalue a una función, invocable con un argumento de tipo unique_ptr:: puntero`

Veo esto como aproximadamente equivalente a que proporcionemos rasgos divididos Alloc y Dealloc .

@cramertj Sí, tienes razón. Aún así, se requieren dos rasgos: con estado y sin estado Dealloc .

¿No sería suficiente un ZST Dealloc?

El martes 12 de junio de 2018 a las 3:08 p.m. Evgeniy Moiseenko [email protected]
escribió:

@cramertj https://github.com/cramertj Sí, tienes razón. Aún así, dos
Se requieren rasgos: Dealloc con estado y sin estado.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-396716689 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AEAJtWkpF0ofVc18NwbfV45G4QY6SCFBks5t8B_AgaJpZM4IDYUN
.

¿No sería suficiente un ZST Dealloc?

@remexre supongo que sí :)

No sabía que el compilador rust es compatible con ZST de fábrica.
En C ++, requeriría al menos algunos trucos sobre la optimización de la base vacía.
Soy bastante nuevo en Rust, lo siento por algunos errores obvios.

No creo que necesitemos rasgos separados para stateful vs stateless.

Con Box aumentado con un parámetro de tipo A , contendría un valor de A directamente, no una referencia o puntero a A . Ese tipo puede ser de tamaño cero para un (des) asignador sin estado. O A sí mismo puede ser algo así como una referencia o identificador a un asignador con estado que se puede compartir entre múltiples objetos asignados. Entonces, en lugar de impl Alloc for MyAllocator , es posible que desee hacer algo como impl<'r> Alloc for &'r MyAllocator

Por cierto, un Box que solo sepa cómo desasignar y no cómo asignar no implementaría Clone .

@SimonSapin Esperaría que Clone ing requiriera especificar un asignador nuevamente, de la misma manera que crearía un nuevo Box (es decir, no se haría usando el Clone rasgo).

@cramertj ¿No sería inconsistente comparado con Vec y otros contenedores que implementan Clone ?
¿Cuáles son las desventajas de almacenar una instancia de Alloc dentro de Box lugar de Dealloc ?
Entonces Box podría implementar Clone así como clone_with_alloc .

No creo que los rasgos divididos realmente afecten a Clone de una manera enorme: el impl se vería como impl<T, A> Clone for Box<T, A> where A: Alloc + Dealloc + Clone { ... } .

@sfackler No me opondría a ese impl, pero también esperaría tener un clone_into o algo que use un asignador proporcionado.

¿Tendría sentido utilizar un método alloc_copy con Alloc ? Esto podría usarse para proporcionar implementaciones de memcpy ( Copy/Clone ) más rápidas para asignaciones grandes, por ejemplo, haciendo clones de páginas de copia en escritura.

Sería genial y trivial proporcionar una implementación predeterminada para.

¿Qué estaría usando una función alloc_copy ? impl Clone for Box<T, A> ?

Sí, lo mismo ocurre con Vec .

Habiéndolo investigado un poco más, parece que los enfoques para crear páginas de copia sobre escritura dentro del mismo proceso oscilan entre hacky e imposible, al menos si quieres hacerlo a más de un nivel de profundidad. Entonces alloc_copy no sería un gran beneficio.

En cambio, una trampilla de escape más general que permita futuras travesuras de memoria virtual podría ser de alguna utilidad. Es decir, si una asignación es grande, respaldada por mmap de todos modos y sin estado, entonces el asignador podría prometer no ser consciente de los cambios futuros en la asignación. El usuario podría entonces mover esa memoria a una tubería, desasignarla o cosas similares.
Alternativamente, podría haber un asignador tonto de mmap-all-the-things y una función try-transfer.

En cambio, una trampilla de escape más general que permite la memoria virtual futura

Los asignadores de memoria (malloc, jemalloc, ...) generalmente no te permiten robarles ningún tipo de memoria, y generalmente no te permiten consultar o cambiar cuáles son las propiedades de la memoria que poseen. Entonces, ¿qué tiene que ver esta trampilla de escape general con los asignadores de memoria?

Además, el soporte de memoria virtual difiere enormemente entre plataformas, tanto que el uso efectivo de la memoria virtual a menudo requiere diferentes algoritmos por plataforma, a menudo con garantías completamente diferentes. He visto algunas abstracciones portátiles sobre la memoria virtual, pero todavía no he visto una que no esté tan estropeada hasta el punto de ser inútil en algunas situaciones debido a su "portabilidad".

Tienes razón. Cualquier caso de uso de este tipo (estaba pensando principalmente en optimizaciones específicas de la plataforma) probablemente se sirva mejor si se usa un asignador personalizado en primer lugar.

¿Alguna idea sobre la API Composable Allocator descrita por Andrei Alexandrescu en su presentación de CppCon? El video está disponible en YouTube aquí: https://www.youtube.com/watch?v=LIb3L4vKZ7U (comienza a describir su diseño propuesto alrededor de las 26:00, pero la charla es lo suficientemente entretenida, tal vez prefiera verla completa) .

Parece que la conclusión inevitable de todo esto es que las bibliotecas de colecciones deben ser una asignación WRT genérica, y el programador de la aplicación debe poder componer asignadores y colecciones libremente en el sitio de construcción.

¿Alguna idea sobre la API Composable Allocator descrita por Andrei Alexandrescu en su presentación de CppCon?

La API actual Alloc permite escribir asignadores componibles (por ejemplo, MyAlloc<Other: Alloc> ) y puede usar rasgos y especialización para lograr prácticamente todo lo que se logró en la charla de Andreis. Sin embargo, más allá de la "idea" de que uno debería poder hacer eso, casi nada de la charla de Andrei se puede aplicar a Rust, ya que la forma en que Andrei construye la API se basa en genéricos sin restricciones + SFINAE / static si desde el principio y en el sistema genérico de Rust. es completamente diferente a ese.

Me gustaría proponer estabilizar el resto de los métodos Layout . Estos ya son útiles con la API de asignación global actual.

¿Son estos todos los métodos a los que te refieres?

  • 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 Sí.

@Amanieu Me

de asignadores y vidas :

  1. (para las implicaciones del asignador): mover un valor de asignador no debe invalidar sus bloques de memoria pendientes.

    Todos los clientes pueden asumir esto en su código.

    Entonces, si un cliente asigna un bloque de un asignador (llámelo a1) y luego a1 se mueve a un nuevo lugar (por ejemplo, vialet a2 = a1;), entonces sigue siendo correcto que el cliente desasigne ese bloque a través de a2.

¿Esto implica que un Asignador debe ser Unpin ?

¡Buena atrapada!

Dado que el rasgo Alloc aún es inestable, creo que aún podemos cambiar las reglas si queremos y modificar esta parte del RFC. Pero de hecho es algo a tener en cuenta.

@gnzlbg Sí, soy consciente de las grandes diferencias en los sistemas genéricos, y que no todo lo que él detalla se puede implementar de la misma manera en Rust. Sin embargo, he estado trabajando en la biblioteca de forma intermitente desde que publiqué, y estoy progresando bastante.

¿Esto implica que un Asignador _debe_ ser Unpin ?

No es así. Unpin trata sobre el comportamiento de un tipo cuando está envuelto en un Pin , no hay una conexión particular con esta API.

Pero, ¿no se puede usar Unpin para hacer cumplir la restricción mencionada?

Otra pregunta con respecto a dealloc_array : ¿Por qué la función devuelve Result ? En la implementación actual, esto puede fallar en dos casos:

  • n es cero
  • desbordamiento de capacidad por n * size_of::<T>()

Para el primero tenemos dos casos (como en la documentación, el implementador puede elegir entre estos):

  • La asignación devuelve Ok en cero n => dealloc_array también debería devolver Ok .
  • La asignación devuelve Err en cero n => no hay puntero que pueda pasarse a dealloc_array .

El segundo está garantizado por la siguiente restricción de seguridad:

el diseño de [T; n] debe ajustarse a ese bloque de memoria.

Esto significa que debemos llamar a dealloc_array con el mismo n que en la asignación. Si se pudiera asignar una matriz con elementos n , n es válido para T . De lo contrario, la asignación habría fallado.

Editar: Con respecto al último punto: incluso si usable_size devuelve un valor superior a n * size_of::<T>() , esto sigue siendo válido. De lo contrario, la implementación viola esta restricción de rasgo:

El tamaño del bloque debe estar en el rango [use_min, use_max] , donde:

  • [...]
  • use_max es la capacidad que fue (o habría sido) devuelta cuando (si) el bloque se asignó mediante una llamada a alloc_excess o realloc_excess .

Esto solo es válido, ya que el rasgo requiere unsafe impl .

Para el primero tenemos dos casos (como en la documentación, el implementador puede elegir entre estos):

  • La asignación devuelve Ok en cero n

De dónde obtuviste esta información?

Todos los métodos Alloc::alloc_ en los documentos especifican que el comportamiento de las asignaciones de tamaño cero no está definido bajo su cláusula "Seguridad".

Documentos de core::alloc::Alloc (partes relevantes resaltadas):

Una nota con respecto a los tipos de tamaño cero y los diseños de tamaño cero: muchos métodos en el rasgo Alloc establecen que las solicitudes de asignación deben tener un tamaño distinto de cero, de lo contrario puede resultar un comportamiento indefinido.

  • Sin embargo, algunos métodos de asignación de nivel superior ( alloc_one , alloc_array ) están bien definidos en tipos de tamaño cero y, opcionalmente, pueden admitirlos : se deja en manos del implementador o si se debe devolver Err , o para devolver Ok con algún puntero.
  • Si una implementación Alloc elige devolver Ok en este caso (es decir, el puntero denota un bloque inaccesible de tamaño cero), entonces ese puntero devuelto debe considerarse "asignado actualmente". En tal asignador, todos los métodos que toman punteros asignados actualmente como entradas deben aceptar estos punteros de tamaño cero, sin causar un comportamiento indefinido.

  • En otras palabras, si un puntero de tamaño cero puede fluir fuera de un asignador, entonces ese asignador debe aceptar igualmente ese puntero que fluye de regreso a sus métodos de desasignación y reasignación .

Entonces, una de las condiciones de error de dealloc_array es definitivamente sospechosa:

/// # 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.

Si [T; N] no cumple con las restricciones de alineación o tamaño del asignador, entonces AFAICT no encaja en el bloque de memoria de la asignación y el comportamiento no está definido (según la cláusula de seguridad).

La otra condición de error es "Siempre devuelve Err en caso de desbordamiento aritmético". que es bastante genérico. Es difícil saber si se trata de una condición de error útil. Para cada implementación de rasgo de Alloc , uno podría encontrar uno diferente que podría hacer algo de aritmética que, en teoría, podría ajustarse, así que 🤷‍♂️


Documentos de core::alloc::Alloc (partes relevantes resaltadas):

En efecto. Me parece extraño que tantos métodos (por ejemplo, Alloc::alloc ) indiquen que las asignaciones de tamaño cero son un comportamiento indefinido, pero luego solo proporcionamos Alloc::alloc_array(0) con un comportamiento definido por la implementación. En cierto sentido, Alloc::alloc_array(0) es una prueba de fuego para comprobar si un asignador admite asignaciones de tamaño cero o no.

Si [T; N] no cumple con el tamaño del asignador o las restricciones de alineación, entonces AFAICT no se ajusta al bloque de memoria de la asignación y el comportamiento no está definido (según la cláusula de seguridad).

Sí, creo que esta condición de error se puede descartar porque es redundante. Necesitamos la cláusula de seguridad o una condición de error, pero no ambas.

La otra condición de error es "Siempre devuelve Err en caso de desbordamiento aritmético". que es bastante genérico. Es difícil saber si se trata de una condición de error útil.

OMI, está protegido por la misma cláusula de seguridad anterior; si la capacidad de [T; N] se desborda, no cabe en ese bloque de memoria para desasignar. ¿Quizás @pnkfelix podría dar más detalles sobre esto?

En cierto sentido, Alloc::alloc_array(1) es una prueba de fuego para comprobar si un asignador admite asignaciones de tamaño cero o no.

¿Quiso decir Alloc::alloc_array(0) ?

OMI, está protegido por la misma cláusula de seguridad anterior; si la capacidad de [T; N] se desbordara, no _ajusta_ ese bloque de memoria para desasignar.

Tenga en cuenta que los usuarios pueden implementar este rasgo para sus propios asignadores personalizados y que estos usuarios pueden anular las implementaciones predeterminadas de estos métodos. Entonces, al considerar si esto debería devolver Err para el desbordamiento aritmético o no, uno no solo debe enfocarse en lo que hace la implementación predeterminada actual del método predeterminado, sino también considerar lo que podría tener sentido para los usuarios que implementan estos para otros asignadores.

¿Quiso decir Alloc::alloc_array(0) ?

Si, lo siento.

Tenga en cuenta que los usuarios pueden implementar este rasgo para sus propios asignadores personalizados y que estos usuarios pueden anular las implementaciones predeterminadas de estos métodos. Entonces, al considerar si esto debería devolver Err para el desbordamiento aritmético o no, uno no solo debe centrarse en lo que hace la implementación predeterminada actual del método predeterminado, sino también considerar lo que podría tener sentido para los usuarios que implementan estos para otros asignadores.

Ya veo, pero implementar Alloc requiere unsafe impl y los implementadores deben seguir las reglas de seguridad mencionadas en https://github.com/rust-lang/rust/issues/32838#issuecomment -467093527 .

Cada API que queda apuntando aquí para un problema de seguimiento es el rasgo Alloc o está relacionada con el rasgo Alloc . @ rust-lang / libs, ¿cree que es útil mantener esto abierto además de https://github.com/rust-lang/rust/issues/42774?

Pregunta de antecedentes simple: ¿Cuál es la motivación detrás de la flexibilidad con ZST? Me parece que, dado que sabemos en tiempo de compilación que un tipo es un ZST, podemos optimizar completamente tanto la asignación (para devolver un valor constante) como la desasignación. Teniendo en cuenta eso, me parece que deberíamos decir una de las siguientes:

  • Siempre depende del implementador admitir las ZST, y no pueden devolver Err por las ZST
  • Siempre es UB asignar ZST, y es responsabilidad de la persona que llama hacer un cortocircuito en este caso
  • Hay una especie de método alloc_inner que implementan las personas que llaman, y un método alloc con una implementación predeterminada que hace el cortocircuito; alloc debe admitir ZST, pero alloc_inner NO se puede llamar para un ZST (esto es solo para que podamos agregar la lógica de cortocircuito en un solo lugar, en la definición del rasgo, en orden para ahorrar a los implementadores algo de repetición)

¿Hay alguna razón por la que se necesite la flexibilidad que tenemos con la API actual?

¿Hay alguna razón por la que se necesite la flexibilidad que tenemos con la API actual?

Es una compensación. Podría decirse que el rasgo de Alloc se usa con más frecuencia de lo que se implementa, por lo que podría tener sentido hacer que el uso de Alloc sea lo más fácil posible proporcionando soporte integrado para ZST.

Esto significaría que los implementadores del rasgo Alloc necesitarán encargarse de esto, pero lo más importante para mí es que aquellos que intentan desarrollar el rasgo Alloc deberán tener en cuenta las ZST en cada cambio de API. También complica los documentos de la API al explicar cómo se manejan las ZST (o podría serlo si está "definido por la implementación").

Los asignadores de C ++ siguen este enfoque, donde el asignador intenta resolver muchos problemas diferentes. Esto no solo los hizo más difíciles de implementar y más difíciles de evolucionar, sino también más difíciles de usar para los usuarios debido a la forma en que todos estos problemas interactúan en la API.

Creo que el manejo de ZST y la asignación / desasignación de memoria bruta son dos problemas ortogonales y diferentes y, por lo tanto, deberíamos mantener la API de características de Alloc simple simplemente sin manejarlos.

Los usuarios de Alloc como libstd necesitarán manejar ZST, por ejemplo, en cada colección. Definitivamente es un problema que vale la pena resolver, pero no creo que el rasgo de Alloc sea el lugar para eso. Esperaría que una utilidad para resolver este problema apareciera dentro de libstd por necesidad, y cuando eso suceda, tal vez podamos intentar RFC tal utilidad y exponerla en std :: heap.

Todo eso suena razonable.

Creo que el manejo de ZST y la asignación / desasignación de memoria bruta son dos problemas ortogonales y diferentes y, por lo tanto, deberíamos mantener la API de características de Alloc simple simplemente sin manejarlos.

¿No implica eso que deberíamos hacer que la API no maneje explícitamente las ZST en lugar de estar definida por la implementación? En mi opinión, un error "no admitido" no es muy útil en tiempo de ejecución, ya que la gran mayoría de las personas que llaman no podrán definir una ruta de respaldo y, por lo tanto, deberán asumir que los ZST no son compatibles de todos modos. Parece más limpio simplemente simplificar la API y declarar que _nunca_ son compatibles.

¿Los usuarios de alloc utilizarían la especialización para manejar ZST? ¿O solo if size_of::<T>() == 0 cheques?

¿Los usuarios de alloc utilizarían la especialización para manejar ZST? ¿O solo if size_of::<T>() == 0 cheques?

Esto último debería ser suficiente; las rutas de código apropiadas se eliminarían trivialmente en tiempo de compilación.

¿No implica eso que deberíamos hacer que la API no maneje explícitamente las ZST en lugar de estar definida por la implementación?

Para mí, una restricción importante es que si prohibimos las asignaciones de tamaño cero, los métodos Alloc deberían poder asumir que el Layout que se les pasa no es de tamaño cero.

Hay varias formas de lograrlo. Una sería agregar otra cláusula Safety a todos los métodos Alloc indicando que si Layout es de tamaño cero, el comportamiento no está definido.

Alternativamente, podríamos prohibir Layout s de tamaño cero, luego Alloc no necesita decir nada sobre las asignaciones de tamaño cero, ya que no pueden suceder de manera segura, pero hacerlo tendría algunas desventajas.

Por ejemplo, algunos tipos como HashMap construyen el Layout partir de múltiples Layout s, y aunque el Layout final puede no ser de tamaño cero, el los intermedios pueden ser (por ejemplo, en HashSet ). Por lo tanto, estos tipos necesitarían usar "algo más" (por ejemplo, un tipo LayoutBuilder ) para acumular sus Layout s finales y pagar un cheque de "tamaño distinto de cero" (o usar un método _unchecked ) al convertir a Layout .

¿Los usuarios de alloc utilizarían la especialización para manejar ZST? O simplemente si size_of ::() == 0 cheques?

Todavía no podemos especializarnos en ZST. En este momento, todo el código usa size_of::<T>() == 0 .

Hay varias formas de lograrlo. Una sería agregar otra cláusula Safety a todos los métodos Alloc indicando que si Layout es de tamaño cero, el comportamiento no está definido.

Sería interesante considerar si hay formas de hacer que esto sea una garantía de tiempo de compilación, pero incluso un debug_assert que el diseño no es de tamaño cero debería ser suficiente para detectar el 99% de los errores.

No he prestado atención a las discusiones sobre asignadores, lo siento mucho. Pero durante mucho tiempo deseé que el asignador tuviera acceso al tipo de valor que asigna. Puede haber diseños de asignadores que podrían usarlo.

Entonces probablemente tengamos los mismos problemas que C ++ y su api de asignación.

Pero durante mucho tiempo deseé que el asignador tuviera acceso al tipo de valor que asigna. T

¿Para qué lo necesitas?

@gnzblg @brson Hoy tuve un caso de uso potencial para saber _algo_ sobre el tipo de valor que se asigna.

Estoy trabajando en un asignador global que se puede cambiar entre tres asignadores subyacentes: uno local de subprocesos, uno global con bloqueos y uno para que las utilicen las corrutinas, con la idea de que puedo restringir una corrutina que representa una conexión de red al máximo cantidad de uso de memoria dinámica (en ausencia de poder controlar asignadores en colecciones, especialmente en código de terceros) *.

Posiblemente sería útil saber si estoy asignando un valor que podría moverse entre subprocesos (por ejemplo, Arc) frente a uno que no lo hará. O puede que no. Pero es un escenario posible. Por el momento, el asignador global tiene un conmutador que el usuario usa para decirle desde qué asignador hacer las asignaciones (no es necesario para reasignar o liberar; solo podemos mirar la dirección de memoria para eso).

* [También me permite usar la memoria local NUMA siempre que sea posible sin ningún bloqueo y, con un modelo de 1 núcleo y 1 hilo, para limitar el uso total de memoria].

@raphaelcohn

Estoy trabajando en un asignador global

No creo que nada de esto se aplicaría (o podría) aplicarse al rasgo GlobalAlloc , y el rasgo Alloc ya tiene métodos genéricos que pueden hacer uso de información de tipo (p alloc_array<T>(1) Ej T , donde T es el tipo real, por lo que el asignador puede tener en cuenta el tipo mientras realiza la asignación). Creo que sería más útil para los propósitos de esta discusión ver realmente código implementando asignadores que hacen uso de información de tipo. No he escuchado ningún buen argumento sobre por qué estos métodos deben ser parte de algún rasgo genérico del asignador, en lugar de ser solo parte de la API del asignador, o algún otro rasgo del asignador.

Creo que también sería muy interesante saber cuál de los tipos parametrizados por Alloc pretendes combinar con asignadores que utilizan información de tipo y cuál esperas que sea el resultado.

AFAICT, el único tipo interesante para eso sería Box porque asigna un T directamente. Casi todos los demás tipos en std nunca asignan un T , sino algún tipo interno privado del que su asignador no puede saber nada. Por ejemplo, Rc y Arc podrían asignar (InternalRefCounts, T) , List / BTreeSet / etc.asignar tipos de nodos internos, Vec / Deque / ... asignar matrices de T s, pero no T s ellos mismos, etc.

Por Box y Vec podríamos agregar en formas compatibles con versiones anteriores un rasgo BoxAlloc y un ArrayAlloc con implicaciones generales por Alloc que los asignadores podrían especialícese en secuestrar cómo se comportan, si alguna vez es necesario atacar estos problemas de una manera genérica. Pero, ¿hay alguna razón por la que proporcionar sus propios tipos MyAllocBox y MyAllocVec que conspiren con su asignador para explotar la información del tipo no sea una solución viable?

Como ahora tenemos un repositorio dedicado para el grupo de trabajo de asignadores , y la lista en el OP está desactualizada, este problema puede cerrarse para mantener las discusiones y el seguimiento de esta función en un solo lugar.

¡Buen punto @TimDiekmann! Voy a seguir adelante y cerrar esto a favor de los hilos de discusión en ese repositorio.

Este sigue siendo el problema de seguimiento al que apuntan algunos atributos #[unstable] . Creo que no debería cerrarse hasta que estas funciones se hayan estabilizado o desaprobado. (O podríamos cambiar los atributos para señalar un problema diferente).

Sí, las características inestables a las que se hace referencia en git master definitivamente deberían tener un problema de seguimiento abierto.

Convenido. También se agregó un aviso y un enlace al OP.

¿Fue útil esta página
0 / 5 - 0 calificaciones