Partage
  • Partager sur Facebook
  • Partager sur Twitter

mutable et utilisation de la mémoire cache

C++

Sujet résolu
    6 février 2019 à 12:39:45

    Bonjour OC

    j'ai quelques connaissances qui me permettent de comprendre le principe de la mémoire partagée/distribuée, MPI.., de la prog. parallèle, threads ,  que je développe

    J'ai besoin d'aide pour comprendre plusieurs points dans un nouveau conseil de Mr Scott Meyers.

    Par exemple dans le code suivant de Mr Soctt Meyers,

    class Polynomial{
    public:
    using RootsType = std::vector<double>;
    
    RootType roots() const
    {
      if(!rootsAreValid){
    
      ...
      rootsAreValid = true;
    }
    
    return rootVals;
    }
    
    private:
     mutable bool rootsAreVlid{false;}
     mutable RootsType rootsVals{};
    
    };


    il est question de "mutable", mot-cléf que j'avais déjà vu plusieurs fois, et que je souhaite comprendre après avoir regardé un peu la doc sur google.

    1) à quoi sert précisément le mot-clé "mutable" ?

    2) Pourquoi parle-t-il de la mémoire cache ? Est-ce que c'est une "mémoire vive" qui est plus rapide ? Je me souviens avoir configuré un temps la mémoire cache sous linux, mais on disait qu'elle était de plus faible taille ? J'ai un doute de ce en quoi cela consiste. Est-elle plus rapide d'accès que la mémoire vive ?

    https://fr.wikipedia.org/wiki/M%C3%A9moire_cache

    Soctt Meyers dit qu'il "place les racines du polynôme dans un cache après leur calcul" et dit "implémenter roots de façon qu'elle retourne ces valeurs en cache."

    3) Je souhaite saisir la phrase car je ne suis pas certain de la comprendre : "conceptuellement, roots ne modifie pas l'objet Polynomial qu'elle manipule, mais dans sa gestion du cache, elle peut modifier rootVals et rootsAreValid. Il s'agit d'un cas classique d'utilisation de mutable et c'est pourquoi nous allons l'utiliser dans la déclaration de ces données membres."

    Merci par avance pour votre aide

    CREDIT : "Programmez efficacement en C++, de Scott Meyers (Dunod). Copyright 2016 Dunod pour la version française 978-2-10-074391-9, et 2015 Scott Meyers pour la versio d'origine 978-1-491-90399-5"

    -
    Edité par pseudo-simple 6 février 2019 à 12:52:07

    • Partager sur Facebook
    • Partager sur Twitter
      6 février 2019 à 13:09:17

      1) a modifier des membres d'une instance de classe const.

      2) un objet const pourra être gardé en cache sans risque d'être modifié. oui, oui. configurer la mémoire cache ? c'est hardware la cache, laisse la machine faire. Plus petite taille, plus rapide.

      • Partager sur Facebook
      • Partager sur Twitter

      Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

        6 février 2019 à 13:37:30

        D'accord. Merci à vous

        Un peu après, il y a une expression que je ne comprends pas, c'est quand Mr Scott Meyers ajoute :

        "Le problème vient du fait que roots est déclarée const sans qu'elle soit sûre vis-à-vis des threads".

        Je souhaite saisir le sens du passage que j'ai mis en caractère gras.

        Je précise que j'ai compris le principe de la concurrence sur les données. Il s'agit de la problématique d'accès à un Polynomial p par des threads différents et de la synchronisation de ces mouvements pour éviter que deux threads différents ne tentent de modifier en même temps la même donnée, dégénérant ainsi en comportement indéfini.

        Par avance merci

        -
        Edité par pseudo-simple 6 février 2019 à 14:17:06

        • Partager sur Facebook
        • Partager sur Twitter
          6 février 2019 à 14:23:59

          Bah dans roots il manque la fameuse "synchronisation" dont vous parlez, qui permet "d'éviter que deux threads différents ne tentent de modifier en même temps la même donnée" comme vous l'expliquez si bien dans le cas où on a un "accès à un Polynomial p par des threads différents" comme vous le présentez.

          Par conséquent il y a un risque que cela "dégénère en comportement indéfini" tel sont vos propos, et cela permet que vous en déduisiez "qu'elle ne soit pas sûre vis à vis des threads".

          C'est fort aimable à vous de nous expliquer tout cela mais nous n'en avions pas fait la demande. :)

          -
          Edité par Maluna34 6 février 2019 à 14:24:19

          • Partager sur Facebook
          • Partager sur Twitter
            6 février 2019 à 14:42:12

            1) Déjà bien répondu par @Fvirtman

            2) Vous, ou la traduction, confondez "mémoire cache" et "se servir de la mémoire comme cache des données calculables". Et pour votre histoire de "mémoire cache" sous Linux, je pense à un abus de langage comme configurer de la mémoire vive pour servir de "cache disk" ou autre. "Cache" est un terme valise extrêmement dépendant du contexte d'utilisation.

            3) je trouve la phrase assez claire, si on comprend par "cache", la mémoire vive contenant les racines du polynôme calculées à la volé et pas à la création/modification de l'objet.

            4) <question sur les thread> : L'utilisateur d'un objet "const" peut croire, à tord à cause de mutable, qu'un objet "const" est thread-safe. "thread-safe", c'est un objet qui peut être utilisé simultanément par deux threads différents, ce qu'est un objet "const", s'il n'utilise pas de "mutable".

            Il est donc très intéressant de rendre les objets "const+mutable" thread-safe pour ne pas piéger l'utilisateur de la classe.

            Ici, l'implémentation de la classe est très loin d'être thread-safe.

            • Partager sur Facebook
            • Partager sur Twitter
            Je recherche un CDI/CDD/mission freelance comme Architecte Logiciel/ Expert Technique sur technologies Microsoft.
              6 février 2019 à 15:23:41

              Salut,

              Déjà, je soupçonne Meyers de parler de "cache" et non de "mémoire cache"... :

              Le cache, de manière générale, c'est un moyen mis en oeuvre pour permettre la récupération "plus rapide" d'une information qui a déjà été recherchée et dont la recherche risque de prendre "un temps bête"; alors que "la mémoire cache" est généralement associée aux caches du processeur (généralement désignés sous les termes de L1 / L2 / L3).

              La plupart des serveurs (web, de base de données ou autres) disposent de moyen de "mettre en cache" certaines informations qu'ils "ont mis longtemps à trouver" de manière à pouvoir les réutiliser plus facilement, et à ne pas perdre le temps nécessaire à l'obtention de ces informations "à chaque fois qu'ils en ont besoin".

              Le processeur, quant à lui, fait un pari "plus ou moins risqué" lorsqu'il doit aller récupérer une information qui se trouve en RAM, parce qu'il est possible d'avoir de la mémoire très rapide, ou d'avoir une très grande quantité de mémoire, mais que plus la quantité de mémoire augmente, plus le temps pour pouvoir accéder à une adresse particulière augmente (et donc, la vitesse de la mémoire diminue):

              Quand il a besoin d'une information qu'il ne trouve pas dans le cache, au lieu d'aller chercher uniquement les deux ou trois bytes dont il a besoin, il va directement dans un de ses caches toutes les informations qu'il peut trouver dans un espace de 1Kb (valeur fictive) et qui sont contigues à la donnée dont il a besoin, dans l'espoir que "la donnée dont il aura besoin par la suite" se trouve dans  la quantité d'informations qu'il aura copiées.

              Cela prend un certain temps, parce qu'il faut copier toutes ces informations, mais d'un autre coté, c'est surtout le fait d'indiquer l'adresse de la mémoire à laquelle on veut accéder qui prend du temps... accéder aux adresses suivantes, c'est "relativement" rapide; et, surtout, les caches L1 / L2 / L3 -- qui sont des quantités de mémoire beaucoup moins importantes que les 16Gb de RAM -- permettent d'accéder beaucoup plus rapidement aux données.

              Si bien que, au final, si le processeur "gagne son pari" et que les 10, 15, 50 (ou plus) données auxquelles il doit accéder "après" avoir accédé à la donnée qu'il devait aller chercher en mémoire se trouvent déjà dans le cache, il gagnera "énormément de temps".

              A l'inverse, s'il perd son pari, et que "la donnée d'après" ne se trouve pas dans le cache, il aura perdu "énormément de temps" :P.

              Le mot clé mutable n'a rien à voir avec ce mécanisme propre au processeur.

              Le rôle de mutable, c'est de lever les restrictions imposées par la const-correctness:

              Tu n'est en effet pas sans savoir que si tu as une classe qui prend une forme proche de

              class MaClasse{
              public:
                  void foo();
                  Ret bar(Param p) const;
              private:
                  Type data_;
              }

              tu ne pourras appeler que la fonction bar à partir de n'importe quelle instance (considérée) comme constante de MaClasse, qu'il s'agisse d'une variable ou d'une référence; et que la fonction bar ne pourra en aucun cas modifier data_, sous peine de te faire insulter par le compilateur.

              Dans la plupart des cas, cette restriction est particulièrement intéressante, car elle te donne la garantie que ton instance de classe ne sera pas modifiée, même si tu la transmet sous forme d'une référence (constante, bien sur) à une fonction.

              Le problème survient lorsque data_ est une donnée complexe (par exemple : une collection de données), que bar doit évaluer la valeur de retour, sur base de p et que cette valeur dépend du résultat ... du calcul effectué pour une autre valeur de p.

              Comme cette explication n'est pas très claire, précisons le problème en disant que notre classe s'appelle Fibionnacci et que bar s'appelle en réalité compute et que le paramètre indique le numéro d'ordre de l'élément souhaité dans la suite de Fibionnacci.

              Pour rappel, chaque élément de cette suite correspond à la somme des deux éléments qui le précède.

              Nous pourrions ne pas utiliser la moindre donnée et implementer compute sous une forme récursive sous une forme proche de

              class Fibionacci{
              public:
                  size_t compute( size_t elem) const{
                      if(elem <= 1)
                          return elem;
                      return copmute(elem-1) + compute(elem - 2);
                  }
              };

              (La fonction peut parfaitement être constante, parce que la suite en elle-même ne change pas au cours du temps ;) )

              Le problème de cette approche, c'est que chaque appel de la fonction va devoir ... recalculer tous les éléments de la suite par lesquels il faut passer, et que le temps d'exécution augmente de manière exponentielle avec la position de l'élément que l'on souhaite dans la liste (en plus de tous les problèmes liés à la récursivité non terminale).

              Si bien qu'un code proche de

              int main(){
                  Fibionacci fib;
                  std::cout <<"elem 100 : " << fib.compute(100)<<"\n"
                           << "elem 99 : "<< fib.compute(99)<<"\n";
                  return 0;
              }

              demandera pour ainsi dire autant de temps pour calculer compute(99) que pour calculer compute(100)...  Ce qui est moche, parce que, pour pouvoir calculer compute(100), il a déjà fallu calculer compute(99) :p :'(.

              L'idéal est donc de s'assurer qu'il ne faudra calculer qu'une seule fois les différents éléments par lesquels on passe, ce qui permettra de ne devoir calculer que les éléments ... qui n'ont pas encore été calculé.

              Cette "sauvegarde" des éléments de la suite prendra le nom de "cache" et permettra, si à un moment quelconque, on demande la valeur du 1 000 eme élément de la liste, de n'avoir plus à calculer  (une fois de plus ! ) la valeur des 999 éléments qui le précède : si après avoir demandé le 1 000 élément, on demande le 999eme,  il suffira d'aller chercher la valeur de cet élément dans le cache, ce qui peut être aussi rapide que ... l'accès à un élément particulier d'un tableau.

              Seulement, pour maintenir la liste de ces éléments en mémoire, nous allons avoir besoin ... d'une collections de valeurs (un tableau d'entier, en fait), qui devra de temps en temps (quand l'élément demandé n'a pas encore été calculé) être modifié.

              Oui, mais cela ne justifie pas de supprimer la constance de la fonction compute : ce n'est pas parce que l'on ajoute des éléments connus à la liste que l'on modifie la suite en elle même!

              Or, si on se contentait d'un code proche de

              class Fibionacci{
              public:
                  size_t compute( size_t elem) const{
                      /* ... la logique va suivre */
                  }
              private:
                  std::vector<size_t> cache_;
              };

              la fonction compute ne pourrait pas modifier le cotenu de cache_ :P.

              Et c'est pour cela que nous aurons besoin du mot clé mutable, qui indiquera clairement que, même si l'instance de Fibionacci est considérée comme étant constante, les fonctions membres (qui se sont engagée à ne pas modifier l'instance en cours) peuvent malgré tout aller modifier cache_, sous une forme proche de

              class Fibionacci{
              public:
                  uint64_t compute( size_t elem) const{
                      if(cache_.empty())
                          cache_.push_back(0);
                      if(cache_.size() < 2)
                          cache_.push_back(1);
                      while(cache_.size() < elem ){
                          auto size = cache_.size();
                          cache_.push_back(cache_[size -1] + cache_[size -2]); 
                      }
                      return cache_[elem -1];
                  }
              private:
                  mutable std::vector<uint64_t> cache_;
              };
              • Partager sur Facebook
              • Partager sur Twitter
              Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                6 février 2019 à 15:59:14

                Donc si je comprends vos explications détaillées (et je vous remercie copieusement pour cela), cela veut dire que de manière exceptionnelle, mutable vient briser la const-correctnesse , mais pas l'encapsulation , afin d'effectuer de manière très ponctuelle des changements sur des éléments qui ne peuvent pas être maintenus const ?

                L'exemple de Fibonacci avec le tableau pour éviter d'avoir à toute recalculer par exemple..

                Est-ce que une variable mutable est par principe stockée dans la mémoire cache ?

                -
                Edité par pseudo-simple 6 février 2019 à 16:03:06

                • Partager sur Facebook
                • Partager sur Twitter
                  6 février 2019 à 16:06:24

                  >Est-ce que une variable mutable est par principe stockée dans la mémoire cache ?

                  Absolument pas, ça n'a rien à voir avec la mémoire cache.

                  C'est juste pour implémenter le CONCEPT de "cache".

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Je recherche un CDI/CDD/mission freelance comme Architecte Logiciel/ Expert Technique sur technologies Microsoft.
                    6 février 2019 à 18:26:29

                    YES, man a écrit:

                    Est-ce que une variable mutable est par principe stockée dans la mémoire cache ?

                    Il me semblait pourtant avoir été clair :

                    koala01 a écrit:

                    <snip>

                    Le mot clé mutable n'a rien à voir avec ce mécanisme propre au processeur.

                    Le rôle de mutable, c'est de lever les restrictions imposées par la const-correctness:

                    <snip>

                    • Partager sur Facebook
                    • Partager sur Twitter
                    Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait

                    mutable et utilisation de la mémoire cache

                    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                    • Editeur
                    • Markdown