Rust: Utilisez #[repr(C)] HList pour déduire des pointeurs fmt fn effacés dans les données statiques de format_args!.

Créé le 5 sept. 2017  ·  3Commentaires  ·  Source: rust-lang/rust

À l'heure actuelle, format_args! utilise, par exemple ArgumentV1::new(&runtime_data, Debug::fmt) (pour {:?} ), à l'exécution, en utilisant jusqu'à deux pointeurs par argument au lieu d'un seul ( &runtime_data ) .

Avec allow_internal_unsafe et #44240, nous pouvons placer les (par exemple Debug::fmt ) fn pointeurs dans (rvalue-promoted) 'static data, l'obstacle restant est de savoir comment pour déduire le type des données d'exécution.
C'est-à-dire que Debug::fmt est vraiment <_ as Debug>::fmt et que _ est maintenant déduit à cause de la signature de ArgumentV1::new qui les a tapés ensemble. S'ils sont séparés, nous avons besoin de quelque chose de nouveau.

Je propose d'utiliser le motif HList ( struct HCons<H, T>(H, T); struct HNil; - donc pour 3 éléments, de types A , B et C vous auriez HCons<A, HCons<B, HCons<C, HNil>>> ), avec #[repr(C)] , ce qui lui donnerait une disposition déterministe qui correspond à celle d'un tableau, c'est-à-dire ces deux :

  • &'static HCons<fn(&A), HCons<fn(&B), HCons<fn(&C), HNil>>>
  • &'static [unsafe fn(*const Opaque); 3]

ont la même représentation, et cette dernière peut être dédimensionnée en tranche. Cette transformation de HList en tableau (puis slice) peut être effectuée sur un HCons sécurisé et promu par rvalue, ce qui est une condition nécessaire pour déplacer les pointeurs fn en données 'static .

Pour l'inférence, nous pouvons simplement insérer des appels de fonction pour faire correspondre les types, par exemple pour déduire B nous pourrions faire fmt::unify_fn_with_data((list.1).0, &b) , ce qui ferait B en typeof b .

Il pourrait en fait être plus simple d'avoir une interface "builder" complètement sûre, qui combine les HList de formateurs avec un HList de références d'exécution, unifiant les types, mais je suis un peu inquiet à propos de temps de compilation en raison de tous les caractéristiques de répartition - dans tous les cas, l'impact doit être mesuré.


A-fmt C-enhancement I-heavy T-libs

Commentaire le plus utile

Pour l'inférence, nous pouvons simplement insérer des appels de fonction pour faire correspondre les types, par exemple pour déduire B nous pourrions faire fmt::unify_fn_with_data((list.1).0, &b) , ce qui ferait B en typeof b .

Je ne sais pas trop à quoi je pensais, ça devrait être beaucoup plus facile que ça !

struct ArgMetadata<T: ?Sized> {
    // Only `unsafe` because of the later cast we do from `T` to `Opaque`.
    fmt: unsafe fn(&T, &mut Formatter<'_>) -> Result,
    // ... flags, constant string fragments, etc.
}

// TODO: maybe name this something else to emphasize repr(C)?
#[repr(C)]
struct HCons<T, Rest>(T, Rest);

// This would have to be in a "sealed module" to make it impossible to implement on more types.
trait MetadataFor<D> {
    const LEN: usize;
}
impl MetadataFor<()> for () {
    const LEN: usize = 0;
}
impl<'a, T: ?Sized, D, M> MetadataFor<HCons<&'a T, D>> for HCons<ArgMetadata<T>, M>
    where M: MetadataFor<D>
{
    const LEN: usize = M::LEN;
}

impl<'a> Arguments<'a> {
    fn new<M, D>(meta: &'a M, data: &'a D) -> Self
        where M: MetadataFor<D>
    {
        Self {
            meta: unsafe { &*(meta as *const _ as *const [ArgMetadata<Opaque>; M::LEN]) },
            data: unsafe { &*(data as *const _ as *const [&Opaque; M::LEN]) },
        }
    }
}

c'est-à-dire que nous construisons deux HList s "en parallèle", l'un avec des métadonnées entièrement constantes, et l'autre avec les références aux données d'exécution, puis toute l'inférence de type peut provenir du where clause sur fmt::Arguments::new , avec zéro codegen cruft !

EDIT : @m-ou-se a dû me rappeler pourquoi je suis parti avec l'astuce d'inférence explicite en premier lieu : arguments d'accès aléatoire :déçu:
(peut-être qu'avec suffisamment d'utilisation de génériques const ab , nous pourrions avoir D: IndexHList<i, Output = T> mais c'est beaucoup d'efforts)

Tous les 3 commentaires

réclamation @rustbot

Pour l'inférence, nous pouvons simplement insérer des appels de fonction pour faire correspondre les types, par exemple pour déduire B nous pourrions faire fmt::unify_fn_with_data((list.1).0, &b) , ce qui ferait B en typeof b .

Je ne sais pas trop à quoi je pensais, ça devrait être beaucoup plus facile que ça !

struct ArgMetadata<T: ?Sized> {
    // Only `unsafe` because of the later cast we do from `T` to `Opaque`.
    fmt: unsafe fn(&T, &mut Formatter<'_>) -> Result,
    // ... flags, constant string fragments, etc.
}

// TODO: maybe name this something else to emphasize repr(C)?
#[repr(C)]
struct HCons<T, Rest>(T, Rest);

// This would have to be in a "sealed module" to make it impossible to implement on more types.
trait MetadataFor<D> {
    const LEN: usize;
}
impl MetadataFor<()> for () {
    const LEN: usize = 0;
}
impl<'a, T: ?Sized, D, M> MetadataFor<HCons<&'a T, D>> for HCons<ArgMetadata<T>, M>
    where M: MetadataFor<D>
{
    const LEN: usize = M::LEN;
}

impl<'a> Arguments<'a> {
    fn new<M, D>(meta: &'a M, data: &'a D) -> Self
        where M: MetadataFor<D>
    {
        Self {
            meta: unsafe { &*(meta as *const _ as *const [ArgMetadata<Opaque>; M::LEN]) },
            data: unsafe { &*(data as *const _ as *const [&Opaque; M::LEN]) },
        }
    }
}

c'est-à-dire que nous construisons deux HList s "en parallèle", l'un avec des métadonnées entièrement constantes, et l'autre avec les références aux données d'exécution, puis toute l'inférence de type peut provenir du where clause sur fmt::Arguments::new , avec zéro codegen cruft !

EDIT : @m-ou-se a dû me rappeler pourquoi je suis parti avec l'astuce d'inférence explicite en premier lieu : arguments d'accès aléatoire :déçu:
(peut-être qu'avec suffisamment d'utilisation de génériques const ab , nous pourrions avoir D: IndexHList<i, Output = T> mais c'est beaucoup d'efforts)

J'ai une nouvelle implémentation de fmt::Arguments qui n'a que deux pointeurs en taille en utilisant une nouvelle forme de 'métadonnées statiques qui contient à la fois les morceaux de chaîne et toutes les options de formatage (le cas échéant). (Il tient donc maintenant dans une paire de registres, ce qui est vraiment bien.) Il ne nécessite également qu'un seul pointeur sur la pile par argument au lieu de deux, comme @eddyb l'a apparemment déjà suggéré il y a trois ans dans ce numéro. ^^ ( J'ai complètement raté ce problème jusqu'à ce que

Il ne reste plus qu'à mettre format_args!() jour ArgumentV1 ), en tant que pointeur de fonction devrait maintenant aller dans les « métadonnées statiques ». La suggestion dans ce numéro semble être un bon moyen de le faire. J'essaierai de le mettre en œuvre bientôt. J'attends avec impatience le perf run :)

Cette page vous a été utile?
0 / 5 - 0 notes