La fonction appelle-t-elle une barrière de mémoire?

Considérez ce code C:

extern volatile int hardware_reg; void f(const void *src, size_t len) { void *dst = ; hardware_reg = 1; memcpy(dst, src, len); hardware_reg = 0; } 

L’appel memcpy() doit avoir lieu entre les deux assignations. En général, puisque le compilateur ne sait probablement pas ce que fera la fonction appelée, il ne peut pas réorganiser l’appel de la fonction avant ou après les assignations. Cependant, dans ce cas, le compilateur sait ce que fera la fonction (et pourrait même insérer un substitut intégré en ligne) et il peut en déduire que memcpy() ne pourrait jamais accéder à hardware_reg . Ici, il me semble que le compilateur n’aurait aucune difficulté à déplacer l’appel memcpy() s’il le voulait.

Donc, la question: un appel de fonction suffit-il à lui seul pour émettre une barrière de mémoire empêchant toute réorganisation, ou est-ce une barrière de mémoire explicite nécessaire dans ce cas avant et après l’appel à memcpy() ?

S’il vous plaît, corrigez-moi si je ne comprends pas bien les choses.

Le compilateur ne peut pas réorganiser l’opération memcpy() avant hardware_reg = 1 ou après hardware_reg = 0 – c’est ce que volatile va assurer – au moins dans la mesure du stream d’instructions émis par le compilateur. Un appel de fonction n’est pas nécessairement une “barrière de mémoire”, mais un sharepoint séquence.

La norme C99 dit ceci à propos de volatile (5.1.2.3/5 “Exécution du programme”):

Aux points de la séquence, les objects volatils sont stables en ce sens que les access précédents sont complets et que les access suivants ne se sont pas encore produits.

Ainsi, au sharepoint séquence représenté par la fonction memcpy() , l’access volatile de l’écriture 1 doit avoir lieu et l’access volatile de l’écriture 0 ne peut s’être produit.

Cependant, il y a 2 choses que je voudrais souligner:

  1. En fonction de ce que est , si rien d’autre n’est fait avec le tampon de destination, le compilateur pourrait peut-être supprimer complètement l’opération memcpy() . C’est la raison pour laquelle Microsoft a proposé la fonction SecureZeroMemory() . SecureZeroMemory() fonctionne sur volatile pointeurs qualifiés volatile pour empêcher l’optimisation des écritures.

  2. volatile n’implique pas nécessairement une barrière de mémoire (ce qui est matériel, pas seulement un ordre de code), donc si vous utilisez une machine multi-proc ou certains types de matériel, vous devrez peut-être explicitement appeler une barrière de mémoire. (peut-être wmb() sur Linux).

    À partir de MSVC 8 (VS 2005), Microsoft indique que le mot clé volatile implique la barrière de mémoire appropriée. Par conséquent, un appel distinct à la barrière de mémoire spécifique peut ne pas être nécessaire:

    De même, lors de l’optimisation, le compilateur doit maintenir l’ordre entre les références aux objects volatils et les références à d’autres objects globaux. En particulier,

    • Une écriture dans un object volatile (écriture volatile) a la sémantique de Release; une référence à un object global ou statique qui se produit avant une écriture dans un object volatile de la séquence d’instructions se produira avant cette écriture volatile dans le binary compilé.

    • Une lecture d’object volatile (lecture volatile) possède la sémantique Acquire; une référence à un object global ou statique qui survient après une lecture de mémoire volatile dans la séquence d’instructions se produira après cette lecture volatile dans le binary compilé.

Pour autant que je puisse voir votre raisonnement conduisant à

le compilateur ne verrait aucune difficulté à déplacer l’appel memcpy

est correct. La définition du langage ne répond pas à votre question et ne peut être traitée qu’en faisant référence à des compilateurs spécifiques.

Désolé de ne plus avoir d’informations utiles.

Mon hypothèse serait que le compilateur ne réordonne jamais les assignations volatiles car il doit supposer qu’elles doivent être exécutées exactement à la position où elles apparaissent dans le code.

Il est probablement optimisé, soit parce que le compilateur insère l’appel mecpy et élimine la première affectation, soit parce qu’il est compilé en code RISC ou en code machine et y est optimisé.

Voici un exemple légèrement modifié, compilé avec gcc 7.2.1 sur x86-64:

 #include  static int temp; extern volatile int hardware_reg; int foo (int x) { hardware_reg = 0; memcpy(&temp, &x, sizeof(int)); hardware_reg = 1; return temp; } 

gcc sait que memcpy() est identique à une affectation et sait que temp n’est accessible nulle part ailleurs, donc temp et memcpy() disparaissent complètement du code généré:

 foo: movl $0, hardware_reg(%rip) movl %edi, %eax movl $1, hardware_reg(%rip) ret