Pourquoi le compilateur Rust n'optimise-t-il pas le code en supposant que deux références mutables ne peuvent pas être alias?

301

Pour autant que je sache, l'alias de référence / pointeur peut entraver la capacité du compilateur à générer du code optimisé, car ils doivent garantir que le binaire généré se comporte correctement dans le cas où les deux références / pointeurs sont effectivement des alias. Par exemple, dans le code C suivant,

void adds(int  *a, int *b) {
    *a += *b;
    *a += *b;
}

une fois compilé clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)avec le -O3drapeau, il émet

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)  # The first time
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)  # The second time
   a:    c3                       retq

Ici, le code stocke (%rdi)deux fois au cas où int *aet int *balias.

Lorsque nous disons explicitement au compilateur que ces deux pointeurs ne peuvent pas être alias avec le restrictmot clé:

void adds(int * restrict a, int * restrict b) {
    *a += *b;
    *a += *b;
}

Ensuite, Clang émettra une version plus optimisée du code binaire:

0000000000000000 <adds>:
   0:    8b 06                    mov    (%rsi),%eax
   2:    01 c0                    add    %eax,%eax
   4:    01 07                    add    %eax,(%rdi)
   6:    c3                       retq

Étant donné que Rust s'assure (sauf dans le code non sûr) que deux références mutables ne peuvent pas être alias, je pense que le compilateur devrait être capable d'émettre la version la plus optimisée du code.

Lorsque je teste avec le code ci-dessous et que je le compile rustc 1.35.0avec -C opt-level=3 --emit obj,

#![crate_type = "staticlib"]
#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

il génère:

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)
   a:    c3                       retq

Cela ne profite pas de la garantie que aet bne peut pas alias.

Est-ce parce que le compilateur Rust actuel est toujours en développement et n'a pas encore intégré d'analyse d'alias pour faire l'optimisation?

Est-ce parce qu'il y a encore une chance aet bpourrait alias, même en toute sécurité Rust?

Zhiyao
la source
3
godbolt.org/z/aEDINX , étrange
Stargateur
76
Remarque secondaire: " Puisque Rust s'assure (sauf dans le code non sûr) que deux références mutables ne peuvent pas alias " - il convient de mentionner que même dans le unsafecode, les références mutables d'alias ne sont pas autorisées et entraînent un comportement indéfini. Vous pouvez avoir des pointeurs bruts d'alias, mais le unsafecode ne vous permet pas d'ignorer les règles standard de Rust. C'est juste une idée fausse commune et mérite donc d'être soulignée.
Lukas Kalbertodt
6
Il m'a fallu un certain temps pour comprendre à quoi sert cet exemple, car je ne suis pas habile à lire asm, donc au cas où cela aiderait quelqu'un d'autre: cela revient à savoir si les deux +=opérations dans le corps de addspeuvent être réinterprétées comme *a = *a + *b + *b. Si les pointeurs ne font pas alias, ils peuvent, vous pouvez même voir ce qui équivaut à b* + *bla deuxième liste asm: 2: 01 c0 add %eax,%eax. Mais s'ils font un alias, ils ne peuvent pas, car au moment où vous ajoutez *bpour la deuxième fois, il contiendra une valeur différente de la première fois (celle que vous stockez en ligne 4:de la première liste asm).
dlukes

Réponses:

364

Rust à l' origine fait permettre de LLVM noaliasattribut, mais cela fait un code mal compilée . Lorsque toutes les versions de LLVM prises en charge ne compileront plus le code, il sera réactivé .

Si vous ajoutez -Zmutable-noalias=yesaux options du compilateur, vous obtenez l'assembly attendu:

adds:
        mov     eax, dword ptr [rsi]
        add     eax, eax
        add     dword ptr [rdi], eax
        ret

Autrement dit, Rust a mis l'équivalent du restrictmot - clé C partout , bien plus répandu que n'importe quel programme C habituel. Cela a exercé les cas d'angle de LLVM plus qu'il n'était capable de gérer correctement. Il s'avère que les programmeurs C et C ++ n'utilisent tout simplement pas restrictaussi fréquemment que &mutdans Rust.

Cela s'est produit plusieurs fois .

  • Rust 1.0 à 1.7 - noaliasactivé
  • Rust 1.8 à 1.27 - noaliasdésactivé
  • Rust 1.28 à 1.29 - noaliasactivé
  • Rouille 1,30 à ??? - noaliasdésactivé

Problèmes de rouille associés

Shepmaster
la source
12
Ce n'est pas surprenant. Nonobstant ses prétentions largement étendues de convivialité multilingue, LLVM a été spécifiquement conçu comme un backend C ++ et il a toujours eu une forte tendance à s'étouffer avec des choses qui ne ressemblent pas assez à C ++.
Mason Wheeler
47
@MasonWheeler si vous cliquez sur certains des problèmes, vous pouvez trouver des exemples de code C qui utilisent restrictet se compilent mal sur Clang et GCC. Il n'est pas limité aux langages qui ne sont pas «suffisamment en C ++», sauf si vous comptez C ++ lui-même dans ce groupe .
Shepmaster
6
@MasonWheeler: Je ne pense pas que LLVM ait été vraiment conçu autour des règles de C ou C ++, mais plutôt autour des règles de LLVM. Il fait des hypothèses qui se vérifient généralement pour le code C ou C ++, mais d'après ce que je peux dire, la conception repose sur un modèle de dépendances de données statiques qui ne peut pas gérer les cas d'angle difficiles. Ce serait bien s'il supposait de façon pessimiste des dépendances de données qui ne peuvent pas être réfutées, mais à la place, il traite comme des actions sans opération qui écriraient le stockage avec le même modèle de bits qu'il détenait, et qui ont des dépendances de données potentielles mais non prouvables sur le lire et écrire.
supercat
8
@supercat J'ai lu vos commentaires plusieurs fois, mais j'avoue que je suis perplexe - je n'ai aucune idée de ce qu'ils ont à faire avec cette question ou réponse. Un comportement indéfini n'entre pas en jeu ici, il s'agit "simplement" d'un cas de plusieurs passes d'optimisation interagissant mal entre elles.
Shepmaster
2
@avl_sweden pour réitérer, c'est juste un bug . L'étape d'optimisation du déroulement de la boucle n'a pas (ne?) Pas pleinement pris noaliasen compte les pointeurs lors de l'exécution. Il a créé de nouveaux pointeurs basés sur des pointeurs d'entrée, copiant de façon incorrecte l' noaliasattribut même si les nouveaux pointeurs faisaient un alias.
Shepmaster