Mengapa kompiler Rust tidak mengoptimalkan kode dengan asumsi bahwa dua referensi yang bisa diubah tidak bisa alias?

301

Sejauh yang saya tahu, referensi / pointer aliasing dapat menghambat kemampuan kompiler untuk menghasilkan kode yang dioptimalkan, karena mereka harus memastikan biner yang dihasilkan berperilaku dengan benar dalam kasus di mana dua referensi / pointer memang alias. Misalnya, dalam kode C berikut,

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

ketika dikompilasi oleh clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)dengan -O3bendera, itu memancarkan

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

Di sini kode menyimpan kembali (%rdi)dua kali dalam kasus int *adan int *balias.

Ketika kami secara eksplisit memberi tahu kompiler bahwa kedua petunjuk ini tidak dapat alias dengan restrictkata kunci:

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

Kemudian Dentang akan mengeluarkan versi yang lebih optimal dari kode biner:

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

Karena Rust memastikan (kecuali dalam kode yang tidak aman) bahwa dua referensi yang bisa diubah tidak bisa alias, saya akan berpikir bahwa kompiler harus dapat memancarkan versi kode yang lebih optimal.

Ketika saya menguji dengan kode di bawah ini dan mengompilasinya rustc 1.35.0dengan -C opt-level=3 --emit obj,

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

itu menghasilkan:

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

Ini tidak memanfaatkan jaminan itu adan btidak bisa alias.

Apakah ini karena compiler Rust saat ini masih dalam pengembangan dan belum memasukkan analisis alias untuk melakukan optimasi?

Apakah ini karena masih ada kemungkinan itu adan bbisa alias, bahkan di Rust yang aman?

Zhiyao
sumber
3
godbolt.org/z/aEDINX , aneh
Stargateur
76
Komentar samping: " Karena Rust memastikan (kecuali dalam kode yang tidak aman) bahwa dua referensi yang tidak dapat diubah tidak dapat alias " - perlu disebutkan bahwa bahkan dalam unsafekode, alias referensi yang dapat diubah tidak diperbolehkan dan menghasilkan perilaku yang tidak ditentukan. Anda dapat memiliki alias mentah pointer, tetapi unsafekode sebenarnya tidak memungkinkan Anda untuk mengabaikan aturan standar Rust. Itu hanya kesalahpahaman umum dan dengan demikian layak untuk ditunjukkan.
Lukas Kalbertodt
6
Butuh beberapa saat untuk mencari tahu apa contohnya, karena saya tidak pandai membaca asm, jadi jika itu membantu orang lain: intinya adalah apakah dua +=operasi dalam tubuh addsdapat ditafsirkan kembali sebagai *a = *a + *b + *b. Jika pointer tidak alias, mereka bisa, Anda bahkan dapat melihat apa yang berjumlah b* + *bdi asm kedua listing: 2: 01 c0 add %eax,%eax. Tetapi jika mereka melakukan alias, mereka tidak bisa, karena pada saat Anda menambahkan *buntuk kedua kalinya, itu akan berisi nilai yang berbeda dari yang pertama kali ada (yang Anda simpan pada baris 4:daftar asm pertama).
dlukes

Jawaban:

364

Rust awalnya memang mengaktifkan noaliasatribut LLVM , tetapi ini menyebabkan kode yang salah dikompilasi . Ketika semua versi LLVM yang didukung tidak lagi mengkompilasi kode, itu akan diaktifkan kembali .

Jika Anda menambahkan -Zmutable-noalias=yeske opsi kompiler, Anda mendapatkan unit yang diharapkan:

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

Sederhananya, Rust menempatkan setara dengan restrictkata kunci C di mana-mana , jauh lebih lazim daripada program C biasa. Ini menggunakan kasus sudut LLVM lebih daripada yang bisa ditangani dengan benar. Ternyata programmer C dan C ++ tidak menggunakan restrictsesering &mutyang digunakan di Rust.

Ini telah terjadi beberapa kali .

  • Karat 1.0 hingga 1.7 - noaliasdiaktifkan
  • Karat 1.8 hingga 1.27 - noaliasdinonaktifkan
  • Karat 1,28 hingga 1,29 - noaliasdiaktifkan
  • Karat 1,30 sampai ??? - noaliasdinonaktifkan

Masalah karat terkait

Shepmaster
sumber
12
Ini tidak mengejutkan. Meskipun memiliki klaim multi-bahasa-keramahan yang luas, LLVM secara khusus dirancang sebagai backend C ++ dan selalu memiliki kecenderungan kuat untuk tersedak hal-hal yang tidak terlihat cukup seperti C ++.
Mason Wheeler
47
@MasonWheeler jika Anda mengeklik beberapa masalah, Anda dapat menemukan contoh kode C yang menggunakan restrictdan salah mengompilasi pada Dentang dan GCC. Ini tidak terbatas pada bahasa yang tidak “cukup C ++”, kecuali jika Anda menghitung C ++ itu sendiri di grup itu .
Shepmaster
6
@MasonWheeler: Saya tidak berpikir LLVM benar-benar dirancang berdasarkan aturan C atau C ++, tetapi lebih pada aturan LLVM. Itu membuat asumsi yang biasanya berlaku untuk kode C atau C ++, tetapi dari apa yang saya tahu desain didasarkan pada model statis-data-dependensi yang tidak dapat menangani kasus sudut rumit. Itu akan baik-baik saja jika secara pesimis diasumsikan dependensi data yang tidak dapat disangkal, tetapi sebaliknya diperlakukan sebagai tindakan no-ops yang akan menulis penyimpanan dengan pola bit yang sama seperti yang dipegangnya, dan yang memiliki dependensi data yang potensial tetapi tidak dapat dibuktikan pada baca dan tulis.
supercat
8
@supercat Saya sudah membaca komentar Anda beberapa kali, tapi saya akui saya bingung - saya tidak tahu apa yang harus mereka lakukan dengan pertanyaan atau jawaban ini. Perilaku yang tidak terdefinisi tidak ikut berperan di sini, ini adalah "hanya" kasus beberapa optimisasi melewati berinteraksi buruk satu sama lain.
Shepmaster
2
@avl_sweden untuk mengulangi, itu hanya bug . Langkah optimalkan loop membuka gulungan tidak (tidak?) Sepenuhnya mempertimbangkan noaliaspointer ketika mengeksekusi. Itu menciptakan pointer baru berdasarkan input pointer, menyalin noaliasatribut secara tidak benar meskipun pointer baru melakukan alias.
Shepmaster