12
votes

Optimisation du compilateur C # autorisé sur les variables locales et la représentation de la valeur de la mémoire

edit strong>: je demande ce qui se passe lorsque deux threads accèdent simultanément les mêmes données sans synchronisation appropriée forte> (avant que ce point n'a pas été exprimé clairement).

J'ai une question sur les optimisations effectuées par le compilateur C # et par le compilateur JIT. P>

Considérez l'exemple simplifié suivant: P>

class Example {
    private Action _action;

    private void InvokeAction() {
        if (this._action != null) {
            this._action(); // might be set to null by an other thread.
        }
    }
}


3 commentaires

L'optimiseur n'est pas autorisé à changer la sémantique d'une déclaration.


Je pense que le deuxième exemple ne changerait pas la sémantique de la déclaration vue d'un seul fil (!). En outre, supposer que la mémoire ne change pas simultanément (dans ce cas une hypothèse valide, l'optimiseur pourrait faire de l'OMHO).


Je recommande d'ajouter les balises [Languant-Libres] et [Language-Avocat].


3 Réponses :


2
votes

C'est une optimisation légale selon le modèle de mémoire défini dans la spécification de l'ECMA. Si l'établissement _Action était volatil, le modèle de mémoire garantirait que la valeur est lue une seule fois et cette optimisation ne pouvait donc pas arriver.

Cependant, je pense que les implémentations CLR actuelles de Microsoft n'offrent pas les variables locales.


6 commentaires

CLR via C #, page 264-265, le recule, à la fois avec des exemples de code différents et des discussions sur eux. Il indique également que "tous les compilateurs JIT de Microsoft respectent l'invariant de ne pas introduire de nouvelles lectures à la mémoire de la mémoire de la mémoire et, par conséquent, la mise en cache d'une référence dans une variable locale garantit que la référence du tas n'est accessible qu'une fois".


@mgronber Ce n'est pas une réponse très satisfaisante :-) Mais je suppose que vous avez raison. Merci


@Simon je l'ai regardé dans CLRVIAC # et vous avez raison. Richter parle exactement de ce problème. À l'origine, je n'étais pas inquiet à propos de cet exemple simple avec le _Action ci-dessus, mais plutôt le motif sans relâche (comme la Richter l'appelle). Là, vous avez également lu une valeur une fois sans volatile / verrouillée, et utilisez-la pour initialiser courcentval et en même temps pour déterminer veutval . Je crains que le même problème s'applique là-bas, car vous lisez la mémoire une fois la mémoire (en l'attribuant à une variable locale). Cependant, l'optimiseur lirait plutôt cible deux fois, cela pourrait échouer. Merci.


@Thataller, je suis à peu près sûr qu'il devrait y avoir une barrière mémoire après que le courcentval est lu depuis cible . Sinon, le modèle de mémoire permet de lire deux fois la valeur.


@mgronber, je suis d'accord et c'est ce que j'utilise maintenant. Je suppose que CLR via C # montre la version sans barrière mémoire, car jeffrey Richter l'a sur une bonne autorité que les jit-gars de Microsoft n'introduiraient pas une telle optimisation (comme il le dit). Néanmoins, je préfère être aussi sûr que humainement possible.


Je ne crois pas que ce soit en fait valide selon la spécification de la CECMA. Je sais que les gens l'ont affirmé, en particulier, un article de MSDN largement cité - mais je ne crois pas que ce soit en réalité le cas. Malheureusement, la spécification ECMA n'est pas aussi claire que cela pourrait être sur ce front :(



8
votes

Je dirai (partiellement) le contraire de mgronber :-) aaaah ... à la fin je dis les mêmes choses ... seulement que je cite un article :-( Je vais lui donner un +1.

C'est une optimisation légale dans le cadre des spécifications de l'ECMA, mais c'est une optimisation illégale sous les spécifications .NET> = 2.0 ".

du Comprendre l'impact des techniques de verrouillage faible dans les applications multithreaded

lire ici modèle fort 2: .NET Framework 2.0

point 2:

lit et écrit ne peut pas être introduit.

L'explication ci-dessous:

Le modèle n'autorise pas les lectures à introduire, car cela impliquerait une modification d'une valeur de la mémoire, et dans la mémoire de code à verrouillage faible pourrait changer.

Mais note sous la même page, sous Technique 1: Éviter les verrous sur certaines lectures

Dans les systèmes utilisant le modèle ECMA, il existe une subtilité supplémentaire. Même Si un seul emplacement de mémoire est récupéré dans une variable locale et que Local est utilisé plusieurs fois, chaque utilisation peut avoir une valeur différente! En effet, le modèle ECMA permet au compilateur d'éliminer le local variable et récupérez l'emplacement sur chaque utilisation. Si les mises à jour sont Se passe simultanément, chaque fetch pourrait avoir une valeur différente. Ce comportement peut être supprimé avec des déclarations volatiles, mais le problème est facile à manquer.

Si vous écrivez sous Mono, vous devriez être informé qu'au moins jusqu'en 2008, il travaillait sur le modèle de mémoire ECMA (ou a donc écrit dans leur )


3 commentaires

Je pense que Mgronber parle du modèle ECMA, vous parlez du modèle .NET.


@CODEINCHAOS AAAAAAH ... Si c'est vrai ce que j'ai lu, mono utilise toujours le modèle de mémoire ECMA, ou plus probablement, il utilise un modèle de mémoire "Ce qui se passe arrive"! :-(


@Xanatos, merci. Maintenant, ce problème est beaucoup plus clair pour moi. Surtout l'article que vous citez est très informatif. Il montre à nouveau comment ces motifs de faible verrouillage sont faciles de se tromper.



0
votes

avec C # 7 strong>, vous devriez écrire l'exemple comme suit et, en fait, l'IDE le suggérera comme une "simplification" pour vous. Le compilateur générera du code qui utilise un local temporaire pour lire uniquement l'emplacement de _action code> à partir de la mémoire principale un seul moment (peu importe son être null em> ou non), et cela aide. Empêcher la course commune montrant le deuxième exemple de l'OP, c'est-à-dire où _action code> est accessible deux fois et peut être réglé sur null em> par un autre fil d'entre eux.

class Example
{
    private Action _action;

    private void InvokeAction()
    {
        this._action?.Invoke();
    }
}


3 commentaires

Définir "threadsafe". Je peux penser à de nombreux problèmes de filetage qui ne sont pas protégés par ce modèle. (1) Si la variable n'est pas volatile, aucune barrière n'est introduite; La valeur pourrait être obsolète. (2) Si la variable est un type de valeur nullable plutôt que d'une action, la lecture peut être déchirée. (3) Supposons que la valeur actuelle de l'action dépend de l'état mondial. La valeur actuelle est lue avant l'invocation, puis sur une autre action de thread est mutée et que l'état mondial est détruit. L'invocation se bloque alors. Aucune primitive de synchronisation ne garantit que cela ne peut pas arriver.


L'opérateur d'accès conditionnel n'est pas magique; Le code généré est la même chose que si vous avez utilisé un local explicite. Il est parfaitement raisonnable d'utiliser l'opérateur si vous craignez que la valeur soit nulle, mais ne vous trompez pas en pensant que cela rend comme magiquement votre programme plus «threadsafe» qu'auparavant. C'est juste un petit sucre bon marché.


Merci pour le commentaire; Je ne voulais pas être exhaustif sur la concurrence ici; J'ai composé mes revendications. D'autre part, vous affaiblissez votre note un peu avec le point (3). Si comme vous le dites, il ne peut pas être réparé; Il semble que ça ne semble pas mentionner des problèmes qui ne peuvent jamais être résolus. Quoi qu'il en soit, je simplifiais l'évangélisation de tout débutant qui ne reconnaissait pas automatiquement les courses les plus courantes, car la question de la puissance est conforme à ce niveau. J'ai mis à jour mon message, mais je suis en désaccord sur votre dernier point. Le sucre suffisant fait de la substance et l'implication du modèle de mémoire que j'ai mentionné à l'abri de manière tangieuse en fonction de l'exactitude.