IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Delphi Discussion :

Pourquoi InterlockedCompare n'existe pas ?


Sujet :

Delphi

  1. #1
    Expert éminent sénior
    Avatar de ShaiLeTroll
    Homme Profil pro
    Développeur C++\Delphi
    Inscrit en
    Juillet 2006
    Messages
    13 469
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Seine Saint Denis (Île de France)

    Informations professionnelles :
    Activité : Développeur C++\Delphi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Juillet 2006
    Messages : 13 469
    Points : 24 905
    Points
    24 905
    Par défaut Pourquoi InterlockedCompare n'existe pas ?
    Bonjour,

    Je cherche des pistes d'optimisations dans le projet de l'équipe dont je fais parti.

    L'un des sujets sont les compteurs purs et compteurs type verrou

    Les compteurs purs sont systématiquement incrémentés, leur lecture n'a pas d'impact particulier.
    Un test multi-thread a démontré que la non protection de l'incrément était très impactant
    Ce même test a démontré les performances de AtomicIncrement (un alias pour InterlockedIncrement) et sa pertinence de protection.
    Par exemple 16 threads incrémentant de 1 une même variable Integer 250 000 fois, soit un total de 4 000 000 à la fin, c'est à tout juste 500 000 sans protection.

    Version Delphi Seatle donc InterlockedIncrement est désormais en ASM comme sous Kylix sans utiliser les API Windows

    J'utilise le couple InterlockedIncrement et InterlockedDecrement depuis 2004 lorsque j'ai découvert le TInterfacedObject et le modèle COM (j'avais même joué avec TClassInstancing et TThreadingModel)
    Je l'utilise toujours de la même façon, Increment dont je n'ai que faire de la valeur et surtout Decrement où je fais une action sur Zéro, généralement une libération ou le signal d'une fin des traitements parallélisés.
    Je ne l'ai encore pas utilisé en deux temps comme ces "compteurs type verrou" où un code pose son verrou et un autre code lit la valeur.


    les compteurs type verrou, ce sont principalement des coupe-circuits, typiquement une tache n'est pas effectuée si d'autres ne sont pas en cours.

    exemple "compteurs type verrou"

    Pose du verrou
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
            try
              Context.RunningCount := Context.RunningCount + 1 ;
              // Code Traitement couteux en temps
              // ...
            finally
              Context.RunningCount := Context.RunningCount - 1 ;
            end ;
    Utilisation du Verrou
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
            DoAction := Context.Chose and Context.Machin and (Context.RunningCount = 0);
            if DoAction  then
              // Code potentiellement conflictuel avec le Traitement couteux en temps
    Déclaration du Verrou

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
        property RunningCount: Integer read GetRunningCount write SetRunningCount;

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function TContext.GetRunningCount: Integer;
    begin
      FCriticalSection.Enter('Context_GetRunningCount');
      try
        result := FRunningCount;
      finally
        FCriticalSection.Release;
      end;
    end;
     
    procedure TLoginSession.SetRunningCount(AValue: Integer);
    begin
      FCriticalSection.Enter('Context_SetRunningCount');
      try
        FRunningCount := AValue;
      finally
        FCriticalSection.Release;
      end;
    end;
    En réalité cette Section Critique n'apporte pas de protection car le +1 est en dehors de la protection
    Par exemple 16 threads incrémentant de 1 une même variable Integer 250 000 fois, soit un total de 4 000 000 à la fin, c'est à peine 600 000 avec cette protection incomplète.


    Voici le code que je propose à mon responsable pour les compteurs verrous, il en sera de même pour les compteurs purs mais Inc uniquement.

    Pose du verrou
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
            try
              Context.IncRunningCount();
              // Code Traitement couteux en temps
              // ...
            finally
              Context.DecRunningCount();
            end ;
    Utilisation du Verrou inchangée

    Déclaration du Verrou

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
        property RunningCount: Integer read GetRunningCount;
    Nouvelles fonctions puisque le compteur est en lecture seule désormais

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function TContext.IncRunningCount(): Integer;
    begin
      Result := AtomicIncrement(FRunningCount);
    end;
     
    function TContext.DecRunningCount(): Integer;
    begin
      Result := AtomicDecrement(FRunningCount);
    end;
    et je m'interroge pour GetRunningCount et c'est la question de ce post
    InterlockedCompare n'existe pas, sur Stackoverflow et d'autres forums anglophones, je n'ai pas trouvé de réponse satisfaisante sur l'inutilité de cette fonction.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    function TContext.GetRunningCount: Integer;
    begin
      Result := FRunningCount;
    end;
    Ou

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    function TContext.GetRunningCount: Integer;
    begin
      Result := AtomicCmpExchange(FRunningCount, 0, 0);
    end;
    AtomicCmpExchange modifie FRunningCount si Comparand (le second 0) est égale à FRunningCount pour mettre la valeur NewValue (le premier 0)
    Le retour de AtomicCmpExchange c'est la dernière valeur de FRunningCount avant l'échange
    Donc soit différent de Zéro en cas d'échec de l'échange avec comparaison à Zéro
    Et finalement égale à Zéro en cas de réussite de l'échange avec comparaison à Zéro

    Deux questions :
    - AtomicCompare est inexistant ! Pourquoi ?
    - AtomicCmpExchange est-il utile ou une hérésie pour une protection de lecture ?
    Aide via F1 - FAQ - Guide du développeur Delphi devant un problème - Pensez-y !
    Attention Troll Méchant !
    "Quand un homme a faim, mieux vaut lui apprendre à pêcher que de lui donner un poisson" Confucius
    Mieux vaut se taire et paraître idiot, Que l'ouvrir et de le confirmer !
    L'ignorance n'excuse pas la médiocrité !

    L'expérience, c'est le nom que chacun donne à ses erreurs. (Oscar Wilde)
    Il faut avoir le courage de se tromper et d'apprendre de ses erreurs

  2. #2
    Expert éminent sénior
    Avatar de Paul TOTH
    Homme Profil pro
    Freelance
    Inscrit en
    Novembre 2002
    Messages
    8 964
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Freelance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2002
    Messages : 8 964
    Points : 28 445
    Points
    28 445
    Par défaut
    alors à mon avis, pour la lecture ça n'a pas d'importance, tu peux simplement retourner sa valeur directement

    mais une chose m'échappe...dans ce code

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
            DoAction := Context.Chose and Context.Machin and (Context.RunningCount = 0);
            if DoAction  then
              // Code potentiellement conflictuel avec le Traitement couteux en temps
    entre la première et la deuxième ligne, dans un environnement multitâche, tu as toutes les chances que ton DoAction ne soit plus cohérant avec le RunningCount

    il serait plus logique de faire un truc genre
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
      FCriticalSection.Enter('Context_GetRunningCount');
      try
            DoAction := Context.Chose and Context.Machin and (Context.RunningCount = 0);
            if DoAction  then
              Context.IncRunningCount;
      finally
        FCriticalSection.Release;
      end;
      if DoAction then
       // Code potentiellement conflictuel avec le Traitement couteux en temps
    après ça fait longtemps que je n'ai pas regardé, mais tu as d'autres objets de synchronisation, comme les sémaphores
    http://docwiki.embarcadero.com/Libra...bjs.TSemaphore
    Developpez.com: Mes articles, forum FlashPascal
    Entreprise: Execute SARL
    Le Store Excute Store

  3. #3
    Expert éminent sénior
    Avatar de ShaiLeTroll
    Homme Profil pro
    Développeur C++\Delphi
    Inscrit en
    Juillet 2006
    Messages
    13 469
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Seine Saint Denis (Île de France)

    Informations professionnelles :
    Activité : Développeur C++\Delphi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Juillet 2006
    Messages : 13 469
    Points : 24 905
    Points
    24 905
    Par défaut
    Merci de ta réponse, je m'attendais un peu que tu sois le premier.

    Citation Envoyé par Paul TOTH Voir le message
    alors à mon avis, pour la lecture ça n'a pas d'importance, tu peux simplement retourner sa valeur directement
    La lecture étant une opération presque atomique, c'est ce que je me suis dit aussi
    Contrairement à l'incrémentation qui en fait composer de plusieurs micro-instructions soit une lecture à l'adresse indiquée vers un registre puis une incrémentation du registre puis une écriture à l'adresse indiquée à partir du registre.
    Et si à l'époque d'un CPU mono coeur, normalement l'ordonnanceur n'aurait pas, il me semble, changer de thread durant une instruction ASM, quoi que, j'en sais rien en fait
    Avec un 8 cœurs, il est tout à fait possible que l'instruction d'increméntation lancée par plusieurs thread se collisionne sur la micro-instruction de lecture d'où le LOCK ASM
    Par contre, que cela soit A B ou C qui soient le premier à lire la valeur et que l'un deux est celui qui fait le " = 0 ", disons que c'est juste une question de temporalité mais il n'y a pas d'impact sur l'exactitude du résultat.

    Citation Envoyé par Paul TOTH Voir le message
    il serait plus logique de faire un truc genre
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
      FCriticalSection.Enter('Context_GetRunningCount');
      try
            DoAction := Context.Chose and Context.Machin and (Context.RunningCount = 0);
            if DoAction  then
              Context.IncRunningCount;
      finally
        FCriticalSection.Release;
      end;
      if DoAction then
       // Code potentiellement conflictuel avec le Traitement couteux en temps
    Alors oui et non

    Non car IncRunningCount euh ben non, il n'a rien à faire là et ne serait jamais décrémenté
    J'ai un peu changé les noms, DoAction va s'appler ABadFlag
    et le teste n'est pas if DoAction then mais l'inverse if not ABadFlag then.
    L'idée est que l'on ne fait pas une action couteuse en temps si une autre opération couteuse est lancée mais ce n'est pas seule raison, j'omis la suite mais il y a des cas d'anomalie.
    Mais encore plus important c'est la valeur retournée ABadFlag peut avoir des conséquence, genre libérer le Context alors qu'il y a ailleurs une traitement en cours pour ce Context.
    En fait ABadFlag ne doit pas prendre la valeur True dans certains cas dont le cas d'un RunningCount positif indiquant un traitement en cours.


    Oui car tu as presque deviné le code mais FCriticalSection c'est un membre privé de TContextList à ne pas confondre avec l'autre FCriticalSection membre privée de TContext
    Abus de Section Critique = Dead Lock

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function TContextList.Find(const AID: string; var ABadFlag: Boolean): TContext;
    var
       I: Integer;
    begin
      FCriticalSection.Enter('ContextList.Find') ;
      try
        ABadFlag := False;
        I := IndexOf(ASessionID); // TContextList est une TStringList améliorée et je n'ai pas obtenu mieux avec un TObjectDictionnay<string, TContext> sur NexusMM (alors qu'avec FastMM c'est à voir)
        if I > -1 then
        begin
          result := TContext(Objects[i]) ;
          if result <> NIL then
          begin
            ABadFlag := (not Result.Chose and not Result.Machin) and (Result.RunningCount = 0) and ((Result.Dummy > AgeDuCapitaine) or (Result.Reponse <> 42) or Result.EstMalade);
            if not ABadFlag then
              Result.CheckBidule(); // Ce code n'aurait jamais du se retrouver là en réalité, une chose qui changera !
          end
        end
        else
          result := NIL ;
      finally
      	FCriticalSection.Release();
      end;
    end;

    Si CheckBidule n'est pas lancé parce qu'il y a un léger déphasage avec RunningCount, aucune conséquence
    Au pire CheckBidule sera appelé plus tard, ce Find est lancé tellement de fois pour chaque thread de context, qu'il sera bien lancé à un moment donné.

    Même l'idée serait de mettre CheckBidule dans une CronTask a été soulevé ce matin, il faudra bosser sur la synchronisation pour éviter que l'on supprime l'objet en cours alors que CheckBidule tourne dessus.
    Mais ça c'est à mon chef de voir l'impact, moi mon boulot c'est la performance, trouver ce qui coince et ce que l'on peut améliorer sans non plus tout modifié, en fait si possible, je dois changer l'implémentation sans que oblige des changer les appels, même si des fois, il n'y a pas le choix.


    Citation Envoyé par Paul TOTH Voir le message
    après ça fait longtemps que je n'ai pas regardé, mais tu as d'autres objets de synchronisation, comme les sémaphores
    http://docwiki.embarcadero.com/Libra...bjs.TSemaphore
    Il est fort probable que la synchronisation disparaisse de toute façon, le code ayant évolué au fil des années, il semblerait que ce compteur-verrou ne soit pas concerné par le multi-thread ... c'est Context.Chose and Context.Machin qui exclue le cas du multi-thread en réalité.
    Les sémaphores sont souvent codés à partir d'un Compteur-Verrou justement
    Pour les Semaphore, je n'ai jamais dépassé le stade du Mutex pour coder un unique-instance, sinon, je suis très TThreadList ou TCriticalSection, parfois TMREWS et TSimpleRWSync (qui utilisent tous les deux un Spin-Lock soit un Compteur-Verrou)



    Et finalement, en dehors de tout ça, est-ce que cet étrange appel à AtomicCmpExchange qui remet la même valeur si elle est déjà là a un sens ou pas ?
    En théorie, on vérifie que l'on est à zéro pour remettre zéro, il n'y aucun risque
    En pratique, j'ai une autre tache avant de m'inquiéter de ça et je fais donc "Appel au public" parce que le 50/50 n'a pas été déterminant.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    function TContext.GetRunningCount: Integer;
    begin
      Result := AtomicCmpExchange(FRunningCount, 0, 0);
    end;
    Aide via F1 - FAQ - Guide du développeur Delphi devant un problème - Pensez-y !
    Attention Troll Méchant !
    "Quand un homme a faim, mieux vaut lui apprendre à pêcher que de lui donner un poisson" Confucius
    Mieux vaut se taire et paraître idiot, Que l'ouvrir et de le confirmer !
    L'ignorance n'excuse pas la médiocrité !

    L'expérience, c'est le nom que chacun donne à ses erreurs. (Oscar Wilde)
    Il faut avoir le courage de se tromper et d'apprendre de ses erreurs

  4. #4
    Membre actif
    Homme Profil pro
    libre
    Inscrit en
    Juin 2019
    Messages
    205
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Autre

    Informations professionnelles :
    Activité : libre

    Informations forums :
    Inscription : Juin 2019
    Messages : 205
    Points : 292
    Points
    292
    Par défaut
    J'ai trouvé cet exemple sous win32 et me suis rappelé de ce topic c'est un vieux code Embarcadero pour un simple GC

    pour la lecture j'ai vu un accès direct sans protection ... l'unité "stGC" est long je ne peut pas mettre tout le code

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    property ObjectCount: Integer read fObjectCount;
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     
    function MarkInSweep(aExpected, aNewValue: Boolean; var aInSweep: Boolean): Boolean;
    asm
      LOCK CMPXCHG [ECX], DL
    end;
    ..
    procedure tGCManager.Sweep;
    var
      lFirst, lNext,
      lPrevLast, lFirstUnknown, lFirstLive: pGCHeader;
     
    begin
      if MarkInSweep(False,True,fInSweep) then Exit;
      try
     
        .... 
      finally
        MarkInSweep(True,False,fInSweep);
      end;
    end;

Discussions similaires

  1. Réponses: 0
    Dernier message: 01/03/2016, 19h17
  2. Pourquoi il n'existe pas de framework/bibliothèques orientés "métier"?
    Par kisitomomotene dans le forum Débats sur le développement - Le Best Of
    Réponses: 31
    Dernier message: 26/04/2012, 18h04
  3. Pourquoi les objets courbés n'existent pas ?
    Par bricechris dans le forum Moteurs 3D
    Réponses: 24
    Dernier message: 09/05/2011, 09h09
  4. Pourquoi ai-je une erreur lors d'une recherche si la valeur n'existe pas ?
    Par blackndoor dans le forum Macros et VBA Excel
    Réponses: 5
    Dernier message: 17/03/2009, 11h22
  5. Pourquoi option execution Format Word existe pas?
    Par jeffidf dans le forum Cognos
    Réponses: 5
    Dernier message: 13/03/2009, 15h41

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo