Rust: Pengoptimalan loop LLVM dapat membuat program yang aman macet

Dibuat pada 29 Sep 2015  ·  97Komentar  ·  Sumber: rust-lang/rust

Cuplikan berikut mengalami error saat dikompilasi dalam mode rilis pada stable saat ini, beta, dan nightly:

enum Null {}

fn foo() -> Null { loop { } }

fn create_null() -> Null {
    let n = foo();

    let mut i = 0;
    while i < 100 { i += 1; }
    return n;
}

fn use_null(n: Null) -> ! {
    match n { }
}


fn main() {
    use_null(create_null());
}

https://play.rust-lang.org/?gist=1f99432e4f2dccdf7d7e&version=stable

Ini didasarkan pada contoh LLVM berikut yang menghapus loop yang saya ketahui: https://github.com/simnalamburt/snippets/blob/12e73f45f3/rust/infinite.rs.
Apa yang tampaknya terjadi adalah karena C memungkinkan LLVM untuk menghapus loop tanpa akhir yang tidak memiliki efek samping, kami akhirnya mengeksekusi match yang harus dilakukan.

A-LLVM C-bug E-medium I-needs-decision I-unsound 💥 P-medium T-compiler WG-embedded

Komentar yang paling membantu

Jika ada yang ingin bermain golf kode kasus uji:

pub fn main() {
   (|| loop {})()
}

Dengan bendera -Z insert-sideeffect rustc , ditambahkan oleh

sebelum:

main:
  ud2

setelah:

main:
.LBB0_1:
  jmp .LBB0_1

Semua 97 komentar

LLVM IR dari kode yang dioptimalkan adalah

; Function Attrs: noreturn nounwind readnone uwtable
define internal void @_ZN4main20h5ec738167109b800UaaE() unnamed_addr #0 {
entry-block:
  unreachable
}

Pengoptimalan semacam ini mematahkan asumsi utama yang biasanya berpegang pada jenis yang tidak berpenghuni: nilai jenis itu seharusnya tidak mungkin.
rust-lang / rfcs # 1216 mengusulkan untuk secara eksplisit menangani jenis seperti itu di Rust. Mungkin efektif dalam memastikan bahwa LLVM tidak pernah harus menanganinya dan memasukkan kode yang sesuai untuk memastikan divergensi bila diperlukan (IIUIC ini dapat dicapai dengan atribut yang sesuai atau panggilan intrinsik).
Topik ini juga baru saja dibahas di milis LLVM: http://lists.llvm.org/pipermail/llvm-dev/2015-July/088095.html

triase: nominasi saya

Sepertinya buruk! Jika LLVM tidak memiliki cara untuk mengatakan "ya, putaran ini benar-benar tidak terbatas" meskipun kemudian kita mungkin hanya harus duduk dan menunggu diskusi hulu selesai.

Cara untuk mencegah pengulangan tak terbatas agar tidak dioptimalkan adalah dengan menambahkan unsafe {asm!("" :::: "volatile")} di dalamnya. Ini mirip dengan llvm.noop.sideeffect intrinsic yang telah diusulkan di milis LLVM, tetapi mungkin mencegah beberapa pengoptimalan.
Untuk menghindari hilangnya kinerja dan untuk tetap menjamin bahwa fungsi / loop divergen tidak dioptimalkan, saya percaya bahwa itu harus cukup untuk memasukkan loop non-optimisable kosong (yaitu loop { unsafe { asm!("" :::: "volatile") } } ) jika nilai yang tidak berpenghuni ada di cakupan.
Jika LLVM mengoptimalkan kode yang seharusnya menyimpang ke titik yang tidak lagi menyimpang, loop seperti itu akan memastikan bahwa aliran kontrol masih tidak dapat dilanjutkan.
Dalam kasus "beruntung" di mana LLVM tidak dapat mengoptimalkan kode divergen, loop seperti itu akan dihapus oleh DCE.

Apakah ini terkait dengan # 18785? Yang itu tentang rekursi tak terbatas menjadi UB, tetapi sepertinya penyebab mendasar mungkin serupa: LLVM tidak menganggap tidak berhenti sebagai efek samping, jadi jika suatu fungsi tidak memiliki efek samping selain tidak berhenti, itu senang untuk dioptimalkan itu pergi.

@tokopedia

Itu masalah yang sama.

Ya, sepertinya sama saja. Lebih jauh ke bawah masalah itu, mereka menunjukkan bagaimana mendapatkan undef , dari mana saya berasumsi tidak sulit untuk membuat program crash (yang tampaknya aman).

: +1:

Crash, atau, mungkin lebih menyedihkan lagi https://play.rust-lang.org/?gist=15a325a795244192bdce&version=stable

Jadi saya bertanya-tanya berapa lama sampai seseorang melaporkan ini. :) Menurut pendapat saya, solusi terbaik tentu saja jika kita dapat memberi tahu LLVM untuk tidak terlalu agresif tentang kemungkinan loop tak terbatas. Jika tidak, satu-satunya hal yang menurut saya dapat kita lakukan adalah melakukan analisis konservatif di Rust itu sendiri yang menentukan apakah:

  1. loop akan menghentikan OR
  2. loop akan memiliki efek samping (operasi I / O dll, saya lupa persis bagaimana ini didefinisikan di C)

Salah satu dari ini seharusnya cukup untuk menghindari perilaku yang tidak ditentukan.

triase: P-medium

Kami ingin melihat apa yang akan dilakukan LLVM sebelum kami menginvestasikan banyak upaya di pihak kami, dan ini tampaknya relatif tidak mungkin menyebabkan masalah dalam praktiknya (meskipun saya secara pribadi telah mengenai ini saat mengembangkan kompiler juga). Tidak ada masalah ketidakcocokan mundur yang perlu dikhawatirkan.

Mengutip dari diskusi milis LLVM:

 The implementation may assume that any thread will eventually do one of the following:
   - terminate
   - make a call to a library I/O function
   - access or modify a volatile object, or
   - perform a synchronization operation or an atomic operation

 [Note: This is intended to allow compiler transformations such as removal of empty loops, even
  when termination cannot be proven. — end note ]

@dotdash Kutipan yang Anda kutip berasal dari spesifikasi C ++; itu pada dasarnya adalah jawaban untuk "bagaimana itu [memiliki efek samping] didefinisikan dalam C" (juga dikonfirmasi oleh komite standar: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528 .htm).

Mengenai apa perilaku yang diharapkan dari LLVM IR ada beberapa kebingungan. https://llvm.org/bugs/show_bug.cgi?id=24078 menunjukkan bahwa tampaknya tidak ada spesifikasi yang akurat & eksplisit dari semantik loop tak terbatas di LLVM IR. Ini sejalan dengan semantik C ++, kemungkinan besar karena alasan historis dan untuk kenyamanan (saya hanya berhasil melacak https://groups.google.com/forum/#!topic/llvm-dev/j2vlIECKkdE yang tampaknya mengacu pada waktu ketika loop tak terbatas tidak dioptimalkan, beberapa saat sebelum spesifikasi C / C ++ diperbarui untuk mengizinkannya).

Dari utas jelas bahwa ada keinginan untuk mengoptimalkan kode C ++ seefektif mungkin (yaitu juga mempertimbangkan peluang untuk menghapus loop tak terbatas), tetapi di utas yang sama beberapa pengembang (termasuk beberapa yang secara aktif berkontribusi pada LLVM) memiliki menunjukkan minat pada kemampuan untuk mempertahankan loop tak terbatas, karena diperlukan untuk bahasa lain.

@ Ranma42 Saya menyadari hal itu, saya baru saja mengutipnya sebagai referensi, karena satu kemungkinan untuk mengatasinya adalah dengan mendeteksi loop seperti itu dalam karat dan menambahkan salah satu hal di atas untuk menghentikan LLVM melakukan pengoptimalan ini.

Apakah ini masalah kesehatan? Jika demikian, kita harus menandainya seperti itu.

Ya, mengikuti contoh @ ranma42 , cara ini menunjukkan bagaimana ia dengan mudah mengalahkan pemeriksaan batas array. link taman bermain

@buss

Kebijakannya adalah masalah kode salah yang juga merupakan masalah kesehatan (yaitu kebanyakan dari masalah tersebut) harus diberi tag I-wrong .

Jadi sekadar rekap pembahasan sebelumnya, sebenarnya ada dua pilihan di sini yang bisa saya lihat:

  • Tunggu LLVM memberikan solusi.
  • Perkenalkan pernyataan no-op asm di mana pun mungkin ada loop tak hingga atau rekursi tak hingga (# 18785).

Yang terakhir agak buruk karena dapat menghambat pengoptimalan, jadi kami ingin melakukannya dengan hemat - pada dasarnya di mana pun kami sendiri tidak dapat membuktikan penghentian. Anda juga dapat membayangkan mengaitkannya sedikit lebih ke bagaimana LLVM mengoptimalkan - yaitu, memperkenalkan hanya jika kita dapat mendeteksi skenario yang mungkin dianggap LLVM sebagai loop / rekursi tak terbatas - tetapi itu akan (a) memerlukan pelacakan LLVM dan (b ) membutuhkan pengetahuan yang lebih dalam daripada yang saya miliki.

Tunggu LLVM memberikan solusi.

Apa bug LLVM yang melacak masalah ini?

catatan samping: while true {} menunjukkan perilaku ini . Mungkin lint harus ditingkatkan ke error-by-default dan mendapatkan catatan yang menyatakan bahwa saat ini ini dapat menunjukkan perilaku yang tidak ditentukan?

Juga, perhatikan bahwa ini tidak valid untuk C. LLVM membuat argumen ini berarti ada bug dalam clang.

void foo() { while (1) { } }

void create_null() {
        foo();

        int i = 0;
        while (i < 100) { i += 1; }
}

__attribute__((noreturn))
void use_null() {
        __builtin_unreachable();
}


int main() {
        create_null();
        use_null();
}

Ini macet dengan pengoptimalan; ini adalah perilaku tidak valid menurut standar C11:

An iteration statement whose controlling expression is not a constant
expression, [note 156] that performs no  input/output  operations,
does  not  access  volatile  objects,  and  performs  no synchronization or
atomic operations in its body, controlling expression, or (in the case of
a for statement) its expression-3, may be   assumed   by   the
implementation to terminate. [note 157]

156: An omitted controlling expression is replaced by a nonzero constant,
     which is a constant expression.
157: This  is  intended  to  allow  compiler  transformations  such  as
     removal  of  empty  loops  even  when termination cannot be proven. 

Perhatikan "yang ekspresi pengontrolnya bukan ekspresi konstan" - while (1) { } , 1 adalah ekspresi konstan, dan karenanya tidak boleh dihapus .

Apakah penghapusan loop merupakan pengoptimalan yang dapat kami hapus begitu saja?

@ubsan

Apakah Anda menemukan laporan bug untuk itu di bugzilla LLVM atau yang terisi? Tampaknya dalam loop tak terbatas C ++ yang _can_ tidak pernah hentikan adalah perilaku yang tidak terdefinisi, tetapi di C mereka didefinisikan sebagai perilaku (baik mereka dapat dihapus dengan aman dalam beberapa kasus, atau tidak dapat di kasus lain).

Mengulangi diri saya sendiri dari # 42009: bug ini dapat, dalam beberapa keadaan, menyebabkan emisi fungsi yang dapat dipanggil secara eksternal yang tidak mengandung instruksi mesin sama sekali. Ini seharusnya tidak pernah terjadi. Jika LLVM menyimpulkan bahwa pub fn tidak pernah dapat dipanggil dengan kode yang benar, ia harus mengeluarkan setidaknya instruksi perangkap sebagai badan dari fungsi itu.

Bug LLVM untuk ini adalah https://bugs.llvm.org/show_bug.cgi?id=965 (dibuka tahun 2006).

@zackw LLVM memiliki flag untuk itu: TrapUnreachable . Saya belum menguji ini, tetapi tampaknya menambahkan Options.TrapUnreachable = true; ke LLVMRustCreateTargetMachine harus mengatasi kekhawatiran Anda. Sepertinya ini memiliki biaya yang cukup rendah sehingga dapat dilakukan secara default, meskipun saya belum melakukan pengukuran apa pun.

@ oli-obk Sayangnya ini bukan hanya pass loop-deletion. Masalah muncul dari asumsi yang luas, misalnya: (a) cabang tidak memiliki efek samping, (b) fungsi yang tidak berisi instruksi dengan efek samping tidak memiliki efek samping, dan (c) panggilan ke fungsi tanpa efek samping dapat dipindahkan atau dihapus.

Sepertinya ada tambalan: https://reviews.llvm.org/D38336

@sunfishcode , sepertinya patch LLVM Anda di https://reviews.llvm.org/D38336 telah "diterima" pada tanggal 3 Oktober, dapatkah Anda memberikan update tentang apa artinya mengenai proses rilis LLVM? Apa langkah selanjutnya selain penerimaan, dan apakah Anda memiliki gambaran tentang rilis LLVM apa yang akan berisi tambalan ini?

Saya berbicara dengan beberapa orang secara offline yang menyarankan kami memiliki utas llvmdev. Utasnya ada di sini:

http://lists.llvm.org/pipermail/llvm-dev/2017-October/118558.html

Sekarang sudah disimpulkan, dengan hasil bahwa saya perlu melakukan perubahan tambahan. Saya pikir perubahannya akan bagus, meskipun saya membutuhkan lebih banyak waktu untuk melakukannya.

Terima kasih atas pembaruannya, dan terima kasih banyak atas usaha Anda!

Perhatikan bahwa https://reviews.llvm.org/rL317729 telah mendarat di LLVM. Patch ini direncanakan memiliki patch tindak lanjut yang membuat loop tak terbatas menunjukkan perilaku yang ditentukan secara default, jadi AFAICT yang perlu kita lakukan hanyalah menunggu dan akhirnya ini akan diselesaikan untuk kita di upstream.

@zackw Sekarang saya telah membuat # 45920 untuk memperbaiki masalah fungsi yang tidak mengandung kode.

@bstrie Ya, langkah pertama telah dilakukan, dan saya sedang mengerjakan langkah kedua untuk membuat LLVM memberikan perilaku yang ditentukan loop tak terbatas secara default. Ini adalah perubahan yang kompleks, dan saya belum tahu berapa lama waktu yang dibutuhkan untuk menyelesaikannya, tetapi saya akan memposting pembaruan di sini.

@jsg Masih

@kennytm Waduh , sudahlah.

Perhatikan bahwa https://reviews.llvm.org/rL317729 telah mendarat di LLVM. Patch ini direncanakan memiliki patch tindak lanjut yang membuat loop tak terbatas menunjukkan perilaku yang ditentukan secara default, jadi AFAICT yang perlu kita lakukan hanyalah menunggu dan akhirnya ini akan diselesaikan untuk kita di upstream.

Sudah beberapa bulan sejak komentar ini. Adakah yang tahu jika patch tindak lanjut terjadi atau akan tetap terjadi?

Alternatifnya, tampaknya llvm.sideeffect intrinsic ada dalam versi LLVM yang kita gunakan: dapatkah kita memperbaiki ini sendiri dengan menerjemahkan loop tak terbatas Rust ke dalam loop LLVM yang berisi intrinsik efek samping?

Seperti yang terlihat di https://github.com/rust-lang/rust/issues/38136 dan https://github.com/rust-lang/rust/issues/54214 , ini sangat buruk terutama dengan panic_implementation , sebagai implementasi logisnya akan menjadi loop {} , dan ini akan membuat semua kemunculan panic! UB tanpa kode unsafe . Yang… mungkin lebih buruk yang bisa terjadi.

Baru saja menemukan masalah ini dari sudut pandang lain. Berikut contohnya:

pub struct Container<'f> {
    string: &'f str,
    num: usize,
}

impl<'f> From<&'f str> for Container<'f> {
    #[inline(always)]
    fn from(string: &'f str) -> Container<'f> {
        Container::from(string)
    }
}

fn main() {
    let x = Container::from("hello");
    println!("{} {}", x.string, x.num);

    let y = Container::from("hi");
    println!("{} {}", y.string, y.num);

    let z = Container::from("hello");
    println!("{} {}", z.string, z.num);
}

Contoh ini secara andal memisahkan default pada stable, beta, dan nightly, dan menunjukkan betapa mudahnya membuat nilai yang tidak diinisialisasi dari jenis apa pun. Ini dia di taman bermain .

@SergioBenitez program itu tidak segfault, ia berakhir dengan stack overflow (Anda perlu menjalankannya dalam mode debug). Ini adalah perilaku yang benar, karena program Anda berulang tanpa batas yang membutuhkan jumlah ruang stack yang tak terbatas, yang pada titik tertentu akan melebihi ruang stack yang tersedia. Contoh kerja minimal .

Dalam rilis build, LLVM dapat berasumsi bahwa Anda tidak memiliki rekursi tak terbatas, dan mengoptimalkannya ( mwe ). Ini tidak ada hubungannya dengan loop AFAICT, melainkan dengan https://stackoverflow.com/a/5905171/1422197

@gnzlbg Maaf, tapi Anda tidak benar.

Program segfault dalam mode rilis. Itulah intinya; bahwa hasil pengoptimalan dalam perilaku yang tidak sehat - bahwa LLVM dan semantik Rust tidak sesuai di sini - bahwa saya dapat menulis dan menyusun program Rust yang aman dengan rustc yang memungkinkan saya menggunakan memori yang tidak diinisialisasi, memeriksa memori sewenang-wenang, dan secara sewenang-wenang mentransmisikan antar jenis, melanggar semantik bahasa. Itu poin yang sama yang diilustrasikan di utas ini. Perhatikan bahwa program asli juga tidak segfault dalam mode debug.

Anda juga tampaknya mengusulkan bahwa ada _different_, pengoptimalan non-loop yang terjadi di sini. Itu tidak mungkin, meskipun sebagian besar tidak relevan, meskipun mungkin memerlukan masalah terpisah jika memang demikian. Dugaan saya adalah bahwa LLVM memperhatikan rekursi ekor, memperlakukannya sebagai loop tak terbatas, dan mengoptimalkannya, sekali lagi, tentang apa sebenarnya masalah ini.

@gnzlbg Nah, sedikit mengubah mwe pengoptimalan Anda dari rekursi tak terbatas (di sini ), itu menghasilkan nilai yang tidak diinisialisasi NonZeroUsize (yang ternyata… 0, sehingga nilai tidak valid).

Dan itulah yang juga dilakukan @SergioBenitez dengan contoh mereka, kecuali dengan pointer, dan dengan demikian menghasilkan segfault.

Apakah kami setuju bahwa program @SergioBenitez memiliki stack overflow di debug dan rilis?

Jika demikian, saya tidak dapat menemukan loop s dalam contoh @SergioBenitez , jadi saya tidak tahu bagaimana masalah ini akan berlaku untuk itu (masalah ini tentang loop s tak terbatas). Jika saya salah, arahkan saya ke loop dalam contoh Anda.

Seperti disebutkan, LLVM mengasumsikan bahwa rekursi tak terbatas tidak dapat terjadi (mengasumsikan bahwa semua utas pada akhirnya akan berhenti), tetapi itu akan menjadi masalah yang berbeda dari yang satu ini.

Saya belum memeriksa pengoptimalan yang dilakukan LLVM atau kode yang dihasilkan untuk salah satu program, tetapi perhatikan bahwa segfault tidak rusak, jika hanya segfault yang terjadi. Secara khusus, tumpukan overflows yang tertangkap (dengan pemeriksaan tumpukan + halaman pelindung yang tidak dipetakan setelah akhir tumpukan) dan tidak menyebabkan masalah keamanan memori juga muncul sebagai segfault. Tentu saja, segfault juga dapat menunjukkan kerusakan memori atau penulisan / pembacaan liar atau masalah kesehatan lainnya.

@rkruppe Program saya segfaults karena referensi ke lokasi memori acak diizinkan untuk dibuat, dan referensi tersebut kemudian dibaca. Program ini dapat dimodifikasi secara sepele untuk menulis lokasi memori acak, dan tanpa terlalu banyak kesulitan, membaca / menulis lokasi memori _particular_.

@ Ggnzlbg Program _tidak_ stack overflow dalam mode rilis. Dalam mode rilis, program membuat panggilan fungsi nol; tumpukan didorong ke beberapa kali tertentu, murni untuk mengalokasikan penduduk setempat.

Program tidak menumpuk overflow dalam mode rilis.

Begitu? Satu-satunya hal yang penting adalah bahwa program contoh, yang pada dasarnya fn foo() { foo() } , memiliki rekursi tak terbatas, yang tidak diizinkan oleh LLVM.

Satu-satunya hal yang penting adalah bahwa program contoh, yang pada dasarnya fn foo () {foo ()}, memiliki rekursi tak terbatas, yang tidak diizinkan oleh LLVM.

Saya tidak tahu mengapa Anda mengatakan ini seperti itu menyelesaikan apa pun. LLVM mempertimbangkan rekursi tak terbatas dan mengulang UB dan mengoptimalkannya, namun aman di Rust, adalah inti dari keseluruhan masalah ini!

Penulis https://reviews.llvm.org/rL317729 di sini, mengonfirmasi bahwa saya belum mengimplementasikan patch tindak lanjut.

Anda dapat memasukkan panggilan @llvm.sideeffect hari ini untuk memastikan bahwa loop tidak dioptimalkan. Itu mungkin menonaktifkan beberapa pengoptimalan, tetapi secara teori tidak terlalu banyak, karena pengoptimalan besar telah diajarkan bagaimana memahaminya. Jika seseorang menempatkan @llvm.sideeffect panggilan di semua loop atau hal-hal yang mungkin berubah menjadi loop (rekursi, unwinding, inline asm , lainnya?), Itu secara teoritis cukup untuk memperbaiki masalah di sini.

Jelas akan lebih baik untuk memiliki patch kedua di tempat, jadi tidak perlu melakukan ini. Saya tidak tahu kapan saya akan kembali menerapkannya.

Ada beberapa perbedaan kecil, tapi saya tidak yakin apakah itu material atau tidak.

Pengulangan

#[allow(unconditional_recursion)]
#[inline(never)]
pub fn via_recursion<T>() -> T {
    via_recursion()
}

fn main() {
    let a: String = via_recursion();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* <strong i="9">@rust_eh_personality</strong> {
_ZN4core3ptr13drop_in_place17h95538e539a6968d0E.exit:
  ret void
}

Loop

#[inline(never)]
pub fn via_loop<T>() -> T {
    loop {}
}

fn main() {
    let b: String = via_loop();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 {
start:
  unreachable
}

Meta

Rust 1.29.1, mengompilasi dalam mode rilis, melihat IR LLVM.

Saya tidak berpikir bahwa kita dapat, secara umum, mendeteksi rekursi (objek sifat, C FFI, dll.), Jadi kita harus menggunakan llvm.sideeffect pada hampir setiap situs panggilan kecuali kita dapat membuktikan bahwa panggilan situs tidak akan muncul kembali. Membuktikan tidak adanya rekursi untuk kasus yang dapat dibuktikan memerlukan analisis antarprocedural kecuali untuk program yang paling sepele seperti fn main() { main() } . Mungkin bagus untuk mengetahui apa dampak penerapan perbaikan ini, dan apakah ada solusi alternatif untuk masalah ini.

@gnzlbg Itu benar, meskipun Anda dapat meletakkan efek @ llvm.side pada entri fungsi, daripada di situs panggilan.

Anehnya, saya tidak dapat mereproduksi SEGFAULT dalam kasus uji @SergioBenitez secara lokal.

Selain itu, untuk stack overflow, bukankah seharusnya ada pesan kesalahan yang berbeda? Saya pikir kami memiliki beberapa kode untuk dicetak "The stack overflowed" atau lebih?

@RalfJung apakah Anda mencoba dalam mode debug? (Saya dapat dengan andal mereproduksi stack overflow dalam mode debug di mesin saya dan taman bermain, jadi mungkin Anda perlu mengisi bug jika tidak demikian halnya bagi Anda). Di --release Anda tidak akan mendapatkan stack overflow karena semua kode itu salah dioptimalkan.


@tokopedia

Itu benar, meskipun Anda dapat menempatkan @ llvm.sideeffects pada entri fungsi, daripada di situs panggilan.

Sulit untuk mengatakan apa cara terbaik untuk maju tanpa mengetahui secara pasti pengoptimalan mana yang mencegah llvm.sideeffects . Apakah layak untuk mencoba menghasilkan @llvm.sideeffects sesedikit mungkin? Jika tidak, menempatkannya di setiap pemanggilan fungsi mungkin merupakan hal yang paling mudah untuk dilakukan. Jika tidak, IIUC, apakah @llvm.sideeffect diperlukan tergantung pada apa yang dilakukan situs panggilan:

trait Foo {
    fn foo(&self) { self.bar() }
    fn bar(&self);
}

struct A;
impl Foo for A {
    fn bar(&self) {} // not recursive
}
struct B;
impl Foo for B {
    fn bar(&self) { self.foo() } // recursive
}

fn main() {
    let a = A;
    a.bar(); // Ok - no @llvm.sideeffect needed anywhere
    let b = B;
    b.bar(); // We need @llvm.sideeffect on this call site
    let c: &[&dyn Foo] = &[&a, &b];
    for i in c {
        i.bar(); // We need @lvm.sideeffect here too
    }
}

AFAICT, kita harus meletakkan @llvm.sideeffect di dalam fungsi untuk mencegahnya dihapus, jadi meskipun "pengoptimalan" ini sepadan, menurut saya mereka tidak langsung melakukannya dengan model saat ini. Sekalipun demikian, pengoptimalan ini akan mengandalkan kemampuan untuk membuktikan bahwa tidak ada rekursi.

apakah Anda mencoba dalam mode debug? (Saya dapat dengan andal mereproduksi stack overflow dalam mode debug di mesin saya dan taman bermain, jadi mungkin Anda perlu mengisi bug jika tidak demikian halnya bagi Anda)

Tentu, tetapi dalam mode debug, LLVM tidak melakukan pengoptimalan loop sehingga tidak ada masalah.

Jika program stackoverflows dalam mode debug, itu seharusnya tidak memberikan lisensi LLVM untuk membuat UB. Masalahnya adalah mencari tahu apakah program terakhir memiliki UB, dan dari menatap IR saya tidak tahu. Ini segfaults, tapi saya tidak tahu kenapa. Tapi bagi saya sepertinya bug untuk "mengoptimalkan" program stackoverflowing menjadi program yang segfault.

Jika program stackoverflows dalam mode debug, itu seharusnya tidak memberikan lisensi LLVM untuk membuat UB.

Tapi bagi saya sepertinya bug untuk "mengoptimalkan" program stackoverflowing menjadi program yang segfault.

Di C, thread eksekusi diasumsikan berhenti, melakukan akses memori volatile, I / O, atau operasi atom sinkronisasi. Ini akan mengejutkan saya jika LLVM-IR tidak berevolusi untuk memiliki semantik yang sama baik secara tidak sengaja atau karena desain.

Kode Rust berisi utas eksekusi yang tidak pernah berakhir, dan tidak melakukan operasi apa pun yang diperlukan untuk ini agar tidak menjadi UB di C. Saya menduga bahwa kami menghasilkan LLVM-IR yang sama dengan program C dengan perilaku tidak terdefinisi. , jadi saya rasa tidak mengherankan bahwa LLVM salah mengoptimalkan program Rust ini.

Ini segfaults, tapi saya tidak tahu kenapa.

LLVM menghapus rekursi tak terbatas, sehingga @SergioBenitez yang disebutkan di atas, program kemudian melanjutkan ke:

referensi ke lokasi memori acak diizinkan untuk dibuat, dan referensi tersebut kemudian dibaca.

Bagian dari program yang melakukannya adalah yang ini:

let x = Container::from("hello");  // invalid reference created here
println!("{} {}", x.string, x.num);  // invalid reference dereferenced here

di mana Container::from memulai rekursi tak terbatas, yang menurut LLVM tidak akan pernah terjadi, dan menggantikannya dengan beberapa nilai acak, yang kemudian akan dirujuk. Anda dapat melihat salah satu dari banyak cara kesalahan dioptimalkan di sini: https://rust.godbolt.org/z/P7Snex Di taman bermain (https://play.rust-lang.org/?gist=f00d41cc189f9f6897d429350f3781ec&version=stable&mode = release & edition = 2015) ada kepanikan yang berbeda dalam rilis dari debug build karena optimasi ini, tetapi UB adalah UB adalah UB.

Kode Rust berisi utas eksekusi yang tidak pernah berakhir, dan tidak melakukan operasi apa pun yang diperlukan untuk ini agar tidak menjadi UB di C. Saya menduga bahwa kami menghasilkan LLVM-IR yang sama dengan program C dengan perilaku tidak terdefinisi. , jadi saya rasa tidak mengherankan bahwa LLVM salah mengoptimalkan program Rust ini.

Saya mendapat kesan bahwa Anda berpendapat di atas bahwa ini bukan bug yang sama dengan masalah loop tak terbatas. Sepertinya saya salah membaca pesan Anda. Maaf bila membingungkan.

Jadi, sepertinya langkah selanjutnya yang baik adalah menaburkan llvm.sideffect ke IR yang kita buat, dan melakukan beberapa tolok ukur?

Di C, thread eksekusi diasumsikan berhenti, melakukan akses memori volatile, I / O, atau operasi atom sinkronisasi.

Btw, ini tidak sepenuhnya benar - loop dengan bersyarat konstan (seperti while (true) { /* ... */ } ) secara eksplisit diperbolehkan oleh standar, bahkan jika itu tidak mengandung efek samping. Ini berbeda di C ++. LLVM tidak menerapkan standar C dengan benar di sini.

Saya mendapat kesan bahwa Anda berpendapat di atas bahwa ini bukan bug yang sama dengan masalah loop tak terbatas.

Perilaku program Rust non-terminating selalu ditentukan, sedangkan perilaku program LLVM-IR non-terminating hanya ditentukan jika kondisi tertentu terpenuhi.

Saya pikir masalah ini adalah tentang memperbaiki implementasi Rust untuk loop tak terbatas sehingga perilaku LLVM-IR yang dihasilkan menjadi ditentukan, dan untuk ini, @llvm.sideeffect , terdengar seperti solusi yang cukup bagus.

@SergioBenitez menyebutkan bahwa seseorang juga dapat membuat program Rust non-terminating menggunakan rekursi, dan @rkruppe berpendapat bahwa rekursi tak terbatas dan loop tak terbatas adalah setara, sehingga keduanya merupakan bug yang sama.

Saya tidak setuju bahwa kedua masalah ini terkait, atau bahkan merupakan bug yang sama, tetapi bagi saya, kedua masalah ini terlihat sedikit berbeda:

  • solusi bijaksana, kita pergi dari menerapkan penghalang optimasi ( @llvm.sideeffect ) secara eksklusif ke loop non-terminating, untuk menerapkannya ke setiap fungsi Rust.

  • nilai-bijaksana, tak terbatas loop s berguna karena program tidak pernah berakhir. Untuk rekursi tak terbatas, apakah program berakhir tergantung pada tingkat pengoptimalan (misalnya apakah LLVM mengubah rekursi menjadi loop atau tidak), dan kapan dan bagaimana program berakhir tergantung pada platform (ukuran tumpukan, halaman pelindung yang dilindungi, dll.). Memperbaiki keduanya diperlukan untuk membuat implementasi Rust terdengar, tetapi untuk kasus rekursi tak terbatas, jika pengguna menginginkan program mereka untuk berulang selamanya, implementasi yang baik akan tetap "salah" dalam arti bahwa itu tidak akan selalu berulang selamanya.

solusi bijaksana, kita mulai dari menerapkan penghalang pengoptimalan (@ llvm.sideeffect) secara eksklusif ke loop non-terminating, untuk menerapkannya ke setiap fungsi Rust.

Analisis yang diperlukan untuk menunjukkan bahwa badan loop sebenarnya memiliki efek samping (tidak hanya berpotensi , seperti panggilan ke fungsi eksternal) dan dengan demikian tidak memerlukan penyisipan llvm.sideeffect cukup rumit, mungkin dalam urutan yang kira-kira sama besarnya yang menunjukkan hal yang sama untuk fungsi yang mungkin menjadi bagian dari rekursi tak hingga. Membuktikan bahwa perulangan sedang berhenti juga sulit tanpa melakukan banyak pengoptimalan terlebih dahulu, karena kebanyakan perulangan Rust melibatkan iterator. Jadi saya pikir kami akhirnya akan menempatkan llvm.sideeffect ke dalam sebagian besar loop. Memang, ada beberapa fungsi yang tidak mengandung loop, tetapi bagi saya itu masih tidak terlihat seperti perbedaan kualitatif.

Jika saya memahami masalah dengan benar, untuk memperbaiki kasus pengulangan tak terbatas, itu harus cukup untuk memasukkan llvm.sideeffect ke dalam loop { ... } dan while <compile-time constant true> { ... } mana badan pengulangan tidak berisi break ekspresi. Ini menangkap perbedaan antara semantik C ++ dan semantik Rust untuk pengulangan tak terbatas: di Rust, tidak seperti C ++, kompilator tidak diizinkan untuk berasumsi bahwa perulangan berakhir ketika ia dapat diketahui pada waktu kompilasi bahwa _doesn't_. (Saya tidak yakin seberapa besar kita perlu khawatir tentang kebenaran dalam menghadapi putaran di mana tubuh mungkin panik, tetapi itu selalu dapat ditingkatkan nanti.)

Saya tidak tahu apa yang harus dilakukan tentang rekursi tak terbatas, tapi saya setuju dengan RalfJung bahwa mengoptimalkan rekursi tak terbatas menjadi segfault yang tidak terkait bukanlah perilaku yang diinginkan.

@tokopedia

Jika saya memahami masalahnya dengan benar, untuk memperbaiki kasus loop tak terbatas, itu harus cukup untuk memasukkan llvm.sideeffect ke dalam loop {...} dan sementara{...} di mana badan perulangan tidak berisi ekspresi break.

Saya tidak berpikir sesederhana itu, misalnya, loop { if false { break; } } adalah pengulangan tak terbatas yang berisi ekspresi break , namun kita perlu memasukkan @llvm.sideeffect untuk mencegah llvm menghapusnya. AFAICT kita harus memasukkan @llvm.sideeffect kecuali kita dapat membuktikan bahwa loop selalu berakhir.

@bayu_joo

loop { if false { break; } } adalah pengulangan tak terbatas yang berisi ekspresi break, namun kita perlu memasukkan @llvm.sideeffect untuk mencegah llvm menghapusnya.

Hm, ya, itu merepotkan. Tapi kita tidak harus _perfect_, cukup benar secara konservatif. Seperti lingkaran

while spinlock.load(Ordering::SeqCst) != 0 {}

(dari dokumentasi std::sync::atomic ) akan dengan mudah terlihat tidak membutuhkan @llvm.sideeffect , karena kondisi pengendalian tidak konstan (dan operasi beban atom sebaiknya dihitung sebagai efek samping untuk tujuan LLVM , atau kami memiliki masalah yang lebih besar). Jenis loop hingga yang mungkin dipancarkan oleh generator program,

loop {
    if /* runtime-variable condition */ { break }
    /* more stuff */
}

seharusnya juga tidak merepotkan. Faktanya, adakah kasus di mana aturan "tanpa putus ekspresi di badan perulangan" menjadi salah _besides_

loop {
    if /* provably false at compile time */ { break }
}

?

Saya pikir masalah ini adalah tentang memperbaiki implementasi Rust untuk loop tak terbatas sehingga perilaku LLVM-IR yang dihasilkan menjadi jelas, dan untuk ini, @ llvm.sideeffect, terdengar seperti solusi yang cukup bagus.

Cukup adil. Namun, seperti yang Anda katakan, masalah (ketidakcocokan antara semantik Rust dan semantik LLVM) sebenarnya adalah tentang non-terminasi, bukan tentang loop. Jadi saya pikir itulah yang harus kita lacak di sini.

@tokopedia

Jika saya memahami masalahnya dengan benar, untuk memperbaiki kasus loop tak terbatas, itu harus cukup untuk memasukkan llvm.sideeffect ke dalam loop {...} dan sementara{...} di mana badan perulangan tidak berisi ekspresi break. Ini menangkap perbedaan antara semantik C ++ dan semantik Rust untuk pengulangan tak terbatas: di Rust, tidak seperti C ++, kompilator tidak diizinkan untuk berasumsi bahwa perulangan berakhir ketika ia dapat diketahui pada waktu kompilasi padahal tidak. (Saya tidak yakin seberapa besar kita perlu khawatir tentang kebenaran dalam menghadapi putaran di mana tubuh mungkin panik, tetapi itu selalu dapat ditingkatkan nanti.)

Apa yang Anda jelaskan berlaku untuk C. Dalam Rust, setiap loop diizinkan untuk menyimpang. Segala sesuatu yang lain tidak akan sehat untuk dilakukan.

Jadi misalnya

while test_fermats_last_theorem_on_some_random_number() { }

adalah program yang baik-baik saja di Rust (tetapi tidak di C atau C ++), dan itu akan berputar selamanya tanpa menyebabkan efek samping. Jadi, itu harus semua loop, kecuali yang dapat kami buktikan akan dihentikan.

@tokopedia

apakah ada kasus di mana "ekspresi tidak ada break dalam tubuh loop" menjadi salah disamping

Bukan hanya if /*compile-time condition */ . Semua aliran kontrol terpengaruh ( while , match , for , ...) dan kondisi waktu proses juga terpengaruh.

Tetapi kita tidak harus sempurna, cukup koreksi secara konservatif.

Mempertimbangkan:

fn foo(x: bool) { loop { if x { break; } } }

dimana x adalah kondisi run-time. Jika kita tidak mengeluarkan @llvm.sideeffect sini, maka jika pengguna menulis foo(false) suatu tempat, foo dapat disisipkan dan dengan propagasi konstan dan penghapusan kode mati, loop dioptimalkan menjadi loop tak terbatas tanpa efek samping, mengakibatkan kesalahan pengoptimalan.

Jika itu masuk akal, satu transformasi yang LLVM boleh lakukan adalah mengganti foo dengan foo_opt :

fn foo_opt(x: bool) { if x { foo(true) } else { foo(false) } }

di mana kedua cabang dioptimalkan secara independen, dan cabang kedua akan salah dioptimalkan jika kita tidak menggunakan @llvm.sideeffect .

Artinya, untuk dapat menghilangkan @llvm.sideeffect , kita perlu membuktikan bahwa LLVM tidak dapat salah mengoptimalkan loop dalam keadaan apa pun. Satu-satunya cara untuk membuktikan hal ini adalah dengan membuktikan bahwa loop selalu berakhir, atau untuk membuktikan bahwa jika loop tidak berhenti, maka loop tanpa syarat melakukan salah satu hal yang mencegah salah optimasi. Bahkan kemudian, pengoptimalan seperti loop splitting / peeling dapat mengubah satu loop menjadi serangkaian loop, dan itu akan cukup bagi salah satu dari mereka untuk tidak memiliki @llvm.sideeffect agar kesalahan pengoptimalan terjadi.

Segala sesuatu tentang bug ini bagi saya sepertinya akan jauh lebih mudah untuk diselesaikan dari LLVM daripada dari rustc . (penafian: Saya tidak terlalu tahu basis kode dari salah satu proyek ini)

Seperti yang saya pahami, perbaikan dari LLVM akan mengubah pengoptimalan dari berjalan (buktikan non-terminasi || juga tidak dapat dibuktikan) menjadi berjalan hanya ketika non-terminasi dapat dibuktikan (atau sebaliknya). Saya tidak mengatakan ini mudah (dengan cara apa pun), tetapi LLVM sudah (saya kira) menyertakan kode untuk mencoba membuktikan (non-) penghentian loop.

Di sisi lain, rustc hanya dapat melakukan ini dengan menambahkan @llvm.sideeffect , yang berpotensi berdampak lebih besar pada pengoptimalan daripada "hanya" menonaktifkan pengoptimalan yang membuat penggunaan non-terminasi tidak tepat. Dan rustc harus menyematkan kode baru untuk mencoba mendeteksi (bukan) penghentian pengulangan.

Jadi menurut saya jalan ke depan adalah:

  1. Tambahkan @llvm.sideeffect pada setiap loop dan pemanggilan fungsi untuk memperbaiki masalah
  2. Perbaiki LLVM agar tidak melakukan pengoptimalan yang salah pada loop non-terminating, dan hapus @llvm.sideeffects

Apa pendapat Anda tentang ini? Saya berharap dampak kinerja dari langkah 1 tidak terlalu buruk, meskipun itu dimaksudkan untuk menghilang setelah 2 diterapkan…

@Ekleog itulah yang mungkin terjadi pada patch kedua @sunfishcode : https://lists.llvm.org/pipermail/llvm-dev/2017-October/118595.html

bagian dari proposal atribut fungsi adalah untuk
ubah semantik default LLVM IR agar perilaku yang ditentukan aktif
loop tak terbatas, lalu tambahkan atribut memilih ke UB potensial. Begitu
jika kita melakukan itu, maka peran @ llvm.sideeffect menjadi sedikit
halus - ini akan menjadi cara untuk frontend untuk bahasa seperti C untuk memilih
menjadi potensi-UB untuk suatu fungsi, tetapi kemudian memilih keluar untuk individu
loop dalam fungsi itu.

Agar adil bagi LLVM, penulis kompiler tidak mendekati topik ini dari perspektif "Saya akan menulis pengoptimalan yang membuktikan loop tidak berhenti, sehingga saya dapat mengoptimalkannya dengan pedant!" Sebaliknya, asumsi bahwa loop akan berhenti atau memiliki efek samping muncul secara alami dalam beberapa algoritme kompiler umum. Memperbaiki ini bukan hanya mengubah kode yang ada; itu akan membutuhkan kompleksitas baru yang signifikan.

Pertimbangkan algoritma berikut untuk menguji apakah suatu fungsi tubuh "tidak memiliki efek samping": jika ada instruksi dalam tubuh yang memiliki potensi efek samping, maka fungsi tubuh mungkin memiliki efek samping. Bagus dan sederhana. Kemudian, panggilan ke fungsi "tanpa efek samping" dihapus. Keren. Kecuali, instruksi cabang dianggap tidak memiliki efek samping, jadi fungsi yang hanya berisi cabang akan tampak tidak memiliki efek samping, meskipun mungkin berisi loop tak terbatas. Ups.

Itu bisa diperbaiki. Jika ada orang lain yang tertarik untuk melihat ini, ide dasar saya adalah untuk membagi konsep "memiliki efek samping" menjadi konsep independen "memiliki efek samping yang sebenarnya" dan "mungkin tidak dapat dihentikan". Dan kemudian telusuri seluruh pengoptimal dan temukan semua tempat yang peduli "memiliki efek samping" dan cari tahu konsep mana yang sebenarnya mereka butuhkan. Dan kemudian ajarkan loop melewati untuk menambahkan metadata ke cabang yang bukan bagian dari loop, atau loop tempat mereka berada terbukti terbatas, untuk menghindari pesimisasi.


Kemungkinan kompromi mungkin memiliki penyisipan rustc @ llvm.sideeffect ketika pengguna benar-benar menulis loop { } kosong (atau serupa) atau rekursi tanpa syarat (yang sudah memiliki lint). Kompromi ini akan memungkinkan orang-orang yang benar-benar bermaksud untuk melakukan putaran pemintalan tanpa efek yang tak terbatas untuk mendapatkannya, sambil menghindari overhead untuk orang lain. Tentu saja, kompromi ini tidak akan membuat kode aman tidak mungkin rusak, tetapi kemungkinan akan mengurangi kemungkinan terjadinya secara tidak sengaja, dan sepertinya itu mudah diterapkan.

Sebaliknya, asumsi bahwa loop akan berhenti atau memiliki efek samping muncul secara alami dalam beberapa algoritme kompiler umum.

Ini sama sekali tidak wajar jika Anda bahkan mulai berpikir tentang kebenaran dari transformasi tersebut. Sejujurnya saya masih berpikir itu adalah kesalahan besar dari C untuk membiarkan asumsi ini, tapi baiklah.

jika instruksi apapun dalam tubuh memiliki potensi efek samping, maka fungsi tubuh mungkin memiliki efek samping.

Ada alasan bagus bahwa "non-termination" biasanya dianggap berpengaruh saat Anda mulai melihat sesuatu secara formal. (Haskell tidak murni, ini memiliki dua efek: Non-terminasi dan pengecualian.)

Kemungkinan kompromi mungkin memiliki penyisipan rustc @ llvm.sideeffect ketika pengguna benar-benar menulis loop kosong {} (atau serupa) atau rekursi tanpa syarat (yang sudah memiliki lint). Kompromi ini akan memungkinkan orang-orang yang benar-benar bermaksud untuk melakukan putaran pemintalan tanpa efek yang tak terbatas untuk mendapatkannya, sambil menghindari overhead untuk orang lain. Tentu saja, kompromi ini tidak akan membuat kode aman tidak mungkin rusak, tetapi kemungkinan akan mengurangi kemungkinan terjadinya secara tidak sengaja, dan sepertinya itu mudah diterapkan.

Seperti yang Anda catat sendiri, ini masih salah. Saya tidak berpikir kita harus menerima "solusi" yang kita tahu tidak benar. Kompiler adalah bagian integral dari infrastruktur kami, kami seharusnya tidak hanya berharap tidak ada yang salah. Ini bukanlah cara untuk membangun fondasi yang kokoh.


Apa yang terjadi di sini adalah bahwa gagasan kebenaran dibangun di sekitar apa yang dilakukan kompiler, alih-alih memulai dengan "Apa yang kita inginkan dari kompiler kita" dan kemudian menjadikannya sebagai spesifikasi mereka. Kompilator yang benar tidak mengubah program yang selalu menyimpang menjadi program yang berhenti, titik. Saya menemukan ini agak terbukti dengan sendirinya, tetapi dengan Rust memiliki sistem tipe yang masuk akal, ini bahkan terlihat jelas dalam tipe, itulah sebabnya masalah ini muncul secara teratur.

Mengingat kendala yang kita hadapi (yaitu, LLVM), apa yang harus kita lakukan adalah mulai dengan menambahkan llvm.sideeffect di tempat yang cukup sehingga setiap eksekusi divergen dijamin untuk "mengeksekusi" banyak dari itu tanpa batas. Kemudian kami telah mencapai dasar yang masuk akal (seperti dalam, sehat dan benar) dan dapat berbicara tentang perbaikan dengan cara menghapus anotasi ini ketika kami dapat menjamin bahwa anotasi tersebut tidak diperlukan.

Untuk membuat poin saya lebih tepat, saya pikir berikut ini adalah peti Rust suara, dengan pick_a_number_greater_2 kembali (non-deterministik) semacam big-int:

fn test_fermats_last_theorem() -> bool {
  let x = pick_a_number_greater_2();
  let y = pick_a_number_greater_2();
  let z = pick_a_number_greater_2();
  let n = pick_a_number_greater_2();
  // x^n + y^n = z^n is impossible for n > 2
  pow(x, n) + pow(y, n) != pow(z, n)
}

pub fn diverge() -> ! {
  while test_fermats_last_theorem() { }
  // This code is unreachable, as proven by Andrew Wiles
  unsafe { mem::transmute(()) }
}

Jika kita mengkompilasi loop divergen itu, itu adalah bug dan harus diperbaiki.

Kami bahkan tidak memiliki angka sejauh ini untuk berapa banyak biaya yang harus dikeluarkan untuk memperbaikinya secara naif. Sampai kami melakukannya, saya tidak melihat alasan untuk dengan sengaja menghentikan program seperti di atas.

Dalam praktiknya, fn foo() { foo() } akan selalu terhenti karena kehabisan sumber daya, tetapi karena mesin abstrak Rust memiliki kerangka tumpukan yang sangat besar (AFAIK), adalah valid untuk mengubah kode itu menjadi fn foo() { loop {} } yang tidak akan pernah berakhir (atau lebih lama lagi, saat alam semesta membeku). Haruskah transformasi ini valid? Saya akan mengatakan ya, karena jika tidak, kami tidak dapat melakukan pengoptimalan panggilan balik kecuali kami dapat membuktikan penghentian, yang akan sangat disayangkan.

Apakah masuk akal untuk memiliki unsafe intrinsic yang menyatakan bahwa loop, rekursi, ... selalu berakhir? N1528 memberikan contoh, jika loop tidak dapat diasumsikan untuk dihentikan, fusi loop tidak dapat diterapkan ke kode penunjuk yang melintasi daftar tertaut, karena daftar tertaut bisa melingkar, dan membuktikan bahwa daftar tertaut tidak melingkar bukanlah sesuatu yang dapat dilakukan oleh kompiler modern. melakukan.

Saya sangat setuju kami perlu memperbaiki masalah kesehatan ini untuk selamanya. Namun, cara kita melakukannya harus memperhatikan kemungkinan bahwa "tambahkan llvm.sideeffect mana pun kami tidak dapat membuktikan bahwa itu tidak perlu" dapat menurunkan kualitas kode program yang dikompilasi dengan benar hari ini. Sementara kekhawatiran seperti itu pada akhirnya dikesampingkan oleh kebutuhan untuk memiliki kompiler suara, mungkin lebih bijaksana untuk melanjutkan dengan cara yang sedikit menunda perbaikan yang tepat sebagai imbalan untuk menghindari regresi kinerja dan meningkatkan kualitas hidup rata-rata programmer Rust di maksud waktu. Saya melamar:

  • Seperti halnya perbaikan yang berpotensi menurunkan kinerja untuk bug kesehatan yang sudah berlangsung lama (# 10184), kita harus menerapkan perbaikan di belakang tanda -Z agar dapat mengevaluasi dampak kinerja pada basis kode di alam liar.
  • Jika dampaknya ternyata dapat diabaikan, bagus, kita bisa mengaktifkan perbaikan secara default.
  • Tetapi jika ada regresi nyata darinya, kami dapat membawa data itu ke orang-orang LLVM dan mencoba meningkatkan LLVM terlebih dahulu (atau kami dapat memilih untuk memakan regresi dan memperbaikinya nanti, tetapi bagaimanapun kami akan membuat keputusan yang tepat)
  • Jika kita memutuskan untuk tidak mengaktifkan perbaikan secara default karena regresi, kita setidaknya dapat melanjutkan dengan menambahkan llvm.sideeffect ke loop yang secara sintaksis kosong: mereka agak umum dan salah dikompilasi telah menyebabkan banyak orang menghabiskan banyak uang berjam-jam men-debug masalah aneh (# 38136, # 47537, # 54214, dan tentunya masih ada lagi), jadi meskipun mitigasi ini tidak ada hubungannya dengan bug kesehatan, ini akan memiliki manfaat nyata bagi pengembang sementara kami mengatasi masalah tersebut dengan benar. perbaikan bug.

Harus diakui, perspektif ini diinformasikan oleh fakta bahwa isu ini telah berlangsung bertahun-tahun. Jika ini adalah regresi baru, saya akan lebih terbuka untuk memperbaikinya lebih cepat atau mengembalikan PR yang memperkenalkannya.

Sementara itu, apakah ini harus disebutkan di https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html selama masalah ini terbuka?

Apakah masuk akal untuk memiliki unsafe intrinsic yang menyatakan bahwa perulangan, rekursi, ... selalu berakhir?

std::hint::reachable_unchecked ?

Kebetulan saya menemukan ini menulis kode nyata untuk sistem pesan TCP. Saya memiliki loop tak terbatas sebagai stopgap sampai saya memasukkan mekanisme nyata untuk menghentikan tetapi utas segera keluar.

Jika ada yang ingin bermain golf kode kasus uji:

fn main() {
    (|| loop {})()
}

``
$ cargo run --release
Instruksi ilegal (core dumped)

Jika ada yang ingin bermain golf kode kasus uji:

pub fn main() {
   (|| loop {})()
}

Dengan bendera -Z insert-sideeffect rustc , ditambahkan oleh

sebelum:

main:
  ud2

setelah:

main:
.LBB0_1:
  jmp .LBB0_1

Ngomong-ngomong, pelacakan bug LLVM ini adalah https://bugs.llvm.org/show_bug.cgi?id=965 , yang belum saya lihat diposting di utas ini.

@RalfJung Dapatkah Anda memperbarui hyperlink https://github.com/simnalamburt/snippets/blob/master/rust/src/bin/infinite.rs dalam deskripsi masalah ke https://github.com/simnalamburt/snippets/blob /12e73f45f3/rust/infinite.rs ini? Tautan sebelumnya rusak untuk waktu yang lama karena di bukan tautan permanen. Terima kasih! 😛

@simnalamburt selesai, terima kasih!

Meningkatkan tingkat opt ​​MIR tampaknya menghindari kesalahan optimalisasi dalam kasus berikut:

pub fn main() {
   (|| loop {})()
}

--emit=llvm-ir -C opt-level=1

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  unreachable
}

--emit=llvm-ir -C opt-level=1 -Z mir-opt-level=2

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  br label %bb1, !dbg !10

bb1:                                              ; preds = %bb1, %start
  br label %bb1, !dbg !11
}

https://godbolt.org/z/N7VHnj

rustc 1.45.0-nightly (5fd2f06e9 2020-05-31)

pub fn oops() {
   (|| loop {})() 
}

pub fn main() {
   oops()
}

Ini membantu dengan kasus khusus itu tetapi tidak menyelesaikan masalah secara umum. https://godbolt.org/z/5hv87d

Secara umum, masalah ini hanya dapat diselesaikan jika rustc atau LLVM dapat membuktikan fungsi murni total sebelum menggunakan optimisasi yang relevan.

Memang, saya tidak menyatakan bahwa itu menyelesaikan masalah. Efek halusnya cukup menarik bagi orang lain sehingga layak untuk disebutkan di sini juga. -Z insert-sideeffect terus memperbaiki kedua kasus.

Ada sesuatu yang bergerak di sisi LLVM: ada proposal untuk menambahkan atribut tingkat fungsi untuk mengontrol jaminan kemajuan. https://reviews.llvm.org/D85393

Saya tidak yakin mengapa semua orang (di sini dan di utas LLVM) tampaknya menekankan klausul tentang kemajuan ke depan.

Penghapusan loop tampaknya merupakan konsekuensi langsung dari model memori: penghitungan nilai diizinkan untuk dipindahkan, selama hal itu terjadi-sebelum nilai digunakan. Sekarang, jika ada bukti bahwa nilai tidak dapat digunakan, itu adalah bukti bahwa tidak ada kejadian sebelumnya, dan kode dapat dipindahkan jauh ke masa depan, dan masih memenuhi model memori.

Atau, jika Anda tidak terbiasa dengan model memori, pertimbangkan bahwa seluruh loop disarikan menjadi fungsi yang menghitung nilai. Sekarang ganti semua pembacaan nilai di luar loop dengan panggilan fungsi itu. Transformasi ini memang benar. Sekarang, jika tidak ada penggunaan nilai, tidak ada pemanggilan fungsi yang melakukan pengulangan tak terbatas.

perhitungan nilai diperbolehkan untuk dipindahkan, selama itu terjadi-sebelum nilai digunakan. Sekarang, jika ada bukti bahwa nilai tidak dapat digunakan, itu adalah bukti bahwa tidak ada kejadian sebelumnya, dan kode dapat dipindahkan jauh ke masa depan, dan masih memenuhi model memori.

Pernyataan ini benar hanya jika perhitungan itu dijamin akan berakhir. Non-terminasi adalah efek samping, dan seperti halnya Anda tidak dapat menghapus komputasi yang mencetak ke stdout (ini "tidak murni"), Anda tidak dapat menghapus komputasi yang tidak berhenti.

Tidak diperbolehkan untuk menghapus pemanggilan fungsi berikut, meskipun hasilnya tidak digunakan:

fn sideeffect() -> u32 {
  println!("Hello!");
  42
}

fn main() {
  let _ = sideffect(); // May not be removed.
}

Ini berlaku untuk semua jenis efek samping, dan tetap benar saat Anda mengganti cetakan dengan loop {} .

Klaim tentang non-terminasi sebagai efek samping membutuhkan tidak hanya kesepakatan bahwa itu (yang tidak kontroversial), tetapi juga kesepakatan tentang _kapan_ harus ditaati.

Non-termination sure diamati, jika loop menghitung nilainya. Non-terminasi tidak diperhatikan, jika Anda diizinkan untuk menyusun ulang perhitungan yang tidak bergantung pada hasil loop.

Seperti contoh di thread LLVM.

x = y % 42;
if y < 0 return 0;
...

Properti penghentian divisi tidak ada hubungannya dengan penataan ulang. CPU modern akan mencoba untuk mengeksekusi pembagian, perbandingan, prediksi cabang dan prefetching cabang yang berhasil secara paralel. Jadi Anda tidak dijamin melihat pembagian selesai pada saat Anda mengamati 0 dikembalikan, jika y negatif. (Dengan "amati" di sini maksud saya benar-benar mengukur dengan osilometer di mana CPU berada, bukan dengan program)

Jika Anda tidak dapat mengamati pembagian selesai, Anda tidak dapat mengamati pembagian dimulai. Jadi pembagian dalam contoh di atas biasanya akan diizinkan untuk diatur ulang, yang mungkin dilakukan oleh compiler:

if y < 0 return 0;
x = y % 42;
...

Saya mengatakan "biasanya", karena mungkin ada bahasa yang melarang hal ini. Saya tidak tahu apakah Rust adalah bahasa seperti itu.

Loop murni tidak berbeda.


Saya tidak mengatakan itu bukan masalah. Saya hanya mengatakan jaminan kemajuan ke depan bukanlah hal yang memungkinkan hal itu terjadi.

Klaim tentang non-terminasi sebagai efek samping membutuhkan tidak hanya kesepakatan bahwa hal itu (yang tidak kontroversial), tetapi juga kesepakatan tentang kapan hal itu harus diamati.

Apa yang saya ungkapkan adalah konsensus dari seluruh bidang penelitian bahasa pemrograman dan kompiler. Tentu Anda bebas untuk tidak setuju, tetapi sebaiknya Anda mendefinisikan ulang istilah seperti "ketepatan kompiler" - ini tidak berguna untuk diskusi dengan orang lain.

Pengamatan yang diizinkan selalu ditentukan di tingkat sumber. Spesifikasi bahasa mendefinisikan "Mesin Abstrak", yang menjelaskan (idealnya dalam detail matematis yang teliti) apa perilaku yang dapat diamati yang diizinkan dari suatu program. Dokumen ini tidak berbicara tentang pengoptimalan apa pun.

Ketepatan kompiler kemudian diukur apakah program yang dihasilkannya hanya menunjukkan perilaku yang dapat diamati yang menurut spesifikasi dapat dimiliki oleh program sumber. Ini adalah cara kerja setiap bahasa pemrograman yang menganggap kebenaran benar, dan ini adalah satu-satunya cara yang kita tahu tentang cara menangkap dengan cara yang tepat ketika kompiler benar.

Apa yang terserah pada setiap bahasa adalah untuk menentukan apa yang sebenarnya dianggap dapat diamati pada tingkat sumber, dan perilaku sumber mana yang dianggap "tidak ditentukan" dan dengan demikian dapat dianggap oleh kompilator untuk tidak pernah terjadi. Masalah ini muncul karena C ++ mengatakan bahwa pengulangan tak terbatas tanpa efek samping lain ("divergensi diam") adalah perilaku tidak terdefinisi, tetapi Rust tidak mengatakan hal seperti itu. Ini berarti bahwa non-terminasi di Rust selalu dapat diamati, dan harus dipertahankan oleh penyusun. Sebagian besar bahasa pemrograman membuat pilihan ini, karena pilihan C ++ dapat membuatnya sangat mudah untuk secara tidak sengaja memperkenalkan perilaku tidak terdefinisi (dan juga bug kritis) ke dalam program. Rust berjanji bahwa tidak ada perilaku tidak terdefinisi yang dapat muncul dari kode aman, dan karena kode aman dapat berisi loop tak terbatas, maka loop tak terbatas di Rust harus didefinisikan (dan dengan demikian dipertahankan) perilaku.

Jika hal ini membingungkan, saya sarankan untuk membaca latar belakang. Saya dapat merekomendasikan "Jenis dan Bahasa Pemrograman" oleh Benjamin Pierce. Anda mungkin juga akan menemukan banyak posting blog di luar sana, meskipun sulit untuk menilai seberapa baik informasi penulis sebenarnya.

Untuk konkret, jika contoh pembagian Anda diubah menjadi

x = 42 % y;
if y <= 0 { return 0; }

maka saya harap Anda setuju bahwa _cannot_ bersyarat diangkat di atas pembagian, karena itu akan mengubah perilaku yang dapat diamati ketika y adalah nol (dari menabrak menjadi nol kembali).

Dengan cara yang sama, dalam

x = if y == 0 { loop {} } else { y % 42 };
if y < 0 { return 0; }

mesin abstrak Rust memungkinkan ini untuk ditulis ulang sebagai

if y == 0 { loop {} }
else if y < 0 { return 0; }
x = y % 42;

tetapi kondisi pertama dan loop tidak dapat dibuang.

Ralf, saya tidak berpura-pura tahu setengah dari apa yang Anda lakukan, dan saya tidak ingin memperkenalkan arti baru. Saya sangat setuju dengan definisi tentang apa yang benar (urutan eksekusi harus sesuai dengan urutan program). Saya hanya berpikir "ketika" non-terminasi dapat diamati adalah bagian darinya, seperti dalam: jika Anda tidak menonton hasil loop, Anda tidak memiliki saksi penghentiannya (jadi tidak dapat mengklaim ketidaktepatannya) . Saya perlu meninjau kembali model eksekusi.

Terima kasih telah mendukung saya

@zackw Terima kasih. Itu kode yang berbeda, yang tentunya akan menghasilkan optimasi yang berbeda.

Premis saya tentang loop yang dioptimalkan dengan cara yang sama seperti pembagian itu cacat (tidak dapat melihat hasil dari pembagian == tidak dapat melihat loop berhenti), jadi sisanya tidak masalah.

@olotenko Saya tidak tahu apa yang Anda maksud dengan "menonton hasil loop". Perulangan non-terminating membuat seluruh program menyimpang, yang dianggap sebagai perilaku yang dapat diamati - ini berarti dapat diamati di luar program. Seperti halnya, pengguna dapat menjalankan program dan melihat bahwa program itu berlangsung selamanya. Program yang berlangsung selamanya mungkin tidak dapat dikompilasi menjadi program yang berhenti, karena hal itu mengubah apa yang dapat diamati pengguna tentang program tersebut.

Tidak peduli apa loop itu menghitung atau apakah "nilai kembali" dari loop digunakan atau tidak. Yang penting adalah apa yang dapat diamati oleh pengguna saat menjalankan program. Kompilator harus memastikan bahwa perilaku yang dapat diamati ini tetap sama. Non-terminasi dianggap dapat diamati.

Untuk memberikan contoh lain:

fn main() {
  loop {}
  println!("Hello");
}

Program ini tidak akan pernah mencetak apapun, karena loop. Tetapi jika Anda mengoptimalkan pengulangan (atau menyusun ulang pengulangan dengan cetakan), tiba-tiba program akan mencetak "Halo". Jadi, pengoptimalan ini mengubah perilaku program yang dapat diamati, dan tidak diizinkan.

@RalfJung tidak apa-apa, saya mengerti sekarang. Masalah awal saya adalah peran apa yang dimainkan oleh "jaminan kemajuan ke depan" di sini. Pengoptimalan sepenuhnya dimungkinkan dari ketergantungan data. Kesalahan saya adalah bahwa sebenarnya ketergantungan data bukan bagian dari urutan program: secara harfiah ekspresi benar-benar diurutkan sesuai semantik bahasa. Jika urutan program adalah total, maka tanpa jaminan kemajuan maju (yang dapat kita nyatakan kembali sebagai "sub-jalur urutan program apa pun terbatas") kita dapat menyusun ulang (dalam urutan eksekusi) hanya ekspresi yang dapat kita _prove_ sebagai penghentian (dan mempertahankan beberapa properti lainnya, seperti observasi tindakan sinkronisasi, panggilan OS, IO, dll.).

Saya perlu memikirkannya lagi, tapi saya rasa saya dapat melihat alasan mengapa kita dapat "berpura-pura" terjadi pembagian dalam contoh dengan x = y % 42 , meskipun tidak benar-benar dieksekusi untuk beberapa masukan, tetapi mengapa hal yang sama tidak berlaku untuk loop arbitrer. Maksud saya, seluk-beluk korespondensi dari total (program) order dan parsial (eksekusi) order.

Menurut saya "perilaku yang dapat diamati" mungkin sedikit lebih halus dari itu, karena rekursi tak terbatas akan berakhir dengan error stack overflow ("dihentikan" dalam arti "pengguna mengamati hasil"), tetapi pengoptimalan panggilan balik akan mengubahnya menjadi loop non-terminating. Setidaknya ini adalah satu hal lain yang akan dilakukan Rust / LLVM. Tetapi kita tidak perlu membahas pertanyaan itu karena sebenarnya bukan itu masalah saya (kecuali Anda mau! Saya yakin senang memahami jika itu yang diharapkan).

tumpukan meluap

Stack overflows memang menantang untuk dimodelkan, pertanyaan bagus. Sama untuk situasi di luar memori. Sebagai perkiraan pertama, kami secara resmi menganggap itu tidak terjadi. Pendekatan yang lebih baik adalah mengatakan bahwa setiap kali Anda memanggil suatu fungsi, Anda mungkin mendapatkan kesalahan karena stack overflow, atau program dapat melanjutkan - ini adalah pilihan non-deterministik yang dibuat pada setiap panggilan. Dengan cara ini Anda dapat memperkirakan apa yang sebenarnya terjadi.

kita dapat menyusun ulang (dalam urutan eksekusi) hanya ekspresi yang dapat kita buktikan sebagai terminating

Memang. Morevoer mereka harus "murni", yaitu, bebas efek samping - Anda tidak dapat memesan ulang dua println! . Itulah mengapa kami biasanya menganggap non-terminasi sebagai efek juga, karena kemudian ini semua tereduksi menjadi "ekspresi murni dapat diatur ulang", dan "ekspresi non-terminating tidak murni" (tidak murni = memiliki efek samping).

Divison juga berpotensi tidak murni, tetapi hanya jika membagi dengan 0 - yang menyebabkan kepanikan, yaitu efek kontrol. Ini tidak dapat diamati secara langsung tetapi secara tidak langsung (misalnya dengan meminta panic handler mencetak sesuatu ke stdout, yang kemudian dapat diamati). Jadi pembagian hanya dapat diatur ulang jika kita yakin tidak membaginya dengan 0.

Saya memiliki beberapa kode demo yang menurut saya mungkin menjadi masalah ini, tetapi saya tidak sepenuhnya yakin. Jika perlu, saya dapat memasukkan ini ke dalam laporan bug baru.
Saya meletakkan kode untuk itu di repo git di https://github.com/uglyoldbob/rust_demo

Loop tak terbatas saya (dengan efek samping) dioptimalkan dan instruksi jebakan dihasilkan.

Saya tidak tahu apakah itu adalah contoh dari masalah ini atau sesuatu yang lain ... perangkat yang disematkan sama sekali bukan keahlian saya dan dengan semua ketergantungan peti eksternal ini saya tidak tahu apa lagi yang dilakukan kode itu. ^^ Tetapi program Anda adalah tidak aman dan memang memiliki akses yang mudah menguap di loop, jadi menurut saya ini adalah masalah terpisah. Ketika saya meletakkan contoh Anda di taman bermain , saya pikir itu dikompilasi dengan benar, jadi saya menduga masalahnya ada pada salah satu ketergantungan tambahan.

Tampaknya semua yang ada di loop adalah referensi ke variabel lokal (tidak ada yang lolos ke thread lain). Dalam keadaan ini, mudah untuk membuktikan tidak adanya simpanan yang mudah menguap dan tidak adanya efek yang dapat diamati (tidak ada simpanan yang dapat disinkronkan). Jika Rust tidak menambahkan arti khusus pada volatile, maka loop ini dapat direduksi menjadi loop tak terbatas murni.

@uglyoldbob Apa yang sebenarnya terjadi dalam contoh Anda akan lebih jelas jika llvm-objdump tidak benar-benar tidak membantu (dan tidak akurat). bl #4 (yang sebenarnya bukan sintaks assembly yang valid) di sini berarti cabang ke 4 byte setelah akhir instruksi bl , alias akhir dari fungsi main , alias fungsi mulai dari fungsi selanjutnya. Fungsi selanjutnya dipanggil (ketika saya membuatnya) _ZN11broken_loop18__cortex_m_rt_main17hbe300c9f0053d54dE , dan itu adalah fungsi main Anda yang sebenarnya. Fungsi dengan nama yang tidak berubah main bukanlah fungsi Anda, tetapi fungsi yang sama sekali berbeda yang dihasilkan oleh makro #[entry] disediakan oleh cortex-m-rt . Kode Anda sebenarnya tidak dioptimalkan. (Faktanya, pengoptimal bahkan tidak berjalan karena Anda sedang membangun dalam mode debug.)

Apakah halaman ini membantu?
0 / 5 - 0 peringkat