Rust: Caractéristiques d'allocateur et std :: heap

Créé le 8 avr. 2016  ·  412Commentaires  ·  Source: rust-lang/rust

📢 Cette fonctionnalité dispose d'un groupe de travail dédié , veuillez adresser vos commentaires et vos préoccupations au repo du groupe de travail .

Message original:


Proposition FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336957415
Cases à cocher FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336980230


Problème de suivi pour rust-lang / rfcs # 1398 et le module std::heap .

  • [x] land struct Layout , trait Allocator et implémentations par défaut dans alloc crate (https://github.com/rust-lang/rust/pull/42313)
  • [x] décider où les pièces doivent vivre (par exemple impls par défaut a une dépendance sur alloc crate, mais Layout / Allocator _could_ être dans libcore ...) (https://github.com/rust-lang/rust/pull/42313)
  • [] fixme from source code: auditer les implémentations par défaut (en Layout pour les erreurs de dépassement de capacité, (passer potentiellement à overflowing_add et overflowing_mul si nécessaire).
  • [x] décider si realloc_in_place doit être remplacé par grow_in_place et shrink_in_place ( commentaire ) (https://github.com/rust-lang/rust/pull/42313)
  • [] examiner les arguments pour / contre le type d'erreur associé (voir la sous-discussion ici )
  • [] déterminez quelles sont les exigences relatives à l'alignement fourni à fn dealloc . (Voir la discussion sur l' allocateur rfc et l'allocateur global rfc et le trait Alloc PR .)

    • Est-il nécessaire de désallouer avec le align exact avec lequel vous allouez? Des inquiétudes ont été soulevées quant au fait que les allocateurs comme jemalloc n'en ont pas besoin, et il est difficile d'envisager un allocateur qui l'exige. ( plus de discussion ). @ruuda et @rkruppe semblent avoir le plus réfléchi à ce sujet à ce jour.

  • [] Est-ce que AllocErr devrait être Error place? ( commentaire )
  • [x] Est-il nécessaire de désallouer avec la taille exacte avec laquelle vous allouez? Avec l'entreprise usable_size , nous pouvons souhaiter autoriser, par exemple, que vous, si vous allouez avec (size, align) vous devez désallouer avec une taille quelque part dans la plage de size...usable_size(size, align) . Il semble que jemalloc soit totalement d'accord avec cela (ne vous oblige pas à désallouer avec un size précis avec lequel vous allouez) et cela permettrait également à Vec de profiter naturellement de la capacité excédentaire jemalloc le donne quand il fait une allocation. (bien que cela soit également quelque peu orthogonal à cette décision, nous autorisons simplement Vec ). Jusqu'à présent, @Gankro a la plupart des réflexions à ce sujet. ( @alexcrichton pense que cela a été réglé sur https://github.com/rust-lang/rust/pull/42313 en raison de la définition de «correspondances»)
  • [] similaire à la question précédente: est-il nécessaire de désallouer avec l'alignement exact avec lequel vous avez alloué? (Voir commentaire du 5 juin 2017 )
  • [x] OSX / alloc_system bogue sur des alignements 1 << 32 ) https://github.com/rust-lang/rust/issues/30170 # 43217
  • [] Layout devrait-il fournir une méthode fn stride(&self) ? (Voir aussi https://github.com/rust-lang/rfcs/issues/1397, https://github.com/rust-lang/rust/issues/17027)
  • [x] Allocator::owns comme méthode? https://github.com/rust-lang/rust/issues/44302

État de std::heap après https://github.com/rust-lang/rust/pull/42313 :

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

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

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

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

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

pub struct CannotReallocInPlace;

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

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

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

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

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

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

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

impl Alloc for System {
    // ...
}

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

Commentaire le plus utile

@alexcrichton La décision de passer de -> Result<*mut u8, AllocErr> à -> *mut void peut être une surprise importante pour les personnes qui ont suivi le développement original des RFC d'allocateur.

Je ne suis pas en désaccord avec les arguments que vous faites , mais il semblait néanmoins qu'un bon nombre de personnes auraient été disposées à vivre avec le "poids lourd" de Result rapport à la probabilité accrue de manquer un nul- vérifier la valeur renvoyée.

  • J'ignore les problèmes d'efficacité d'exécution imposés par l'ABI parce que, comme je suppose que nous pourrions les résoudre d'une manière ou d'une autre via des astuces de compilateur.

Y a-t-il moyen d'obtenir une visibilité accrue sur ce changement tardif à lui seul?

Une façon (par surprise): changez la signature maintenant, dans un PR seul, sur la branche principale, tandis que Allocator est toujours instable. Et puis voyez qui se plaint sur le PR (et qui célèbre!).

  • Est-ce trop sévère? Il semble que les définitions soient moins lourdes que de coupler un tel changement avec la stabilisation ...

Tous les 412 commentaires

Je n'ai malheureusement pas accordé suffisamment d'attention pour le mentionner dans la discussion RFC, mais je pense que realloc_in_place devrait être remplacé par deux fonctions, grow_in_place et shrink_in_place , pour deux les raisons:

  • Je ne peux pas penser à un cas d'utilisation unique (à moins d'implémenter realloc ou realloc_in_place ) où l'on ne sait pas si la taille de l'allocation augmente ou diminue. L'utilisation de méthodes plus spécialisées rend un peu plus clair ce qui se passe.
  • Les chemins de code pour augmenter et réduire les allocations ont tendance à être radicalement différents - la croissance implique de tester si les blocs de mémoire adjacents sont libres et de les réclamer, tandis que la réduction implique de découper des sous-blocs correctement dimensionnés et de les libérer. Alors que le coût d'une branche à l'intérieur de realloc_in_place est assez petit, l'utilisation de grow et shrink capture mieux les tâches distinctes qu'un allocateur doit effectuer.

Notez que ceux-ci peuvent être ajoutés de manière rétrocompatible à côté de realloc_in_place , mais cela limiterait les fonctions qui seraient implémentées par défaut et les autres.

Par souci de cohérence, realloc voudrait probablement aussi être divisé en grow et split , mais le seul avantage d'avoir une fonction realloc surchargeable que je connaisse est de pouvoir utiliser l'option de remappage de mmap , qui n'a pas une telle distinction.

De plus, je pense que les implémentations par défaut de realloc et realloc_in_place devraient être légèrement ajustées - au lieu de vérifier par rapport aux usable_size , realloc devrait d'abord essayer de realloc_in_place . À son tour, realloc_in_place devrait par défaut vérifier la taille utilisable et renvoyer le succès dans le cas d'un petit changement au lieu d'un échec de retour universel.

Cela facilite la production d'une implémentation haute performance de realloc : il suffit d'améliorer realloc_in_place . Cependant, la performance par défaut de realloc ne souffre pas, car la vérification par rapport au usable_size est toujours effectuée.

Un autre problème: la documentation pour fn realloc_in_place dit que si elle renvoie Ok, alors on est assuré que ptr maintenant "correspond" new_layout .

Pour moi, cela implique qu'il doit vérifier que l'alignement de l'adresse donnée correspond à toute contrainte impliquée par new_layout .

Cependant, je ne pense pas que la spécification de la fonction fn reallocate_inplace sous-jacente implique que _it_ effectuera une telle vérification.

  • De plus, il semble raisonnable que tout client plongeant dans l'utilisation de fn realloc_in_place s'assurera lui-même que les alignements fonctionnent (en pratique, je soupçonne que cela signifie que le même alignement est requis partout pour le cas d'utilisation donné ...)

Donc, l'implémentation de fn realloc_in_place devrait-elle vraiment être chargée de vérifier que l'alignement du ptr est compatible avec celui de new_layout ? Il est probablement préférable _dans ce cas_ (de cette méthode) de repousser cette exigence à l'appelant ...

@gereeter vous faites de bons points; Je les ajouterai à la liste de contrôle que j'accumule dans la description du problème.

(à ce stade, j'attends le support #[may_dangle] pour monter le train dans le canal beta afin que je puisse ensuite l'utiliser pour les collections std dans le cadre de l'intégration d'allocateur)

Je suis nouveau sur Rust, alors pardonnez-moi si cela a été discuté ailleurs.

Y a-t-il une réflexion sur la façon de prendre en charge les allocateurs spécifiques aux objets? Certains allocateurs tels que les allocateurs de slab et les magasin sont liés à un type particulier et effectuent le travail de construction de nouveaux objets, de mise en cache des objets construits qui ont été "libérés" (plutôt que de les supprimer), de retour d'objets mis en cache déjà construits déposer des objets avant de libérer la mémoire sous-jacente vers un allocateur sous-jacent si nécessaire.

Actuellement, cette proposition n'inclut rien du genre ObjectAllocator<T> , mais ce serait très utile. En particulier, je travaille sur une implémentation d'une couche de mise en cache d'objet d'allocateur de magazine (lien ci-dessus), et bien que je puisse l'avoir seulement envelopper un Allocator et faire le travail de construction et de suppression d'objets dans la mise en cache couche elle-même, ce serait génial si je pouvais également avoir cette enveloppe d'autres allocateurs d'objets (comme un allocateur de dalle) et vraiment être une couche de cache générique.

Où un type d'allocateur d'objet ou un trait s'insérerait-il dans cette proposition? Serait-il laissé pour une future RFC? Autre chose?

Je ne pense pas que cela ait encore été discuté.

Vous pouvez écrire votre propre ObjectAllocator<T> , puis faire impl<T: Allocator, U> ObjectAllocator<U> for T { .. } , de sorte que chaque allocateur régulier puisse servir d'allocateur spécifique à l'objet pour tous les objets.

Le travail futur consistera à modifier les collections pour utiliser votre trait pour leurs nœuds, au lieu de simples allocateurs (génériques) directement.

@pnkfelix

(à ce stade, j'attends le support # [may_dangle] pour monter le train dans le canal bêta afin que je puisse ensuite l'utiliser pour les collections std dans le cadre de l'intégration d'allocateur)

Je suppose que c'est arrivé?

@ Ericson2314 Oui, écrire le mien est certainement une option à des fins expérimentales, mais je pense qu'il y aurait beaucoup plus d'avantages à le standardiser en termes d'interopérabilité (par exemple, je prévois d'implémenter également un allocateur de dalle, mais ce serait bien si un utilisateur tiers de mon code pouvait utiliser l'allocateur de slab de quelqu'un _else's_ avec ma couche de mise en cache de magazine). Ma question est simplement de savoir si un trait ObjectAllocator<T> ou quelque chose comme ça vaut la peine d'être discuté. Bien qu'il semble que ce soit mieux pour un RFC différent? Je ne suis pas très familier avec les directives pour savoir combien appartient à un seul RFC et quand les choses appartiennent à des RFC séparés ...

@joshlf

Où un type d'allocateur d'objet ou un trait s'insérerait-il dans cette proposition? Serait-il laissé pour une future RFC? Autre chose?

Oui, ce serait une autre RFC.

Je ne suis pas très familier avec les directives pour savoir combien appartient à un seul RFC et quand les choses appartiennent à des RFC séparés ...

cela dépend de la portée de la RFC elle-même, qui est décidée par la personne qui l'écrit, puis la rétroaction est donnée par tout le monde.

Mais vraiment, comme il s'agit d'un problème de suivi pour cette RFC déjà acceptée, penser aux extensions et aux changements de conception n'est pas vraiment pour ce fil; vous devez en ouvrir un nouveau sur le repo RFC.

@joshlf Ah, je pensais que ObjectAllocator<T> était censé être un trait. Je voulais dire prototyper le trait pas un allocateur spécifique. Oui, ce trait mériterait sa propre RFC comme le dit @steveklabnik .


@steveklabnik ouais maintenant, la discussion serait meilleure ailleurs. Mais @joshlf soulevait également le problème de peur d'exposer une faille jusqu'alors imprévue dans la conception d'API acceptée mais non mise en œuvre. En ce sens, il correspond aux articles précédents de ce fil.

@ Ericson2314 Ouais, je pensais que c'était ce que tu voulais dire. Je pense que nous sommes sur la même longueur d'onde :)

@steveklabnik Sonne bien; Je vais fouiller avec ma propre implémentation et soumettre un RFC si cela finit par sembler une bonne idée.

@joshlf Je n'ai aucune raison pour laquelle les allocateurs personnalisés iraient dans le compilateur ou la bibliothèque standard. Une fois cette RFC arrivée, vous pouvez facilement publier votre propre caisse qui effectue une sorte d'allocation arbitraire (même un allocateur à part entière comme jemalloc pourrait être implémenté sur mesure!)

@alexreg Il ne s'agit pas d'un allocateur personnalisé particulier, mais plutôt d'un trait qui spécifie le type de tous les allocateurs qui sont paramétriques sur un type particulier. Donc, tout comme la RFC 1398 définit un trait ( Allocator ) qui est le type de tout allocateur de bas niveau, je demande un trait ( ObjectAllocator<T> ) qui est le type de tout allocateur qui peut allouer / désallouer et construire / supprimer des objets de type T .

@alexreg Voir mon point de départ sur l'utilisation des collections de bibliothèques standard avec des allocateurs spécifiques aux objets personnalisés.

Bien sûr, mais je ne suis pas sûr que cela appartienne à la bibliothèque standard. Pourrait facilement entrer dans une autre caisse, sans perte de fonctionnalité ou d'utilisation.

Le 4 janvier 2017, à 21 h 59, Joshua Liebow-Feeser [email protected] a écrit:

@alexreg https://github.com/alexreg Il ne s'agit pas d'un allocateur personnalisé particulier, mais plutôt d'un trait qui spécifie le type de tous les allocateurs qui sont paramétriques sur un type particulier. Donc, tout comme la RFC 1398 définit un trait (Allocator) qui est le type de tout allocateur de bas niveau, je pose des questions sur un trait (ObjectAllocator) qui est le type de tout allocateur qui peut allouer / désallouer et construire / supprimer des objets de type T.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, visualisez-le sur GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499064 , ou désactivez le fil https://github.com/notifications/unsubscribe-auth/ AAEF3IhyyPhFgu1EGHr_GM_Evsr0SRzIks5rPBZGgaJpZM4IDYUN .

Je pense que vous voudriez utiliser des collections de bibliothèques standard (toute valeur allouée au tas) avec un allocateur personnalisé arbitraire ; c'est-à-dire non limité aux objets spécifiques.

Le 4 janvier 2017, à 22 h 01, John Ericson [email protected] a écrit:

@alexreg https://github.com/alexreg Voir mon point initial sur l'utilisation des collections de bibliothèques standard avec des allocateurs spécifiques aux objets personnalisés.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499628 , ou désactivez le fil https://github.com/notifications/unsubscribe-auth/ AAEF3CrjYIXqcv8Aqvb4VTyPcajJozICks5rPBbOgaJpZM4IDYUN .

Bien sûr, mais je ne suis pas sûr que cela appartienne à la bibliothèque standard. Pourrait facilement entrer dans une autre caisse, sans perte de fonctionnalité ou d'utilisation.

Oui, mais vous voulez probablement que certaines fonctionnalités de bibliothèque standard reposent dessus (comme ce que @ Ericson2314 a suggéré).

Je pense que vous voudriez utiliser des collections de bibliothèques standard (toute valeur allouée au tas) avec un allocateur personnalisé arbitraire ; c'est-à-dire non limité aux objets spécifiques.

Idéalement, vous voudriez les deux - accepter l'un ou l'autre type d'allocateur. Il y a des avantages très importants à utiliser la mise en cache spécifique aux objets; par exemple, l'allocation de dalles et la mise en cache de magazine offrent des avantages de performances très importants - jetez un œil aux articles que j'ai liés ci-dessus si vous êtes curieux.

Mais le trait d'allocateur d'objet pourrait simplement être un sous-portrait du trait d'allocateur général. C'est aussi simple que ça, en ce qui me concerne. Bien sûr, certains types d'allocateurs peuvent être plus efficaces que les allocateurs à usage général, mais ni le compilateur ni le standard n'ont vraiment besoin de (ou ne devraient) savoir à ce sujet.

Le 4 janvier 2017, à 22h13, Joshua Liebow-Feeser [email protected] a écrit:

Bien sûr, mais je ne suis pas sûr que cela appartienne à la bibliothèque standard. Pourrait facilement entrer dans une autre caisse, sans perte de fonctionnalité ou d'utilisation.

Oui, mais vous voulez probablement que certaines fonctionnalités de bibliothèque standard reposent dessus (comme ce que @ Ericson2314 a suggéré https://github.com/Ericson2314 ).

Je pense que vous voudriez utiliser des collections de bibliothèques standard (toute valeur allouée au tas) avec un allocateur personnalisé arbitraire; c'est-à-dire non limité aux objets spécifiques.

Idéalement, vous voudriez les deux - accepter l'un ou l'autre type d'allocateur. Il y a des avantages très importants à utiliser la mise en cache spécifique aux objets; par exemple, l'allocation de dalles et la mise en cache de magazine offrent des avantages de performances très importants - jetez un œil aux articles que j'ai liés ci-dessus si vous êtes curieux.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270502231 , ou désactivez le fil https://github.com/notifications/unsubscribe-auth/ AAEF3L9F9r_0T5evOtt7Es92vw6gBxR9ks5rPBl9gaJpZM4IDYUN .

Mais le trait d'allocateur d'objet pourrait simplement être un sous-portrait du trait d'allocateur général. C'est aussi simple que ça, en ce qui me concerne. Bien sûr, certains types d'allocateurs peuvent être plus efficaces que les allocateurs à usage général, mais ni le compilateur ni le standard n'ont vraiment besoin de (ou ne devraient) savoir à ce sujet.

Ah, le problème est que la sémantique est différente. Allocator alloue et libère les blobs d'octets bruts. ObjectAllocator<T> , d'autre part, allouerait des objets déjà construits et serait également responsable de la suppression de ces objets (y compris la possibilité de mettre en cache des objets construits qui pourraient être distribués plus tard lors de la construction d'un objet nouvellement alloué , ce qui est cher). Le trait ressemblerait à quelque chose comme ceci:

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

Ceci n'est pas compatible avec Allocator , dont les méthodes traitent des pointeurs bruts et n'ont aucune notion de type. De plus, avec Allocator s, il est de la responsabilité de l'appelant de drop l'objet étant libéré en premier. C'est vraiment important - connaître le type T permet à ObjectAllocator<T> de faire des choses comme appeler la méthode T de drop , et depuis free(t) déplace t dans free , l'appelant _cannot_ drop t premier - c'est plutôt la responsabilité de ObjectAllocator<T> . Fondamentalement, ces deux traits sont incompatibles l'un avec l'autre.

Ah oui, je vois. Je pensais que cette proposition incluait déjà quelque chose comme ça, c'est-à-dire un allocateur «de niveau supérieur» sur le niveau octet. Dans ce cas, une proposition parfaitement juste!

Le 4 janvier 2017, à 22h29, Joshua Liebow-Feeser [email protected] a écrit:

Mais le trait d'allocateur d'objet pourrait simplement être un sous-portrait du trait d'allocateur général. C'est aussi simple que ça, en ce qui me concerne. Bien sûr, certains types d'allocateurs peuvent être plus efficaces que les allocateurs à usage général, mais ni le compilateur ni le standard n'ont vraiment besoin de (ou ne devraient) savoir à ce sujet.

Ah, le problème est que la sémantique est différente. L'allocateur alloue et libère les objets blob d'octets bruts. ObjectAllocator, d'autre part, allouerait des objets déjà construits et serait également responsable de la suppression de ces objets (y compris être capable de mettre en cache des objets construits qui pourraient être distribués plus tard lors de la construction d'un nouvel objet alloué, ce qui est coûteux). Le trait ressemblerait à quelque chose comme ceci:

trait ObjectAllocator{
fn alloc () -> T;
fn libre (t T);
}
Ceci n'est pas compatible avec Allocator, dont les méthodes traitent des pointeurs bruts et n'ont aucune notion de type. De plus, avec les allocateurs, il incombe à l'appelant de supprimer en premier l'objet à libérer. Ceci est vraiment important - connaître le type T permet à ObjectAllocatorpour faire des choses comme appeler la méthode drop de T, et puisque free (t) déplace t vers free, l'appelant ne peut pas abandonner t en premier - c'est à la place l'ObjectAllocatorresponsabilité. Fondamentalement, ces deux traits sont incompatibles l'un avec l'autre.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270505704 , ou désactivez le fil https://github.com/notifications/unsubscribe-auth/ AAEF3GViJBefuk8IWgPauPyL5tV78Fn5ks5rPB08gaJpZM4IDYUN .

@alexreg Ah oui, je l'espérais aussi :) Eh bien, il faudra attendre un autre RFC.

Oui, lancez cette RFC, je suis sûr qu'elle bénéficierait d'un soutien important! Et merci pour la clarification (je n'avais pas du tout suivi les détails de cette RFC).

Le 5 janvier 2017, à 00:53, Joshua Liebow-Feeser [email protected] a écrit:

@alexreg https://github.com/alexreg Ah oui, je l'espérais aussi :) Eh bien, il faudra attendre un autre RFC.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270531535 , ou désactivez le fil https://github.com/notifications/unsubscribe-auth/ AAEF3MQQeXhTliU5CBsoheBFL26Ee9WUks5rPD8RgaJpZM4IDYUN .

Une caisse pour tester les allocateurs personnalisés serait utile.

Pardonnez-moi si je rate quelque chose d'évident, mais y a-t-il une raison pour laquelle le trait Layout décrit dans cette RFC ne met pas en œuvre Copy ainsi que Clone , car c'est juste COSSE?

Je ne peux penser à aucun.

Désolé d'en parler si tard dans le processus, mais ...

Cela pourrait-il valoir la peine d'ajouter la prise en charge d'une fonction de type dealloc qui n'est pas une méthode, mais plutôt une fonction? L'idée serait d'utiliser l'alignement pour pouvoir déduire à partir d'un pointeur où se trouve en mémoire son allocateur parent et ainsi pouvoir libérer sans avoir besoin d'une référence d'allocateur explicite.

Cela pourrait être une grande victoire pour les structures de données qui utilisent des allocateurs personnalisés. Cela leur permettrait de ne pas garder une référence à l'allocateur lui-même, mais plutôt d'avoir besoin d'être paramétriques sur le _type_ de l'allocateur pour pouvoir appeler la bonne fonction dealloc . Par exemple, si Box est finalement modifié pour prendre en charge les allocateurs personnalisés, alors il pourrait continuer à n'être qu'un seul mot (juste le pointeur) au lieu d'avoir à être étendu à deux mots pour stocker une référence à l'allocateur également.

Sur une note connexe, il peut également être utile de prendre en charge une fonction non-méthode alloc pour permettre des allocateurs globaux. Cela se composerait bien avec une fonction non-méthode dealloc - pour les allocateurs globaux, il n'y aurait pas besoin de faire une sorte d'inférence pointeur vers allocateur car il n'y aurait qu'une seule instance statique de la allocateur pour l'ensemble du programme.

@joshlf La conception actuelle vous permet d'obtenir cela en faisant simplement que votre allocateur soit un type d'unité (de taille zéro) - c'est-à-dire struct MyAlloc; lequel vous implémentez ensuite le trait Allocator .
Stocker des références ou rien du tout, toujours , est moins général que stocker l'allocateur par valeur.

Je pourrais voir que c'est vrai pour un type directement intégré, mais qu'en est-il si une structure de données décide de conserver une référence à la place? Une référence à un type de taille zéro occupe-t-elle un espace nul? Autrement dit, si j'ai:

struct Foo()

struct Blah{
    foo: &Foo,
}

Est-ce que Blah a une taille nulle?

En fait, même si c'est possible, vous ne voudrez peut-être pas que votre allocateur ait une taille nulle. Par exemple, vous pouvez avoir un allocateur avec une taille différente de zéro que vous allouez _à partir_, mais qui a la capacité de libérer des objets sans connaître l'allocateur d'origine. Cela serait toujours utile pour faire en sorte qu'un Box ne prenne qu'un mot. Vous auriez quelque chose comme Box::new_from_allocator qui devrait prendre un allocateur comme argument - et ce pourrait être un allocateur de taille différente de zéro - mais si l'allocateur supportait la libération sans la référence d'allocateur d'origine, le Box<T> renvoyé Box::new_from_allocator d'origine.

Par exemple, vous pouvez avoir un allocateur avec une taille non nulle à partir de laquelle vous allouez, mais qui a la capacité de libérer des objets sans connaître l'allocateur d'origine.

Je me souviens qu'il y a longtemps, très longtemps, j'ai proposé de factoriser des traits d'allocateur et de désallocateur séparés (avec des types d'associés reliant les deux) pour cette raison.

Le compilateur est-il / devrait-il être autorisé à optimiser les allocations avec ces allocateurs?

Le compilateur est-il / devrait-il être autorisé à optimiser les allocations avec ces allocateurs?

@Zoxc Que voulez-vous dire?

Je me souviens qu'il y a longtemps, très longtemps, j'ai proposé de factoriser des traits d'allocateur et de désallocateur séparés (avec des types d'associés reliant les deux) pour cette raison.

Pour la postérité, laissez-moi clarifier cette affirmation (j'en ai parlé à @ Ericson2314 hors ligne): L'idée est qu'un Box pourrait être paramétrique uniquement sur un désallocateur. Vous pourriez donc avoir l'implémentation suivante:

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 cette façon, lors de l'appel de new_from_allocator , si A::D est un type de taille nulle, alors le champ d de Box<T, A::D> prend une taille nulle, et donc le la taille du résultat Box<T, A::D> est un seul mot.

Y a-t-il un calendrier pour quand cela arrivera? Je travaille sur des trucs d'allocateur, et ce serait bien si ce truc était là pour moi pour construire.

S'il y a un intérêt, je serais heureux de prêter quelques cycles à cela, mais je suis relativement nouveau dans Rust, donc cela pourrait simplement créer plus de travail pour les responsables en termes de révision du code d'un débutant. Je ne veux marcher sur les orteils de personne et je ne veux pas faire plus de travail pour les gens.

Ok, nous nous sommes récemment rencontrés pour évaluer l'état des allocateurs et je pense qu'il y a aussi de bonnes nouvelles pour cela! Il semble que le support n'ait pas encore atterri dans libstd pour ces API, mais tout le monde est toujours satisfait de leur atterrissage à tout moment!

Une chose dont nous avons discuté est que changer tous les types de libstd peut être un peu prématuré en raison de problèmes d'inférence possibles, mais indépendamment de cela, il semble être une bonne idée d'obtenir le trait Allocator et le Layout tapez le module std::heap proposé pour l'expérimentation ailleurs dans l'écosystème!

@joshlf si vous souhaitez aider ici, je pense que ce serait plus que bienvenu! La première pièce sera probablement l'atterrissage du type / trait de base de cette RFC dans la bibliothèque standard, puis à partir de là, nous pouvons également commencer à expérimenter et à jouer avec les collections dans libstd.

@alexcrichton Je pense que votre lien est rompu? Cela revient ici.

Une chose dont nous avons discuté est que changer tous les types de libstd peut être un peu prématuré en raison de problèmes d'inférence possibles

L'ajout du trait est une bonne première étape, mais sans refactoriser les API existantes pour l'utiliser, elles ne verront pas beaucoup d'utilisation. Dans https://github.com/rust-lang/rust/issues/27336#issuecomment -300721558, je propose que nous puissions refactoriser les caisses derrière la façade immédiatement, mais ajouter des wrappers newtype dans std . Gênant à faire, mais nous permet de progresser.

@alexcrichton Quel serait le processus pour obtenir des allocateurs d'objets? Mes expériences jusqu'à présent (bientôt publiques; je peux vous ajouter au repo privé GH si vous êtes curieux) et la discussion ici m'ont amené à croire qu'il y aura une symétrie presque parfaite entre les traits d'allocateur et l'objet traits d'allocateur. Par exemple, vous aurez quelque chose comme (j'ai changé Address en *mut u8 pour la symétrie avec *mut T de ObjectAllocator<T> ; nous finirions probablement par Address<T> ou quelque chose comme ça):

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

Ainsi, je pense qu'expérimenter à la fois les allocateurs et les allocateurs d'objets en même temps pourrait être utile. Je ne suis pas sûr que ce soit le bon endroit pour cela, cependant, ou s'il devrait y avoir un autre RFC ou, à tout le moins, un PR séparé.

Oh, je voulais créer un lien ici, qui contient également des informations sur l'allocateur global. @joshlf c'est ce que tu penses?

On dirait que @alexcrichton veut un PR qui fournit le trait Allocator et le type Layout , même s'il n'est intégré à aucune collection de libstd .

Si je comprends bien, je peux mettre en place un PR pour cela. Je ne l'avais pas fait parce que j'essaie toujours d'obtenir au moins une intégration avec RawVec et Vec prototypés. (À ce stade, j'ai RawVec fait, mais Vec est un peu plus difficile en raison des nombreuses autres structures qui en découlent, comme Drain et IntoIter etc ...)

en fait, ma branche actuelle semble être en train de se construire (et le seul test d'intégration avec RawVec réussi), alors je suis allé de l'avant et l'ai posté: # 42313

@hawkw a demandé:

Pardonnez-moi si je manque quelque chose d'évident, mais y a-t-il une raison pour que le trait de mise en page décrit dans cette RFC n'implémente pas Copie aussi bien que Cloner, puisqu'il ne s'agit que de POD?

La raison pour laquelle j'ai fait que Layout que Clone et non Copy est que je voulais laisser ouverte la possibilité d'ajouter plus de structure au type Layout . En particulier, je suis toujours intéressé à essayer de faire en sorte que Layout tente de suivre toute structure de type utilisée pour la construire (par exemple, 16 tableaux de struct { x: u8, y: [char; 215] } ), de sorte que les allocateurs aient l'option de exposer des routines d'instrumentation qui rapportent de quels types leur contenu actuel est composé.

Cela devrait presque certainement être une fonctionnalité facultative, c'est-à-dire qu'il semble que la tendance est fermement contre le fait d'obliger les développeurs à utiliser les constructeurs enrichis Layout type

Néanmoins, des fonctionnalités comme celle-ci étaient la principale raison pour laquelle je n'ai pas choisi de faire implémenter Copy Layout Copy ; J'ai essentiellement pensé qu'une implémentation de Copy serait une contrainte prématurée sur Layout lui-même.

@alexcrichton @pnkfelix

On dirait que @pnkfelix a couvert celui-ci, et que les relations publiques

Actuellement, la signature pour Allocator::oom est:

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

Il a été porté à mon attention , cependant, que Gecko aime au moins connaître la taille d'allocation également sur MOO. Nous souhaitons peut-être considérer cela lors de la stabilisation pour peut-être ajouter un contexte comme Option<Layout> pour expliquer pourquoi le MOO se produit.

@alexcrichton
Cela pourrait-il valoir la peine d'avoir plusieurs variantes oom_xxx ou une énumération de différents types d'arguments? Il y a quelques signatures différentes pour les méthodes qui pourraient échouer (par exemple, alloc prend une mise en page, realloc prend un pointeur, une mise en page originale et une nouvelle mise en page, etc.), et il pourrait être des cas dans lesquels une méthode de type oom voudrait les connaître tous.

@joshlf c'est vrai, oui, mais je ne sais pas si c'est utile. Je ne voudrais pas simplement ajouter des fonctionnalités parce que nous le pouvons, elles devraient continuer à être bien motivées.


Un point de stabilisation ici est également de "déterminer quelles sont les exigences sur l'alignement fourni à fn dealloc ", et l' implémentation actuelle de dealloc sur Windows utilise align pour déterminer comment correctement gratuit. @ruuda vous pouvez être intéressé par ce fait.

Un point de stabilisation ici est également de "déterminer quelles sont les exigences sur l'alignement fourni à fn dealloc ", et l'implémentation actuelle de dealloc sur Windows utilise align pour déterminer comment correctement gratuit.

Oui, je pense que c'est ainsi que j'ai rencontré cela au départ; mon programme s'est écrasé sur Windows à cause de cela. Comme HeapAlloc ne donne aucune garantie d'alignement, allocate alloue une région plus grande et stocke le pointeur d'origine dans un en-tête, mais en tant qu'optimisation, cela est évité si les exigences d'alignement sont quand même satisfaites. Je me demande s'il existe un moyen de convertir HeapAlloc en un allocateur sensible à l'alignement qui ne nécessite pas d'alignement sur free, sans perdre cette optimisation.

@ruuda

Comme HeapAlloc ne donne aucune garantie d'alignement

Il fournit une garantie d'alignement minimum de 8 octets pour 32 bits ou 16 octets pour 64 bits, il ne fournit tout simplement aucun moyen de garantir un alignement plus élevé que cela.

Le _aligned_malloc fourni par le CRT sur Windows peut fournir des allocations d'alignement supérieur, mais il doit notamment être associé à _aligned_free , l'utilisation de free est illégale. Donc, si vous ne savez pas si une allocation a été effectuée via malloc ou _aligned_malloc vous êtes coincé dans la même énigme que alloc_system sous Windows si vous ne le faites pas ' t connais l'alignement pour deallocate . Le CRT ne fournit pas la fonction standard aligned_alloc qui peut être associée à free , donc même Microsoft n'a pas été en mesure de résoudre ce problème. (Bien qu'il soit une fonction de C11 et Microsoft ne prend pas en charge C11 si c'est un argument faible.)

Notez que deallocate ne se soucie que de l'alignement pour savoir s'il est suraligné, la valeur réelle elle-même n'est pas pertinente. Si vous vouliez un deallocate qui soit vraiment indépendant de l'alignement, vous pouvez simplement traiter toutes les allocations comme suralignées, mais vous gaspilleriez beaucoup de mémoire sur de petites allocations.

@alexcrichton a écrit :

Actuellement, la signature pour Allocator::oom est:

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

Il a été porté à mon attention , cependant, que Gecko aime au moins connaître la taille d'allocation également sur MOO. Nous souhaitons peut-être considérer cela lors de la stabilisation pour peut-être ajouter un contexte comme Option<Layout> pour expliquer pourquoi le MOO se produit.

Le AllocErr porte déjà le Layout dans la variante AllocErr::Exhausted . Nous pourrions simplement ajouter la variante Layout à la variante AllocErr::Unsupported , ce qui, à mon avis, serait le plus simple en termes d'attentes des clients. (Cela a l'inconvénient d'augmenter silencieusement le côté de l'enum AllocErr lui-même, mais peut-être ne devrions-nous pas nous en préoccuper ...)

Oh je suppose que c'est tout ce qu'il faut, merci pour la correction @pnkfelix!

Je vais commencer à réutiliser ce problème pour le problème de suivi pour std::heap en général, car il le sera après https://github.com/rust-lang/rust/pull/42727 . Je vais fermer quelques autres questions connexes en faveur de cela.

Existe-t-il un problème de suivi pour la conversion des collections? Maintenant que les PR sont fusionnés, j'aimerais

  • Discutez du type d'erreur associé
  • Discutez de la conversion des collections pour utiliser n'importe quel allocateur local (en particulier en tirant parti du type d'erreur associé)

J'ai ouvert https://github.com/rust-lang/rust/issues/42774 pour suivre l'intégration de Alloc dans les collections std. Avec une discussion historique dans l'équipe des libs, il est probable que ce soit sur une piste de stabilisation distincte d'une première passe du module std::heap .

En examinant les problèmes liés aux allocateurs, je suis également tombé sur https://github.com/rust-lang/rust/issues/30170 qui @pnkfelix il y a quelque temps. Il semble que l'allocateur système OSX soit bogué avec des alignements élevés et lors de l'exécution de ce programme avec jemalloc, il segfault lors de la désallocation sous Linux au moins. À considérer lors de la stabilisation!

J'ai ouvert # 42794 pour discuter de la question spécifique de savoir si les allocations de taille zéro doivent correspondre à l'alignement demandé.

(oh attendez, les allocations de taille zéro sont illégales dans les allocateurs d'utilisateurs!)

Depuis que la fonction alloc::heap::allocate et les amis sont maintenant partis dans Nightly, j'ai mis à jour Servo pour utiliser cette nouvelle API. Cela fait partie du diff:

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

J'ai l'impression que l'ergonomie n'est pas excellente. Nous sommes passés de l'importation d'un élément à l'importation de trois à partir de deux modules différents.

  • Serait-il judicieux d'avoir une méthode pratique pour allocator.alloc(Layout::from_size_align(…)) ?
  • Serait-il judicieux de rendre les méthodes <Heap as Alloc>::_ disponibles en tant que fonctions libres ou méthodes inhérentes? (Pour avoir un élément de moins à importer, le trait Alloc .)

Sinon, le trait Alloc pourrait-il être dans le prélude ou est-il trop niche d'un cas d'utilisation?

@SimonSapin IMO il ne sert à rien d'optimiser l'ergonomie d'une API de bas niveau.

@SimonSapin

J'ai l'impression que l'ergonomie n'est pas excellente. Nous sommes passés de l'importation d'un élément à l'importation de trois à partir de deux modules différents.

J'avais exactement le même sentiment avec ma base de code - c'est plutôt maladroit maintenant.

Serait-il judicieux d'avoir une méthode pratique pour allocator.alloc(Layout::from_size_align(…))?

Voulez-vous dire dans le trait Alloc , ou juste pour Heap ? Une chose à considérer ici est qu'il y a maintenant une troisième condition d'erreur: Layout::from_size_align renvoie un Option , donc il pourrait renvoyer None en plus des erreurs normales que vous pouvez obtenir lors de l'allocation .

Alternativement, le trait Alloc pourrait-il être dans le prélude ou est-il trop niche d'un cas d'utilisation?

OMI, il n'y a pas grand-chose à faire pour optimiser l'ergonomie d'une API de bas niveau.

Je conviens que c'est probablement un niveau trop bas pour être placé dans le prélude, mais je pense toujours qu'il y a de la valeur à optimiser l'ergonomie (égoïstement, du moins - c'était un refactor vraiment ennuyeux 😝).

@SimonSapin n'avez-vous pas std les trois types sont disponibles dans le module std::heap (ils sont censés être dans un module). Vous n'avez pas non plus traité le cas des tailles débordantes auparavant? Ou des types de taille zéro?

n'avez-vous pas géré OOM auparavant?

Lorsqu'elle existait, la fonction alloc::heap::allocate retournait un pointeur sans Result et ne laissait pas de choix dans la gestion du MOO. Je pense que cela a interrompu le processus. Maintenant, j'ai ajouté .unwrap() pour paniquer le fil.

ils sont censés être dans un module

Je vois maintenant que heap.rs contient pub use allocator::*; . Mais quand j'ai cliqué sur Alloc dans l'impl listé sur la page rustdoc pour Heap j'ai été envoyé à alloc::allocator::Alloc .

Quant au reste, je ne l'ai pas étudié. Je porte sur un nouveau compilateur un gros tas de code écrit il y a des années. Je pense que ce sont des rappels pour FreeType, une bibliothèque C.

Lorsqu'elle existait, la fonction alloc :: heap :: allocate renvoyait un pointeur sans résultat et ne laissait pas de choix dans la gestion du MOO.

Cela vous a donné le choix. Le pointeur qu'il a renvoyé aurait pu être un pointeur nul qui indiquerait que l'allocateur de tas n'a pas pu être alloué. C'est pourquoi je suis si heureux qu'il soit passé à Result pour que les gens n'oublient pas de gérer ce cas.

Eh bien, peut-être que le FreeType a fini par faire une vérification nulle, je ne sais pas. Quoi qu'il en soit, oui, renvoyer un résultat est bon.

Étant donné les # 30170 et # 43097, je suis tenté de résoudre le problème OS X avec des alignements ridiculement grands en spécifiant simplement que les utilisateurs ne peuvent pas demander d'alignements> = 1 << 32 .

Un moyen très simple d'appliquer ceci: changez l' interface Layout pour que align soit désigné par un u32 au lieu d'un usize .

@alexcrichton avez-vous des idées à ce sujet? Dois-je juste faire un PR qui fait ça?

@pnkfelix Layout::from_size_align prendrait toujours usize et renverrait une erreur sur u32 débordement, non?

@SimonSapin quelle raison y a-t-il pour qu'il continue à prendre usize align, si une condition préalable statique est qu'il n'est pas sûr de passer une valeur> = 1 << 32 ?

et si la réponse est "bien certains allocateurs pourraient soutenir un alignement> = 1 << 32 ", alors nous sommes de retour au statu quo et vous pouvez ignorer ma suggestion. Le point de ma suggestion est essentiellement un "+1" à des commentaires comme celui-ci

Parce que std::mem::align_of renvoie usize

@SimonSapin ah, bonne vieille API stable ... soupir.

@pnkfelix se limiter à 1 << 32 me semble raisonnable!

@rfcbot fcp fusionner

Ok, ce trait et ses types ont cuit pendant un certain temps maintenant et ont également été l'implémentation sous-jacente des collections standard depuis sa création. Je proposerais de partir d'une offre initiale particulièrement conservatrice, à savoir ne stabiliser que l'interface suivante:

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

Proposition originale

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

Notamment:

  • Stabiliser uniquement les méthodes alloc , alloc_zeroed et dealloc sur le trait Alloc pour le moment. Je pense que cela résout le problème le plus urgent que nous ayons aujourd'hui, en définissant un allocateur global personnalisé.
  • Supprimez le type Error pour n'utiliser que des pointeurs bruts.
  • Changez le type u8 dans l'interface en void
  • Une version allégée du type Layout .

Il y a encore des questions ouvertes telles que que faire avec dealloc et l'alignement (alignement précis? Ajustement? Incertain?), Mais j'espère que nous pourrons les résoudre pendant FCP car ce ne sera probablement pas une API changement de rupture.

+1 pour obtenir quelque chose de stabilisé!

Renomme AllocErr en Error et déplace l'interface pour qu'elle soit un peu plus conservatrice.

Cela élimine-t-il la possibilité pour les allocateurs de spécifier Unsupported ? Au risque d'insister davantage sur quelque chose sur lequel j'ai beaucoup insisté, je pense que # 44557 est toujours un problème.

Layout

Il semble que vous ayez supprimé certaines des méthodes de Layout . Vouliez-vous faire supprimer ceux que vous avez laissés de côté ou simplement les laisser comme instables?

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

Est-ce un constructeur pour ce qui est aujourd'hui AllocErr::Exhausted ? Si oui, ne devrait-il pas avoir un paramètre Layout ?

Le membre de l'équipe @alexcrichton a proposé de fusionner cela. L'étape suivante est l'examen par le reste des équipes marquées:

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

Préoccupations:

Une fois que ces examinateurs parviendront à un consensus, cela entrera dans sa période de commentaires finale. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler!

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe marqués peuvent me donner.

Je suis vraiment ravi de pouvoir stabiliser une partie de ce travail!

Une question: dans le fil ci-dessus, @joshlf et @ Ericson2314 ont soulevé un point intéressant sur la possibilité de séparer les traits Alloc et Dealloc afin d'optimiser les cas dans lesquels alloc nécessite des données, mais dealloc ne nécessite aucune information supplémentaire, donc le type Dealloc peut être de taille zéro.

Cette question a-t-elle jamais été résolue? Quels sont les inconvénients de séparer les deux traits?

@joshlf

Cela élimine-t-il la possibilité pour les allocateurs de spécifier Non pris en charge?

Oui et non, cela signifierait qu'une telle opération n'est pas prise en charge immédiatement dans la rouille stable , mais nous pourrions continuer à la soutenir dans la rouille instable.

Vouliez-vous faire supprimer ceux que vous avez laissés de côté ou simplement les laisser comme instables?

En effet! Encore une fois, bien que je propose simplement une surface d'API stable, nous pouvons laisser toutes les autres méthodes comme instables. Avec le temps, nous pouvons continuer à stabiliser davantage les fonctionnalités. Je pense qu'il vaut mieux commencer aussi conservateur que possible.


@SimonSapin

Est-ce un constructeur pour ce qui est aujourd'hui AllocErr :: Exhausted? Si tel est le cas, ne devrait-il pas avoir un paramètre de mise en page?

Aha bon point! Je voulais en quelque sorte laisser la possibilité de faire de Error un type de taille zéro si nous en avions vraiment besoin, mais nous pouvons bien sûr garder les méthodes de prise de disposition instables et les stabiliser si nécessaire. Ou pensez-vous que la mise en page préservant Error devrait être stabilisée lors du premier passage?


@cramertj

Je n'avais pas encore vu personnellement une telle question / préoccupation (je pense que je l'ai ratée!), Mais je ne verrais pas personnellement que cela en valait la peine. Deux traits sont deux fois plus simples en général, car maintenant tout le monde devrait taper Alloc + Dealloc dans les collections par exemple. Je m'attendrais à ce qu'une telle utilisation spécialisée ne veuille pas informer l'interface que tous les autres utilisateurs finissent par utiliser personnellement.

@cramertj @alexcrichton

Je n'avais pas encore vu personnellement une telle question / préoccupation (je pense que je l'ai ratée!), Mais je ne verrais pas personnellement que cela en valait la peine.

En général, je conviens que cela ne vaut pas la peine avec une exception flagrante: Box . Box<T, A: Alloc> serait, compte tenu de la définition actuelle de Alloc , doivent être au moins deux mots grands (le pointeur qu'il a déjà et une référence à un Alloc au moins ) sauf dans le cas des singletons globaux (qui peuvent être implémentés en tant que ZST). Une explosion 2x (ou plus) dans l'espace requis pour stocker un type aussi commun et fondamental me préoccupe.

@alexcrichton

comme maintenant tout le monde devrait taper Alloc + Dealloc dans les collections par exemple

Nous pourrions ajouter quelque chose comme ceci:

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

Une explosion 2x (ou plus) dans l'espace requis pour stocker un type aussi commun et fondamental

Uniquement lorsque vous utilisez un allocateur personnalisé qui n'est pas global au processus. std::heap::Heap (par défaut) est de taille zéro.

Ou pensez-vous que l'erreur de préservation de la mise en page devrait être stabilisée lors du premier passage?

@alexcrichton Je ne comprends pas vraiment pourquoi ce premier passage proposé est du tout. Il n'y a guère plus que ce qui pourrait déjà être fait en abusant de Vec , et pas assez par exemple pour utiliser https://crates.io/crates/jemallocator.

Que faut-il encore résoudre pour stabiliser le tout?

Uniquement lorsque vous utilisez un allocateur personnalisé qui n'est pas global au processus. std :: heap :: Heap (la valeur par défaut) est de taille zéro.

Cela semble être le cas d'utilisation principal des allocateurs paramétriques, non? Imaginez la définition simple suivante d'un arbre:

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

Un arbre construit à partir de ceux avec un 1 mot Alloc aurait une taille ~ 1,7x agrandie pour toute la structure de données par rapport à un ZST Alloc . Cela me semble assez mauvais, et ces types d'applications sont en quelque sorte le but de faire de Alloc un trait.

@cramertj

Nous pourrions ajouter quelque chose comme ceci:

Nous allons également avoir des alias de trait réels :) https://github.com/rust-lang/rust/issues/41517

@glaebhoerl Oui, mais la stabilisation semble encore loin car il n'y a pas encore d'implémentation. Si nous désactivons les impls manuels de Allocator je pense que nous pouvons passer aux alias de trait de manière rétrocompatible quand ils arrivent;)

@joshlf

Une explosion 2x (ou plus) dans l'espace requis pour stocker un type aussi commun et fondamental me préoccupe.

J'imagine que toutes les implémentations d'aujourd'hui sont juste un type de taille zéro ou un pointeur de grande taille, non? L'optimisation possible n'est-elle pas que certains types de taille de pointeur peuvent être de taille nulle? (ou quelque chose comme ça?)


@cramertj

Nous pourrions ajouter quelque chose comme ceci:

En effet! Nous avons ensuite pris un trait à trois . Dans le passé, nous n'avons jamais eu une grande expérience avec de tels traits. Par exemple, Box<Both> ne convertit pas en Box<OnlyOneTrait> . Je suis sûr que nous pourrions attendre que les fonctionnalités linguistiques lissent tout cela, mais il semble que celles-ci soient encore loin, au mieux.


@SimonSapin

Que faut-il encore résoudre pour stabiliser le tout?

Je ne sais pas. Je voulais commencer par la plus petite chose absolue pour qu'il y ait moins de débat.

J'imagine que toutes les implémentations d'aujourd'hui sont juste un type de taille zéro ou un pointeur de grande taille, non? L'optimisation possible n'est-elle pas que certains types de taille de pointeur peuvent être de taille nulle? (ou quelque chose comme ça?)

Oui, l'idée est que, étant donné un pointeur vers un objet alloué à partir de votre type d'allocateur, vous pouvez déterminer de quelle instance il provient (par exemple, en utilisant des métadonnées en ligne). Ainsi, les seules informations dont vous avez besoin pour désallouer sont les informations de type, pas les informations d'exécution.

Pour revenir à l'alignement sur la désallocation, je vois deux voies à suivre:

  • Stabiliser comme proposé (avec alignement sur désallouer). Céder la propriété de la mémoire allouée manuellement serait impossible à moins que le Layout soit inclus. En particulier, il est impossible de construire un conteneur Vec ou Box ou String ou autre std avec un alignement plus strict que nécessaire (par exemple parce que vous ne Je ne veux pas que l'élément encadré chevauche une ligne de cache), sans le déconstruire et le désallouer manuellement plus tard (ce qui n'est pas toujours une option). Un autre exemple de quelque chose qui serait impossible, est de remplir un Vec utilisant des opérations simd, puis de le donner.

  • N'exigez pas d'alignement sur désallouer et supprimez l'optimisation des petites allocations de Windows ' HeapAlloc -based alloc_system . Stockez toujours l'alignement. @alexcrichton , lorsque vous avez HeapAlloc arrondisse de toute façon les tailles.)

Dans tous les cas, c'est un compromis très difficile à faire; l'impact sur la mémoire et les performances dépendra fortement du type d'application, et celle pour laquelle optimiser est également spécifique à l'application.

Je pense que nous sommes peut-être Just Fine (TM). Citant les documents Alloc :

Certaines des méthodes exigent qu'une disposition s'adapte à un bloc de mémoire.
Ce que cela signifie pour une mise en page de "s'adapter" à un bloc de mémoire signifie (ou
de manière équivalente, pour qu'un bloc de mémoire "s'adapte" à une mise en page) est que le
les deux conditions suivantes doivent être remplies:

  1. L'adresse de départ du bloc doit être alignée sur layout.align() .

  2. La taille du bloc doit être comprise dans la plage [use_min, use_max] , où:

    • use_min est self.usable_size(layout).0 , et

    • use_max est la capacité qui était (ou aurait été)
      renvoyé lorsque (si) le bloc a été alloué via un appel à
      alloc_excess ou realloc_excess .

Notez que:

  • la taille de la mise en page la plus récemment utilisée pour allouer le bloc
    est garanti dans la plage [use_min, use_max] , et

  • une borne inférieure sur use_max peut être approchée en toute sécurité par un appel à
    usable_size .

  • si un layout k correspond à un bloc mémoire (noté ptr )
    actuellement alloué via un allocateur a , alors il est légal de
    utilisez cette disposition pour le désallouer, c'est- a.dealloc(ptr, k); dire

Notez cette dernière puce. Si j'alloue avec une mise en page avec alignement a , alors il devrait être légal pour moi de désallouer avec alignement b < a car un objet aligné sur a est également aligné sur b , et donc une mise en page avec alignement b correspond à un objet alloué avec une mise en page avec alignement a (et de même taille).

Cela signifie que vous devriez pouvoir allouer avec un alignement supérieur à l'alignement minimum requis pour un type particulier, puis permettre à un autre code de se désallouer avec l'alignement minimum, et cela devrait fonctionner.

L'optimisation possible n'est-elle pas que certains types de taille de pointeur peuvent être de taille nulle? (ou quelque chose comme ça?)

Il y avait une RFC pour cela récemment et il semble très peu probable que cela puisse être fait en raison de problèmes de compatibilité: https://github.com/rust-lang/rfcs/pull/2040

Par exemple, Box<Both> ne convertit pas en Box<OnlyOneTrait> . Je suis sûr que nous pourrions attendre que les fonctionnalités linguistiques lissent tout cela, mais il semble que celles-ci soient encore loin, au mieux.

L'upcasting des objets de trait, d'un autre côté, semble incontestablement souhaitable, et principalement une question d'effort / de bande passante / de volonté pour le mettre en œuvre. Il y avait un fil récemment: https://internals.rust-lang.org/t/trait-upcasting/5970

@ruuda C'est alloc_system origine. alexcrichton l'a simplement déplacé pendant les grands refacteurs d'allocations de <time period> .

L'implémentation actuelle nécessite que vous désallouiez avec le même alignement spécifié que vous avez alloué un bloc de mémoire donné. Indépendamment de ce que la documentation peut prétendre, c'est la réalité actuelle que tout le monde doit respecter jusqu'à ce que alloc_system sous Windows soit changé.

Les allocations sous Windows utilisent toujours un multiple de MEMORY_ALLOCATION_ALIGNMENT (bien qu'elles se souviennent de la taille que vous leur avez allouée à l'octet). MEMORY_ALLOCATION_ALIGNMENT vaut 8 sur 32 bits et 16 sur 64 bits. Pour les types suralignés, comme l'alignement est supérieur à MEMORY_ALLOCATION_ALIGNMENT , la surcharge causée par alloc_system est toujours le montant d'alignement spécifié, donc une allocation alignée de 64 octets aurait 64 octets de surcharge.

Si nous décidions d'étendre cette astuce suraligné à toutes les allocations (ce qui éliminerait l'obligation de désallouer avec le même alignement que celui que vous avez spécifié lors de l'allocation), alors plus d'allocations auraient une surcharge. Les allocations dont les alignements sont identiques à MEMORY_ALLOCATION_ALIGNMENT subiront une surcharge constante de MEMORY_ALLOCATION_ALIGNMENT octets. Les allocations dont les alignements sont inférieurs à MEMORY_ALLOCATION_ALIGNMENT subiront une surcharge de MEMORY_ALLOCATION_ALIGNMENT octets environ la moitié du temps. Si la taille de l'allocation arrondie à MEMORY_ALLOCATION_ALIGNMENT est supérieure ou égale à la taille de l'allocation plus la taille d'un pointeur, alors il n'y a pas de surcharge, sinon il y en a. Étant donné que 99,99% des allocations ne seront pas suralignées, voulez-vous vraiment engager ce genre de frais généraux sur toutes ces allocations?

@ruuda

Personnellement, je pense que la mise en œuvre de alloc_system aujourd'hui sur Windows est un plus grand avantage que d'avoir la possibilité de renoncer à la propriété d'une allocation à un autre conteneur comme Vec . AFAIK bien qu'il n'y ait pas de données pour mesurer l'impact du remplissage toujours avec l'alignement et ne nécessitant pas d'alignement sur la désallocation.

@joshlf

Je pense que ce commentaire est faux, comme l'a souligné alloc_system sur Windows repose sur le même alignement transmis à la désallocation que celui transmis lors de l'allocation.

Étant donné que 99,99% des allocations ne seront pas suralignées, voulez-vous vraiment engager ce genre de frais généraux sur toutes ces allocations?

Cela dépend de l'application si la surcharge est importante et s'il faut optimiser la mémoire ou les performances. Je soupçonne que pour la plupart des applications, c'est bien, mais une petite minorité se soucie profondément de la mémoire et ne peut vraiment besoin.

@alexcrichton

Je pense que ce commentaire est faux, comme l'a souligné alloc_system sur Windows repose sur le même alignement passé à la désallocation que celui transmis lors de l'allocation.

Cela n'implique-t-il pas que alloc_system sous Windows n'implémente pas correctement le trait Alloc (et donc peut-être devrions-nous changer les exigences du trait Alloc )?


@ retep998

Si je lis correctement votre commentaire, cette surcharge d'alignement n'est-elle pas présente pour toutes les allocations, que nous ayons besoin de pouvoir désallouer avec un alignement différent? Autrement dit, si j'alloue 64 octets avec un alignement de 64 octets et que je désalloue également avec un alignement de 64 octets, la surcharge que vous avez décrite est toujours présente. Ainsi, ce n'est pas une fonctionnalité de pouvoir désallouer avec différents alignements, mais plutôt une fonctionnalité de demande d'alignements plus grands que la normale.

@joshlf La surcharge causée par alloc_system actuellement est due à la demande d'alignements plus grands que la normale. Si votre alignement est inférieur ou égal à MEMORY_ALLOCATION_ALIGNMENT , il n'y a pas de surcharge causée par alloc_system .

Cependant, si nous modifions l'implémentation pour permettre la désallocation avec différents alignements, la surcharge s'appliquerait à presque toutes les allocations, indépendamment de l'alignement.

Ah, je vois; logique.

Quelle est la signification de l'implémentation d'Alloc pour Heap et & Heap? Dans quels cas l'utilisateur utiliserait-il l'un de ces impls par rapport à l'autre?

Est-ce la première API de bibliothèque standard dans laquelle *mut u8 signifierait "pointeur vers n'importe quoi"? Il y a String :: from_raw_parts mais celui-là signifie vraiment pointeur vers des octets. Je ne suis pas fan de *mut u8 signifie "pointeur vers n'importe quoi" - même C fait mieux. Quelles sont les autres options? Peut-être qu'un pointeur vers un type opaque serait plus significatif.

@rfcbot concerne * mut u8

@dtolnay Alloc for Heap est une sorte de "standard" et Alloc for &Heap est comme Write for &T où le trait requiert &mut self mais pas l'implémentation. Cela signifie notamment que les types comme Heap et System sont threadsafe et n'ont pas besoin d'être synchronisés lors de l'allocation.

Plus important encore, l'utilisation de #[global_allocator] nécessite que le statique auquel il est attaché, qui a le type T , ait Alloc for &T . (alias tous les allocateurs globaux doivent être threadsafe)

Pour *mut u8 je pense que *mut () peut être intéressant, mais personnellement, je ne me sens pas trop obligé de "bien faire les choses" ici en soi.

Le principal avantage de *mut u8 est qu'il est très pratique d'utiliser .offset avec des décalages d'octets.

Pour *mut u8 je pense que *mut () peut être intéressant, mais personnellement, je ne me sens pas trop obligé de "bien faire" ici en soi.

Si nous utilisons *mut u8 dans une interface stable, ne nous enfermons-nous pas? En d'autres termes, une fois que nous aurons stabilisé cela, nous n'aurons plus la possibilité de «bien faire les choses» à l'avenir.

De plus, *mut () me semble un peu dangereux au cas où nous ferions une optimisation comme la RFC 2040 à l'avenir.

Le principal avantage de *mut u8 est qu'il est très pratique d'utiliser .offset avec des décalages d'octets.

C'est vrai, mais vous pouvez facilement faire let ptr = (foo as *mut u8) et ensuite aller à votre guise. Cela ne semble pas être une motivation suffisante pour s'en tenir à *mut u8 dans l'API s'il existe des alternatives convaincantes (ce qui, pour être juste, je ne suis pas sûr qu'il y en ait).

De plus, * mut () me semble un peu dangereux au cas où nous ferions une optimisation comme la RFC 2040 à l'avenir.

Cette optimisation ne se produira probablement jamais - cela briserait trop de code existant. Même si c'était le cas, il serait appliqué à &() et &mut () , pas à *mut () .

Si la RFC 1861 était sur le point d'être implémentée / stabilisée, je suggérerais de l'utiliser:

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

C'est probablement trop loin, non?

@joshlf Je pensais avoir vu un PR ouvert à leur sujet, l'inconnu restant est DynSized .

Cela fonctionnera-t-il pour les struct hack comme des objets? Disons que j'ai un Node<T> qui ressemble à ceci:

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

et un type de valeur:

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

Maintenant, je veux allouer Node<V> avec une chaîne de taille 7 en une seule allocation. Idéalement, je veux faire une allocation de taille 16 aligner 4 et tout y adapter: 4 pour u32 , 5 pour V et 7 pour les octets de chaîne. Cela fonctionne car le dernier membre de V a l'alignement 1 et les octets de chaîne ont également l'alignement 1.

Notez que cela n'est pas autorisé en C / C ++ si les types sont composés comme ci-dessus, car l'écriture dans le stockage compressé est un comportement non défini. Je pense que c'est un trou dans la norme C / C ++ qui ne peut malheureusement pas être corrigé. Je peux expliquer pourquoi cela est cassé, mais concentrons-nous plutôt sur Rust. Cela peut-il fonctionner? :-)

En ce qui concerne la taille et l'alignement de la structure Node<V> elle-même, vous êtes à peu près à la merci du compilateur Rust. C'est UB (comportement non défini) à allouer avec n'importe quelle taille ou alignement plus petit que ce que Rust exige puisque Rust peut faire des optimisations basées sur l'hypothèse que tout objet Node<V> - sur la pile, sur le tas, derrière une référence, etc. - a une taille et un alignement correspondant à ce qui est attendu au moment de la compilation.

En pratique, il semble que la réponse soit malheureusement non: j'ai exécuté ce programme et j'ai trouvé que, au moins sur le Rust Playground, Node<V> une taille de 12 et un alignement de 4, ce qui signifie que tous les objets après le Node<V> doit être décalé d'au moins 12 octets. Il semble que le décalage du champ data.b dans le champ Node<V> est de 8 octets, ce qui implique que les octets 9 à 11 sont un remplissage de fin. Malheureusement, même si ces octets de remplissage "ne sont pas utilisés" dans un certain sens, le compilateur les traite toujours comme faisant partie du Node<V> , et se réserve le droit de faire tout ce qu'il veut avec eux (le plus important, y compris l'écriture à eux lorsque vous affectez à un Node<V> , ce qui implique que si vous essayez d'éliminer des données supplémentaires là-bas, elles peuvent être écrasées).

(note, btw: vous ne pouvez pas traiter un type comme compressé que le compilateur Rust ne pense pas qu'il est compacté. Cependant, vous _pouvez_ dire au compilateur Rust que quelque chose est compressé, ce qui modifiera la disposition du type (suppression du remplissage), en utilisant repr(packed) )

Cependant, en ce qui concerne la disposition d'un objet après l'autre sans les avoir tous les deux du même type Rust, je suis presque sûr à 100% que c'est valide - après tout, c'est ce que fait Vec . Vous pouvez utiliser les méthodes du type Layout pour calculer dynamiquement l'espace nécessaire pour l'allocation totale:

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

Quelque chose comme ce travail?

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

… Puis allouez avec une taille plus grande et utilisez slice::from_raw_parts_mut(node.bytes.as_mut_ptr(), size) ?

Merci @joshlf pour la réponse détaillée! Le TLDR pour mon cas d'utilisation est que je peux obtenir un Node<V> de taille 16 mais seulement si V est repr(packed) . Sinon, le mieux que je puisse faire est la taille 19 (12 + 7).

@SimonSapin pas sûr; Je vais essayer.

Je n'ai pas vraiment rattrapé ce fil, mais je suis encore fermement contre la stabilisation de quoi que ce soit. Nous n'avons pas encore progressé dans la mise en œuvre des problèmes difficiles:

  1. Collections polymorphes d'allocateur

    • même pas de boîte non gonflée!

  2. Collections falliables

Je pense que la conception des traits fondamentaux aura une incidence sur les solutions de ceux -ci : j'ai eu peu de temps pour Rust depuis quelques mois, mais ont soutenu à certains moments. Je doute que j'aie le temps de faire valoir pleinement mon cas ici non plus, donc je ne peux qu'espérer que nous rédigerons d'abord au moins une solution complète à tous ces problèmes: quelqu'un me prouve qu'il est impossible d'être rigoureux (forcer l'utilisation correcte), flexible , et ergonomique avec les traits actuels. Ou même simplement terminer de cocher les cases en haut.

Commentaire de Re:

Je pense qu'une question pertinente liée au conflit entre cette perspective et le désir de @alexcrichton de stabiliser quelque chose est: quel avantage Alloc (même la plupart des collections utiliseront probablement Box ou un autre conteneur similaire), donc la vraie question devient: qu'est-ce que la stabilisation achète pour les utilisateurs qui ne pas appeler directement les méthodes Alloc ? Honnêtement, le seul cas d'utilisation sérieux auquel je puisse penser est qu'il ouvre la voie aux collections polymorphes d'allocations (qui seront probablement utilisées par un ensemble beaucoup plus large d'utilisateurs), mais il semble que ce soit bloqué sur # 27336, ce qui est loin d'être en cours de résolution. Il me manque peut-être d'autres cas d'utilisation importants, mais sur la base de cette analyse rapide, je suis enclin à m'éloigner de la stabilisation car n'ayant que des avantages marginaux au prix de nous enfermer dans une conception que nous pourrions trouver plus tard sous-optimale .

@joshlf cela permet aux gens de définir et d'utiliser leurs propres allocateurs globaux.

Hmmm bon point. serait-il possible de stabiliser en spécifiant l'allocateur global sans stabiliser Alloc ? C'est-à-dire que le code qui implémente Alloc devrait être instable, mais cela serait probablement encapsulé dans sa propre caisse, et le mécanisme pour marquer cet allocateur comme allocateur global serait lui-même stable. Ou est-ce que je ne comprends pas comment stable / instable et le compilateur stable / compilateur nocturne interagissent?

Ah @joshlf rappelez-vous que # 27336 est une distraction, selon https://github.com/rust-lang/rust/issues/42774#issuecomment -317279035. Je suis presque sûr que nous allons rencontrer d' autres problèmes - des problèmes avec les traits tels qu'ils existent, c'est pourquoi je veux travailler pour commencer à travailler là-dessus maintenant. Il est beaucoup plus facile de discuter de ces problèmes une fois qu'ils sont arrivés à la vue de tous que de débattre des futurs prévus après le # 27336.

@joshlf Mais vous ne pouvez pas compiler la caisse qui définit l'allocateur global avec un compilateur stable.

@sfackler Ah oui, il y a ce malentendu dont j'avais peur: P

Je trouve le nom Excess(ptr, usize) un peu déroutant car le usize n'est pas la taille excess de l'allocation demandée (comme dans la taille supplémentaire allouée), mais le total taille de l'allocation.

IMO Total , Real , Usable , ou tout nom indiquant que la taille est la taille totale ou la taille réelle de l'allocation est meilleure que "excédent", ce que je trouve trompeur. Il en va de même pour les méthodes _excess .

Je suis d'accord avec @gnzlbg ci-dessus, je pense qu'un tuple simple (ptr, usize) conviendrait parfaitement.

Notez que Excess n'est pas proposé pour être stabilisé dans la première passe, cependant

Publié ce fil de discussion sur reddit, qui préoccupe certaines personnes: https://www.reddit.com/r/rust/comments/78dabn/custom_allocators_are_on_the_verge_of_being/

Après une discussion plus approfondie avec @ rust-lang / libs aujourd'hui, j'aimerais apporter quelques modifications à la proposition de stabilisation qui peut être résumée par:

  • Ajoutez alloc_zeroed à l'ensemble des méthodes stabilisées, sinon en ayant la même signature que alloc .
  • Remplacez *mut u8 par *mut void dans l'API en utilisant le support extern { type void; } , en résolvant le problème de @dtolnay et en fournissant une voie à suivre pour unifier c_void travers l'écosystème.
  • Changez le type de retour de alloc en *mut void , en supprimant le Result et le Error

Le dernier point est peut-être le plus litigieux, alors je veux aussi m'étendre là-dessus. Ceci est sorti de la discussion avec l'équipe libs aujourd'hui et tournait spécifiquement autour de la façon dont (a) l'interface basée sur Result a un ABI moins efficace qu'un pointeur retournant et (b) presque pas d'allocateurs de «production» aujourd'hui fournir la possibilité d'apprendre quelque chose de plus que "ceci juste OOM'd". Pour les performances, nous pouvons principalement le masquer avec des incrustations et autres, mais il reste que le Error est une charge utile supplémentaire difficile à supprimer aux couches les plus basses.

L'idée de renvoyer des charges utiles d'erreurs est que les allocateurs peuvent fournir une introspection spécifique à l'implémentation pour savoir pourquoi une allocation a échoué et sinon presque tous les consommateurs devraient seulement avoir besoin de savoir si l'allocation a réussi ou échoué. De plus, il s'agit d'une API de très bas niveau qui n'est pas appelée aussi souvent (au lieu de cela, les API typées qui résument bien les choses devraient être appelées à la place). En ce sens, il n'est pas primordial que nous disposions de l'API la plus utilisable et la plus ergonomique pour cet emplacement, mais il est plutôt plus important d'activer des cas d'utilisation sans sacrifier les performances.

Le principal avantage de *mut u8 est qu'il est très pratique d'utiliser .offset avec des décalages d'octets.

Lors de la réunion libs, nous avons également suggéré impl *mut void { fn offset } qui n'entre pas en conflit avec le offset existant défini pour T: Sized . Peut aussi être byte_offset .

+1 pour utiliser *mut void et byte_offset . Y aura-t-il un problème avec la stabilisation de la fonctionnalité des types externes, ou pouvons-nous éviter ce problème car seule la définition est instable (et liballoc peut faire des choses instables en interne) et non l'utilisation (par exemple, let a: *mut void = ... isn pas instable)?

Oui, nous n'avons pas besoin de bloquer la stabilisation de type externe. Même si le support de type extern est supprimé, le void nous définissons pour cela peut toujours être le pire des cas de type magique.

Y a-t-il eu d'autres discussions lors de la réunion libs sur la question de savoir si Alloc et Dealloc devraient être des traits séparés?

Nous n'avons pas abordé cela spécifiquement, mais nous avons généralement estimé que nous ne devrions pas nous détourner de l'état de la technique à moins d'avoir une raison particulièrement convaincante de le faire. En particulier, le concept d'allocateur de C ++ n'a pas de répartition similaire.

Je ne suis pas sûr que ce soit une comparaison appropriée dans ce cas. En C ++, tout est explicitement libéré, il n'y a donc pas d'équivalent à Box qui a besoin de stocker une copie (ou une référence à) de son propre allocateur. C'est ce qui cause l'explosion de grande taille pour nous.

@joshlf unique_ptr est l'équivalent de Box , vector est l'équivalent de Vec , unordered_map est l'équivalent de HashMap , etc.

@cramertj Ah, intéressant, je ne regardais que les types de collections. Il semble que ce soit une chose à faire alors. Nous pouvons toujours l'ajouter plus tard via des impls de couverture, mais il serait probablement plus propre d'éviter cela.

L'approche de la couverture impl pourrait être plus propre, en fait:

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 trait de moins à craindre pour la majorité des cas d'utilisation.

  • Changez le type de retour d'alloc en * mut void, en supprimant le résultat et l'erreur

Le dernier point est peut-être le plus litigieux, alors je veux aussi m'étendre là-dessus. Ceci est sorti de la discussion avec l'équipe libs aujourd'hui et a spécifiquement tourné autour de la façon dont (a) l'interface basée sur les résultats a un ABI moins efficace qu'un pointeur retournant et (b) presque aucun allocateur de «production» ne permet aujourd'hui d'apprendre rien de plus que "ceci juste OOM'd". Pour les performances, nous pouvons principalement le masquer avec des incrustations et autres, mais il reste que l'erreur est une charge utile supplémentaire difficile à supprimer aux couches les plus basses.

Je crains que cela rende très facile l'utilisation du pointeur retourné sans vérifier null. Il semble que la surcharge pourrait également être supprimée sans ajouter ce risque en retournant Result<NonZeroPtr<void>, AllocErr> et en faisant AllocErr de taille nulle?

( NonZeroPtr est un ptr::Shared fusionné et ptr::Unique comme proposé dans https://github.com/rust-lang/rust/issues/27730#issuecomment-316236397.)

@SimonSapin quelque chose comme Result<NonZeroPtr<void>, AllocErr> nécessite trois types pour être stabilisés, qui sont tous neufs et dont certains ont toujours beaucoup langui pour la stabilisation. Quelque chose comme void n'est même pas nécessaire et est agréable à avoir (à mon avis).

Je suis d'accord qu'il est "facile de l'utiliser sans vérifier la valeur nulle", mais c'est, encore une fois, une API de très bas niveau qui n'est pas destinée à être fortement utilisée, donc je ne pense pas que nous devrions optimiser l'ergonomie de l'appelant.

Les gens peuvent également créer des abstractions de plus haut niveau comme alloc_one en plus du bas niveau alloc qui pourraient avoir des types de retour plus complexes comme Result<NonZeroPtr<void>, AllocErr> .

Je suis d'accord que AllocErr ne serait pas utile en pratique, mais qu'en est-il de seulement Option<NonZeroPtr<void>> ? Les API qu'il est impossible d'utiliser accidentellement à mauvais escient, sans frais généraux, sont l'une des choses qui distinguent Rust de C, et revenir aux pointeurs nuls de style C me semble être un pas en arrière. Dire qu'il s'agit d'une «API de très bas niveau qui n'est pas destinée à être fortement utilisée», c'est comme dire que nous ne devrions pas nous soucier de la sécurité de la mémoire sur des architectures de microcontrôleurs inhabituelles car elles sont de très bas niveau et peu utilisées.

Chaque interaction avec l'allocateur implique un code non sécurisé quel que soit le type de retour de cette fonction. Les API d'allocation de bas niveau peuvent être mal utilisées, que le type de retour soit Option<NonZeroPtr<void>> ou *mut void .

Alloc::alloc en particulier est l'API de bas niveau et non destinée à être fortement utilisée. Des méthodes comme Alloc::alloc_one<T> ou Alloc::alloc_array<T> sont les alternatives qui seraient les plus utilisées et qui auraient un type de retour "plus agréable".

Un stateful AllocError ne vaut pas, mais un type de taille zéro qui implémente erreur et a un Display de allocation failure est agréable d'avoir. Si nous suivons la route NonZeroPtr<void> , je vois Result<NonZeroPtr<void>, AllocError> comme préférable à Option<NonZeroPtr<void>> .

Pourquoi se précipiter pour stabiliser :( !! Result<NonZeroPtr<void>, AllocErr> est indiscutablement plus agréable à utiliser pour les clients. Dire qu'il s'agit d'une "API de très bas niveau" qui n'a pas besoin d'être agréable est tout simplement déprimant. Le code à tous les niveaux devrait être aussi sûr et maintenable que possible; code obscur qui n'est pas constamment édité (et donc paginé dans la mémoire à court terme des gens) d'autant plus!

De plus, si nous voulons avoir des collections d'allocations polymorphes écrites par l'utilisateur, ce que j'espère certainement, c'est une quantité ouverte de code assez complexe utilisant directement des allocateurs.

Concernant la désallocation, opérationnellement, nous voulons presque certainement référencer / cloner l'alloactor une seule fois par collection arborescente. Cela signifie passer l'allocateur à chaque boîte d'allocation personnalisée détruite. Mais, c'est un problème ouvert sur la meilleure façon de faire cela dans Rust sans types linéaires. Contrairement à mon commentaire précédent, je serais d'accord avec du code dangereux dans les implémentations de collections pour cela, car le cas d'utilisation idéal change l'implémentation de Box , pas l'implémentation des traits d'allocateur et de désallocateur fractionnés. C'est-à-dire que nous pouvons faire des progrès stabilisables sans bloquer la linéarité.

@sfackler Je pense que nous avons besoin de certains types associés connectant le désallocateur à l'allocateur; il n'est peut-être pas possible de les moderniser.

@ Ericson2314 Il y a une "ruée" pour stabiliser parce que les gens veulent utiliser des allocateurs pour des choses réelles dans le monde réel. Ce n'est pas un projet scientifique.

À quoi ce type associé serait-il utilisé?

Les gens toujours , à moins que nous ne voulions diviser l'écosystème avec une nouvelle version 2.0 std.

Les types associés relieraient le désallocateur à l'allocateur. Chacun doit connaître cet autre pour que cela fonctionne. [Il y a toujours le problème d'utiliser le mauvais (dé) allocateur du bon type, mais j'accepte que personne n'ait proposé à distance de solution à cela.]

Si les gens peuvent simplement épingler une soirée, pourquoi avons-nous des versions stables? L'ensemble des personnes qui interagissent directement avec les API d'allocateur est beaucoup plus petit que les personnes qui souhaitent tirer parti de ces API en remplaçant par exemple l'allocateur global.

Pouvez-vous écrire du code qui montre pourquoi un désallocateur a besoin de connaître le type de son allocateur associé? Pourquoi l'API d'allocateur de C ++ n'a-t-elle pas besoin d'un mappage similaire?

Si les gens peuvent simplement épingler une soirée, pourquoi avons-nous des versions stables?

Pour indiquer la stabilité de la langue. Le code que vous écrivez contre cette version des choses ne se cassera jamais. Sur un compilateur plus récent. Vous épinglez un soir quand vous avez besoin de quelque chose de si mauvais, cela ne vaut pas la peine d'attendre l'itération finale de la fonctionnalité jugée de qualité digne de cette garantie.

L'ensemble des personnes qui interagissent directement avec les API d'allocateur est beaucoup plus petit que les personnes qui souhaitent tirer parti de ces API en remplaçant par exemple l'allocateur global.

Ah! Ce serait pour déplacer jemalloc hors de l'arbre, etc.? Personne n'a proposé de stabiliser les horribles hacks qui permettent de choisir l'allocateur global, juste le tas statique lui-même? Ou ai-je mal lu la proposition?

Il est proposé de stabiliser les horribles hacks qui permettent de choisir l'allocateur global, ce qui représente la moitié de ce qui nous permet de sortir jemalloc de l'arbre. Ce problème est l'autre moitié.

#[global_allocator] stabilisation des attributs: https://github.com/rust-lang/rust/issues/27389#issuecomment -336955367

Yikes

@ Ericson2314 Selon vous, quelle serait une façon non horrible de sélectionner l'allocateur global?

(Répondu sur https://github.com/rust-lang/rust/issues/27389#issuecomment-342285805)

La proposition a été modifiée pour utiliser * mut void.

@rfcbot résolu * mut u8

@rfcbot a évalué

Après quelques discussions sur IRC, j'approuve cela en sachant que nous _ n'avons pas_ l'intention de stabiliser un Box générique sur Alloc , mais plutôt sur un trait Dealloc avec un couverture appropriée impl, comme suggéré par @sfackler ici . S'il vous plaît laissez-moi savoir si j'ai mal compris l'intention.

@cramertj Juste pour clarifier, il est possible d'ajouter cette couverture impl après coup et de ne pas casser la définition Alloc que nous stabilisons ici?

@joshlf ouais , ça ressemblerait à ceci: https://github.com/rust-lang/rust/issues/32838#issuecomment -340959804

Comment allons-nous spécifier le Dealloc pour un Alloc ? J'imagine quelque chose comme ça?

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

Je suppose que cela nous met dans un territoire épineux WRT https://github.com/rust-lang/rust/issues/29661.

Ouais, je ne pense pas qu'il y ait un moyen pour que l'ajout de Dealloc soit rétrocompatible avec les définitions existantes de Alloc (qui n'ont pas ce type associé) sans avoir une valeur par défaut.

Si vous vouliez pouvoir saisir automatiquement le désallocateur correspondant à un allocateur, vous auriez besoin de plus qu'un simple type associé, mais d'une fonction pour produire une valeur de désallocateur.

Mais, cela peut être géré à l'avenir avec ce truc étant attaché à un sous-portrait séparé de Alloc je pense.

@sfackler je ne suis pas sûr de comprendre. Pouvez-vous écrire la signature de Box::new sous votre dessin?

Cela ignore la syntaxe de placement et tout cela, mais une façon de le faire serait

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

Notamment, nous devons être en mesure de produire une instance du désallocateur, pas seulement de connaître son type. Vous pouvez également paramétrer le Box sur le Alloc et stocker A::Dealloc place, ce qui pourrait aider à l'inférence de type. Nous pouvons faire ce travail après cette stabilisation en déplaçant Dealloc et deallocator vers un trait distinct:

pub trait SplitAlloc: Alloc {
    type Dealloc;

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

Mais à quoi ressemblerait l'implication 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);
        }
    }
}

Mais en supposant que nous stabilisons d'abord Alloc , tous les Alloc n'implémenteront pas Dealloc , n'est-ce pas? Et je pensais que la spécialisation impl était encore loin? En d'autres termes, en théorie, vous voudriez faire quelque chose comme ceci, mais je ne pense pas que cela fonctionne encore?

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

Si quoi que ce soit, nous aurions un

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

Mais je ne pense pas que ce soit vraiment nécessaire. Les cas d'utilisation des allocateurs personnalisés et des allocateurs globaux sont suffisamment distincts pour que je ne suppose pas qu'il y aurait une tonne de chevauchement entre eux.

Je suppose que cela pourrait fonctionner. Cela me semble beaucoup plus propre, cependant, d'avoir juste Dealloc dès le départ afin que nous puissions avoir une interface plus simple. J'imagine que nous pourrions avoir une interface assez simple et non controversée qui ne nécessiterait aucune modification du code existant qui implémente déjà 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 }
    ...
}

Je pensais que les valeurs par défaut associées aux types étaient problématiques?

Un Dealloc qui est une référence mutable à l'allocateur ne semble pas du tout utile - vous ne pouvez allouer qu'une chose à la fois, non?

Je pensais que les valeurs par défaut associées aux types étaient problématiques?

Oh, je suppose que les types par défaut associés sont suffisamment éloignés pour que nous ne puissions pas nous y fier.

Pourtant, nous pourrions avoir le plus 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;
    ...
}

et exiger juste que le réalisateur écrive un peu de passe-partout.

Un Dealloc qui est une référence mutable à l'allocateur ne semble pas très utile - vous ne pouvez allouer qu'une seule chose à la fois, non?

Ouais, bon point. Probablement un point discutable de toute façon compte tenu de votre autre commentaire.

Est-ce que deallocator prend self , &self ou &mut self ?

Probablement &mut self pour être cohérent avec les autres méthodes.

Y a-t-il des allocateurs qui préféreraient se prendre par valeur pour ne pas avoir à cloner l'état?

Le problème avec la prise de self par valeur est qu'elle empêche d'obtenir un Dealloc puis de continuer à allouer.

Je pense à un allocateur hypothétique «one-shot», bien que je ne sache pas à quel point c'est réel.

Un tel allocateur peut exister, mais prendre self par valeur exigerait que _tous_ les allocateurs fonctionnent de cette façon, et exclurait tout allocateur autorisant l'allocation après l'appel de deallocator .

J'aimerais toujours voir une partie de cela implémentée et utilisée dans les collections avant de penser à la stabiliser.

Pensez-vous que https://github.com/rust-lang/rust/issues/27336 ou les points discutés dans https://github.com/rust-lang/rust/issues/32838#issuecomment -339066870 nous permettront de avancer sur les collections?

Je m'inquiète de l'impact de l'approche d'alias de type sur la lisibilité de la documentation. Une façon (très verbeuse) d'autoriser la progression serait d'encapsuler les types:

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

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

Je sais que c'est pénible, mais il semble que les changements dont nous discutons ici sont suffisamment importants pour que si nous décidons d'aller de l'avant avec des traits d'allocation / dealloc fractionnés, nous devrions d'abord les essayer dans std, puis re-FCP.

Quel est le délai d'attente pour que ces éléments soient mis en œuvre?

La méthode grow_in_place ne renvoie aucun type de capacité excédentaire. Il appelle actuellement usable_size avec une mise en page, étend l'allocation à _au moins_ cette mise en page, mais si l'allocation est étendue au-delà de cette mise en page, les utilisateurs n'ont aucun moyen de le savoir.

J'ai du mal à comprendre l'avantage des méthodes alloc et realloc sur alloc_excess et realloc_excess .

Un allocateur a besoin de trouver un bloc mémoire adapté pour effectuer une allocation: cela nécessite de connaître la taille du bloc mémoire. Le fait que l'allocateur renvoie ensuite un pointeur ou le tuple "pointeur et taille du bloc de mémoire" ne fait aucune différence de performances mesurables.

Ainsi, alloc et realloc augmentent simplement la surface de l'API et semblent encourager l'écriture de code moins performant. Pourquoi les avons-nous dans l'API? Quel est leur avantage?


EDIT: ou en d'autres termes: toutes les fonctions potentiellement allouantes dans l'API doivent retourner Excess , ce qui supprime fondamentalement le besoin de toutes les méthodes _excess .

L'excès n'est intéressant que pour les cas d'utilisation qui impliquent des tableaux qui peuvent croître. Ce n'est ni utile ni pertinent pour Box ou BTreeMap , par exemple. Il peut y avoir un certain coût pour calculer l'excédent, et il y a certainement une API plus complexe, donc il ne me semble pas que du code qui ne se soucie pas de la capacité excédentaire devrait être obligé de payer pour cela.

Le calcul de l'excédent peut entraîner des frais

Pouvez-vous donner un exemple? Je ne connais pas, et je ne peux pas imaginer, un allocateur capable d'allouer de la mémoire mais qui ne sait pas combien de mémoire il alloue réellement (qui est ce que Excess est: quantité réelle de mémoire allouée; nous devrions renommez-le).

Le seul Alloc ator couramment utilisé où cela pourrait être légèrement controversé est POSIX malloc , qui même s'il calcule toujours le Excess interne, ne l'expose pas dans le cadre de son C API. Cependant, renvoyer la taille demandée comme Excess est correct, portable, simple, n'entraîne aucun coût du tout, et c'est ce que tout le monde qui utilise POSIX malloc suppose de toute façon.

jemalloc et fondamentalement tout autre Alloc ator fournit des API qui retournent le Excess sans encourir de frais, donc pour ces allocateurs, le retour du Excess est zéro coût aussi bien.

Il peut y avoir un certain coût pour calculer l'excédent, et il y a certainement une API plus complexe, donc il ne me semble pas que du code qui ne se soucie pas de la capacité excédentaire devrait être obligé de payer pour cela.

À l'heure actuelle, tout le monde paie déjà le prix du trait d'allocateur ayant deux API pour allouer de la mémoire. Et bien que l'on puisse construire une API Excess -less en plus d'une API Excess -full`, l'inverse n'est pas vrai. Alors je me demande pourquoi ce n'est pas fait comme ça:

  • Alloc méthodes de trait retournent toujours Excess
  • ajoutez un trait ExcessLessAlloc qui ne fait que supprimer les méthodes Excess de Alloc pour tous les utilisateurs qui 1) se soucient suffisamment d'utiliser Alloc mais 2) ne le font pas se soucier de la quantité réelle de mémoire actuellement allouée (cela me semble être une niche, mais je pense toujours qu'une telle API est agréable à avoir)
  • si un jour quelqu'un découvre un moyen d'implémenter des Alloc ators avec des chemins rapides pour des méthodes Excess sans, nous pouvons toujours fournir une implémentation personnalisée de ExcessLessAlloc pour cela.

FWIW Je viens de revenir sur ce fil parce que je ne peux pas implémenter ce que je veux en plus de Alloc . J'ai mentionné qu'il manquait grow_in_place_excess avant, mais je viens de me retrouver coincé car il manque également alloc_zeroed_excess (et qui sait quoi d'autre).

Je serais plus à l'aise si la stabilisation ici se concentrait d'abord sur la stabilisation d'une API Excess -full. Même si son API n'est pas la plus ergonomique pour tous les usages, une telle API permettrait au moins toutes les utilisations ce qui est une condition nécessaire pour montrer que le design n'est pas vicié.

Pouvez-vous donner un exemple? Je ne connais pas, et je ne peux pas imaginer, un allocateur capable d'allouer de la mémoire mais qui ne sait pas combien de mémoire il alloue réellement (qui est ce que Excess est: quantité réelle de mémoire allouée; nous devrions renommez-le).

La plupart des allocateurs utilisent aujourd'hui des classes de taille, où chaque classe de taille alloue uniquement des objets d'une taille fixe particulière, et les demandes d'allocation qui ne correspondent pas à une classe de taille particulière sont arrondies à la classe de taille la plus petite à l'intérieur. Dans ce schéma, il est courant de faire des choses comme avoir un tableau d'objets de classe de taille, puis de faire classes[size / SIZE_QUANTUM].alloc() . Dans ce monde, déterminer quelle classe de taille est utilisée nécessite des instructions supplémentaires: par exemple, let excess = classes[size / SIZE_QUANTUM].size . Ce n'est peut-être pas beaucoup, mais les performances des allocateurs hautes performances (comme jemalloc) sont mesurées en cycles d'horloge uniques, ce qui pourrait représenter une surcharge significative, surtout si cette taille finit par être transmise à travers une chaîne de retours de fonctions.

Pouvez-vous donner un exemple?

Au moins en passant de votre PR à alloc_jemalloc, alloc_excess exécute assez clairement plus de code que alloc : https://github.com/rust-lang/rust/pull/45514/files.

Dans ce schéma, il est courant de faire des choses comme avoir un tableau d'objets de classe de taille et ensuite faire des classes [size / SIZE_QUANTUM] .alloc (). Dans ce monde, déterminer quelle classe de taille est utilisée nécessite des instructions supplémentaires: par exemple, laissez excès = classes [size / SIZE_QUANTUM] .size

Alors laissez-moi voir si je suis bien:

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

Est-ce que c'est ça?


Au moins en passant de votre PR à alloc_jemalloc, alloc_excess exécute assez clairement plus de code que alloc

Ce PR était un correctif (pas un correctif de perf), il y a beaucoup de choses qui ne vont pas avec l'état actuel de notre couche jemalloc en termes de performances, mais comme ce PR, il renvoie au moins ce qu'il devrait:

  • nallocx est une fonction const au sens GCC, c'est-à-dire une vraie fonction pure. Cela signifie qu'il n'a pas d'effets secondaires, que ses résultats ne dépendent que de ses arguments, qu'il n'accède à aucun état global, que ses arguments ne sont pas des pointeurs (donc la fonction ne peut pas accéder à l'état global, lancez-les), et pour les programmes C / C ++, LLVM peut l'utiliser informations pour éluder l'appel si le résultat n'est pas utilisé. AFAIK Rust ne peut actuellement pas marquer les fonctions FFI C comme const fn ou similaire. C'est donc la première chose qui pourrait être corrigée et qui rendrait realloc_excess un coût nul pour ceux qui n'utilisent pas l'excédent tant que l'inlining et les optimisations fonctionnent correctement.
  • nallocx est toujours calculé pour les allocations alignées dans mallocx , c'est-à-dire que tout le code le compte déjà, mais mallocx rejette son résultat, donc ici nous le calculons en fait deux fois , et dans certains cas nallocx est presque aussi cher que mallocx ... J'ai un fork de jemallocator qui a des repères pour des choses comme ça dans ses branches, mais cela doit être corrigé en amont par jemalloc en fournissant une API qui ne gâche pas cela. Ce correctif, cependant, n'affecte que ceux qui utilisent actuellement le Excess .
  • et puis est le problème que nous calculons les drapeaux d'alignement deux fois, mais c'est quelque chose que LLVM peut optimiser de notre côté (et trivial à corriger).

Donc oui, cela ressemble à plus de code, mais ce code supplémentaire est du code que nous appelons en fait deux fois, car la première fois que nous l'avons appelé, nous avons jeté les résultats. Ce n'est pas impossible à réparer, mais je n'ai pas encore trouvé le temps de le faire.


EDIT: @sfackler J'ai réussi à libérer du temps pour cela aujourd'hui et j'ai pu rendre alloc_excess "gratuit" par rapport à alloc dans le chemin lent jemallocs, et n'avoir qu'une surcharge de ~ 1ns dans le chemin rapide de jemallocs. Je n'ai pas vraiment examiné la voie rapide en détail, mais il serait peut-être possible de l'améliorer davantage. Les détails sont ici: https://github.com/jemalloc/jemalloc/issues/1074#issuecomment -345040339

Est-ce que c'est ça?

Oui.

C'est donc la première chose qui pourrait être corrigée et qui rendrait realloc_excess un coût nul pour ceux qui n'utilisent pas l'excédent tant que l'inlining et les optimisations fonctionnent correctement.

Lorsqu'il est utilisé comme allocateur global, rien de tout cela ne peut être intégré.

Même si son API n'est pas la plus ergonomique pour tous les usages, une telle API permettrait au moins toutes les utilisations ce qui est une condition nécessaire pour montrer que le design n'est pas vicié.

Il n'y a littéralement aucun code sur Github qui appelle alloc_excess . Si c'est une caractéristique si cruciale, pourquoi personne ne l'a-t-elle jamais utilisée? Les API d'allocation de C ++ ne permettent pas d'accéder à la capacité excédentaire. Il semble incroyablement simple d'ajouter / stabiliser ces fonctionnalités à l'avenir de manière rétrocompatible s'il existe des preuves concrètes qu'elles améliorent les performances et que tout le monde se soucie suffisamment de les utiliser.

Lorsqu'il est utilisé comme allocateur global, rien de tout cela ne peut être intégré.

Alors c'est un problème que nous devrions essayer de résoudre, au moins pour les builds LTO, car les allocateurs globaux comme jemalloc s'appuient sur ceci: nallocx est la façon dont c'est _par conception_, et la première recommandation Les développeurs de jemalloc nous ont fait comprendre que les performances de alloc_excess sont que nous devrions avoir ces appels en ligne, et nous devrions propager correctement les attributs C, afin que le compilateur supprime les appels nallocx des sites utilisez le Excess , comme le font les compilateurs C et C ++.

Même si nous ne pouvons pas faire cela, l'API Excess peut toujours être rendue sans coût en corrigeant l'API jemalloc (j'ai une implémentation initiale d'un tel correctif dans mon rust-lang / fourche jemalloc). Nous pourrions soit maintenir cette API nous-mêmes, soit essayer de la placer en amont, mais pour que cela arrive en amont, nous devons expliquer pourquoi ces autres langages peuvent effectuer ces optimisations et Rust ne le peut pas. Ou nous devons avoir un autre argument, comme cette nouvelle API est nettement plus rapide que mallocx + nallocx pour les utilisateurs qui ont besoin du Excess .

Si c'est une caractéristique si cruciale, pourquoi personne ne l'a-t-elle jamais utilisée?

C'est une bonne question. std::Vec est l'affiche pour utiliser l'API Excess , mais il ne l'utilise pas actuellement, et tous mes commentaires précédents indiquant "ceci et cela sont absents du Excess API "J'essayais de faire en sorte que Vec utilise. L'API Excess :

Je ne peux pas savoir pourquoi personne n'utilise cette API. Mais étant donné que même la bibliothèque std ne peut pas l'utiliser pour la structure de données pour laquelle elle convient le mieux ( Vec ), si je devais deviner, je dirais que la raison principale est que cette API est actuellement cassée.

Si je devais deviner encore plus loin, je dirais que même ceux qui ont conçu cette API ne l'ont pas utilisée, principalement parce qu'aucune collection std utilise (c'est là que je m'attends à ce que cette API soit testée dans un premier temps) , et aussi parce qu'utiliser _excess et Excess partout pour signifier usable_size / allocation_size est extrêmement déroutant / ennuyeux à programmer avec.

C'est probablement parce que plus de travail a été mis dans les API sans Excess , et lorsque vous avez deux API, il est difficile de les garder synchronisées, il est difficile pour les utilisateurs de découvrir les deux et de savoir laquelle utiliser, et enfin, il est difficile pour les utilisateurs de préférer la commodité à faire la bonne chose.

Ou en d'autres termes, si j'ai deux API concurrentes et que je consacre 100% du travail à l'amélioration de l'une et 0% du travail à l'amélioration de l'autre, il n'est pas surprenant d'arriver à la conclusion que l'une est en pratique de manière significative. mieux que l'autre.

Autant que je sache, ce sont les deux seuls appels à nallocx dehors des tests jemalloc sur 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

Aucun d'entre eux ne ressemble à l'API alloc_excess , mais est plutôt utilisé de manière autonome pour calculer une taille d'allocation avant qu'elle ne soit faite.

Apache Arrow a envisagé d'utiliser nallocx dans leur implémentation, mais a constaté que les choses ne fonctionnaient pas bien:

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

Ce sont essentiellement les seules références à nallocx je puisse trouver. Pourquoi est-il important que la mise en œuvre initiale des API d'allocations prenne en charge une fonctionnalité aussi obscure?

Autant que je sache, ce sont les deux seuls appels à nallocx en dehors des tests jemalloc sur Github:

Du haut de ma tête, je sais qu'au moins le type de vecteur de Facebook l'utilise via l'implémentation de malloc de Facebook (politique de croissance de malloc et fbvector ; c'est une grande partie des vecteurs de C ++ sur facebook l'utilisent) et aussi que Chapel l'a utilisé pour améliorer le performances de leur type String ( ici et le problème de suivi ). Alors peut-être qu'aujourd'hui n'était pas le meilleur jour de Github?

Pourquoi est-il important que la mise en œuvre initiale des API d'allocations prenne en charge une fonctionnalité aussi obscure?

L'implémentation initiale d'une API d'allocateur n'a pas besoin de prendre en charge cette fonctionnalité.

Mais une bonne prise en charge de cette fonctionnalité devrait bloquer la stabilisation d'une telle API.

Pourquoi devrait-il bloquer la stabilisation s'il peut être ajouté ultérieurement de manière rétrocompatible?

Pourquoi devrait-il bloquer la stabilisation s'il peut être ajouté ultérieurement de manière rétrocompatible?

Parce que pour moi au moins, cela signifie que seule la moitié de l'espace de conception a été suffisamment explorée.

Pensez-vous que les parties non excédentaires de l'API seront affectées par la conception de la fonctionnalité excédentaire? J'avoue que je n'ai suivi cette discussion que sans enthousiasme, mais cela me semble peu probable.

Si nous ne pouvons pas créer cette 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

aussi efficace que celui-ci:

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

alors nous avons de plus gros problèmes.

Pensez-vous que les parties non excédentaires de l'API seront affectées par la conception de la fonctionnalité excédentaire?

Alors oui, je m'attends à ce qu'une bonne API excédentaire ait un effet énorme sur la conception de la fonctionnalité liée non excédentaire: elle la supprimerait complètement.

Cela empêcherait la situation actuelle d'avoir deux API qui ne sont pas synchronisées, et dans lesquelles l'excès-api a moins de fonctionnalités que celui qui est sans excès. Bien que l'on puisse créer une API sans excès sur une API trop pleine, l'inverse n'est pas vrai.

Ceux qui veulent abandonner le Excess devraient simplement le laisser tomber.

Pour clarifier, s'il y avait un moyen d'ajouter une méthode alloc_excess après coup de manière rétrocompatible, alors vous seriez d'accord? (mais bien sûr, stabiliser sans alloc_excess signifie que l'ajouter plus tard serait un changement radical; je demande juste que je comprenne votre raisonnement)

@joshlf C'est très simple de le faire.

: bell: Ceci entre maintenant dans sa période de commentaires finale , selon l' examen ci-dessus . :cloche:

Ceux qui veulent abandonner l'Excès devraient simplement le laisser tomber.

Alternativement, les 0,01% de personnes qui se soucient de la capacité excédentaire peuvent utiliser une autre méthode.

@sfackler C'est ce que j'obtiens en prenant une pause de deux semaines avec la rouille - j'oublie la méthode impls par défaut :)

Alternativement, les 0,01% de personnes qui se soucient de la capacité excédentaire peuvent utiliser une autre méthode.

Où obtenez-vous ce numéro?

Toutes mes structures de données Rust sont à plat en mémoire. La capacité de le faire est la seule raison pour laquelle j'utilise Rust; si je pouvais tout simplement Boxer, j'utiliserais une langue différente. Donc je me fiche des Excess les 0.01% du temps, je m'en soucie tout le temps.

Je comprends que cela est spécifique à un domaine et que dans d'autres domaines, les gens ne se soucieraient jamais du Excess , mais je doute que seulement 0,01% des utilisateurs de Rust s'en soucient (je veux dire, beaucoup de gens utilisent Vec et String , qui sont les structures de données affiche-enfant pour Excess ).

J'obtiens ce nombre du fait qu'il y a environ 4 choses au total qui utilisent nallocx, par rapport à l'ensemble des choses qui utilisent malloc.

@gnzlbg

Êtes-vous en train fn alloc(layout) -> (ptr, excess) suggérer que si nous le faisions «correctement» dès le début, nous n'aurions que fn alloc(layout) -> ptr ? Cela me semble loin d'être évident. Même si l'excès est disponible, il semble naturel d'avoir cette dernière API pour les cas d'utilisation où l'excès n'a pas d'importance (par exemple, la plupart des arborescences), même s'il est implémenté comme alloc_excess(layout).0 .

@rkruppe

Cela me semble loin d'être évident. Même si un excès est disponible, il semble naturel d'avoir cette dernière API pour les cas d'utilisation où l'excès n'a pas d'importance (par exemple, la plupart des arborescences), même si elle est implémentée en tant que alloc_excess (layout) .0.

Actuellement, l'API en excès est implémentée en plus de celle sans excès. L'implémentation de Alloc pour un allocateur sans excès nécessite que l'utilisateur fournisse les méthodes alloc et dealloc .

Cependant, si je veux implémenter Alloc pour un allocateur excédentaire, je dois fournir plus de méthodes (au moins alloc_excess , mais cela augmente si nous entrons dans realloc_excess , alloc_zeroed_excess , grow_in_place_excess , ...).

Si nous devions le faire dans l'autre sens, c'est-à-dire implémenter l'API sans excès comme une subtilité en plus de celle excédentaire, puis implémenter alloc_excess et dealloc suffit pour prendre en charge les deux types d'allocateurs.

Les utilisateurs qui ne se soucient pas ou ne peuvent pas retourner ou interroger l'excédent peuvent simplement renvoyer la taille d'entrée ou la mise en page (ce qui est un petit inconvénient), mais les utilisateurs qui peuvent gérer et veulent gérer l'excès n'ont pas besoin de mettre en œuvre plus de méthodes.


@sfackler

J'obtiens ce nombre du fait qu'il y a environ 4 choses au total qui utilisent nallocx, par rapport à l'ensemble des choses qui utilisent malloc.

Compte tenu de ces faits sur l'utilisation de _excess dans l'écosystème Rust:

  • 0 choses au total utilisent _excess dans l'écosystème de la rouille
  • 0 choses au total utilisent _excess dans la bibliothèque rust std
  • même pas Vec et String peuvent utiliser correctement l'API _excess dans la bibliothèque rust std
  • l'API _excess est instable, désynchronisée avec l'API sans excès, boguée jusqu'à très récemment (n'a même pas renvoyé le excess du tout), ...

    et étant donné ces faits sur l'utilisation de _excess dans d'autres langues:

  • L'API de jemalloc n'est pas supportée nativement par les programmes C ou C ++ en raison de la compatibilité descendante

  • Les programmes C et C ++ qui souhaitent utiliser l'API excédentaire de jemalloc doivent faire tout leur possible pour l'utiliser en:

    • désactivation de l'allocateur système et de jemalloc (ou tcmalloc)

    • ré-implémenter la bibliothèque std de leur langage (dans le cas de C ++, implémenter une bibliothèque std incompatible)

    • écrivez toute leur pile au-dessus de cette bibliothèque std incompatible

  • certaines communautés (Firefox l'utilise, Facebook réimplémente les collections de la bibliothèque standard C ++ pour pouvoir l'utiliser, ...) font encore tout leur possible pour l'utiliser.

Ces deux arguments me semblent plausibles:

  • L'API excess dans std n'est pas utilisable, donc la bibliothèque std ne peut pas l'utiliser, donc personne ne le peut, c'est pourquoi elle n'est pas utilisée une seule fois dans l'écosystème Rust .
  • Même si C et C ++ rendent presque impossible l'utilisation de cette API, les grands projets avec de la main-d'œuvre se donnent beaucoup de mal pour l'utiliser, donc au moins une communauté potentiellement minuscule de personnes se soucie beaucoup de cela.

Votre argument:

  • Personne n'utilise l'API _excess , donc seulement 0,01% des gens s'en soucient.

ne fait pas.

@alexcrichton La décision de passer de -> Result<*mut u8, AllocErr> à -> *mut void peut être une surprise importante pour les personnes qui ont suivi le développement original des RFC d'allocateur.

Je ne suis pas en désaccord avec les arguments que vous faites , mais il semblait néanmoins qu'un bon nombre de personnes auraient été disposées à vivre avec le "poids lourd" de Result rapport à la probabilité accrue de manquer un nul- vérifier la valeur renvoyée.

  • J'ignore les problèmes d'efficacité d'exécution imposés par l'ABI parce que, comme je suppose que nous pourrions les résoudre d'une manière ou d'une autre via des astuces de compilateur.

Y a-t-il moyen d'obtenir une visibilité accrue sur ce changement tardif à lui seul?

Une façon (par surprise): changez la signature maintenant, dans un PR seul, sur la branche principale, tandis que Allocator est toujours instable. Et puis voyez qui se plaint sur le PR (et qui célèbre!).

  • Est-ce trop sévère? Il semble que les définitions soient moins lourdes que de coupler un tel changement avec la stabilisation ...

Sur le point de savoir s'il faut retourner *mut void ou retourner Result<*mut void, AllocErr> : Il est possible que nous revoyions l'idée de traits d'allocateur séparés "de haut niveau" et "de bas niveau", comme indiqué dans la prise II du RFC d'allocateur .

(Évidemment, si j'avais une objection sérieuse à la valeur de retour *mut void , je la classerais comme une préoccupation via le fcpbot. Mais à ce stade, je fais assez confiance au jugement de l'équipe libs, peut-être en une partie due à la fatigue de cette saga d'allocateurs.)

@pnkfelix

La décision de passer de -> Result<*mut u8, AllocErr> à -> *mut void peut être une surprise importante pour les personnes qui ont suivi le développement original des RFC d'allocateur.

Ce dernier implique que, comme discuté, la seule erreur que nous souhaitons exprimer est MOO. Ainsi, un poids intermédiaire légèrement plus léger qui a toujours l'avantage d'une protection contre l'échec accidentel de la vérification des erreurs est -> Option<*mut void> .

@gnzlbg

L'API excédentaire dans std n'est pas utilisable, donc la bibliothèque std ne peut pas l'utiliser, donc personne ne le peut, c'est pourquoi elle n'est pas utilisée une seule fois dans l'écosystème Rust.

Alors allez le réparer.

@pnkfelix

Sur le point de savoir s'il faut renvoyer mut void ou renvoyer Result < mut void, AllocErr>: Il est possible que nous revoyions l'idée de traits d'allocateur séparés "haut niveau" et "bas niveau", comme discuté dans la prise II de la RFC Allocator.

C'étaient essentiellement nos pensées, sauf que l'API de haut niveau serait dans Alloc elle-même sous la forme alloc_one , alloc_array etc. caractéristiques pour voir sur quelles API les gens convergent.

@pnkfelix

La raison pour laquelle j'ai fait que Layout n'implémente que Clone et non Copy est que je voulais laisser ouverte la possibilité d'ajouter plus de structure au type de mise en page. En particulier, je suis toujours intéressé à essayer de faire en sorte que la mise en page tente de suivre toute structure de type utilisée pour la construire (par exemple, 16 tableaux de struct {x: u8, y: [char; 215]}), de sorte que les allocateurs aient la possibilité d'exposer des routines d'instrumentation qui indiquent de quels types leur contenu actuel est composé.

Cela a-t-il été expérimenté quelque part?

@sfackler J'ai déjà fait la plupart d'entre eux, et tout cela peut être fait avec l'API dupliquée (sans excès + méthodes _excess ). Je serais bien d'avoir deux API et de ne pas avoir une API _excess complète

La seule chose qui m'inquiète encore un peu est que pour implémenter un allocateur dès maintenant, il faut implémenter alloc + dealloc , mais alloc_excess + dealloc devrait également fonctionner. Serait-il possible de donner à alloc une implémentation par défaut en termes de alloc_excess plus tard ou est-ce un changement impossible ou cassant? En pratique, la plupart des allocateurs vont de toute façon implémenter la plupart des méthodes, donc ce n'est pas un gros problème, mais plutôt un souhait.


jemallocator implémente Alloc deux fois (pour Jemalloc et &Jemalloc ), où l'implémentation Jemalloc pour certains method est juste un (&*self).method(...) qui transmet l'appel de méthode à l'implémentation &Jemalloc . Cela signifie qu'il faut conserver manuellement les deux implémentations de Alloc pour Jemalloc synchronisées. Si obtenir des comportements différents pour les implémentations &/_ peut être tragique ou non, je ne sais pas.


J'ai trouvé très difficile de savoir ce que les gens font réellement avec le trait Alloc dans la pratique. Les seuls projets que j'ai trouvés qui l'utilisent continueront de toute façon de l'utiliser la nuit (servo, redox) et ne l'utiliseront que pour changer l'allocateur global. Cela m'inquiète beaucoup de ne pas trouver de projet qui l'utilise comme paramètre de type de collection (peut-être ai-je été malchanceux et il y en a?) Je cherchais en particulier des exemples d'implémentation de SmallVec et ArrayVec en plus d'un type Vec (puisque std::Vec n'a pas de Alloc type encore), et se demandait également comment le clonage entre ces types ( Vec s avec un Alloc ator différent) fonctionnerait (la même chose s'applique probablement au clonage Box es avec différents Alloc s). Y a-t-il des exemples de ce à quoi ces implémentations ressembleraient quelque part?

Les seuls projets que j'ai trouvés qui l'utilisent continueront de toute façon de l'utiliser la nuit (servo, redox)

Pour ce que ça vaut, Servo essaie de se débarrasser des fonctionnalités instables lorsque cela est possible: https://github.com/servo/servo/issues/5286

C'est aussi un problème de poule et d'oeuf. De nombreux projets n'utilisent pas encore Alloc car il est toujours instable.

Je ne vois pas vraiment pourquoi nous devrions avoir un complément complet d'API _excess en premier lieu. Ils existaient à l'origine pour refléter l'API expérimentale * allocm de jemalloc, mais ceux-ci ont été supprimés dans la version 4.0 il y a plusieurs années au profit de ne pas dupliquer toute leur surface d'API. Il semble que nous pourrions suivre leur exemple?

Serait-il possible de donner à alloc une implémentation par défaut en termes d'alloc_excess plus tard ou est-ce un changement impossible ou cassant?

Nous pouvons ajouter une implémentation par défaut de alloc en termes de alloc_excess , mais alloc_excess devra avoir une implémentation par défaut en termes de alloc . Tout fonctionne bien si vous en implémentez un ou les deux, mais si vous ne l'implémentez pas non plus, votre code se compilera mais se répétera à l'infini. Cela s'est déjà produit (peut-être pour Rand ?), Et nous pourrions avoir un moyen de dire que vous devez implémenter au moins une de ces fonctions, mais nous ne nous soucions pas de laquelle.

Cela m'inquiète beaucoup de ne pas trouver de projet qui l'utilise comme paramètre de type de collection (peut-être ai-je été malchanceux et il y en a?)

Je ne connais personne qui fasse ça.

Les seuls projets que j'ai trouvés qui l'utilisent continueront de toute façon de l'utiliser la nuit (servo, redox)

Une grande chose qui empêche cela d'avancer est que les collections stdlib ne supportent pas encore les allocateurs paramétriques. Cela exclut également la plupart des autres caisses, car la plupart des collections externes utilisent des collections internes sous le capot ( Box , Vec , etc.).

Les seuls projets que j'ai trouvés qui l'utilisent continueront de toute façon de l'utiliser la nuit (servo, redox)

Une grande chose qui empêche cela d'avancer est que les collections stdlib ne supportent pas encore les allocateurs paramétriques. Cela exclut également la plupart des autres caisses, car la plupart des collections externes utilisent des caisses internes sous le capot (Box, Vec, etc.).

Cela s'applique à moi - j'ai un noyau jouet, et si je pouvais, j'utiliserais Vec<T, A> , mais à la place, je dois avoir une façade d'allocateur global modifiable par l'intérieur, ce qui est grossier.

@remexre Comment le paramétrage de vos structures de données va-t-il éviter un état global avec une mutabilité intérieure?

Il y aura toujours un état global modifiable à l'intérieur, je suppose, mais il semble beaucoup plus sûr d'avoir une configuration où l'allocateur global est inutilisable jusqu'à ce que la mémoire soit entièrement mappée que d'avoir une fonction globale set_allocator .


EDIT : Je viens de réaliser que je n'ai pas répondu à la question. En ce moment, j'ai quelque chose comme:

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

AreaAllocator est un trait qui me permet (lors de l'exécution) de vérifier que les allocateurs ne se "chevauchent pas" accidentellement (en termes de plages d'adresses dans lesquelles ils allouent). BumpAllocator n'est utilisé que très tôt, pour l'espace de travail lors du mappage du reste de la mémoire pour créer les RealAllocator s.

Idéalement, j'aimerais avoir un Mutex<Option<RealAllocator>> (ou un wrapper qui le rend "insert-only") être le seul allocateur, et que tout alloué au début soit paramétré par le early-boot BumpAllocator . Cela me permettrait également de m'assurer que BumpAllocator ne soit pas utilisé après le démarrage anticipé, car les éléments que j'alloue ne pourraient pas lui survivre.

@sfackler

Je ne vois pas vraiment pourquoi nous devrions avoir un complément complet d'API _excess en premier lieu. Ils existaient à l'origine pour refléter l'API expérimentale * allocm de jemalloc, mais ceux-ci ont été supprimés dans la version 4.0 il y a plusieurs années au profit de ne pas dupliquer toute leur surface d'API. Il semble que nous pourrions suivre leur exemple?

Actuellement shrink_in_place appelle xallocx qui renvoie la taille d'allocation réelle. Parce que shrink_in_place_excess n'existe pas, il jette cette taille, et les utilisateurs doivent appeler nallocx pour le recalculer, dont le coût dépend vraiment de la taille de l'allocation.

Donc au moins certaines fonctions d'allocation jemalloc que nous utilisons déjà nous retournent la taille utilisable, mais l'API actuelle ne nous permet pas de l'utiliser.

@remexre

Quand je travaillais sur mon noyau de jouet, éviter l'allocateur global pour m'assurer qu'aucune allocation ne se produise jusqu'à ce qu'un allocateur soit mis en place était aussi un de mes objectifs. Heureux d'entendre que je ne suis pas le seul!

Je n'aime pas le mot Heap pour l'allocateur global par défaut. Pourquoi pas Default ?

Autre point de clarification: la RFC 1974 met tout cela dans std::alloc mais c'est actuellement dans std::heap . Quel emplacement est proposé pour la stabilisation?

@jethrogb "Heap" est un terme assez canonique pour "cette chose que malloc vous donne des pointeurs vers" - quelles sont vos préoccupations avec le terme?

@sfackler

"cette chose que malloc vous donne des pointeurs vers"

Sauf que dans mon esprit, c'est ce qu'est System .

Ah bien sûr. Global est un autre nom alors peut-être? Puisque vous utilisez #[global_allocator] pour le sélectionner.

Il peut y avoir plusieurs allocateurs de tas (par exemple libc et jemalloc préfixé). Que diriez-vous de renommer std::heap::Heap en std::heap::Default et #[global_allocator] en #[default_allocator] ?

Le fait que c'est ce que vous obtenez si vous ne spécifiez pas le contraire (vraisemblablement lorsque, par exemple, Vec gagne un paramètre / champ de type supplémentaire pour l'allocateur) est plus important que le fait qu'il n'a pas "par -instances "état (ou instances vraiment).

La période de commentaires finale est maintenant terminée.

En ce qui concerne FCP, je pense que le sous-ensemble d'API qui a été proposé pour la stabilisation est d'une utilité très limitée. Par exemple, il ne prend pas en charge la caisse jemallocator .

De quelle manière? jemallocator devra peut-être signaler certains des impls de méthodes instables derrière un indicateur de fonctionnalité, mais c'est tout.

Si jemallocator sur Rust stable ne peut pas implémenter par exemple Alloc::realloc en appelant je_rallocx mais doit s'appuyer sur la valeur par défaut alloc + copy + dealloc impl, alors ce n'est pas un remplacement acceptable pour le la bibliothèque standard alloc_jemalloc crate IMO.

Bien sûr, vous pouvez obtenir quelque chose à compiler, mais ce n'est pas une chose particulièrement utile.

Pourquoi? C ++ n'a aucun concept de réallocation dans son API d'allocation et cela ne semble pas avoir paralysé le langage. Ce n'est évidemment pas idéal, mais je ne comprends pas pourquoi ce serait inacceptable.

Les collections C ++ n'utilisent généralement pas realloc car les constructeurs de déplacement C ++ peuvent exécuter du code arbitraire, pas parce que realloc n'est pas utile.

Et la comparaison n'est pas avec C ++, mais avec la bibliothèque standard Rust actuelle avec le support intégré de jemalloc. Basculer vers et hors de l'allocateur std avec seulement ce sous-ensemble de Alloc API serait une régression.

Et realloc est un exemple. jemallocator implémente également actuellement alloc_zeroed , alloc_excess , usable_size , grow_in_place , etc.

Il est proposé de stabiliser alloc_zeroed. Autant que je sache (regardez en haut), il n'y a littéralement aucune utilisation de alloc_excess dans l'existence. Pourriez-vous montrer du code qui régressera si cela revient à une implémentation par défaut.

Plus généralement, cependant, je ne vois pas pourquoi c'est un argument contre la stabilisation d'une partie de ces API. Si vous ne souhaitez pas utiliser jemallocator, vous pouvez continuer à ne pas l'utiliser.

Peut-on faire de Layout::array<T>() un const fn?

Cela peut paniquer, donc pas pour le moment.

Cela peut paniquer, donc pas pour le moment.

Je vois ... je me contenterais de const fn Layout::array_elem<T>() qui serait un équivalent non paniqué de Layout::<T>::repeat(1).0 .

@mzabaluev Je pense que ce que vous décrivez équivaut à Layout::new<T>() . Il peut actuellement paniquer, mais c'est simplement parce qu'il est implémenté en utilisant Layout::from_size_align puis .unwrap() , et je pense que cela pourrait être fait différemment.

@joshlf Je pense que cette structure a la taille de 5, alors qu'en tant qu'éléments d'un tableau, ils sont placés tous les 8 octets en raison de l'alignement:

struct Foo {
    bar: u32,
    baz: u8
}

Je ne suis pas sûr qu'un tableau de Foo inclurait le remplissage du dernier élément pour son calcul de taille, mais c'est ma forte attente.

Dans Rust, la taille d'un objet est toujours un multiple de son alignement de sorte que l'adresse de l'élément n ème d'un tableau est toujours array_base_pointer + n * size_of<T>() . Ainsi, la taille d'un objet dans un tableau est toujours la même que la taille de cet objet seul. Voir la page Rustonomicon sur repr (Rust) pour plus de détails.

OK, il s'avère qu'une structure est complétée jusqu'à son alignement, mais AFAIK ce n'est pas une garantie stable sauf dans #[repr(C)] .
Quoi qu'il en soit, créer Layout::new a const fn serait également le bienvenu.

C'est le comportement documenté (et donc garanti) d'une fonction stable:

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

Renvoie la taille d'un type en octets.

Plus précisément, il s'agit du décalage en octets entre les éléments successifs d'un tableau avec ce type d'élément comprenant le remplissage d'alignement. Ainsi, pour tout type T et de longueur n , [T; n] a une taille de n * size_of::<T>() .

Merci. Je viens de réaliser que tout const fn qui multiplie le résultat de Layout::new serait intrinsèquement paniqué à son tour (à moins que ce ne soit fait avec saturating_mul ou quelque chose du genre), donc je suis de retour à la case départ. Poursuivant avec une question sur les paniques dans le problème de suivi const fn.

La macro panic!() n'est actuellement pas prise en charge dans les expressions constantes, mais les paniques de l'arithmétique vérifiée sont générées par le compilateur et ne sont pas affectées par cette limitation:

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

Ceci est lié à Alloc::realloc mais pas à la stabilisation de l'interface minimale ( realloc n'en fait pas partie):

Actuellement, parce que Vec::reserve/double appelle RawVec::reserve/double qui appelle Alloc::realloc , l'implémentation par défaut de Alloc::realloc copie les éléments vectoriels morts (dans la plage [len(), capacity()) ) . Dans le cas absurde d'un énorme vecteur vide qui veut insérer des éléments capacity() + 1 et ainsi réallouer, le coût de toucher toute cette mémoire n'est pas négligeable.

En théorie, si l'implémentation par défaut Alloc::realloc prend également une plage "bytes_used", elle pourrait simplement copier la partie pertinente lors de la réallocation. En pratique, au moins jemalloc remplace Alloc::realloc default impl avec un appel à rallocx . Que faire un alloc / dealloc copie de danse uniquement la mémoire appropriée soit plus rapide ou plus lent qu'un appel rallocx dépendra probablement de beaucoup de choses (est-ce que rallocx gère pour étendre le bloc en place? combien de mémoire inutile rallocx copier? etc.).

https://github.com/QuiltOS/rust/tree/allocator-error J'ai commencé à démontrer comment je pense que le type d'erreur associé résout nos problèmes de collections et de gestion des erreurs en faisant la généralisation elle-même. En particulier, notez comment dans les modules je change que je

  • Réutilisez toujours l'implémentation Result<T, A::Err> pour l'implémentation T
  • Jamais unwrap ou quoi que ce soit d'autre partiel
  • Non oom(e) dehors de AbortAdapter .

Cela signifie que les changements que je fais sont tout à fait sûrs et aussi insensés! Travailler à la fois avec le retour d'erreur et l'abandon d'erreur ne devrait pas nécessiter d'effort supplémentaire pour maintenir les invariants mentaux - le vérificateur de type fait tout le travail.

Je me souviens --- je pense que dans le RFC de @Gankro ? ou le fil pré-rfc --- les gens de Gecko / Servo disant que c'était bien de ne pas avoir la faillibilité des collections faire partie de leur type. Eh bien, je peux ajouter un #[repr(transparent)] à AbortAdapter afin que les collections puissent être transmutées en toute sécurité entre Foo<T, A> et Foo<T, AbortAdapter<A>> (dans des enveloppes sûres), ce qui permet de le faire librement basculer d'avant en arrière sans dupliquer chaque méthode. [Pour la rétro-compatibilité, les collections de bibliothèques standard devront être dupliquées dans tous les cas, mais les méthodes utilisateur n'ont pas besoin d'être comme Result<T, !> est de nos jours assez facile à travailler.]

Malheureusement, le code ne sera pas entièrement contrôle de type car la modification des paramètres de type d'un élément de langue (boîte) confond le compilateur (surprise!). Le commit de boîte à l'origine de l'ICE est le dernier - tout avant qu'il ne soit bon. @eddyb a corrigé rustc dans # 47043!

edit @joshlf J'ai été informé de votre https://github.com/rust-lang/rust/pull/45272 , et je l'ai incorporé ici. Merci!

La mémoire persistante (par exemple http://pmem.io ) est la prochaine grande chose, et Rust doit être positionné pour bien fonctionner avec elle.

J'ai récemment travaillé sur un wrapper Rust pour un allocateur de mémoire persistante (en particulier, libpmemcto). Quelles que soient les décisions prises concernant la stabilisation de cette API, elle doit: -

  • Être capable de prendre en charge un wrapper performant autour d'un allocateur de mémoire persistant comme libpmemcto;
  • Être capable de spécifier (paramétrer) les types de collection par allocateur (pour le moment, il faut dupliquer Box, Rc, Arc, etc.)
  • Être capable de cloner des données entre les allocateurs
  • Être capable de prendre en charge les structures stockées en mémoire persistante avec des champs qui sont réinitialisés lors de l'instanciation d'un pool de mémoire persistante, c'est-à-dire que certaines structures de mémoire persistante doivent avoir des champs qui ne sont stockés que temporairement sur le tas. Mes cas d'utilisation actuels font référence au pool de mémoire persistante utilisé pour l'allocation et aux données transitoires utilisées pour les verrous.

En passant, le développement pmem.io (PMDK d'Intel) fait un usage intensif d'un allocateur jemalloc modifié sous les couvertures - il semble donc prudent d'utiliser jemalloc comme exemple de consommateur d'API.

Serait-il possible de réduire la portée de ceci pour ne couvrir que les GlobalAllocator abord jusqu'à ce que nous ayons plus d'expérience avec l'utilisation des Alloc ators dans les collections?

IIUC cela répondrait déjà aux besoins de servo et nous permettrait d'expérimenter le paramétrage des conteneurs en parallèle. À l'avenir, nous pouvons soit déplacer les collections pour utiliser GlobalAllocator place, soit simplement ajouter un impl général de Alloc pour GlobalAllocator afin que ceux-ci puissent être utilisés pour toutes les collections.

Pensées?

@gnzlbg Pour que l' #[global_allocator] soit utile (au-delà de la sélection de heap::System ), le trait Alloc doit également être stable, afin qu'il puisse être implémenté par des caisses comme https: / /crates.io/crates/jemallocator. Il n'y a aucun type ou trait nommé GlobalAllocator pour le moment, proposez-vous une nouvelle API?

il n'y a aucun type ou trait nommé GlobalAllocator pour le moment, proposez-vous une nouvelle API?

Ce que j'ai suggéré, c'est de renommer l'API "minimale" que @alexcrichton a suggéré de stabiliser ici de Alloc à GlobalAllocator pour ne représenter que les allocateurs globaux, et de laisser la porte ouverte pour que les collections soient paramétrées par un autre le trait d'allocateur à l'avenir (ce qui ne signifie pas que nous ne pouvons pas les paramétrer par le trait GlobalAllocator ).

IIUC servo n'a actuellement besoin que de pouvoir changer d'allocateur global (par opposition à être également capable de paramétrer certaines collections par un allocateur). Alors peut-être qu'au lieu d'essayer de stabiliser une solution qui devrait être à l'épreuve du futur pour les deux cas d'utilisation, nous pouvons résoudre uniquement le problème d'allocateur global maintenant, et déterminer comment paramétrer les collections par les allocateurs plus tard.

Je ne sais pas si cela a du sens.

Le servo IIUC n'a actuellement besoin que de pouvoir changer d'allocateur global (au lieu de pouvoir également paramétrer certaines collections par un allocateur).

C'est correct, mais:

  • Si un trait et sa méthode sont stables pour pouvoir être implémentés, alors il peut aussi être appelé directement sans passer par std::heap::Heap . Donc ce n'est pas seulement un attribut global de trait, c'est un trait pour les allocateurs (même si nous finissons par en faire un différent pour les collections génériques par rapport aux allocateurs) et GlobalAllocator n'est pas un nom particulièrement bon.
  • La caisse jemallocator implémente actuellement alloc_excess , realloc , realloc_excess , usable_size , grow_in_place et shrink_in_place qui ne sont pas partie de l'API minimale proposée. Ceux-ci peuvent être plus efficaces que l'impl par défaut, donc les supprimer serait une régression des performances.

Les deux points ont du sens. Je pensais juste que le seul moyen d'accélérer considérablement la stabilisation de cette fonctionnalité était de supprimer une dépendance à ce sujet, ce qui était également un bon trait pour paramétrer les collections dessus.

[Ce serait bien si Servo pouvait être comme (stable | caisse mozilla officielle), et la cargaison pourrait appliquer cela, pour supprimer un peu de pression ici.]

@ Ericson2314 servo n'est pas le seul projet qui souhaite utiliser ces API.

@ Ericson2314 Je ne comprends pas ce que cela signifie, pourriez-vous reformuler?

Pour le contexte: Servo utilise actuellement un certain nombre de fonctionnalités instables (y compris #[global_allocator] ), mais nous essayons de nous en éloigner lentement (soit en mettant à jour vers un compilateur qui a stabilisé certaines fonctionnalités, soit en trouvant des alternatives stables. ) Ceci est suivi sur https://github.com/servo/servo/issues/5286. Donc, stabiliser #[global_allocator] serait bien, mais cela ne bloque aucun travail Servo.

Firefox s'appuie sur le fait que Rust std utilise par défaut l'allocateur système lors de la compilation d'un cdylib , et que mozjemalloc qui finit par être lié dans le même binaire définit des symboles comme malloc et free que "shadow" (je ne connais pas la terminologie appropriée de l'éditeur de liens) ceux de la libc. Ainsi, les allocations du code Rust dans Firefox finissent par utiliser mozjemalloc. (Ceci est sous Unix, je ne sais pas comment cela fonctionne sous Windows.) Cela fonctionne, mais cela me semble fragile. Firefox utilise Rust stable, et j'aimerais qu'il utilise #[global_allocator] pour sélectionner explicitement mozjemalloc pour rendre l'ensemble de la configuration plus robuste.

@SimonSapin plus je joue avec les allocateurs et les collections, plus j'ai tendance à penser que nous ne voulons pas encore paramétrer les collections par Alloc , car en fonction de l'allocateur, une collection pourrait vouloir offrir un API différente, la complexité de certaines opérations change, certains détails de collecte dépendent en fait de l'allocateur, etc.

Je voudrais donc suggérer une manière de faire des progrès ici.

Étape 1: allocateur de tas

Nous pourrions nous limiter au début pour essayer de laisser les utilisateurs sélectionner l'allocateur pour le tas (ou l'allocateur système / plate-forme / global / magasin libre, ou comme vous préférez le nommer) dans Rust stable.

La seule chose que nous paramétrons initialement est Box , qui n'a besoin que d'allouer ( new ) et de désallouer ( drop ) de la mémoire.

Ce trait d'allocateur pourrait initialement avoir l'API proposée par @alexcrichton (ou quelque peu étendue), et ce trait d'allocateur pourrait, tous les soirs, avoir une API légèrement étendue pour prendre en charge les collections std:: .

Une fois que nous y serons, les utilisateurs qui souhaitent migrer vers stable pourront le faire, mais pourraient obtenir un impact négatif sur les performances, en raison de l'instabilité de l'API.

Étape 2: allocateur de tas sans impact sur les performances

À ce stade, nous pouvons réévaluer les utilisateurs qui ne peuvent pas passer à la stabilité en raison d'un problème de performances, et décider comment étendre cette API et la stabiliser.

Étapes 3 à N: prise en charge des allocateurs personnalisés dans les collections std .

Premièrement, c'est difficile, donc cela pourrait ne jamais arriver, et je pense que cela ne se produit jamais n'est pas une mauvaise chose.

Lorsque je souhaite paramétrer une collection avec un allocateur personnalisé, j'ai un problème de performances ou un problème d'utilisabilité.

Si j'ai un problème d'utilisation, je veux généralement une API de collecte différente qui exploite les fonctionnalités de mon allocateur personnalisé, comme par exemple mon SliceDeque crate. Paramétrer une collection par un allocateur personnalisé ne m'aidera pas ici.

Si j'ai un problème de performances, il serait toujours très difficile pour un allocateur personnalisé de m'aider. Je vais considérer Vec dans les sections suivantes uniquement, car c'est la collection que j'ai réimplémentée le plus souvent.

Réduisez le nombre d'appels d'allocateur système (Small Vector Optimization)

Si je veux allouer des éléments à l'intérieur de l'objet Vec pour réduire le nombre d'appels à l'allocateur système, aujourd'hui j'utilise juste SmallVec<[T; M]> . Cependant, un SmallVec n'est pas un Vec :

  • déplacer un Vec est O (1) dans le nombre d'éléments, mais déplacer un SmallVec<[T; M]> est O (N) pour N <M et O (1) par la suite,

  • les pointeurs vers les éléments Vec sont invalides lors du déplacement si len() <= M mais pas autrement, c'est-à-dire si len() <= M opérations comme into_iter doivent iterator lui-même, au lieu de simplement prendre des pointeurs.

Pourrions-nous créer Vec générique sur un allocateur pour prendre en charge cela? Tout est possible, mais je pense que les coûts les plus importants sont:

  • cela rend la mise en œuvre de Vec plus complexe, ce qui peut avoir un impact sur les utilisateurs qui n'utilisent pas cette fonctionnalité
  • la documentation de Vec deviendrait plus complexe, car le comportement de certaines opérations dépendrait de l'allocateur.

Je pense que ces coûts ne sont pas négligeables.

Utilisez les modèles d'allocation

Le facteur de croissance d'un Vec est adapté à un allocateur particulier. En std nous pouvons l'adapter aux plus communs jemalloc / malloc / ... mais si vous utilisez un allocateur personnalisé, il y a de fortes chances que le facteur de croissance que nous choisissions par défaut, ce ne sera pas le meilleur pour votre cas d'utilisation. Chaque allocateur devrait-il être en mesure de spécifier un facteur de croissance pour les modèles d'allocation de type vec? Je ne sais pas mais mon instinct me dit: probablement pas.

Exploitez les fonctionnalités supplémentaires de votre allocateur système

Par exemple, un allocateur de sur-engagement est disponible dans la plupart des cibles de niveau 1 et 2. Dans les systèmes de type Linux et Macos, l'allocateur de tas sur-engage par défaut, tandis que l'API Windows expose VirtualAlloc qui peut être utilisé pour réserver de la mémoire (par exemple sur Vec::reserve/with_capacity ) et valider la mémoire sur push .

Actuellement, le trait Alloc n'expose pas un moyen d'implémenter un tel allocateur sous Windows, car il ne sépare pas les concepts de validation et de réservation de mémoire (sous Linux, un allocateur sans surengagement peut être piraté en touchant simplement chaque page une fois). Cela n'expose pas non plus un moyen pour un allocateur d'indiquer s'il sur-valide ou non par défaut sur alloc .

Autrement dit, nous aurions besoin d'étendre l'API Alloc pour prendre en charge cela pour Vec , et ce serait IMO pour peu de gain. Parce que lorsque vous avez un tel allocateur, la sémantique Vec change à nouveau:

  • Vec n'a plus besoin de croître, donc des opérations comme reserve n'ont pas de sens
  • push est O(1) au lieu de O(1) amorti.
  • les itérateurs vers des objets vivants ne sont jamais invalides tant que l'objet est vivant, ce qui permet certaines optimisations

Exploitez plus de fonctionnalités supplémentaires de votre allocateur système

Certains allocteurs système comme cudaMalloc / cudaMemcpy / ... différencient la mémoire épinglée et non épinglée, vous permettent d'allouer de la mémoire sur des espaces d'adresses disjoints (nous aurions donc besoin d'un type de pointeur associé dans le Attribuer le trait), ...

Mais les utiliser sur des collections comme Vec modifie à nouveau la sémantique de certaines opérations de manière subtile, comme si l'indexation d'un vecteur invoque soudainement un comportement indéfini ou non, selon que vous le faites à partir d'un noyau GPU ou de l'hôte.

Emballer

Je pense qu'essayer de trouver une API Alloc qui peut être utilisée pour paramétrer toutes les collections (ou même seulement Vec ) est difficile, probablement trop difficile.

Peut-être qu'après avoir obtenu les bons allocateurs global / system / platform / heap / free-store, et Box , nous pouvons repenser les collections. Peut-être que nous pouvons réutiliser Alloc , peut-être avons-nous besoin d'un VecAlloc, VecDequeAlloc , HashMapAlloc`, ... ou peut-être que nous disons simplement, "vous savez quoi, si vous en avez vraiment besoin , copiez-collez simplement la collection standard dans une caisse et moulez-la à votre allocateur ". Peut-être que la meilleure solution est simplement de rendre cela plus facile, en ayant des collections std dans sa propre caisse (ou caisses) dans la pépinière et en utilisant uniquement des fonctionnalités stables, peut-être implémentées comme un ensemble de blocs de construction.

Quoi qu'il en soit, je pense qu'essayer de s'attaquer à tous ces problèmes ici en même temps et essayer de trouver un trait Alloc qui est bon pour tout est trop difficile. Nous sommes à l'étape 0. Je pense que la meilleure façon d'arriver rapidement à l'étape 1 et à l'étape 2 est de laisser les collections en dehors de l'image jusqu'à ce que nous y soyons.

Une fois que nous y serons, les utilisateurs qui souhaitent migrer vers stable pourront le faire, mais pourraient obtenir un impact négatif sur les performances, en raison de l'instabilité de l'API.

Choisir un allocateur personnalisé consiste généralement à améliorer les performances, donc je ne sais pas à qui cette stabilisation initiale servirait.

Choisir un allocateur personnalisé consiste généralement à améliorer les performances, donc je ne sais pas à qui cette stabilisation initiale servirait.

Tout le monde? Au moins maintenant. La plupart Certaines des méthodes dont vous vous plaignez manquent dans la proposition de stabilisation initiale ( alloc_excess , par exemple), sont AFAIK qui n'est encore utilisé par rien dans la bibliothèque standard. Ou est-ce que cela a changé récemment?

Vec (et les autres utilisateurs de RawVec ) utilisent realloc dans push

@SimonSapin

La caisse jemallocator implémente actuellement alloc_excess, realloc, realloc_excess, usable_size, grow_in_place et shrink_in_place

A partir de ces méthodes, AFAIK realloc , grow_in_place et shrink_in_place sont utilisés mais grow_in_place n'est qu'un wrapper naïf sur shrink_in_place pour jemalloc à moins si nous implémentions l'implémentation instable par défaut de grow_in_place en termes de shrink_in_place dans le trait Alloc , cela le réduit à deux méthodes: realloc et shrink_in_place .

Choisir un allocateur personnalisé consiste généralement à améliorer les performances,

Bien que cela soit vrai, vous pourriez obtenir plus de performances en utilisant un allocateur plus adapté sans ces méthodes, qu'un mauvais allocateur qui les a.

IIUC, le principal cas d'utilisation du servo était d'utiliser Firefox jemalloc au lieu d'avoir un deuxième jemalloc, n'est-ce pas?

Même si nous ajoutons realloc et shrink_in_place au trait Alloc lors d'une stabilisation initiale, cela ne ferait que retarder les plaintes de performances.

Par exemple, au moment où nous ajoutons une API instable au trait Alloc qui finit par être utilisé par les collections std , vous ne pourrez pas obtenir les mêmes performances sur stable que vous ne le feriez être capable de passer la nuit. Autrement dit, si nous ajoutons realloc_excess et shrink_in_place_excess au trait d'allocation et faisons Vec / String / ... les utiliser, nous avons stabilisé realloc et shrink_in_place ne vous auraient pas aidé du tout.

IIUC, le principal cas d'utilisation du servo était d'utiliser Firefox jemalloc au lieu d'avoir un deuxième jemalloc, n'est-ce pas?

Bien qu'ils partagent du code, Firefox et Servo sont deux projets / applications distincts.

Firefox utilise mozjemalloc, qui est un fork d'une ancienne version de jemalloc avec un tas de fonctionnalités ajoutées. Je pense que certains codes unsafe FFI reposent sur l'exactitude et la justesse de mozjemalloc utilisé par Rust std.

Servo utilise jemalloc qui se trouve être la valeur par défaut de Rust pour les exécutables pour le moment, mais il est prévu de changer cette valeur par défaut pour l'allocateur du système. Servo a également un code de rapport d'utilisation de la mémoire unsafe qui repose sur l'utilisation de jemalloc. (En passant Vec::as_ptr() à je_malloc_usable_size .)

Servo utilise jemalloc qui se trouve être la valeur par défaut de Rust pour les exécutables pour le moment, mais il est prévu de changer cette valeur par défaut pour l'allocateur du système.

Il serait bon de savoir si les allocateurs système des systèmes ciblés par servo fournissent des API realloc et shrink_to_fit optimisées comme le fait jemalloc? realloc (et calloc ) sont très courants, mais shrink_to_fit ( xallocx ) est AFAIK spécifique à jemalloc . La meilleure solution serait peut-être de stabiliser realloc et alloc_zeroed ( calloc ) dans l'implémentation initiale, et de laisser shrink_to_fit pour plus tard. Cela devrait permettre à servo de fonctionner avec les allocateurs système dans la plupart des plates-formes sans problèmes de performances.

Servo a également un code de rapport d'utilisation de la mémoire non sécurisé qui repose sur l'utilisation de jemalloc. (Passer Vec :: as_ptr () à je_malloc_usable_size.)

Comme vous le savez, la caisse jemallocator a des API pour cela. Je m'attends à ce que des caisses similaires à la caisse jemallocator apparaissent pour d'autres allocateurs proposant des API similaires à mesure que l'histoire de l'allocateur mondial commence à se stabiliser. Je ne me suis pas demandé si ces API appartenaient du tout au trait Alloc .

Je ne pense pas que malloc_usable_size doit être dans le trait Alloc . Utiliser #[global_allocator] pour être sûr de quel allocateur est utilisé par Vec<T> et utiliser séparément une fonction de la caisse jemallocator est très bien.

@SimonSapin une fois que le trait Alloc sera stable, nous aurons probablement une caisse comme jemallocator pour Linux malloc et Windows. Ces caisses pourraient avoir une fonctionnalité supplémentaire pour implémenter les parties qu'elles peuvent de l'API instable Alloc (comme, par exemple, usable_size en plus de malloc_usable_size ) et d'autres choses qui ne font pas partie de l'API Alloc , comme les rapports de mémoire en plus de mallinfo . Une fois qu'il y aura des caisses utilisables pour les systèmes ciblés par servo, il serait plus facile de savoir quelles parties du trait Alloc donner la priorité à la stabilisation, et nous découvrirons probablement de nouvelles API qui devraient au moins être expérimentées pour certains allocateurs.

@gnzlbg Je suis un peu sceptique quant aux choses dans https://github.com/rust-lang/rust/issues/32838#issuecomment -358267292. En laissant de côté tous ces éléments spécifiques au système, il n'est pas difficile de généraliser les collections pour alloc - je l'ai fait. Essayer d'intégrer cela semble être un défi distinct.

@SimonSapin Firefox a-t-il une politique

@sfackler Voyez ça ^. J'essayais de faire une distinction entre les projets qui ont besoin de ceux qui veulent cette stabilité, mais Servo est de l'autre côté de cette fracture.

J'ai un projet qui le souhaite et qu'il doit être stable. Il n'y a rien de particulièrement magique à propos de Servo ou de Firefox en tant que consommateurs de Rust.

@ Ericson2314 Correct, Firefox utilise stable: https://wiki.mozilla.org/Rust_Update_Policy_for_Firefox. Comme je l'ai expliqué, il existe une solution de travail aujourd'hui, ce n'est donc pas un véritable bloqueur pour quoi que ce soit. Ce serait plus agréable / plus robuste d'utiliser #[global_allocator] , c'est tout.

Servo utilise certaines fonctionnalités instables, mais comme mentionné, nous essayons de changer cela.

FWIW, les allocateurs paramétriques sont très utiles pour implémenter les allocateurs. Une grande partie de la comptabilité moins sensible aux performances devient beaucoup plus facile si vous pouvez utiliser diverses structures de données en interne et les paramétrer par un allocateur plus simple (comme bsalloc ). Actuellement, la seule façon de faire cela dans un environnement std est d'avoir une compilation en deux phases dans laquelle la première phase est utilisée pour définir votre allocateur plus simple comme allocateur global et la deuxième phase est utilisée pour compiler l'allocateur plus grand et plus compliqué. . En no-std, il n'y a aucun moyen de le faire.

@ Ericson2314

En laissant de côté tous ces éléments spécifiques au système, il n'est pas difficile de généraliser les collections pour alloc - je l'ai fait. Essayer d'intégrer cela semble être un défi distinct.

Avez-vous une implémentation de ArrayVec ou SmallVec en plus de Vec + allocateurs personnalisés que je pourrais examiner? C'était le premier point que j'ai mentionné, et ce n'est pas du tout spécifique au système. Ce serait sans doute les deux allocateurs les plus simples imaginables, l'un n'est qu'un tableau brut comme stockage, et l'autre peut être construit au-dessus du premier en ajoutant un repli au tas une fois que le tableau est à court de capacité. La principale différence est que ces allocateurs ne sont pas "globaux", mais chacun des Vec a son propre allocateur indépendant de tous les autres, et ces allocateurs sont avec état.

De plus, je ne dis pas de ne jamais faire cela. Je dis juste que c'est très difficile: C ++ essaie depuis 30 ans avec un succès partiel: les allocateurs GPU et GC fonctionnent en raison des types de pointeurs génériques, mais implémentent ArrayVec et SmallVec au-dessus de Vec n'aboutit pas à une abstraction à coût nul dans le domaine C ++ ( P0843r1 aborde en détail certains des problèmes de ArrayVec ).

Donc, je préférerais simplement que nous poursuivions cela après avoir stabilisé les pièces qui fournissent quelque chose d'utile tant que celles-ci ne permettent pas de rechercher des allocateurs de collection personnalisés à l'avenir.


J'ai parlé un peu avec @SimonSapin sur IRC et si nous realloc et alloc_zeroed , alors Rust dans Firefox (qui n'utilise que Rust stable) pourrait utiliser mozjemalloc tant qu'allocateur global dans Rust stable sans avoir besoin de hacks supplémentaires. Comme mentionné par @SimonSapin , Firefox a actuellement une solution viable pour cela aujourd'hui, donc même si cela serait bien, cela ne semble pas être une très haute priorité.

Pourtant, nous pourrions commencer par là, et une fois que nous y sommes, déplacer servo vers stable #[global_allocator] sans perte de performance.


@joshlf

FWIW, les allocateurs paramétriques sont très utiles pour implémenter les allocateurs.

Pourriez-vous expliquer un peu plus ce que vous voulez dire? Y a-t-il une raison pour laquelle vous ne pouvez pas paramétrer vos allocateurs personnalisés avec le trait Alloc ? Ou votre propre trait d'allocateur personnalisé et implémentez simplement le trait Alloc sur les allocateurs finaux (ces deux traits n'ont pas nécessairement besoin d'être égaux)?

Je ne comprends pas d'où vient le cas d'utilisation de "SmallVec = Vec + allocator spécial". Ce n'est pas quelque chose que j'ai vu beaucoup mentionné auparavant (ni dans Rust ni dans d'autres contextes), précisément parce que cela pose de nombreux problèmes graves. Quand je pense à «améliorer les performances avec un allocateur spécialisé», ce n'est pas du tout ce à quoi je pense.

En examinant l'API Layout , je m'interrogeais sur les différences de gestion des erreurs entre from_size_align et align_to , où l'ancien retourne None en cas d'erreur , tandis que ce dernier panique (!).

Ne serait-il pas plus utile et cohérent d'ajouter un enum LayoutErr correctement défini et informatif et de renvoyer un Result<Layout, LayoutErr> dans les deux cas (et peut-être l'utiliser pour les autres fonctions qui renvoient actuellement un Option également)?

@rkruppe

Je ne comprends pas d'où vient le cas d'utilisation de "SmallVec = Vec + allocator spécial". Ce n'est pas quelque chose que j'ai vu beaucoup mentionné auparavant (ni dans Rust ni dans d'autres contextes), précisément parce que cela pose de nombreux problèmes graves. Quand je pense à «améliorer les performances avec un allocateur spécialisé», ce n'est pas du tout ce à quoi je pense.

Il existe deux façons indépendantes d'utiliser les allocateurs dans Rust et C ++: l'allocateur système, utilisé par défaut par toutes les allocations, et comme argument de type pour une collection paramétrée par un trait d'allocateur, comme moyen de créer un objet de cette collection particulière qui utilise un allocateur particulier (qui peut être l'allocateur du système ou non).

Ce qui suit se concentre uniquement sur ce deuxième cas d'utilisation: utiliser une collection et un type d'allocateur pour créer un objet de cette collection qui utilise un allocateur particulier.

D'après mon expérience avec C ++, paramétrer une collection avec un allocateur sert deux cas d'utilisation:

  • améliorer les performances d'un objet de collection en faisant en sorte que la collection utilise un allocateur personnalisé ciblé sur un modèle d'allocation spécifique, et / ou
  • ajouter une nouvelle fonctionnalité à une collection lui permettant de faire quelque chose qu'il ne pouvait pas faire auparavant.

Ajout de nouvelles fonctionnalités aux collections

C'est le cas d'utilisation des allocateurs que je vois dans les bases de code C ++ 99% du temps. Le fait que l'ajout d'une nouvelle fonctionnalité à une collection améliore les performances est, à mon avis, une coïncidence. En particulier, aucun des allocateurs suivants n'améliore les performances en ciblant un modèle d'allocation. Ils le font en ajoutant des fonctionnalités qui, dans certains cas, comme le mentionne @ Ericson2314 , peuvent être considérées comme "spécifiques au système". Voici quelques exemples:

  • pile pour faire allocateurs petites optimisations tampons (voir Howard Hinnant document de stack_alloc ). Ils vous permettent d'utiliser std::vector ou flat_{map,set,multimap,...} et en lui passant un allocateur personnalisé, vous ajoutez une petite optimisation de tampon avec ( SmallVec ) ou sans ( ArrayVec ) tas tomber en arrière. Cela permet, par exemple, de placer une collection avec ses éléments sur la pile ou la mémoire statique (là où elle aurait autrement utilisé le tas).

  • architectures de mémoire segmentées (comme les cibles x86 de pointeur de 16 bits de large et les GPGPU). Par exemple, C ++ 17 Parallel STL était, pendant C ++ 14, la spécification technique parallèle. Sa bibliothèque précurseur du même auteur est la bibliothèque Thrust de NVIDIA, qui comprend des allocateurs pour permettre aux classes de conteneurs d'utiliser la mémoire GPGPU (par exemple, thrust :: device_malloc_allocator ) ou la mémoire épinglée (par exemple, thrust :: pinned_allocator ; la mémoire épinglée permet un transfert plus rapide entre l'hôte-périphérique dans certains cas).

  • les allocateurs pour résoudre les problèmes liés au parallélisme, comme le faux partage (par exemple, les blocs de construction Intel Thread cache_aligned_allocator ) ou les exigences de suralignement des types SIMD (par exemple, aligned_allocator Eigen3).

  • Mémoire partagée interprocess: Boost.Interprocess a des allocateurs qui allouent la mémoire de la collection en utilisant les fonctions de mémoire partagée interprocess du système d'exploitation (comme la mémoire partagée System V). Cela permet d'utiliser directement un conteneur std pour gérer la mémoire utilisée pour communiquer entre différents processus.

  • garbage collection: la bibliothèque d'allocation de mémoire différée de Herb Sutter utilise un type de pointeur défini par l'utilisateur pour implémenter des allocateurs qui récupèrent la mémoire. Ainsi, par exemple, lorsqu'un vecteur croît, l'ancien bloc de mémoire est maintenu en vie jusqu'à ce que tous les pointeurs vers cette mémoire aient été détruits, évitant ainsi l'invalidation de l'itérateur.

  • allocateurs instrumentés: blsma_testallocator de la bibliothèque de logiciels de Bloomberg vous permet de consigner les schémas d'alloctation / désallocation de mémoire (et de construction / destruction d'objets spécifiques à C ++) des objets où vous l'utilisez. Vous ne savez pas si un Vec alloué après reserve ? Branchez un tel allocateur, et ils vous diront si cela se produit. Certains de ces allocateurs vous permettent de les nommer, de sorte que vous puissiez les utiliser sur plusieurs objets et obtenir des journaux indiquant quel objet fait quoi.

Ce sont les types d'allocateurs que je vois le plus souvent dans la nature en C ++. Comme je l'ai mentionné précédemment, le fait qu'ils améliorent les performances dans certains cas est, à mon avis, une coïncidence. L'important est qu'aucun d'entre eux n'essaie de cibler un modèle d'allocation particulier.

Cibler les modèles d'allocation pour améliorer les performances.

AFAIK, il n'y a pas d'allocateurs C ++ largement utilisés qui font cela et je vais vous expliquer pourquoi je pense que c'est dans une seconde. Les bibliothèques suivantes ciblent ce cas d'utilisation:

Cependant, ces bibliothèques ne fournissent pas vraiment d'allocateur unique pour un cas d'utilisation particulier. Au lieu de cela, ils fournissent des blocs de construction d'allocateur que vous pouvez utiliser pour créer des allocateurs personnalisés ciblés sur le modèle d'allocation particulier dans une partie particulière d'une application.

Le conseil général que je me souviens de mes jours C ++ était de simplement "ne pas les utiliser" (ils sont le tout dernier recours) parce que:

  • faire correspondre les performances de l'allocateur système est très difficile, les battre est très très difficile,
  • les chances que le modèle d'allocation de mémoire d'application de quelqu'un d'autre corresponde au vôtre sont minces, vous devez donc vraiment connaître votre modèle d'allocation et savoir de quels blocs de construction d'allocateur vous avez besoin pour le faire correspondre
  • ils ne sont pas portables car différents fournisseurs ont différentes implémentations de bibliothèques standard C ++ qui utilisent des modèles d'allocation différents; les fournisseurs ciblent généralement leur mise en œuvre sur leurs allocateurs système. Autrement dit, une solution adaptée à un fournisseur peut fonctionner horriblement (pire que l'allocateur système) dans un autre.
  • il existe de nombreuses alternatives que l'on peut épuiser avant d'essayer de les utiliser: utiliser une collection différente, réserver de la mémoire, ... La plupart des alternatives demandent moins d'effort et peuvent offrir des gains plus importants.

Cela ne signifie pas que les bibliothèques pour ce cas d'utilisation ne sont pas utiles. Ils le sont, c'est pourquoi des bibliothèques comme foonathan / memory fleurissent. Mais au moins d'après mon expérience, ils sont beaucoup moins utilisés dans la nature que les allocateurs qui "ajoutent des fonctionnalités supplémentaires" car pour gagner, vous devez battre l'allocateur système, ce qui nécessite plus de temps que la plupart des utilisateurs ne sont prêts à investir (Stackoverflow est complet des questions du type "J'ai utilisé Boost.Pool et mes performances ont empiré, que puis-je faire? Ne pas utiliser Boost.Pool.").

Emballer

OMI Je pense que c'est génial que le modèle d'allocateur C ++, bien que loin d'être parfait, prenne en charge les deux cas d'utilisation, et je pense que si les collections std de Rust doivent être paramétrées par des allocateurs, elles devraient également prendre en charge les deux cas d'utilisation, car au moins dans Les allocateurs C ++ pour les deux cas se sont avérés utiles.

Je pense juste que ce problème est légèrement orthogonal à la possibilité de personnaliser l'allocateur global / système / plate-forme / par défaut / tas / magasin libre d'une application particulière, et qu'essayer de résoudre les deux problèmes en même temps pourrait retarder la solution d'un d’entre eux inutilement.

Ce que certains utilisateurs veulent faire avec une collection paramétrée par un allocateur peut être très différent de ce que certains autres utilisateurs veulent faire. Si @rkruppe commence par «faire correspondre les modèles d'allocation» et que je commence par «empêcher le faux partage» ou «utiliser une petite optimisation de la mémoire tampon avec repli de tas», il va être difficile de commencer par comprendre les besoins les uns des autres, et ensuite d'arriver à une solution qui fonctionne pour les deux.

@gnzlbg Merci pour la rédaction complète. La plupart ne répond pas à ma question initiale et je ne suis pas d'accord avec certaines d'entre elles, mais il est bon de le préciser pour que nous ne nous parlions pas.

Ma question portait spécifiquement sur cette application:

des allocateurs de pile pour faire de petites optimisations de tampon (voir l'article stack_alloc de Howard Hinnant). Ils vous permettent d'utiliser std :: vector ou flat_ {map, set, multimap, ...} et en lui passant un allocateur personnalisé, vous ajoutez une petite optimisation de tampon avec (SmallVec) ou sans (ArrayVec) le tas de secours. Cela permet, par exemple, de placer une collection avec ses éléments sur la pile ou la mémoire statique (là où elle aurait autrement utilisé le tas).

En lisant sur stack_alloc, je réalise maintenant comment cela peut fonctionner. Ce n'est pas ce que les gens entendent généralement par SmallVec (où le tampon est stocké en ligne dans la collection), c'est pourquoi j'ai manqué cette option, mais cela évite le problème de devoir mettre à jour les pointeurs lorsque la collection se déplace (et rend également ces mouvements moins chers ). Notez également que short_alloc permet à plusieurs collections de partager un arena , ce qui le rend encore plus différent des types SmallVec typiques. Cela ressemble plus à un allocateur de pointeur linéaire / bump avec un retour en douceur à l'allocation de tas lors de l'exécution de l'espace alloué.

Je ne suis pas d'accord pour dire que ce type d'allocateur et cache_aligned_allocator ajoutent fondamentalement de nouvelles fonctionnalités. Ils sont utilisés différemment et, selon votre définition du «modèle d'allocation», ils peuvent ne pas être optimisés pour un modèle d'allocation spécifique. Cependant, ils sont certainement optimisés pour des cas d'utilisation spécifiques et ils ne présentent aucune différence de comportement significative par rapport aux allocateurs de tas à usage général.

Je suis cependant d'accord que les cas d'utilisation comme l'allocation de mémoire différée de Sutter, qui modifient considérablement ce que signifie même un "pointeur", sont une application distincte qui peut nécessiter une conception distincte si nous voulons la supporter.

En lisant sur stack_alloc, je réalise maintenant comment cela peut fonctionner. Ce n'est pas ce que les gens entendent généralement par SmallVec (où le tampon est stocké en ligne dans la collection), c'est pourquoi j'ai manqué cette option, mais cela évite le problème de devoir mettre à jour les pointeurs lorsque la collection se déplace (et rend également ces mouvements moins chers ).

J'ai mentionné stack_alloc parce que c'est le seul allocateur de ce type avec "un papier", mais il a été publié en 2009 et précède C ++ 11 (C ++ 03 ne supportait pas les allocateurs avec état dans les collections).

La façon dont cela fonctionne en C ++ 11 (qui prend en charge les allocateurs avec état), en un mot, est:

  • std :: vector stocke un objet Allocator intérieur comme Rust RawVec fait .
  • l' interface Allocator a une propriété obscure appelée Allocator :: propagate_on_container_move_assignment (POCMA à partir de maintenant) que les allocateurs définis par l'utilisateur peuvent personnaliser; cette propriété est true par défaut. Si cette propriété est false , lors de l'affectation de déplacement, l'allocateur ne peut pas être propagé, donc une collection est requise par la norme pour déplacer chacun de ses éléments vers le nouveau stockage manuellement.

Ainsi, lorsqu'un vecteur avec l'allocateur système est déplacé, d'abord le stockage du nouveau vecteur sur la pile est alloué, puis l'allocateur est déplacé (qui est de taille zéro), puis les 3 pointeurs sont déplacés, qui sont toujours valides. Ces mouvements sont O(1) .

OTOHO, quand un vecteur avec un allocateur POCMA == true est déplacé, d'abord le stockage du nouveau vecteur sur la pile est alloué et initialisé avec un vecteur vide, puis l'ancienne collection est drain ed dans le le nouveau, de sorte que l'ancien soit vide et le nouveau plein. Cela déplace chaque élément de la collection individuellement, en utilisant leurs opérateurs d'affectation de déplacement. Cette étape est O(N) et corrige les pointeurs internes des éléments. Enfin, la collection d'origine désormais vide est supprimée. Notez que cela ressemble à un clone, mais ce n'est pas parce que les éléments eux-mêmes ne sont pas clonés, mais déplacés en C ++.

Cela a-t-il du sens?

Le principal problème avec cette approche en C ++ est que:

  • la politique de croissance vectorielle est définie par la mise en œuvre
  • l'API d'allocateur n'a pas _excess méthodes
  • la combinaison des deux problèmes ci-dessus signifie que si vous savez que votre vecteur peut contenir au plus 9 éléments, vous ne pouvez pas avoir d'allocateur de pile pouvant contenir 9 éléments, car votre vecteur pourrait essayer de croître lorsqu'il en a 8 avec un facteur de croissance de 1,5, vous devez donc pessimiser et allouer de l'espace pour 18 éléments.
  • la complexité de l'opération vectorielle change en fonction des propriétés de l'allocateur (POCMA n'est qu'une des nombreuses propriétés de l'API d'allocateur C ++; l'écriture d'allocateurs C ++ n'est pas triviale). Cela rend la spécification de l'API du vecteur pénible car parfois copier ou déplacer des éléments entre différents allocateurs du même type entraîne des coûts supplémentaires, qui modifient la complexité des opérations. Cela rend également la lecture des spécifications très pénible. De nombreuses sources de documentation en ligne comme cppreference mettent en avant le cas général et les détails obscurs de ce qui change si une propriété d'allocateur est vraie ou fausse en minuscules minuscules pour éviter de déranger 99% des utilisateurs.

De nombreuses personnes travaillent à l'amélioration de l'API d'allocation de C ++ pour résoudre ces problèmes, par exemple, en ajoutant des méthodes _excess et en garantissant que les collections conformes standard les utilisent.

Je ne suis pas d'accord pour dire que ce type d'allocateur et de cache_aligned_allocator ajoutent fondamentalement de nouvelles fonctionnalités.

Peut-être ce que je voulais dire, c'est qu'ils vous permettent d'utiliser des collections std dans des situations ou pour des types pour lesquels vous ne pouviez pas les utiliser auparavant. Par exemple, en C ++, vous ne pouvez pas placer les éléments d'un vecteur dans le segment de mémoire statique de votre binaire sans quelque chose comme un allocateur de pile (pourtant vous pouvez écrire votre propre collection qui le fait). OTOH, le standard C ++ ne prend pas en charge les types suralignés comme les types SIMD, et si vous essayez d'en allouer un avec new vous invoquerez un comportement non défini (vous devez utiliser posix_memalign ou similaire) . L'utilisation de l'objet manifeste généralement le comportement non défini via un segfault (*). Des choses comme aligned_allocator vous permettent d'allouer en tas ces types, et même de les mettre dans des collections std, sans invoquer un comportement indéfini, en utilisant un autre allocateur. Bien sûr, le nouvel allocateur aura des schémas d'allocation différents (ces allocateurs suralignent essentiellement toute la mémoire btw ...), mais ce que les gens les utilisent est de pouvoir faire quelque chose qu'ils ne pouvaient pas faire auparavant.

De toute évidence, Rust n'est pas C ++. Et C ++ a des problèmes que Rust n'a pas (et vice-versa). Un allocateur qui ajoute une nouvelle fonctionnalité en C ++ peut être inutile dans Rust, qui, par exemple, n'a aucun problème avec les types SIMD.

(*) Les utilisateurs d'Eigen3 en souffrent profondément, car pour éviter un comportement indéfini lors de l'utilisation de conteneurs C ++ et STL, vous devez protéger les conteneurs contre les types SIMD ou les types contenant des types SIMD ( documents Eigen3 ) et vous devez également vous protéger contre jamais utiliser new sur vos types en surchargeant l'opérateur new pour eux ( plus de documentation Eigen3 )

@gnzlbg merci, j'ai également été confus par l'exmaple smallvec. Cela nécessiterait des types non déplaçables et une sorte d'allocation dans Rust - deux RFC en révision et ensuite plus de travail de suivi - donc je n'ai aucun scrupule à faire ça pour le moment. La stratégie smallvec existante consistant à toujours utiliser tout l'espace de pile dont vous aurez besoin semble bien pour le moment.

Je suis également d'accord avec @rkruppe pour pas besoin d'être connues de la collection qui utilise l'allocateur. Parfois, le Collection<Allocator> complet a de nouvelles propriétés (existant entièrement dans la mémoire épinglée, disons) mais c'est juste une conséquence naturelle de l'utilisation de l'allocateur.

La seule exception ici que je vois est les allocateurs qui n'allouent qu'une seule taille / type (les NVidia le font, tout comme les allocateurs de dalle). Nous pourrions avoir un trait ObjAlloc<T> distinct implémenté pour les allocateurs normaux: impl<A: Alloc, T> ObjAlloc<T> for A . Ensuite, les collections utiliseraient des limites ObjAlloc si elles avaient juste besoin d'allouer quelques éléments. Mais, je me sens un peu ridicule, même en évoquant cela, car cela devrait être faisable de manière compatible plus tard.

Cela a-t-il du sens?

Bien sûr, mais ce n'est pas vraiment pertinent pour Rust car nous n'avons aucun constructeur de mouvement. Donc, un allocateur (mobile) qui contient directement la mémoire vers laquelle il distribue des pointeurs n'est tout simplement pas possible, point final.

Par exemple, en C ++, vous ne pouvez pas placer les éléments d'un vecteur dans le segment de mémoire statique de votre binaire sans quelque chose comme un allocateur de pile (pourtant vous pouvez écrire votre propre collection qui le fait).

Ce n'est pas un changement de comportement. Il existe de nombreuses raisons valables de contrôler où les collections obtiennent leur mémoire, mais toutes liées à des "externalités" telles que les performances, les scripts de l'éditeur de liens, le contrôle de la disposition de la mémoire du programme dans son ensemble, etc.

Des éléments comme align_allocator vous permettent d'allouer en tas ces types, et même de les placer dans des collections std, sans appeler de comportement indéfini, en utilisant un autre allocateur.

C'est pourquoi j'ai spécifiquement mentionné le cache_aligned_allocator de TBB et non le hidden_allocator d'Eigen. cache_aligned_allocator ne semble pas garantir un alignement spécifique dans sa documentation (il dit simplement que c'est "généralement" 128 octets), et même s'il le faisait, il ne serait généralement pas utilisé à cette fin (car son alignement est probablement trop grand pour Types SIMD et trop petits pour des choses comme DMA aligné sur la page). Son but, comme vous le dites, est d'éviter le faux partage.

@gnzlbg

FWIW, les allocateurs paramétriques sont très utiles pour implémenter les allocateurs.

Pourriez-vous expliquer un peu plus ce que vous voulez dire? Y a-t-il une raison pour laquelle vous ne pouvez pas paramétrer vos allocateurs personnalisés avec le trait Alloc? Ou votre propre trait d'allocateur personnalisé et implémentez simplement le trait Alloc sur les allocateurs finaux (ces deux traits n'ont pas nécessairement besoin d'être égaux)?

Je pense que je n'ai pas été clair; laissez-moi essayer de mieux expliquer. Disons que j'implémente un allocateur que j'espère utiliser soit:

  • En tant qu'allocateur global
  • Dans un environnement sans std

Et disons que j'aimerais utiliser Vec sous le capot pour implémenter cet allocateur. Je ne peux pas utiliser directement Vec tel qu'il existe aujourd'hui car

  • Si je suis l'allocateur global, son utilisation introduira simplement une dépendance récursive sur moi-même
  • Si je suis dans un environnement no-std, il n'y a pas de Vec tel qu'il existe aujourd'hui

Ainsi, ce dont j'ai besoin est de pouvoir utiliser un Vec qui est paramétré sur un autre allocateur que j'utilise en interne pour une simple comptabilité interne. C'est le but de bsalloc (et la source du nom - il est utilisé pour amorcer d'autres allocateurs).

Dans elfmalloc, nous pouvons toujours être un allocateur global en:

  • Lors de notre compilation, compilez statiquement jemalloc en tant qu'allocateur global
  • Produire un fichier objet partagé qui peut être chargé dynamiquement par d'autres programmes

Notez que dans ce cas, il est important que nous ne nous compilions pas en utilisant l'allocateur système comme allocateur global car ensuite, une fois chargé, nous réintroduirions la dépendance récursive car, à ce stade, nous sommes l'allocateur système.

Mais cela ne fonctionne pas quand:

  • Quelqu'un veut nous utiliser comme allocateur global dans Rust de manière "officielle" (par opposition à en créant d'abord un fichier objet partagé)
  • Nous sommes dans un environnement sans std

OTOH, le standard C ++ ne prend pas en charge les types suralignés comme les types SIMD, et si vous essayez d'en allouer un avec new, vous invoquerez un comportement non défini (vous devez utiliser posix_memalign ou similaire).

Puisque notre trait actuel Alloc prend l'alignement comme paramètre, je suppose que cette classe de problème (le problème "Je ne peux pas travailler sans un alignement différent") disparaît pour nous?

@gnzlbg - une

Ce cas d'utilisation doit être pris en compte. En particulier, cela influence fortement ce que la bonne chose à faire est: -

  • Que plus d'un allocateur est utilisé, et surtout, lorsqu'il est utilisé, cet allocateur est pour la mémoire persistante, il ne serait plusieurs allocateurs de mémoire persistants)
  • Le coût de «réimplémentation» des collections standard est élevé et conduit à un code incompatible avec les bibliothèques tierces.
  • La durée de vie de l'allocateur n'est pas nécessairement 'static .
  • Les objets stockés dans la mémoire persistante ont besoin d'un état supplémentaire qui doit être rempli à partir du tas, c'est-à-dire qu'ils ont besoin de l'état réinitialisé. Ceci est particulièrement vrai pour les mutex et autres. Ce qui était autrefois jetable n'est plus éliminé.

Rust a une superbe opportunité de saisir l'initiative ici et d'en faire une plate-forme de premier ordre pour ce qui remplacera les disques durs, les disques SSD et même le stockage PCI.

* Pas de surprise, vraiment, car jusqu'à très récemment, c'était un peu spécial. Il est désormais largement pris en charge sous Linux, FreeBSD et Windows.

@raphaelcohn

Ce n'est vraiment pas le lieu de travailler sur la mémoire persistante. La vôtre n'est pas la seule école de pensée concernant l'interface avec la mémoire persistante - par exemple, il se peut que l'approche dominante consiste simplement à le traiter comme un disque plus rapide, pour des raisons d'intégrité des données.

Si vous avez un cas d'utilisation pour utiliser la mémoire persistante de cette façon, il serait probablement préférable de faire ce cas ailleurs d'une manière ou d'une autre. Prototypez-le, proposez des changements plus concrets à l'interface d'allocateur et, idéalement, faites valoir que ces changements valent l'impact qu'ils auraient sur le cas moyen.

@rpjohnst

Je ne suis pas d'accord. C'est exactement le genre d'endroit auquel il appartient. Je veux éviter qu'une décision soit prise qui crée une conception qui est le résultat d'une focalisation trop étroite et d'une recherche de preuves.

Le PMDK Intel actuel - sur lequel se concentrent de nombreux efforts pour la prise en charge de l'espace utilisateur de bas niveau - l'approche beaucoup plus comme une mémoire allouée régulière avec des pointeurs - une mémoire similaire à celle via mmap , par exemple. En effet, si l'on veut travailler avec de la mémoire persistante sous Linux, alors, je pense que c'est à peu près votre seul port d'escale pour le moment. En substance, l'une des boîtes à outils les plus avancées pour l'utiliser - celle qui prévaut si vous voulez - la traite comme de la mémoire allouée.

Quant au prototypage - eh bien, c'est exactement ce que j'ai dit avoir fait: -

J'ai récemment travaillé sur un wrapper Rust pour un allocateur de mémoire persistante (en particulier, libpmemcto).

(Vous pouvez utiliser une version précoce de ma caisse à https://crates.io/crates/nvml . Il y a beaucoup plus d'expérimentation dans le contrôle de code source dans le module cto_pool ).

Mon prototype est conçu en fonction de ce qui est nécessaire pour remplacer un moteur de stockage de données dans un système réel à grande échelle. Un état d'esprit similaire est derrière beaucoup de mes projets open source. J'ai trouvé pendant de nombreuses années les meilleures bibliothèques, tout comme les meilleures normes, celles qui dérivent _de_ l'utilisation réelle.

Rien de tel que d'essayer d'adapter un allocateur du monde réel à l'interface actuelle. Franchement, l'expérience d'utiliser l'interface Alloc , puis de copier l'ensemble de Vec , puis de la peaufiner, a été douloureuse. Beaucoup d'endroits supposent que les allocateurs ne sont pas passés, par exemple Vec::new() .

Ce faisant, j'ai fait quelques observations dans mon commentaire initial sur ce qui serait exigé d'un allocateur et ce qui serait exigé d'un utilisateur d'un tel allocateur. Je pense que ceux-ci sont très valables sur un fil de discussion sur une interface d'allocateur.

La bonne nouvelle est que vos 3 premières puces de https://github.com/rust-lang/rust/issues/32838#issuecomment -358940992 sont partagées par d'autres cas d'utilisation.

Je voulais juste ajouter que je n'ai pas ajouté de mémoire non volatile à la liste
car la liste répertorie les cas d'utilisation d'allocateurs paramétrant des conteneurs dans
le monde C ++ qui sont «largement» utilisés, du moins d'après mon expérience (ceux
les alloctors que j'ai mentionnés proviennent principalement de bibliothèques très populaires utilisées par beaucoup).
Bien que je connaisse les efforts du SDK Intel (certaines de leurs bibliothèques
cible C ++) Je ne connais personnellement aucun projet les utilisant (ont-ils
un allocateur qui peut être utilisé avec std :: vector? Je ne sais pas). Ce n'est pas
signifie qu'ils ne sont ni utilisés ni importants. Je serais intéressé à savoir
à propos de ceux-ci, mais le point principal de mon article était que le paramétrage
allocateurs par conteneurs est très complexe, et nous devrions essayer de faire
progressez avec les allocateurs de système sans fermer les portes des conteneurs
(mais nous devrions aborder cela plus tard).

Le 21 janvier 2018 à 17:36, John Ericson [email protected] a écrit:

La bonne nouvelle est vos 3 premiers points de # 32838 (commentaire)
https://github.com/rust-lang/rust/issues/32838#issuecomment-358940992
sont partagés par d'autres cas d'utilisation.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-359261305 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AA3Npk95PZBZcm7tknNp_Cqrs_3T1UkEks5tM2ekgaJpZM4IDYUN
.

J'ai essayé de lire la plupart de ce qui a déjà été écrit, donc c'est peut-être déjà ici et dans ce cas, je suis désolé si je l'ai manqué mais voici:

Quelque chose qui est assez courant pour les jeux (en C / C ++) est d'utiliser «l'allocation de scratch par image». un cadre de jeu) puis "détruit".

Détruit dans ce cas, ce qui signifie que vous réinitialisez l'allocateur à sa position de départ. Il n'y a pas du tout de "destruction" d'objets car ces objets doivent être de type POD (donc aucun destructeur n'est en cours d'exécution)

Je me demande si quelque chose comme ça correspondra à la conception actuelle d'allocateur dans Rust?

(modifier: il devrait y avoir aucune destruction d'objets)

@emoon

Quelque chose qui est assez courant pour les jeux (en C / C ++) est d'utiliser «l'allocation de scratch par image». un cadre de jeu) puis "détruit".

Détruit dans ce cas, ce qui signifie que vous réinitialisez l'allocateur à sa position de départ. Il y a "destruction" des objets car ces objets doivent être de type POD (donc aucun destructeur n'est en cours d'exécution)

Cela devrait être faisable. Du haut de ma tête, vous auriez besoin d'un objet pour l'arène elle-même et d'un autre objet qui est une poignée par image sur l'arène. Ensuite, vous pouvez implémenter Alloc pour ce handle, et en supposant que vous utilisiez des wrappers sécurisés de haut niveau pour l'allocation (par exemple, imaginez que Box devienne paramétrique sur Alloc ), le les durées de vie garantiraient que tous les objets alloués étaient supprimés avant que le handle par image ne soit supprimé. Notez que dealloc serait toujours appelé pour chaque objet, mais si dealloc était un no-op, alors toute la logique drop-and-deallocate pourrait être complètement ou en grande partie optimisée.

Vous pouvez également utiliser un type de pointeur intelligent personnalisé qui n'implémente pas Drop , ce qui faciliterait beaucoup de choses ailleurs.

Merci! J'ai fait une faute de frappe dans mon message d'origine. C'est dire qu'il n'y a pas de destruction d'objets.

Pour les personnes qui ne sont pas expertes en allocateurs et ne peuvent pas suivre ce fil, quel est le consensus actuel: prévoyons-nous de prendre en charge les allocateurs personnalisés pour les types de collection stdlib?

@alexreg Je ne sais pas quel est le plan final, mais il n'y a aucune difficulté technique confirmée à le faire. OTOH, nous n'avons pas de bon moyen d'exposer cela dans std parce que les variables de type par défaut sont suspectes, mais je n'ai aucun problème à en faire une chose alloc -seulement pour l'instant donc nous peut progresser du côté lib sans entrave.

@ Ericson2314 D'accord, c'est bon à entendre. Les variables de type par défaut sont-elles encore implémentées? Ou au stade RFC peut-être? Comme vous le dites, s'ils sont simplement limités aux choses liées à alloc / std::heap , tout devrait bien se passer.

Je pense vraiment qu'AllocErr devrait être une erreur. Ce serait plus cohérent avec d'autres modules (par exemple io).

impl Error for AllocError probablement du sens et ne fait pas de mal, mais j'ai personnellement trouvé le trait Error inutile.

Je regardais la fonction Layout :: from_size_align aujourd'hui, et la limitation « align ne doit pas dépasser 2 ^ 31 (c'est- 1 << 31 dire

Je dois dire que c'était là un message de commit assez trompeur, parlant align ajustement de

Ce qui m'amène à cette note: l'élément "OSX / alloc_system est bogué sur des alignements énormes" ici ne doit implémentation d' un allocateur qui se comporte. Et la limitation arbitraire de Layout :: from_size_align fait cela.

@glandium Est-il utile de demander l'alignement sur un multiple de 4 gigaoctets ou plus?

J'imagine des cas où l'on peut souhaiter avoir une allocation de 4 Go alignée sur 4 Go, ce qui n'est pas possible actuellement, mais guère plus. Mais je ne pense pas que des limitations arbitraires devraient être ajoutées simplement parce que nous ne pensons pas à de telles raisons maintenant.

J'imagine des cas où l'on peut souhaiter avoir une allocation de 4 Go alignée sur 4 Go

Quels sont ces cas?

J'imagine des cas où l'on peut souhaiter avoir une allocation de 4 Go alignée sur 4 Go

Quels sont ces cas?

Concrètement, je viens d'ajouter la prise en charge d'alignements arbitrairement grands dans mmap-alloc afin de prendre en charge l'allocation de grandes dalles de mémoire alignées à utiliser dans elfmalloc . L'idée est que la dalle de mémoire soit alignée sur sa taille de sorte que, étant donné un pointeur vers un objet alloué à partir de cette dalle, il vous suffit de masquer les bits bas pour trouver la dalle contenant. Nous n'utilisons actuellement pas de dalles de 4 Go (pour des objets de cette taille, nous allons directement à mmap), mais il n'y a aucune raison que nous ne puissions pas, et je pourrais tout à fait imaginer une application avec de grandes exigences de RAM qui voulait faire que (c'est-à-dire s'il allouait suffisamment fréquemment des objets multi-Go pour ne pas accepter la surcharge de mmap).

Voici un cas d'utilisation possible pour l'alignement> 4 Go: l'alignement sur une grande limite de page. Il existe déjà des plates-formes qui prennent en charge les pages> 4 Gio. Ce document IBM dit que «le processeur POWER5 + prend en charge quatre tailles de page de mémoire virtuelle: 4 Ko, 64 Ko, 16 Mo et 16 Go». Même x86-64 n'est pas loin: les «grandes pages» font généralement 2 Mio, mais il prend également en

Toutes les fonctions non typées du trait Alloc traitent *mut u8 . Ce qui signifie qu'ils pourraient prendre ou renvoyer des pointeurs nuls, et tout l'enfer se déchaînerait. Devraient-ils utiliser NonNull place?

Il y a de nombreux indicateurs qu'ils pourraient revenir desquels tout l'enfer serait
se dégager.
Le dimanche 4 mars 2018 à 03:56, Mike Hommey [email protected] a écrit:

Toutes les fonctions non typées du trait Alloc traitent de * mut u8.
Ce qui signifie qu'ils pourraient prendre ou renvoyer des pointeurs nuls, et tout l'enfer le ferait
se dégager. Devraient-ils utiliser NonNull à la place?

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370223269 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABY2UR2dRxDtdACeRUh_djM-DExRuLxiks5ta9aFgaJpZM4IDYUN
.

Une raison plus convaincante d'utiliser NonNull est qu'elle autoriserait les Result s actuellement renvoyés par les méthodes Alloc (ou Options , si nous passons à cela dans l'avenir) pour être plus petit.

Une raison plus convaincante d'utiliser NonNull est que cela permettrait aux résultats actuellement renvoyés par les méthodes Alloc (ou Options, si nous basculons vers cela à l'avenir) d'être plus petits.

Je ne pense pas que ce serait parce que AllocErr a deux variantes.

Il y a de nombreux indices sur lesquels ils pourraient revenir dont tout l'enfer se déchaînerait.

Mais un pointeur nul est clairement plus faux que tout autre pointeur.

J'aime à penser que le système de type rouille aide avec les pistolets à pied et est utilisé pour encoder les invariants. La documentation pour alloc dit clairement "Si cette méthode renvoie un Ok(addr) , alors l'adresse retournée sera une adresse non nulle", mais pas son type de retour. Dans l'état actuel des choses, Ok(malloc(layout.size())) serait une implémentation valide, alors que ce n'est clairement pas le cas.

Remarque, il y a aussi des notes sur la taille de Layout devant être non nulle, donc je dirais également qu'il devrait encoder cela comme un NonZero.

Ce n'est pas parce que toutes ces fonctions sont intrinsèquement dangereuses que nous ne devrions pas avoir de prévention contre les armes à feu.

Parmi toutes les erreurs possibles lors de l'utilisation (éditer: et de l'implémentation) des allocateurs, le passage d'un pointeur nul est l'une des plus faciles à localiser (vous obtenez toujours un segfault propre lors du déréférencement, du moins si vous avez une MMU et que des choses très étranges avec lui), et généralement l'une des plus triviales à corriger également. Il est vrai que les interfaces non sécurisées peuvent essayer d'empêcher les footguns, mais cette footgun semble disproportionnellement petite (par rapport aux autres erreurs possibles, et à la verbosité de l'encodage de cet invariant dans le système de types).

En outre, il semble probable que les implémentations d'allocateur utiliseraient simplement le constructeur non coché de NonNull "pour les performances": puisque dans un allocateur correct ne retournerait jamais null de toute façon, il voudrait sauter le NonNell::new(...).unwrap() . Dans ce cas, vous n'obtiendrez en fait aucune prévention tangible des armes à feu, juste plus de passe-partout. (Les avantages de la taille Result , s'ils sont réels, peuvent toujours en être une raison convaincante.)

les implémentations d'allocateur utiliseraient simplement le constructeur non coché de NonNull

Le but est moins d'aider l'implémentation d'allocateurs que d'aider leurs utilisateurs. Si MyVec contient un NonNull<T> et Heap.alloc() retourne déjà un NonNull , celui-là moins un appel vérifié ou non vérifié que je dois faire.

Notez que les pointeurs ne sont pas seulement des types de retour, ils sont également des types d'entrée, par exemple dealloc et realloc . Ces fonctions sont-elles censées durcir pour que leur entrée soit éventuellement nulle ou non? La documentation aurait tendance à dire non, mais le système de types aurait tendance à dire oui.

Tout à fait de même avec layout.size (). Les fonctions d'allocation sont-elles censées gérer la taille demandée étant 0 d'une manière ou d'une autre?

(Les avantages de la taille du résultat, s'ils sont réels, peuvent toujours en être une raison convaincante.)

Je doute qu'il y ait des avantages de taille, mais avec quelque chose comme # 48741, il y aurait des avantages de codegen.

Si nous continuons ce principe d'être plus flexible pour les utilisateurs de l'API, les pointeurs devraient être NonNull dans les types de retour mais pas dans les arguments. (Cela ne signifie pas que ces arguments doivent être vérifiés par null au moment de l'exécution.)

Je pense que l'approche de la loi de Postel n'est pas la bonne à adopter ici. Y a-t-il
cas dans lequel le passage d'un pointeur nul vers une méthode Alloc est valide? Si non,
alors cette flexibilité donne simplement à la pédale un peu plus
gâchette sensible.

Le 5 mars 2018 à 8h00, "Simon Sapin" [email protected] a écrit:

Si nous continuons ce principe d'être plus flexible pour les utilisateurs de l'API,
les pointeurs doivent être NonNull dans les types de retour mais pas dans les arguments. (Ce
ne signifie pas que ces arguments doivent être vérifiés par null au moment de l'exécution.)

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370327018 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AA_2L8zrOLyUv5mUc_kiiXOAn1f60k9Uks5tbOJ0gaJpZM4IDYUN
.

Le but est moins d'aider l'implémentation d'allocateurs que d'aider leurs utilisateurs. Si MyVec contient un NonNullet Heap.alloc () renvoie déjà un NonNull, cet appel moins vérifié ou unsafe-non vérifié que je dois faire.

Ah cela a du sens. Ne répare pas l'arme à pied, mais en centralise la responsabilité.

Notez que les pointeurs ne sont pas seulement des types de retour, ils sont également des types d'entrée pour, par exemple, dealloc et realloc. Ces fonctions sont-elles censées durcir pour que leur entrée soit éventuellement nulle ou non? La documentation aurait tendance à dire non, mais le système de types aurait tendance à dire oui.

Existe-t-il un cas dans lequel le passage d'un pointeur nul vers une méthode Alloc est valide? Sinon, cette flexibilité consiste simplement à donner à la pédale un déclencheur légèrement plus sensible.

L'utilisateur doit absolument lire la documentation et garder les invariants à l'esprit. De nombreux invariants ne peuvent pas du tout être appliqués via un système de types - s'ils le pouvaient, la fonction ne serait pas dangereuse pour commencer. Il s'agit donc uniquement de savoir si placer NonNull dans une interface donnée aidera réellement les utilisateurs en

  • en leur rappelant de lire la documentation et de réfléchir aux invariants
  • offrant la commodité (le point de @SimonSapin par rapport à la valeur de retour d'alloc)
  • donnant un avantage matériel (par exemple, optimisations de mise en page)

Je ne vois aucun avantage important à faire, par exemple, l'argument de dealloc en NonNull . Je vois à peu près deux classes d'utilisations de cette API:

  1. Utilisation relativement triviale, où vous appelez alloc , stockez le pointeur retourné quelque part, et après un certain temps, passez le pointeur stocké à dealloc .
  2. Scénarios compliqués impliquant FFI, beaucoup d'arithmétique de pointeurs, etc. où il y a une logique importante impliquée pour s'assurer que vous passez la bonne chose à dealloc à la fin.

Prendre NonNull ici n'aide fondamentalement que le premier type de cas d'utilisation, car ceux-ci stockeront le NonNull dans un endroit agréable et le passeront simplement à NonNull inchangé. Théoriquement, cela pourrait empêcher certaines fautes de frappe (en passant foo quand vous vouliez dire bar ) si vous jonglez avec plusieurs pointeurs et qu'un seul d'entre eux est NonNull , mais cela ne semble pas trop commun ou important. L'inconvénient de dealloc prenant un pointeur brut (en supposant que alloc retourne NonNull que @SimonSapin m'a convaincu devrait arriver) serait qu'il nécessite un as_ptr en l'appel dealloc, qui est potentiellement ennuyeux mais n'a aucun impact sur la sécurité.

Le deuxième type de cas d'utilisation n'est pas aidé car il ne peut probablement pas continuer à utiliser NonNull tout au long du processus, il devrait donc recréer manuellement un NonNull partir du pointeur brut qu'il a obtenu par quelque moyen que ce soit. Comme je l'ai expliqué plus tôt, cela deviendrait probablement une assertion non vérifiée / unsafe plutôt qu'une vérification du temps d'exécution réel, donc aucune pédale n'est empêchée.

Cela ne veut pas dire que je suis en faveur de dealloc prenant un pointeur brut. Je ne vois tout simplement pas les avantages revendiqués par rapport aux pistolets de pied. La cohérence des types l'emporte probablement par défaut.

Je suis désolé mais j'ai lu ceci comme "De nombreux invariants ne peuvent pas du tout être appliqués via le système de types ... donc n'essayons même pas". Ne laissez pas le parfait être l'ennemi du bien!

Je pense que c'est plus sur les compromis entre les garanties fournies par NonNull et l'ergonomie perdue d'avoir à faire des allers-retours entre NonNull et les pointeurs bruts. Je n'ai pas d'opinion particulièrement forte de toute façon - aucune des deux parties ne semble déraisonnable.

@cramertj Ouais, mais je n'achète pas vraiment la prémisse de ce genre d'argument. Les gens disent que Alloc est pour des cas d'utilisation obscurs, cachés et largement dangereux. Eh bien, dans un code obscur et difficile à lire, j'aimerais avoir autant de sécurité que possible - précisément parce qu'ils sont si rarement touchés, il est probable que l'auteur original ne sera pas là. Inversement, si le code est lu des années plus tard, vissez l'égonomie. Si quoi que ce soit, c'est contre-productif. Le code doit s'efforcer d'être très explicite pour qu'un lecteur inconnu puisse mieux comprendre ce qui se passe sur terre. Moins de bruit <invariants plus clairs.

Le deuxième type de cas d'utilisation n'est pas aidé car il ne peut probablement pas continuer à utiliser NonNull tout au long du processus, il devrait donc recréer manuellement un NonNull partir du pointeur brut qu'il a obtenu par quelque moyen que ce soit.

Il s'agit simplement d'un échec de coordination et non d'une fatalité technique. Bien sûr, à l'heure actuelle, de nombreuses API non sécurisées peuvent utiliser des pointeurs bruts. Donc, quelque chose doit ouvrir la voie au passage à une interface supérieure en utilisant NonNull ou d'autres wrappers. Ensuite, d'autres codes peuvent plus facilement suivre le mouvement. Je ne vois aucune raison de se rabattre constamment sur des pointeurs bruts difficiles à lire et non informatifs dans un code nouveau, entièrement rouillé et dangereux.

Salut!

Je veux juste dire que, en tant qu'auteur / mainteneur d'un allocateur personnalisé Rust, je suis en faveur de NonNull . À peu près pour toutes les raisons qui ont déjà été exposées dans ce fil.

De plus, j'aimerais souligner que @glandium est le mainteneur du fork de jemalloc de firefox, et a également beaucoup d'expérience dans le piratage des allocateurs.

Je pourrais écrire beaucoup plus en réponse à @ Ericson2314 et à d'autres, mais cela devient rapidement un débat très détaché et philosophique, alors je vais le couper court ici. Je me disputais contre ce que je crois être une sur-déclaration des avantages de sécurité de NonNull dans ce type d'API (il y a bien sûr d'autres avantages). Cela ne veut pas dire qu'il n'y a pas d'avantages en matière de sécurité, mais comme @cramertj l'a dit, il y a des compromis et je pense que le côté «pro» est exagéré. Quoi qu'il en soit, j'ai déjà dit que je préférais utiliser NonNull à divers endroits pour d'autres raisons - pour la raison que @SimonSapin a donné en alloc , en dealloc pour la cohérence. Alors faisons ça et n'allons plus sur des tangentes.

S'il y a quelques cas d'utilisation NonNull avec lesquels tout le monde est d'

Nous voudrons probablement mettre à jour Unique et ses amis pour utiliser NonNull au lieu de NonZero pour maintenir les frictions au moins dans liballoc bas. Mais cela ressemble à quelque chose que nous faisons déjà de toute façon à un niveau d'abstraction sur l'allocateur, et je ne vois aucune raison pour ne pas le faire également au niveau de l'allocateur. Cela me semble un changement raisonnable.

(Il existe déjà deux implémentations du trait From qui se convertissent en toute sécurité entre Unique<T> et NonNull<T> .)

Considérant que j'ai besoin de quelque chose qui ressemble beaucoup à l'API allocator dans stable rust, j'ai extrait le code du repo rust et je l'ai mis dans une caisse séparée:

Ceci / pourrait / être utilisé pour itérer sur les modifications expérimentales de l'API, mais à partir de maintenant, c'est une copie simple de ce qui se trouve dans le dépôt rust.

[Hors sujet] Oui, ce serait bien si std pouvait utiliser des caisses de code stables hors de l'arbre comme ça, afin que nous puissions expérimenter des interfaces instables dans du code stable. C'est l'une des raisons pour lesquelles j'aime avoir std une façade.

std pourrait dépendre d'une copie d'une caisse de crates.io, mais si votre programme dépend également de la même caisse, il ne "ressemblerait" pas à la même caisse / types / traits à rouiller de toute façon, alors je donne Je ne vois pas comment cela aiderait. Quoi qu'il en soit, quelle que soit la façade, rendre les éléments instables indisponibles sur le canal stable est un choix très délibéré, pas un accident.

Il semble que nous ayons un accord sur l'utilisation de NonNull. Quelle est la voie à suivre pour que cela se produise réellement? juste un PR le faire? un RFC?

Sans aucun rapport, j'ai regardé un assemblage généré à partir de choses de boxe, et les chemins d'erreur sont plutôt grands. Faut-il faire quelque chose à ce sujet?

Exemples:

pub fn bar() -> Box<[u8]> {
    vec![0; 42].into_boxed_slice()
}

pub fn qux() -> Box<[u8]> {
    Box::new([0; 42])
}

compile en:

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

C'est une assez grande quantité de code à ajouter à n'importe quel endroit en créant des boîtes. Comparez avec 1.19, qui n'avait pas l'API d'allocateur:

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 cela est réellement important, alors c'est vraiment ennuyeux. Cependant, peut-être que LLVM optimise cela pour des programmes plus importants?

Il y a 1439 appels à __rust_oom dans le dernier Firefox tous les soirs. Firefox n'utilise pas l'allocateur de rust, cependant, nous obtenons donc des appels directs à malloc / calloc, suivis d'une vérification nulle que le saute au code de préparation oom, qui est généralement deux movq et un lea, remplissant le AllocErr et obtenant son adresse pour le passer à __rust__oom . C'est le meilleur scénario, essentiellement, mais cela reste encore 20 octets de code machine pour les deux movq et le lea.

Si je regarde ripgrep, il y en a 85, et ils sont tous dans des fonctions _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn identiques. Tous mesurent 16 octets. Il y a 685 appels à ces fonctions wrapper, dont la plupart sont précédés d'un code similaire à ce que j'ai collé dans https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485.

@nox cherchait aujourd'hui à activer le pass mergefunc llvm, je me demande si cela fait une différence ici.

mergefunc ne supprime apparemment pas les multiples fonctions _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn identiques (essayées avec -C passes=mergefunc dans RUSTFLAGS ).

Mais ce qui fait une grande différence, c'est LTO, qui est en fait ce qui fait que Firefox appelle directement malloc, laissant la création du AllocErr à droite avant d'appeler __rust_oom . Cela rend également inutile la création du Layout avant d'appeler l'allocateur, en le laissant lors du remplissage du AllocErr .

Cela me fait penser que les fonctions d'allocation, sauf __rust_oom devraient probablement être marquées en ligne.

BTW, après avoir examiné le code généré pour Firefox, je pense qu'il serait idéalement souhaitable d'utiliser moz_xmalloc au lieu de malloc . Cela n'est pas possible sans une combinaison des traits Allocator et sans pouvoir remplacer l'allocateur de tas global, mais apporte le besoin possible d'un type d'erreur personnalisé pour le trait Allocator: moz_xmalloc est infaillible et ne revient jamais en cas de échec. IOW, il gère l'OOM lui-même, et le code de rouille n'aurait pas besoin d'appeler __rust_oom dans ce cas. Ce qui rendrait souhaitable que les fonctions d'allocation retournent éventuellement ! au lieu de AllocErr .

Nous avons discuté de faire de AllocErr une structure de taille zéro, ce qui pourrait également aider ici. Avec le pointeur également fait NonNull , la valeur de retour entière pourrait être de la taille d'un pointeur.

https://github.com/rust-lang/rust/pull/49669 apporte un certain nombre de modifications à ces API, dans le but de stabiliser un sous-ensemble couvrant les allocateurs globaux. Problème de suivi pour ce sous-ensemble: https://github.com/rust-lang/rust/issues/49668. En particulier, un nouveau trait GlobalAlloc est introduit.

Ce PR nous permettra-t-il de faire des choses comme Vec::new_with_alloc(alloc)alloc: Alloc bientôt?

@alexreg non

@sfackler Hmm, pourquoi pas? De quoi avons-nous besoin avant de pouvoir faire cela? Je ne comprends pas vraiment le point de ce PR autrement, à moins que ce ne soit simplement pour changer l'allocateur global.

@alexreg

Je ne comprends pas vraiment le point de ce PR autrement, à moins que ce ne soit simplement pour changer l'allocateur global.

Je pense que c'est simplement pour changer le répartiteur mondial.

@alexreg Si vous voulez dire sur stable, il y a un certain nombre de questions de conception non résolues que nous ne sommes pas prêts à stabiliser. Sur Nightly, cela est pris en charge par RawVec et probablement bien à ajouter comme #[unstable] pour Vec pour tous ceux qui ont envie de travailler dessus.

Et oui, comme mentionné dans le PR, son but est de permettre de changer l'allocateur global, ou d'allouer (par exemple dans un type de collection personnalisé) sans absuing Vec::with_capacity .

FWIW, la caisse allocator_api mentionnée dans https://github.com/rust-lang/rust/issues/32838#issuecomment -376793369 a RawVec<T, A> et Box<T, A> sur le maître branch (pas encore publié). Je pense à cela comme un incubateur pour ce à quoi pourraient ressembler les collections génériques sur le type d'allocation (plus le fait que j'ai besoin d'un type Box<T, A> pour la rouille stable). Je n'ai pas encore commencé à porter vec.rs pour ajouter Vec<T, A> , mais les PR sont les bienvenus. vec.rs est grand.

Je noterai que les "problèmes" de codegen mentionnés dans https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485 devraient disparaître avec les changements dans # 49669.

Maintenant, avec un peu plus de réflexion sur l'utilisation du trait Alloc pour aider à implémenter un allocateur dans les couches, il y a deux choses qui, à mon avis, seraient utiles (du moins pour moi):

  • comme mentionné précédemment, possibilité de spécifier éventuellement un type AllocErr . Cela peut être utile pour faire ! , ou, maintenant qu'AllocErr est vide, pour qu'il transmette éventuellement plus d'informations que «échoué».
  • éventuellement être en mesure de spécifier un type Layout . Imaginez que vous ayez deux couches d'allocateurs: une pour les allocations de pages et une pour les grandes régions. Ce dernier peut s'appuyer sur le premier, mais s'ils prennent tous les deux le même type Layout , les deux couches doivent faire leur propre validation: au niveau le plus bas, cette taille et cet alignement sont un multiple de la taille de la page, et au niveau supérieur, cette taille et cet alignement correspondent aux exigences des grandes régions. Mais ces contrôles sont redondants. Avec les types Layout spécialisés, la validation pourrait être déléguée à la création Layout plutôt que dans l'allocateur lui-même, et les conversions entre les types Layout permettraient d'ignorer les vérifications redondantes.

@cramertj @SimonSapin @glandium D'accord, merci pour cette clarification. Je peux simplement soumettre un PR pour certains des autres types de collections-prime. Est-il préférable de le faire contre votre repo / crate allocator -api,

@alexreg compte tenu du nombre de modifications importantes apportées au trait Alloc dans # 49669, il est probablement préférable d'attendre qu'il fusionne d'abord.

@glandium Assez bien. Cela ne semble pas trop loin de l'atterrissage. Je viens de remarquer le repo https://github.com/pnkfelix/collections-prime aussi ... qu'est-ce que c'est par rapport au vôtre?

J'ajouterais une autre question ouverte:

  • Est-ce que Alloc::oom peut paniquer? Actuellement, la documentation indique que cette méthode doit abandonner le processus. Cela a des implications pour le code qui utilise des allocateurs car ils doivent ensuite être conçus pour gérer correctement le déroulement sans perte de mémoire.

Je pense que nous devrions permettre la panique car un échec dans un allocateur local ne signifie pas nécessairement que l'allocateur global échouera également. Dans le pire des cas, le oom l'allocateur global sera appelé, ce qui interrompra le processus (sinon, le code existant serait cassé).

@alexreg Ce n'est pas le cas. Cela semble juste être une copie simple de ce qui se trouve dans std / alloc / collections. Eh bien, une copie vieille de deux ans. Ma caisse a une portée beaucoup plus limitée (la version publiée n'a que le trait Alloc a quelques semaines, la branche principale n'a que RawVec et Box en plus de cela), et l'un de mes objectifs est de le maintenir en place avec une rouille stable.

@glandium D'accord, dans ce cas, il est probablement logique pour moi d'attendre que ce PR arrive, puis de créer un PR contre Rust Master et de vous taguer, afin que vous sachiez quand il sera fusionné dans master (et que vous pourrez ensuite le fusionner dans votre caisse) , juste?

@alexreg a du sens. Vous / pourriez / commencer à travailler dessus maintenant, mais cela entraînerait probablement un peu de désabonnement de votre côté si / quand le vélo change les choses dans ce PR.

@glandium J'ai d'autres choses pour me tenir occupé avec Rust pour le moment, mais j'y serai quand ce PR sera approuvé. Ce sera génial d'aller chercher l'allocation / les collections de tas génériques d'allocateur à la fois nocturnes et stables bientôt. :-)

Alloc :: oom est-il autorisé à paniquer? Actuellement, la documentation indique que cette méthode doit abandonner le processus. Cela a des implications pour le code qui utilise des allocateurs car ils doivent ensuite être conçus pour gérer correctement le déroulement sans perte de mémoire.

@Amanieu Cette RFC a été fusionnée: https://github.com/rust-lang/rfcs/pull/2116 La documentation et l'implémentation n'ont peut-être pas encore été mises à jour.

Il y a un changement à l'API pour lequel j'envisage de soumettre un PR:

Divisez le trait Alloc en deux parties: "implémentation" et "helpers". Le premier serait des fonctions comme alloc , dealloc , realloc , etc. et le second, alloc_one , dealloc_one , alloc_array , etc. Bien qu'il y ait des avantages hypothétiques à pouvoir avoir une implémentation personnalisée pour ce dernier, c'est loin d'être le besoin le plus courant, et lorsque vous avez besoin d'implémenter des wrappers génériques (ce que j'ai trouvé incroyablement courant, au point que j'ai réellement commencé à écrire un dérivé personnalisé pour cela), vous devez toujours les implémenter tous car le wrappee peut les personnaliser.

OTOH, si un implémenteur de trait Alloc essaie de faire des choses fantaisistes, par exemple alloc_one , il n'est pas garanti que dealloc_one sera appelé pour cette allocation. Il y a plusieurs raisons à cela:

  • Les aides ne sont pas utilisées de manière cohérente. Juste un exemple, raw_vec utilise un mélange de alloc_array , alloc / alloc_zeroed , mais n'utilise que dealloc .
  • Même avec une utilisation cohérente par exemple de alloc_array / dealloc_array , on peut toujours convertir en toute sécurité un Vec en Box , qui utiliserait alors dealloc .
  • Ensuite, il y a certaines parties de l'API qui n'existent tout simplement pas (pas de version à zéro de alloc_one / alloc_array )

Donc, même s'il existe des cas d'utilisation réels pour la spécialisation de par exemple alloc_one (et en fait, j'ai un tel besoin de mozjemalloc), il vaut mieux utiliser un allocateur spécialisé à la place.

En fait, c'est pire que ça, dans le repo rust, il y a exactement une utilisation de alloc_array , et aucune utilisation de alloc_one , dealloc_one , realloc_array , dealloc_array . Même la syntaxe des boîtes n'utilise pas alloc_one , elle utilise exchange_malloc , ce qui prend un size et align . Donc, ces fonctions sont plus destinées à être pratiques pour les clients que pour les exécutants.

Avec quelque chose comme impl<A: Alloc> AllocHelpers for A (ou AllocExt , quel que soit le nom choisi), nous aurions toujours la commodité de ces fonctions pour les clients, tout en ne permettant pas aux implémenteurs de se tirer une balle dans le pied s'ils pensaient ils feraient des choses fantaisistes en les remplaçant (et en facilitant la mise en œuvre des allocateurs de proxy).

Il y a un changement à l'API pour lequel j'envisage de soumettre un PR pour

Je l'ai fait en # 50436

@glandium

(et en fait, j'ai un tel besoin de mozjemalloc),

Pourriez-vous élaborer sur ce cas d'utilisation?

mozjemalloc a un allocateur de base qui fuit délibérément. Sauf pour un type d'objets, où il conserve une liste gratuite. Je peux le faire en superposant les allocateurs plutôt que de faire des tours avec alloc_one .

Est-il nécessaire de désallouer avec l'alignement exact que vous avez alloué?

Juste pour souligner que la réponse à cette question est OUI , j'ai cette jolie citation de Microsoft eux -

align_alloc () ne sera probablement jamais implémenté, comme C11 l'a spécifié d'une manière incompatible avec notre implémentation (à savoir que free () doit être capable de gérer des allocations hautement alignées)

L'utilisation de l'allocateur système sous Windows nécessitera toujours de connaître l'alignement lors de la désallocation afin de désallouer correctement les allocations hautement alignées, alors pouvons-nous simplement marquer cette question comme résolue?

L'utilisation de l'allocateur système sous Windows nécessitera toujours de connaître l'alignement lors de la désallocation afin de désallouer correctement les allocations hautement alignées, alors pouvons-nous simplement marquer cette question comme résolue?

C'est dommage, mais c'est comme ça. Abandonnons alors les vecteurs suralignés. :confus:

Abandonnons alors les vecteurs suralignés

Comment venir? Vous avez juste besoin de Vec<T, OverAlignedAlloc<U16>> qui alloue et désalloue à la fois avec un alignement excessif.

Comment venir? Vous avez juste besoin de Vec<T, OverAlignedAlloc<U16>> qui alloue et désalloue à la fois avec un alignement excessif.

J'aurais dû être plus précis. Je voulais dire déplacer des vecteurs suralignés dans une API hors de votre contrôle, c'est-à-dire une API qui prend un Vec<T> et non Vec<T, OverAlignedAlloc<U16>> . (Par exemple CString::new() .)

Vous devriez plutôt utiliser

#[repr(align(16))]
struct OverAligned16<T>(T);

puis Vec<OverAligned16<T>> .

Vous devriez plutôt utiliser

Ça dépend. Supposons que vous souhaitiez utiliser les intrinsèques AVX (256 bits de large, 32 octets d'alignement requis) sur un vecteur de f32 s:

  • Vec<T, OverAlignedAlloc<U32>> résout le problème, on peut utiliser les intrinsèques AVX directement sur les éléments vectoriels (en particulier, les charges mémoire alignées), et le vecteur se déréfère toujours en une tranche &[f32] rendant ergonomique à utiliser.
  • Vec<OverAligned32<f32>> ne résout pas vraiment le problème. Chaque f32 prend 32 octets d'espace en raison de l'exigence d'alignement. Le remplissage introduit empêche l'utilisation directe des opérations AVX puisque les f32 ne sont plus en mémoire continue. Et personnellement, je trouve le deref à &[OverAligned32<f32>] un peu fastidieux à gérer.

Pour un seul élément dans un Box , Box<T, OverAligned<U32>> vs Box<OverAligned32<T>> , les deux approches sont plus équivalentes, et la deuxième approche pourrait en effet être préférable. Dans tous les cas, c'est bien d'avoir les deux options.

Le post de suivi en haut de ce numéro est horriblement obsolète (a été modifié pour la dernière fois en 2016). Nous avons besoin d'une liste actualisée des préoccupations actives pour poursuivre la discussion de manière productive.

La discussion bénéficierait également considérablement d'un document de conception à jour, contenant les questions non résolues actuelles et la justification des décisions de conception.

Il existe plusieurs threads de différences allant de "ce qui est actuellement implémenté tous les soirs" à "ce qui était proposé dans le RFC original d'Alloc" engendrant des milliers de commentaires sur différents canaux (rfc repo, problème de suivi rust-lang, allocation globale RFC, messages internes, beaucoup énormes PR, etc.), et ce qui est stabilisé dans la RFC GlobalAlloc ne ressemble pas beaucoup à ce qui était proposé dans la RFC originale.

C'est quelque chose dont nous avons besoin de toute façon pour terminer la mise à jour de la documentation et de la référence, et serait également utile dans les discussions en cours.

Je pense qu'avant même de penser à stabiliser le trait Alloc , nous devrions d'abord essayer d'implémenter le support d'allocateur dans toutes les collections de bibliothèques standard. Cela devrait nous donner une certaine expérience de la façon dont ce trait sera utilisé dans la pratique.

Je pense qu'avant même de penser à stabiliser le trait Alloc , nous devrions d'abord essayer d'implémenter le support d'allocateur dans toutes les collections de bibliothèques standard. Cela devrait nous donner une certaine expérience de la façon dont ce trait sera utilisé dans la pratique.

Oui absolument. Surtout Box , puisque nous ne savons pas encore comment éviter que Box<T, A> prenne deux mots.

Oui absolument. Surtout Box, car on ne sait pas encore comment éviter d'avoir Boxprenez deux mots.

Je ne pense pas que nous devrions nous soucier de la taille de Box<T, A> pour l'implémentation initiale, mais c'est quelque chose qui peut être ajouté plus tard de manière rétrocompatible en ajoutant un trait DeAlloc qui ne prend en charge que désallocation.

Exemple:

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

Je pense qu'avant même de penser à stabiliser le trait Alloc, nous devrions d'abord essayer d'implémenter le support d'allocateur dans toutes les collections de bibliothèques standard. Cela devrait nous donner une certaine expérience de la façon dont ce trait sera utilisé dans la pratique.

Je pense que @ Ericson2314 a travaillé là-dessus, par https://github.com/rust-lang/rust/issues/42774. Ce serait bien d'avoir une mise à jour de sa part.

Je ne pense pas que nous devrions nous soucier de la taille de Box<T, A> pour l'implémentation initiale, mais c'est quelque chose qui peut être ajouté plus tard de manière rétrocompatible en ajoutant un trait DeAlloc qui ne prend en charge que désallocation.

C'est une approche, mais ce n'est pas du tout clair pour moi que ce soit certainement la meilleure. Il a les inconvénients distincts, par exemple, que a) il ne fonctionne que lorsqu'un pointeur -> recherche d'allocateur est possible (ce n'est pas le cas, par exemple, de la plupart des allocateurs d'arène) et, b) il ajoute une surcharge significative à dealloc (à savoir, pour faire la recherche inversée). Il se peut que la meilleure solution à ce problème soit un effet ou un système contextuel plus général comme cette proposition ou cette proposition . Ou peut-être quelque chose de complètement différent. Donc je ne pense pas que nous devrions supposer que ce sera facile à résoudre d'une manière qui soit rétrocompatible avec l'incarnation actuelle du trait Alloc .

@joshlf Compte tenu du fait que Box<T, A> n'a accès à lui-même que lorsqu'il est déposé, c'est la meilleure chose que nous puissions faire avec du code sécurisé uniquement. Un tel modèle pourrait être utile pour les allocateurs de type arène qui ont un no-op dealloc et qui libèrent juste de la mémoire lorsque l'allocateur est supprimé.

Pour les systèmes plus compliqués où l'allocateur appartient à un conteneur (par exemple LinkedList ) et gère plusieurs allocations, je m'attends à ce que Box ne soit pas utilisé en interne. Au lieu de cela, les internes LinkedList utiliseront des pointeurs bruts qui sont alloués et libérés avec l'instance Alloc contenue dans l'objet LinkedList . Cela évitera de doubler la taille de chaque pointeur.

Compte tenu du fait que Box<T, A> n'a accès à lui-même que lorsqu'il est abandonné, c'est la meilleure chose que nous puissions faire avec du code sécurisé uniquement. Un tel modèle pourrait être utile pour les allocateurs de type arène qui ont un no-op dealloc et qui libèrent juste de la mémoire lorsque l'allocateur est supprimé.

C'est vrai, mais Box ne sait pas que dealloc est sans opération.

Pour les systèmes plus compliqués où l'allocateur appartient à un conteneur (par exemple LinkedList ) et gère plusieurs allocations, je m'attends à ce que Box ne soit pas utilisé en interne. Au lieu de cela, les internes LinkedList utiliseront des pointeurs bruts qui sont alloués et libérés avec l'instance Alloc contenue dans l'objet LinkedList . Cela évitera de doubler la taille de chaque pointeur.

Je pense que ce serait vraiment dommage d'exiger des gens qu'ils utilisent du code non sécurisé pour écrire des collections. Si l'objectif est de rendre toutes les collections (y compris probablement celles en dehors de la bibliothèque standard) éventuellement paramétriques sur un allocateur, et que Box n'est pas paramétrique d'allocateur, alors un auteur de collections doit soit ne pas utiliser Box du tout ou utilisez du code non sécurisé (et gardez à l'esprit que se souvenir de toujours libérer des choses est l'un des types les plus courants de non-sécurité de la mémoire en C et C ++, il est donc difficile d'obtenir du code non sécurisé). Cela semble être une affaire malheureuse.

C'est vrai, mais Box ne sait pas que dealloc est no-op.

Pourquoi ne pas adapter ce que fait C ++ unique_ptr ?
C'est-à-dire: pour stocker le pointeur vers l'allocateur s'il est "avec état", et ne pas le stocker si l'allocateur est "sans état"
(par exemple, wrapper global autour de malloc ou mmap ).
Cela nécessiterait de diviser le trait actuel Alloc en deux traits: StatefulAlloc et StatelessAlloc .
Je me rends compte que c'est un très grossier et inélégant (et probablement quelqu'un l'a déjà proposé lors de discussions précédentes).
Malgré son inélégance, cette solution est simple et rétrocompatible (sans pénalités de performances).

Je pense que ce serait vraiment dommage d'exiger des gens qu'ils utilisent du code non sécurisé pour écrire des collections. Si l'objectif est de rendre toutes les collections (y compris probablement celles qui ne font pas partie de la bibliothèque standard) éventuellement paramétriques sur un allocateur, et que Box n'est pas paramétrique d'allocateur, alors un auteur de collections doit soit ne pas utiliser du tout Box, soit utiliser du code non sécurisé (et gardez à l'esprit que se souvenir de toujours libérer les choses est l'un des types les plus courants de non-sécurité de la mémoire en C et C ++, il est donc difficile d'obtenir du code dangereux). Cela semble être une affaire malheureuse.

J'ai peur qu'une implémentation d'un système d'effet ou de contexte qui pourrait permettre d'écrire des conteneurs basés sur des nœuds tels que des listes, des arbres, etc. de manière sûre puisse prendre trop de temps (si c'est possible en principe).
Je n'ai vu aucun article ou langage académique qui aborde ce problème (veuillez me corriger si de tels travaux existent réellement).

Donc, recourir à unsafe dans l'implémentation de conteneurs basés sur des nœuds pourrait être un mal nécessaire, du moins à court terme.

@eucpp Notez que unique_ptr ne stocke pas d'allocateur - il stocke un Deleter :

Deleter doit être FunctionObject ou une référence lvalue à un FunctionObject ou une référence lvalue à une fonction, appelable avec un argument de type unique_ptr:: pointeur`

Je pense que cela équivaut à peu près à ce que nous fournissions des traits fractionnés Alloc et Dealloc .

@cramertj Oui, vous avez raison. Pourtant, deux traits sont requis - avec état et sans état Dealloc .

Un Dealloc ZST ne serait-il pas suffisant?

Le mar 12 juin 2018 à 15:08 Evgeniy Moiseenko [email protected]
a écrit:

@cramertj https://github.com/cramertj Oui, vous avez raison. Encore deux
les traits sont requis - Dealloc avec état et sans état.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-396716689 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AEAJtWkpF0ofVc18NwbfV45G4QY6SCFBks5t8B_AgaJpZM4IDYUN
.

Un Dealloc ZST ne serait-il pas suffisant?

@remexre je suppose que ce serait :)

Je ne savais pas que le compilateur de rouille prend en charge ZST hors de la boîte.
En C ++, il faudrait au moins quelques astuces autour de l'optimisation de base vide.
Je suis assez nouveau chez Rust, désolé pour certaines erreurs évidentes.

Je ne pense pas que nous ayons besoin de traits séparés pour les statuts et les apatrides.

Avec Box augmenté d'un paramètre de type A , il contiendrait directement une valeur de A , pas une référence ou un pointeur vers A . Ce type peut être de taille zéro pour un (dé) allocateur sans état. Ou A lui-même peut être quelque chose comme une référence ou un handle vers un allocateur avec état qui peut être partagé entre plusieurs objets alloués. Donc au lieu de impl Alloc for MyAllocator , vous voudrez peut-être faire quelque chose comme impl<'r> Alloc for &'r MyAllocator

Au fait, un Box qui ne sait que désallouer et non comment allouer n'implémentera pas Clone .

@SimonSapin Je m'attendrais à ce que Clone ing nécessite de spécifier à nouveau un allocateur, de la même manière que créer un nouveau Box (c'est-à-dire que cela ne serait pas fait en utilisant le Clone trait).

@cramertj Ne Vec et à d'autres conteneurs qui implémentent Clone ?
Quels sont les inconvénients de stocker une instance de Alloc dans Box plutôt que Dealloc ?
Alors Box pourrait implémenter Clone ainsi que clone_with_alloc .

Je ne pense pas que les traits séparés affectent vraiment Clone de manière énorme - l'implication ressemblerait simplement à impl<T, A> Clone for Box<T, A> where A: Alloc + Dealloc + Clone { ... } .

@sfackler Je ne serais pas opposé à cette implication, mais je m'attendrais aussi à avoir un clone_into ou quelque chose qui utilise un allocateur fourni.

Serait-il logique d'utiliser une méthode alloc_copy pour Alloc ? Cela pourrait être utilisé pour fournir des implémentations plus rapides de memcpy ( Copy/Clone ) pour les grosses allocations, par exemple en faisant des clones de copie sur écriture de pages.

Ce serait plutôt cool et simple de fournir une implémentation par défaut.

Que serait l'utilisation d'une telle fonction alloc_copy ? impl Clone for Box<T, A> ?

Ouais, idem pour Vec .

Après l'avoir examiné un peu plus, il semble que ce soit des approches pour créer des pages de copie sur écriture dans la même gamme de processus entre piratage et impossible, du moins si vous voulez le faire à plus d'un niveau. Donc alloc_copy ne serait pas un énorme avantage.

Au lieu de cela, une trappe d'échappement plus générale qui permet de futures manigances de mémoire virtuelle pourrait être d'une certaine utilité. C'est-à-dire que si une allocation est importante, soutenue par mmap de toute façon et apatride, alors l'allocateur pourrait promettre de ne pas être conscient des changements futurs de l'allocation. L'utilisateur peut alors déplacer cette mémoire vers un tube, le démapper ou des choses similaires.
Alternativement, il pourrait y avoir un allocateur muet mmap-all-the-things et une fonction try-transfer.

Au lieu d'une trappe d'échappement plus générale qui permet la future mémoire virtuelle

Les allocateurs de mémoire (malloc, jemalloc, ...) ne vous permettent généralement pas de leur voler un quelconque type de mémoire, et ils ne vous permettent généralement pas d'interroger ou de modifier les propriétés de la mémoire qu'ils possèdent. Alors, qu'est-ce que cette trappe d'échappement générale a à voir avec les allocateurs de mémoire?

En outre, la prise en charge de la mémoire virtuelle diffère considérablement entre les plates-formes, à tel point que l'utilisation efficace de la mémoire virtuelle nécessite souvent des algorithmes différents par plate-forme, souvent avec des garanties complètement différentes. J'ai vu des abstractions portables sur la mémoire virtuelle, mais je n'en ai pas encore vu une qui n'ait pas été paralysée au point d'être inutile dans certaines situations en raison de leur "portabilité".

Vous avez raison. Un tel cas d'utilisation (je pensais principalement aux optimisations spécifiques à la plate-forme) est probablement mieux servi en utilisant un allocateur personnalisé en premier lieu.

Des réflexions sur l'API Composable Allocator décrite par Andrei Alexandrescu dans sa présentation CppCon? La vidéo est disponible sur YouTube ici: https://www.youtube.com/watch?v=LIb3L4vKZ7U (il commence à décrire son projet de conception vers 26h00, mais la conférence est suffisamment divertissante que vous préférerez peut-être la regarder) .

Il semble que la conclusion inévitable de tout cela est que les bibliothèques de collections devraient être une allocation WRT générique, et que le programmeur d'application lui-même devrait être capable de composer librement des allocateurs et des collections sur le site de construction.

Des réflexions sur l'API Composable Allocator décrite par Andrei Alexandrescu dans sa présentation CppCon?

L'API actuelle Alloc permet d'écrire des allocateurs composables (par exemple MyAlloc<Other: Alloc> ) et vous pouvez utiliser des traits et une spécialisation pour réaliser à peu près tout ce qui est réalisé dans le discours d'Andreis. Cependant, au-delà de «l'idée» que l'on devrait être capable de faire cela, pratiquement rien de la présentation d'Andrei ne peut s'appliquer à Rust puisque la façon dont Andrei construit l'API repose sur des génériques sans contrainte + SFINAE / statique si dès le début et le système générique de Rust est complètement différent de celui-là.

Je voudrais proposer de stabiliser le reste des méthodes Layout . Ceux-ci sont déjà utiles avec l'API d'allocateur global actuelle.

Est-ce que ce sont toutes les méthodes que vous voulez dire?

  • 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 Oui.

@Amanieu Cela me

des Allocateurs et des durées de vie :

  1. (pour allocator impls): déplacer une valeur d'allocateur ne doit pas invalider ses blocs de mémoire en suspens.

    Tous les clients peuvent assumer cela dans leur code.

    Donc, si un client alloue un bloc à partir d'un allocateur (appelez-le a1) puis a1 se déplace vers un nouvel endroit (par exemple vialet a2 = a1;), alors il reste sain pour le client de désallouer ce bloc via a2.

Cela implique-t-il qu'un Allocator doit être Unpin ?

Bonne prise!

Puisque le trait Alloc est toujours instable, je pense que nous pouvons toujours changer les règles si nous le voulons et modifier cette partie de la RFC. Mais c'est effectivement quelque chose à garder à l'esprit.

@gnzlbg Oui, je suis conscient des énormes différences entre les systèmes génériques, et que tout ce qu'il détaille n'est pas implémentable de la même manière dans Rust. Je travaille sur la bibliothèque par intermittence depuis la publication, cependant, et je fais de bons progrès.

Cela implique-t-il qu'un Allocator _doit_ être Unpin ?

Ce n'est pas le cas. Unpin concerne le comportement d'un type lorsqu'il est enveloppé dans un Pin , il n'y a pas de connexion particulière à cette API.

Mais Unpin ne peut-il pas être utilisé pour appliquer la contrainte mentionnée?

Une autre question concernant dealloc_array : Pourquoi la fonction renvoie-t-elle Result ? Dans l'implémentation actuelle, cela peut échouer dans deux cas:

  • n vaut zéro
  • dépassement de capacité pour n * size_of::<T>()

Pour le premier, nous avons deux cas (comme dans la documentation, l'implémenteur peut choisir entre ceux-ci):

  • L'allocation renvoie Ok sur n => dealloc_array devrait également renvoyer Ok .
  • L'allocation renvoie Err sur n => il n'y a pas de pointeur qui peut être passé à dealloc_array .

La seconde est assurée par la contrainte de sécurité suivante:

la disposition de [T; n] doit correspondre à ce bloc de mémoire.

Cela signifie que nous devons appeler dealloc_array avec le même n que dans l'allocation. Si un tableau avec des éléments n peut être alloué, n est valide pour T . Sinon, l'allocation aurait échoué.

Edit: Concernant le dernier point: Même si usable_size renvoie une valeur supérieure à n * size_of::<T>() , cela est toujours valable. Sinon, l'implémentation viole cette contrainte de trait:

La taille du bloc doit être comprise dans la plage [use_min, use_max] , où:

  • [...]
  • use_max est la capacité qui a été (ou aurait été) retournée lorsque (si) le bloc a été alloué via un appel à alloc_excess ou realloc_excess .

Cela n'est vrai que, car le trait nécessite un unsafe impl .

Pour le premier, nous avons deux cas (comme dans la documentation, l'implémenteur peut choisir entre ceux-ci):

  • L'allocation renvoie Ok sur n mis à zéro

D'où avez-vous obtenu ces informations?

Toutes les méthodes Alloc::alloc_ dans la documentation spécifient que le comportement des allocations de taille zéro n'est pas défini dans leur clause "Safety".

Documents de core::alloc::Alloc (parties pertinentes en surbrillance):

Une note concernant les types de taille nulle et les mises en page de taille zéro: de nombreuses méthodes du trait Alloc indiquent que les demandes d'allocation doivent être de taille non nulle, sinon un comportement indéfini peut en résulter.

  • Cependant, certaines méthodes d'allocation de niveau supérieur ( alloc_one , alloc_array ) sont bien définies sur les types de taille zéro et peuvent éventuellement les prendre en charge : il appartient à l'implémenteur de retourner ou non Err , ou pour retourner Ok avec un pointeur.
  • Si une implémentation Alloc choisit de retourner Ok dans ce cas (c'est-à-dire que le pointeur indique un bloc inaccessible de taille zéro), alors ce pointeur retourné doit être considéré comme "actuellement alloué". Sur un tel allocateur, toutes les méthodes qui prennent des pointeurs actuellement alloués comme entrées doivent accepter ces pointeurs de taille zéro, sans provoquer de comportement indéfini.

  • En d'autres termes, si un pointeur de taille zéro peut sortir d'un allocateur, alors cet allocateur doit également accepter que ce pointeur retourne dans ses méthodes de désallocation et de réallocation .

Donc, l'une des conditions d'erreur de dealloc_array est définitivement suspecte:

/// # 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] ne répond pas à la taille de l'allocateur ou aux contraintes d'alignement, alors AFAICT ne rentre pas dans le bloc de mémoire de l'allocation, et le comportement est indéfini (par la clause de sécurité).

L'autre condition d'erreur est "Renvoie toujours Err en cas de dépassement arithmétique." ce qui est assez générique. Il est difficile de dire s'il s'agit d'une condition d'erreur utile. Pour chaque implémentation de trait Alloc on pourrait peut-être en trouver un différent qui pourrait faire de l'arithmétique qui pourrait en théorie s'emballer, donc ️


Documents de core::alloc::Alloc (parties pertinentes en surbrillance):

En effet. Je trouve étrange que tant de méthodes (par exemple Alloc::alloc ) déclarent que les allocations de taille zéro sont un comportement indéfini, mais alors nous fournissons simplement Alloc::alloc_array(0) avec un comportement défini par l'implémentation. Dans un certain sens, Alloc::alloc_array(0) est un test décisif pour vérifier si un allocateur prend en charge les allocations de taille nulle ou non.

Si [T; N] ne répond pas à la taille de l'allocateur ou aux contraintes d'alignement, alors AFAICT ne rentre pas dans le bloc de mémoire de l'allocation, et le comportement est indéfini (par la clause de sécurité).

Oui, je pense que cette condition d'erreur peut être supprimée car elle est redondante. Soit nous avons besoin de la clause de sécurité, soit d'une condition d'erreur, mais pas des deux.

L'autre condition d'erreur est "Renvoie toujours Err en cas de dépassement arithmétique." ce qui est assez générique. Il est difficile de dire s'il s'agit d'une condition d'erreur utile.

OMI, il est protégé par la même clause de sécurité que ci-dessus; si la capacité de [T; N] débordait, il ne rentre pas @pnkfelix pourrait élaborer là-dessus?

Dans un certain sens, Alloc::alloc_array(1) est un test décisif pour vérifier si un allocateur prend en charge les allocations de taille nulle ou non.

Vouliez-vous dire Alloc::alloc_array(0) ?

OMI, il est protégé par la même clause de sécurité que ci-dessus; si la capacité de [T; N] débordait, il ne _fit_ ce bloc de mémoire à désallouer.

Notez que cette caractéristique peut être implémentée par les utilisateurs pour leurs propres allocateurs personnalisés, et que ces utilisateurs peuvent remplacer les implémentations par défaut de ces méthodes. Ainsi, lorsque l'on considère si cela doit retourner Err pour un dépassement arithmétique ou non, il ne faut pas seulement se concentrer sur ce que fait l'implémentation par défaut actuelle de la méthode par défaut, mais aussi considérer ce que cela pourrait avoir de sens pour les utilisateurs qui les implémentent pour d'autres allocateurs.

Vouliez-vous dire Alloc::alloc_array(0) ?

Oui désolé.

Notez que cette caractéristique peut être implémentée par les utilisateurs pour leurs propres allocateurs personnalisés, et que ces utilisateurs peuvent remplacer les implémentations par défaut de ces méthodes. Ainsi, lorsque l'on considère si cela doit renvoyer Err pour un débordement arithmétique ou non, il ne faut pas seulement se concentrer sur ce que fait l'implémentation par défaut actuelle de la méthode par défaut, mais aussi considérer ce que cela pourrait avoir de sens pour les utilisateurs qui les implémentent pour d'autres allocateurs.

Je vois, mais la mise en œuvre de Alloc nécessite un unsafe impl et les implémenteurs doivent suivre les règles de sécurité mentionnées dans https://github.com/rust-lang/rust/issues/32838#issuecomment -467093527 .

Chaque API laissée pointant ici pour un problème de suivi est le trait Alloc ou lié au trait Alloc . @ rust-lang / libs, pensez-vous qu'il est utile de garder cela ouvert en plus de https://github.com/rust-lang/rust/issues/42774?

Question de fond simple: quelle est la motivation derrière la flexibilité avec les ZST? Il me semble que, étant donné que nous savons à la compilation qu'un type est un ZST, nous pouvons complètement optimiser à la fois l'allocation (pour retourner une valeur constante) et la désallocation. Compte tenu de cela, il me semble que nous devrions dire l'une des choses suivantes:

  • C'est toujours à l'implémenteur de prendre en charge les ZST, et ils ne peuvent pas renvoyer Err pour les ZST
  • C'est toujours UB d'allouer des ZST, et c'est la responsabilité de l'appelant de court-circuiter dans ce cas
  • Il existe une sorte de méthode alloc_inner que les appelants implémentent, et une méthode alloc avec une implémentation par défaut qui fait le court-circuit; alloc doit prendre en charge les ZST, mais alloc_inner peut PAS être appelé pour un ZST (c'est juste pour que nous puissions ajouter la logique de court-circuit en un seul endroit - dans la définition du trait pour sauver les implémenteurs un peu standard)

Y a-t-il une raison pour laquelle la flexibilité que nous avons avec l'API actuelle est nécessaire?

Y a-t-il une raison pour laquelle la flexibilité que nous avons avec l'API actuelle est nécessaire?

C'est un compromis. On peut soutenir que le trait Alloc est utilisé plus souvent qu'il n'est implémenté, il peut donc être judicieux de rendre l'utilisation d'Alloc aussi simple que possible en fournissant une prise en charge intégrée des ZST.

Cela signifierait que les implémenteurs du trait Alloc devront s'en occuper, mais plus important pour moi, ceux qui essaient de faire évoluer le trait Alloc devront garder les ZST à l'esprit à chaque changement d'API. Cela complique également la documentation de l'API en expliquant comment les ZST sont (ou pourraient l'être si elles sont "définies par l'implémentation") gérées.

Les allocateurs C ++ poursuivent cette approche, où l'allocateur tente de résoudre de nombreux problèmes différents. Cela les a non seulement rendus plus difficiles à implémenter et à faire évoluer, mais également plus difficiles à utiliser pour les utilisateurs en raison de la manière dont tous ces problèmes interagissent dans l'API.

Je pense que la gestion des ZST et l'allocation / la désallocation de la mémoire brute sont deux problèmes orthogonaux et différents, et par conséquent, nous devrions garder l'API du trait Alloc simple en ne les manipulant pas.

Les utilisateurs d'Alloc comme libstd devront gérer les ZST, par exemple sur chaque collection. C'est certainement un problème qui mérite d'être résolu, mais je ne pense pas que le trait Alloc soit l'endroit idéal. Je m'attendrais à ce qu'un utilitaire pour résoudre ce problème apparaisse dans libstd par nécessité, et quand cela se produit, nous pouvons peut-être essayer de RFC un tel utilitaire et l'exposer dans std :: heap.

Tout cela semble raisonnable.

Je pense que la gestion des ZST et l'allocation / la désallocation de la mémoire brute sont deux problèmes orthogonaux et différents, et par conséquent, nous devrions garder l'API du trait Alloc simple en ne les manipulant pas.

Cela n'implique-t-il pas que l'API ne devrait pas explicitement gérer les ZST plutôt que d'être définie par l'implémentation? OMI, une erreur "non prise en charge" n'est pas très utile au moment de l'exécution car la grande majorité des appelants ne seront pas en mesure de définir un chemin de secours et devront donc supposer que les ZST ne sont de toute façon pas pris en charge. Cela semble plus simple de simplement simplifier l'API et de déclarer qu'elles ne sont jamais prises en charge.

La spécialisation serait-elle utilisée par les utilisateurs alloc pour gérer ZST? Ou juste des chèques if size_of::<T>() == 0 ?

La spécialisation serait-elle utilisée par les utilisateurs alloc pour gérer ZST? Ou juste des chèques if size_of::<T>() == 0 ?

Ce dernier devrait être suffisant; les chemins de code appropriés seraient supprimés de manière triviale au moment de la compilation.

Cela n'implique-t-il pas que l'API ne devrait pas explicitement gérer les ZST plutôt que d'être définie par l'implémentation?

Pour moi, une contrainte importante est que si nous interdisons les allocations de taille nulle, les méthodes Alloc devraient être capables de supposer que le Layout qui leur est passé n'est pas de taille zéro.

Il existe plusieurs façons d'y parvenir. La première consisterait à ajouter une autre clause Safety à toutes les méthodes Alloc indiquant que si le Layout est de taille zéro, le comportement n'est pas défini.

Alternativement, nous pourrions interdire les Layout s de taille zéro, puis Alloc n'a pas besoin de dire quoi que ce soit sur les allocations de taille zéro car elles ne peuvent pas se produire en toute sécurité, mais cela aurait des inconvénients.

Par exemple, certains types comme HashMap construisent le Layout partir de plusieurs Layout s, et bien que le Layout final ne soit pas de taille nulle, le les intermédiaires pourraient être (par exemple dans HashSet ). Donc, ces types devraient utiliser "quelque chose d'autre" (par exemple, un type LayoutBuilder ) pour constituer leur dernier Layout s, et payer un chèque de "taille non nulle" (ou utiliser une méthode _unchecked ) lors de la conversion en Layout .

La spécialisation serait-elle utilisée par les utilisateurs d'allocation pour gérer ZST? Ou juste si size_of ::() == 0 chèques?

Nous ne pouvons pas encore nous spécialiser sur les ZST. À l'heure actuelle, tout le code utilise size_of::<T>() == 0 .

Il existe plusieurs façons d'y parvenir. L'une serait d'ajouter une autre clause Safety à toutes les méthodes Alloc indiquant que si Layout est de taille zéro, le comportement n'est pas défini.

Il serait intéressant de se demander s'il existe des moyens d'en faire une garantie à la compilation, mais même un debug_assert indiquant que la mise en page n'est pas de taille nulle devrait être suffisant pour attraper 99% des bogues.

Je n'ai pas prêté attention aux discussions sur les allocateurs, donc désolé à ce sujet. Mais j'ai longtemps souhaité que l'allocateur ait accès au type de valeur qu'il alloue. Il peut y avoir des conceptions d'allocateur qui pourraient l'utiliser.

Ensuite, nous aurions probablement les mêmes problèmes que C ++ et son API d'allocation.

Mais j'ai longtemps souhaité que l'allocateur ait accès au type de valeur qu'il alloue. T

Pourquoi en avez-vous besoin?

@gnzblg @brson Aujourd'hui, j'ai eu un cas d'utilisation potentiel pour savoir _quelque chose_ sur le type de valeur allouée.

Je travaille sur un allocateur global qui peut être commuté entre trois allocateurs sous-jacents - un thread local, un global avec des verrous et un pour les coroutines à utiliser - l'idée étant que je peux contraindre une coroutine représentant une connexion réseau à un maximum quantité d'utilisation de la mémoire dynamique (en l'absence de pouvoir contrôler les allocateurs dans les collections, en particulier dans le code tiers) *.

Il serait peut-être utile de savoir si j'alloue une valeur qui pourrait se déplacer entre les threads (par exemple Arc) par rapport à une autre qui ne le fera pas. Ou peut-être pas. Mais c'est un scénario possible. Pour le moment, l'allocateur global a un commutateur que l'utilisateur utilise pour lui dire à partir de quel allocateur effectuer les allocations (pas nécessaire pour la réallocation ou la gratuité; nous pouvons simplement regarder l'adresse mémoire pour cela).

* [Cela me permet également d'utiliser la mémoire locale NUMA dans la mesure du possible sans aucun verrouillage, et, avec un modèle à 1 core 1 thread, de limiter l'utilisation totale de la mémoire].

@raphaelcohn

Je travaille sur un allocateur global

Je ne pense pas que tout cela s'appliquerait (ou pourrait) au trait GlobalAlloc , et le trait Alloc a déjà des méthodes génériques qui peuvent utiliser les informations de type (par exemple alloc_array<T>(1) alloue un seul T , où T est le type réel, donc l'allocateur peut prendre le type en compte lors de l'allocation). Je pense qu'il serait plus utile aux fins de cette discussion de voir en fait du code implémentant des allocateurs qui utilisent des informations de type. Je n'ai entendu aucun bon argument sur la raison pour laquelle ces méthodes doivent faire partie d'un trait d'allocateur générique, au lieu de simplement faire partie de l'API d'allocateur, ou d'un autre trait d'allocateur.

Je pense qu'il serait également très intéressant de savoir lesquels des types paramétrés par Alloc avez-vous l'intention de combiner avec des allocateurs qui utilisent des informations de type, et quel en sera le résultat.

AFAICT, le seul type intéressant pour cela serait Box car il alloue directement un T . Presque tous les autres types de std n'allouent jamais un T , mais certains types internes privés dont votre allocateur ne peut rien savoir. Par exemple, Rc et Arc pourraient allouer (InternalRefCounts, T) , List / BTreeSet / etc. allouer des types de nœuds internes, Vec / Deque / ... alloue des tableaux de T s, mais pas T s eux-mêmes, etc.

Pour Box et Vec nous pourrions ajouter de manière rétrocompatible un trait BoxAlloc et un ArrayAlloc avec une couverture implicite pour Alloc que les allocateurs pourraient se spécialiser pour détourner leur comportement, s'il est nécessaire d'attaquer ces problèmes de manière générique. Mais y a-t-il une raison pour laquelle fournir vos propres types MyAllocBox et MyAllocVec qui conspirent avec votre allocateur pour exploiter les informations de type n'est pas une solution viable?

Comme nous avons maintenant un référentiel dédié pour le groupe de travail sur les allocateurs et que la liste dans l'OP est obsolète, ce problème peut être fermé pour garder les discussions et le suivi de cette fonctionnalité en un seul endroit?

Un bon point @TimDiekmann! Je vais aller de l'avant et fermer cela en faveur des fils de discussion dans ce référentiel.

C'est toujours le problème de suivi vers lequel pointent certains attributs #[unstable] . Je pense qu'il ne devrait pas être fermé tant que ces fonctionnalités n'ont pas été stabilisées ou obsolètes. (Ou nous pourrions modifier les attributs pour indiquer un problème différent.)

Oui, les fonctionnalités instables référencées dans git master devraient certainement avoir un problème de suivi ouvert.

D'accord. Ajout d'un avis et d'un lien vers le PO.

Cette page vous a été utile?
0 / 5 - 0 notes