Je parcourais des documents et des questions / réponses et je l’ai vu mentionné. J’ai lu une brève description, indiquant que ce serait essentiellement une promesse du programmeur que le pointeur ne soit pas utilisé pour pointer ailleurs.
Quelqu’un peut-il offrir des cas réalistes où cela vaut vraiment la peine de l’utiliser?
ressortingct
indique que le pointeur est la seule chose qui accède à l’object sous-jacent. Il élimine le risque d’alias de pointeur, ce qui permet une meilleure optimisation par le compilateur.
Par exemple, supposons que je possède une machine avec des instructions spécialisées capables de multiplier les vecteurs de nombres en mémoire et que j’ai le code suivant:
void MultiplyArrays(int* dest, int* src1, int* src2, int n) { for(int i = 0; i < n; i++) { dest[i] = src1[i]*src2[i]; } }
Le compilateur doit gérer correctement si dest
, src1
et src2
chevauchent, ce qui signifie qu'il doit faire une multiplication à la fois, du début à la fin. En ressortingct
, le compilateur est libre d’optimiser ce code en utilisant les instructions vectorielles.
Wikipedia a une entrée sur ressortingct
, avec un autre exemple, ici .
L’ exemple de Wikipedia est très éclairant.
Il montre clairement comment cela permet de sauvegarder une instruction d’assemblage .
Sans ressortingction:
void f(int *a, int *b, int *x) { *a += *x; *b += *x; }
Pseudo-assemblage:
load R1 ← *x ; Load the value of x pointer load R2 ← *a ; Load the value of a pointer add R2 += R1 ; Perform Addition set R2 → *a ; Update the value of a pointer ; Similarly for b, note that x is loaded twice, ; because a may be equal to x. load R1 ← *x load R2 ← *b add R2 += R1 set R2 → *b
Avec ressortingct:
void fr(int *ressortingct a, int *ressortingct b, int *ressortingct x);
Pseudo-assemblage:
load R1 ← *x load R2 ← *a add R2 += R1 set R2 → *a ; Note that x is not reloaded, ; because the comstackr knows it is unchanged ; load R1 ← *x load R2 ← *b add R2 += R1 set R2 → *b
Est-ce que GCC le fait vraiment?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c objdump -S main.o
Avec -O0
, ils sont identiques.
Avec -O3
:
void f(int *a, int *b, int *x) { *a += *x; 0: 8b 02 mov (%rdx),%eax 2: 01 07 add %eax,(%rdi) *b += *x; 4: 8b 02 mov (%rdx),%eax 6: 01 06 add %eax,(%rsi) void fr(int *ressortingct a, int *ressortingct b, int *ressortingct x) { *a += *x; 10: 8b 02 mov (%rdx),%eax 12: 01 07 add %eax,(%rdi) *b += *x; 14: 01 06 add %eax,(%rsi)
Pour les non-initiés, la convention d’appel est la suivante:
rdi
= premier paramètre rsi
= second paramètre rdx
= troisième paramètre La sortie GCC était encore plus claire que l’article wiki: 4 instructions vs 3 instructions.
Tableaux
Jusqu’à présent, nous avons enregistré des économies sur une seule instruction, mais si le pointeur représente des tableaux à mettre en boucle, un cas d’utilisation courant, alors un ensemble d’instructions pourrait être enregistré, comme mentionné par supercat .
Prenons par exemple:
void f(char *ressortingct p1, char *ressortingct p2) { for (int i = 0; i < 50; i++) { p1[i] = 4; p2[i] = 9; } }
En raison de ressortingct
, un compilateur intelligent (ou humain), pourrait optimiser cela pour:
memset(p1, 4, 50); memset(p2, 9, 50);
ce qui est potentiellement beaucoup plus efficace car il peut être optimisé en assemblage sur une implémentation décente de libc (comme glibc): vaut-il mieux utiliser std :: memcpy () ou std :: copy () en termes de performances?
Est-ce que GCC le fait vraiment?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c objdump -dr main.o
Avec -O0
, les deux sont identiques.
Avec -O3
:
avec ressortingct:
3f0: 48 85 d2 test %rdx,%rdx 3f3: 74 33 je 428 3f5: 55 push %rbp 3f6: 53 push %rbx 3f7: 48 89 f5 mov %rsi,%rbp 3fa: be 04 00 00 00 mov $0x4,%esi 3ff: 48 89 d3 mov %rdx,%rbx 402: 48 83 ec 08 sub $0x8,%rsp 406: e8 00 00 00 00 callq 40b 407: R_X86_64_PC32 memset-0x4 40b: 48 83 c4 08 add $0x8,%rsp 40f: 48 89 da mov %rbx,%rdx 412: 48 89 ef mov %rbp,%rdi 415: 5b pop %rbx 416: 5d pop %rbp 417: be 09 00 00 00 mov $0x9,%esi 41c: e9 00 00 00 00 jmpq 421 41d: R_X86_64_PC32 memset-0x4 421: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 428: f3 c3 repz retq
Deux appels memset
comme prévu.
sans ressortingction: pas d'appels stdlib, juste une boucle de 16 itérations en boucle que je n'ai pas l'intention de reproduire ici 🙂
Je n'ai pas eu la patience de les comparer, mais je pense que la version ressortingctive sera plus rapide.
C99
Regardons la norme pour l'intégralité.
ressortingct
indique que deux pointeurs ne peuvent pas pointer vers des régions de mémoire superposées. L'utilisation la plus courante concerne les arguments de fonction.
Cela restreint la façon dont la fonction peut être appelée, mais permet d’optimiser davantage la compilation.
Si l'appelant ne suit pas le contrat ressortingct
, comportement indéfini.
Le C99 N1256 Draft 6.7.3 / 7 "Type qualifiers" dit:
L'utilisation prévue du qualificatif ressortingct (comme la classe de stockage de registre) vise à promouvoir l'optimisation et la suppression de toutes les instances du qualificatif de toutes les unités de prétraitement composant un programme conforme ne change pas sa signification (comportement observable).
et 6.7.3.1 "Définition formelle de ressortingction" donne les détails évidents.
Règle ssortingcte d'aliasing
Le mot-clé ressortingct
n'affecte que les pointeurs des types compatibles (par exemple, deux int*
) car les règles ssortingctes d'alias stipulent que l'aliasing des types incompatibles est un comportement indéfini par défaut. Les compilateurs peuvent donc supposer que cela ne se produit pas et optimiser.
Voir: Quelle est la règle d'aliasing ssortingcte?
Voir également
ressortingct
, mais GCC a __ressortingct__
en tant qu'extension: que signifie le mot clé ressortingctif en C ++? __atsortingbute__((malloc))
, qui indique que la valeur renvoyée par une fonction n’a aucun alias: GCC: __atsortingbute __ ((malloc))