Rust: floating point ke cast integer dapat menyebabkan perilaku tidak terdefinisi

Dibuat pada 31 Okt 2013  ·  234Komentar  ·  Sumber: rust-lang/rust

Status per 2020-04-18

Kami bermaksud untuk menstabilkan perilaku saturating-float-casts untuk as , dan telah menstabilkan fungsi pustaka yang tidak aman yang menangani perilaku sebelumnya. Lihat # 71269 untuk pembahasan terbaru tentang proses stabilisasi tersebut.

Status per 2018-11-05

Sebuah flag telah diimplementasikan di compiler, -Zsaturating-float-casts , yang akan menyebabkan semua float ke integer cast memiliki perilaku "saturating" di mana jika di luar batas, ia akan dijepit ke batas terdekat. Panggilan untuk pembandingan perubahan ini keluar beberapa waktu yang lalu. Hasil, meskipun positif dalam banyak proyek, cukup negatif untuk beberapa proyek dan menunjukkan bahwa kita belum selesai di sini.

Langkah selanjutnya adalah mencari tahu cara memulihkan kinerja untuk kasus ini:

  • Salah satu pilihan adalah mengambil perilaku cor as (yang dalam beberapa kasus UB) dan menambahkan fungsi unsafe untuk jenis yang relevan dan semacamnya.
  • Yang lain adalah menunggu LLVM menambahkan konsep freeze yang berarti kita mendapatkan pola bit sampah, tapi setidaknya bukan UB
  • Cara lainnya adalah mengimplementasikan cast melalui perakitan inline di LLVM IR, karena codegen saat ini tidak terlalu dioptimalkan.

Status lama

PEMBARUAN (oleh @nikomatsakis): Setelah banyak berdiskusi, kami mendapatkan dasar-dasar rencana untuk menangani masalah ini. Namun kami memerlukan bantuan untuk benar-benar menyelidiki dampak kinerja dan mengerjakan detail akhirnya!


MASALAH ASLI BERIKUT:

Jika nilai tidak bisa muat di ty2, hasilnya tidak ditentukan.

1.04E+17 as u8
A-LLVM C-bug I-unsound 💥 P-medium T-lang

Komentar yang paling membantu

Saya telah memulai beberapa pekerjaan untuk mengimplementasikan intrinsik untuk float jenuh ke cast int di LLVM: https://reviews.llvm.org/D54749

Jika itu berhasil, ini akan memberikan cara overhead yang relatif rendah untuk mendapatkan semantik yang menjenuhkan.

Semua 234 komentar

Nominasi

diterima untuk P-high, alasan yang sama dengan # 10183

Saya tidak berpikir ini tidak kompatibel dengan versi sebelumnya pada tingkat bahasa. Ini tidak akan menyebabkan kode yang berfungsi dengan baik berhenti bekerja. Nominasi.

berubah ke P-high, alasan yang sama seperti # 10183

Bagaimana kami mengusulkan untuk menyelesaikan ini dan # 10185? Karena apakah perilaku ditentukan atau tidak tergantung pada nilai dinamis dari angka yang sedang dicetak, tampaknya satu-satunya solusi adalah memasukkan pemeriksaan dinamis. Kami tampaknya setuju bahwa kami tidak ingin melakukan itu untuk luapan aritmatika, apakah kami senang melakukannya untuk luapan cor?

Kita bisa menambahkan intrinsik ke LLVM yang melakukan "konversi aman". @zwari mungkin punya ide lain.

AFAIK satu-satunya solusi saat ini adalah dengan menggunakan intrinsik spesifik-target. Itulah yang dilakukan JavaScriptCore, setidaknya menurut seseorang yang saya tanyakan.

Oh, itu cukup mudah.

ping @pnkfelix apakah ini tercakup oleh pemeriksaan luapan baru?

Gips ini tidak diperiksa oleh rustc dengan pernyataan debug.

Saya senang menangani ini, tetapi saya membutuhkan solusi konkret. Saya pribadi berpikir bahwa itu harus diperiksa bersama dengan aritmatika integer yang melimpah, karena ini adalah masalah yang sangat mirip. Saya tidak terlalu keberatan dengan apa yang kami lakukan.

Perhatikan bahwa masalah ini saat ini menyebabkan ICE saat digunakan dalam ekspresi konstan tertentu.

Ini memungkinkan pelanggaran keamanan memori pada karat yang aman, contoh dari posting forum ini :

Undef, ya? Undef menyenangkan. Mereka cenderung merambat. Setelah beberapa menit bertengkar ..

#[inline(never)]
pub fn f(ary: &[u8; 5]) -> &[u8] {
    let idx = 1e100f64 as usize;
    &ary[idx..]
}

fn main() {
    println!("{}", f(&[1; 5])[0xdeadbeef]);
}

segfaults di sistem saya (malam terbaru) dengan -O.

Menandai dengan I-unsound karena melanggar keamanan memori pada karat yang aman.

@ bluss , ini tidak segfualt bagi saya, hanya memberikan kesalahan pernyataan. tanpa tanda karena saya yang menambahkannya

Sigh, saya lupa -O, penandaan ulang.

mencalonkan kembali untuk P-tinggi. Rupanya ini pada titik tertentu P-tinggi tetapi semakin rendah seiring waktu. Ini tampaknya cukup penting untuk kebenaran.

EDIT: tidak bereaksi terhadap komentar triase, menambahkan label secara manual.

Sepertinya preseden dari hal-hal yang melimpah (misalnya untuk perpindahan) adalah dengan hanya menyelesaikan beberapa perilaku. Java tampaknya menghasilkan hasil modulo kisaran, yang tampaknya tidak masuk akal; Saya tidak yakin jenis kode LLVM apa yang perlu kami tangani.

Menurut https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls -5.1.3 Java juga menjamin bahwa NaN nilai dipetakan ke 0 dan tak terbatas ke bilangan bulat minimum / maksimum yang dapat diwakili. Selain itu, aturan Java untuk konversi lebih kompleks daripada hanya membungkus, ini dapat berupa kombinasi saturasi (untuk konversi ke int atau long ) dan pembungkus (untuk konversi ke tipe integral yang lebih kecil , jika diperlukan). Replikasi seluruh algoritme konversi dari Java memang memungkinkan, tetapi itu akan membutuhkan operasi yang cukup banyak untuk setiap cast. Secara khusus, untuk memastikan bahwa hasil operasi fpto[us]i di LLVM tidak menunjukkan perilaku yang tidak ditentukan, pemeriksaan rentang akan diperlukan.

Sebagai alternatif, saya menyarankan agar float-> int cast dijamin hanya valid jika pemotongan nilai asli dapat direpresentasikan sebagai nilai tipe tujuan (atau mungkin sebagai [iu]size ?) Dan ke memiliki pernyataan tentang build debug yang memicu kepanikan saat nilainya belum disajikan dengan tepat.

Keuntungan utama dari pendekatan Java adalah bahwa fungsi konversinya adalah total, tetapi ini juga berarti bahwa perilaku yang tidak diharapkan dapat masuk: ini akan mencegah perilaku yang tidak ditentukan, tetapi akan mudah untuk ditipu untuk tidak memeriksa apakah pemeran benar-benar masuk akal (sayangnya ini juga berlaku untuk pemeran lainnya: khawatir :).

Pendekatan lain cocok dengan yang saat ini digunakan untuk operasi aritmatika: implementasi sederhana & efisien dalam rilis, kepanikan yang dipicu oleh pemeriksaan rentang dalam debug. Sayangnya tidak seperti pemeran as lainnya, ini akan membuat konversi seperti itu dicentang, yang dapat mengejutkan pengguna (meskipun mungkin analogi operasi aritmatika dapat membantu di sini). Ini juga akan merusak beberapa kode, tetapi AFAICT itu seharusnya hanya terjadi untuk kode yang saat ini mengandalkan perilaku tidak terdefinisi (yaitu akan menggantikan perilaku tidak terdefinisi "mari kembalikan bilangan bulat apa pun, Anda jelas tidak peduli yang mana" dengan panik).

Masalahnya bukan "mari kembalikan bilangan bulat apa pun, Anda jelas tidak peduli yang mana", itu menyebabkan undef yang bukan nilai acak melainkan nilai setan hidung dan LLVM diizinkan untuk menganggap undef tidak pernah terjadi mengaktifkan pengoptimalan yang melakukan hal-hal tidak benar yang mengerikan. Jika itu adalah nilai acak, tetapi yang terpenting bukan undef, maka itu sudah cukup untuk memperbaiki masalah kesehatan. Kita tidak perlu mendefinisikan bagaimana nilai yang tidak dapat direpresentasikan direpresentasikan, kita hanya perlu mencegah undef.

Dibahas dalam pertemuan @ rust-lang / compiler. Tindakan yang paling konsisten tetap:

  1. ketika pemeriksaan overflow diaktifkan, periksa adanya pemain ilegal dan panik.
  2. jika tidak, kita memerlukan perilaku fallback, itu harus sesuatu yang memiliki biaya runtime minimal (idealnya, nol) untuk nilai yang valid, tetapi perilaku yang tepat tidak terlalu penting, selama itu bukan LLVM undef.

Masalah utamanya adalah kita membutuhkan saran konkret untuk opsi 2.

triase: P-medium

@nikomatsakis Apakah as saat ini pernah panik dalam pembuatan debug? Jika tidak, untuk konsistensi dan prediktabilitas tampaknya lebih baik untuk tetap seperti itu. (Saya pikir itu _harus memiliki_, seperti aritmatika, tapi itu adalah perdebatan yang terpisah dan masa lalu.)

jika tidak, kita memerlukan perilaku fallback, itu harus sesuatu yang memiliki biaya runtime minimal (idealnya, nol) untuk nilai yang valid, tetapi perilaku yang tepat tidak terlalu penting, selama itu bukan LLVM undef.

Saran konkret: ekstrak digit dan eksponen sebagai u64 dan digit bitshift dengan eksponen.

fn f64_as_u64(f: f64) -> u64 {
    let (mantissa, exponent, _sign) = f.integer_decode();
    mantissa >> ((-exponent) & 63)
}

Ya, ini bukan biaya nol, tetapi ini agak dapat dioptimalkan (akan lebih baik jika kita menandai integer_decode inline ) dan setidaknya deterministik. MIR-pass masa depan yang mengembangkan float-> int cast mungkin dapat menganalisis apakah float dijamin baik-baik saja untuk cast dan melewati konversi berat ini.

Apakah LLVM tidak memiliki intrinsik platform untuk fungsi konversi?

EDIT : @zwarich berkata (lama sekali):

AFAIK satu-satunya solusi saat ini adalah dengan menggunakan intrinsik spesifik-target. Itulah yang dilakukan JavaScriptCore, setidaknya menurut seseorang yang saya tanyakan.

Mengapa repot-repot panik? AFAIK, @glaebhoerl benar, as seharusnya memotong / memperpanjang, _not_ memeriksa operan.

Pada Sabtu, 05 Mar 2016 pada 03:47:55 AM -0800, Gábor Lehel menulis:

@nikomatsakis Apakah as saat ini pernah panik dalam pembuatan debug? Jika tidak, untuk konsistensi dan prediktabilitas tampaknya lebih baik untuk tetap seperti itu. (Saya pikir itu _harus memiliki_, seperti aritmatika, tapi itu adalah perdebatan yang terpisah dan masa lalu.)

Benar. Menurut saya itu persuasif.

Pada Rabu, 09 Mar 2016 pukul 02:31:05 -0800, Eduard-Mihai Burtescu menulis:

Apakah LLVM tidak memiliki intrinsik platform untuk fungsi konversi?

EDIT :

AFAIK satu-satunya solusi saat ini adalah dengan menggunakan intrinsik spesifik-target. Itulah yang dilakukan JavaScriptCore, setidaknya menurut seseorang yang saya tanyakan.

Mengapa repot-repot panik? AFAIK, @glaebhoerl benar, as seharusnya memotong / memperpanjang, _not_ memeriksa operan.

Ya, saya pikir saya salah sebelumnya. as adalah "pemotongan yang tidak dicentang"
operator, baik atau buruk, dan tampaknya yang terbaik adalah tetap konsisten
dengan filosofi itu. Menggunakan intrinsik spesifik-target mungkin merupakan cara yang sempurna
solusi yang bagus?

@nikomatsakis : sepertinya perilaku belum ditentukan? Bisakah Anda memberikan update tentang perencanaan tentang itu?

Hanya mengalami ini dengan jumlah yang jauh lebih kecil

    let x: f64 = -1.0;
    x as u8

Hasil di 0, 16, dll. Tergantung pada pengoptimalan, saya berharap itu akan didefinisikan sebagai 255 jadi saya tidak perlu menulis x as i16 as u8 .

@gmorenz Apakah Anda mencoba !0u8 ?

Dalam konteks yang tidak masuk akal, saya mendapatkan f64 dari transformasi data yang dikirim melalui jaringan, dengan kisaran [-255, 255]. Saya berharap itu akan membungkus dengan baik (dengan cara yang persis seperti <i32> as u8 membungkus).

Berikut adalah proposal LLVM baru-baru ini untuk "membunuh undef" http://lists.llvm.org/pipermail/llvm-dev/2016-October/106182.html , meskipun saya hampir tidak cukup berpengetahuan untuk mengetahui apakah ini akan menyelesaikan secara otomatis atau tidak masalah ini.

Mereka mengganti undef dengan racun, semantiknya sedikit berbeda. Ini tidak akan membuat int -> float cast menjadi perilaku yang ditentukan.

Kami mungkin harus memberikan beberapa cara eksplisit untuk melakukan pemeran jenuh? Saya menginginkan perilaku yang persis seperti itu sekarang.

Sepertinya ini harus ditandai I-crash, diberikan https://github.com/rust-lang/rust/issues/10184#issuecomment -139858153.

Kami memiliki pertanyaan tentang ini di #rust-beginners hari ini, seseorang bertemu dengannya di alam liar.

Buku yang saya tulis dengan @jimblandy , _Programming Rust_, menyebutkan bug ini.

Beberapa jenis gips diizinkan.

  • Angka dapat ditransmisikan dari salah satu tipe numerik bawaan ke yang lain.

    (...)

    Namun, saat tulisan ini dibuat, mentransmisikan nilai floating-point yang besar ke tipe integer yang terlalu kecil untuk diwakilinya dapat menyebabkan perilaku yang tidak terdefinisi. Hal ini dapat menyebabkan crash bahkan di Rust yang aman. Ini adalah bug di kompiler, github.com/rust-lang/rust/issues/10184 .

Batas waktu kita untuk bab ini adalah 19 Mei. Saya ingin menghapus paragraf terakhir itu, tetapi saya merasa setidaknya kita harus memiliki semacam rencana di sini terlebih dahulu.

Rupanya JavaScriptCore saat ini menggunakan peretasan yang menarik di x86. Mereka menggunakan instruksi CVTTSD2SI, kemudian menggunakan beberapa C ++ berbulu jika nilainya di luar kisaran. Karena nilai out-of-range saat ini meledak, menggunakan instruksi itu (tanpa fallback!) Akan menjadi perbaikan dari apa yang kita miliki sekarang, meskipun hanya untuk satu arsitektur.

Sejujurnya saya pikir kita harus menghentikan cast numerik dengan as dan menggunakan From dan TryFrom atau sesuatu seperti kotak konv.

Mungkin begitu, tapi menurutku itu ortogonal.

Oke, saya baru saja membaca ulang seluruh percakapan ini. Saya pikir ada kesepakatan bahwa operasi ini tidak boleh panik (untuk konsistensi umum dengan as ). Ada dua pesaing utama tentang perilaku yang seharusnya:

  • Semacam hasil yang ditentukan

    • Pro: Ini menurut saya sangat konsisten dengan filosofi umum kami sejauh ini.

    • Kontra: Tampaknya tidak ada cara yang benar-benar portabel untuk menghasilkan hasil tertentu yang ditentukan dalam kasus ini. Ini menyiratkan bahwa kita akan menggunakan intrinsik khusus platform dengan semacam fallback untuk nilai di luar rentang (misalnya, kembali ke saturasi , fungsi yang diusulkan @ oli-obk ini , ke definisi Java , atau ke "hairy C ++" apa pun. JSC menggunakan .

    • Paling buruk, kami hanya dapat memasukkan beberapa jika untuk kasus "di luar jangkauan".

  • Nilai yang tidak ditentukan (bukan perilaku yang tidak ditentukan)

    • Pro: Ini memungkinkan kami hanya menggunakan intrinsik khusus platform yang tersedia di setiap platform.

    • Kontra: Ini bahaya portabilitas. Secara umum, saya merasa kami tidak terlalu sering menggunakan hasil yang tidak ditentukan, setidaknya dalam bahasa (saya yakin kami melakukannya di libs di berbagai tempat).

Tidak jelas bagi saya apakah ada preseden yang jelas tentang hasil yang seharusnya dalam kasus pertama?

Setelah menuliskannya, preferensi saya adalah mempertahankan hasil deterministik. Saya merasa setiap tempat yang bisa kita pegang pada determinisme adalah kemenangan. Saya tidak begitu yakin apa hasil yang seharusnya.

Saya suka saturasi karena saya bisa memahaminya dan sepertinya berguna, tetapi sepertinya tidak sesuai dengan cara u64 as u32 melakukan pemotongan. Jadi mungkin semacam hasil berdasarkan pemotongan masuk akal, yang menurut saya mungkin adalah

Kode saya memberikan nilai yang benar untuk hal-hal dalam kisaran 0..2 ^ 64 dan nilai deterministik tetapi palsu untuk yang lainnya.

float diwakili oleh mantissa ^ eksponen, misalnya 1.0 adalah (2 << 52) ^ -52 dan karena bitshift dan eksponen adalah hal yang sama dalam biner, kita hanya dapat membalikkan shift (sehingga negasi dari eksponen dan kanan bergeser).

1 untuk determinisme.

Saya melihat dua semantik yang masuk akal bagi manusia, dan saya pikir kita harus memilih mana yang lebih cepat untuk nilai yang berada dalam jangkauan, ketika kompiler tidak dapat mengoptimalkan komputasi apa pun . (Ketika kompilator mengetahui bahwa suatu nilai berada dalam rentang, kedua opsi memberikan hasil yang sama, sehingga keduanya sama-sama dapat dioptimalkan.)

  • Saturasi (nilai di luar kisaran menjadi IntType::max_value() / min_value() )
  • Modulo (nilai di luar rentang diperlakukan seolah-olah dengan mengonversi menjadi bigint terlebih dahulu, kemudian memotong)

Tabel di bawah ini dimaksudkan untuk menentukan kedua opsi sepenuhnya. T adalah tipe integer mesin apa pun. Tmin dan Tmax adalah T::min_value() dan T::max_value() . RTZ (v) artinya mengambil nilai matematis dari v dan Round Toward Zero untuk mendapatkan bilangan bulat matematis.

v | v as T (saturasi) | v as T (modulo)
---- | ---- | ----
dalam jangkauan (Tmin <= v <= Tmax) | RTZ (v) | RTZ (v)
nol negatif | 0 | 0
NaN | 0 | 0
Infinity | Tmax | 0
-Infinity | Tmin | 0
v> Tmax | Tmax | RTZ (v) dipotong agar sesuai dengan T
v <Tmin | Tmin | RTZ (v) dipotong agar sesuai dengan T

Standar ECMAScript menentukan operasi ToInt32 , ToUint32, ToInt16, ToUint16, ToInt8, ToUint8, dan maksud saya dengan opsi "modulo" di atas adalah untuk mencocokkan operasi tersebut di setiap kasus.

ECMAScript juga menetapkan ToInt8Clamp yang tidak cocok dengan kedua kasus di atas: ECMAScript melakukan "putaran setengah ke genap" pembulatan pada nilai pecahan daripada "bulat ke nol".

Saran @ oli-obk adalah cara ketiga, layak dipertimbangkan jika lebih cepat untuk menghitung, untuk nilai yang berada dalam jangkauan.

@ oli-obk Bagaimana dengan tipe integer bertanda?

Melempar proposal lain ke dalam campuran: Tandai gips u128 mengambang sebagai tidak aman dan paksa orang untuk secara eksplisit memilih cara menanganinya. u128 cukup langka saat ini.

@Manishearth Saya berharap untuk semantik serupa integers → floats as floats → integers. Karena keduanya UB-ful, dan kita tidak bisa lagi membuat float → integer tidak aman, kita mungkin harus menghindari membuat integer → float tidak aman juga.

Untuk float → integer saturating akan lebih cepat AFAICT (menghasilkan urutan and , uji + lompatan perbandingan float dan lompat, semuanya untuk 0,66 atau 0,5 2-3 siklus pada lengkungan modern). Saya pribadi tidak peduli dengan perilaku apa yang kami putuskan selama nilai dalam jangkauan secepat mungkin.

Bukankah masuk akal untuk membuatnya berperilaku seperti overflow? Jadi dalam build debug, akan panik jika Anda melakukan cast dengan perilaku yang tidak ditentukan. Kemudian Anda dapat memiliki metode untuk menentukan perilaku casting seperti 1.04E+17.saturating_cast::<u8>() , unsafe { 1.04E+17.unsafe_cast::<u8>() } dan kemungkinan lainnya.

Oh, saya pikir masalahnya hanya untuk u128, dan kita dapat membuatnya tidak aman dengan kedua cara.

@cryze UB seharusnya tidak ada bahkan dalam mode rilis dalam kode aman. Hal-hal meluap masih perilaku yang didefinisikan.

Konon, panik saat debug, dansaat rilis akan sangat bagus.

Ini mempengaruhi:

  • f32 -> u8, u16, u32, u64, u128, usize ( -1f32 as _ untuk semua, f32::MAX as _ untuk semua kecuali u128)
  • f32 -> i8, i16, i32, i64, i128, isize ( f32::MAX as _ untuk semua)
  • f64 -> semua int ( f64::MAX as _ untuk semua)

f32::INFINITY as u128 juga UB

@Tokopedia

Bukankah masuk akal untuk membuatnya berperilaku seperti overflow? Jadi dalam build debug, akan panik jika Anda melakukan cast dengan perilaku yang tidak ditentukan.

Ini adalah apa yang saya pikirkan pada awalnya, tetapi saya diingatkan bahwa konversi as tidak pernah panik saat ini (kami tidak melakukan pemeriksaan overflow dengan as , baik atau buruk). Jadi hal yang paling analog adalah untuk "melakukan sesuatu yang ditentukan".

FWIW, hal "kill undef" sebenarnya akan memberikan cara untuk memperbaiki ketidakamanan memori, tetapi membiarkan hasilnya nondeterministik. Salah satu komponen utamanya adalah:

3) Buat instruksi baru, '% y = freeze% x', yang menghentikan propagasi
meracuni. Jika masukan adalah racun, maka ia mengembalikan sewenang-wenang, tetapi tetap,
nilai. (seperti undef lama, tetapi setiap penggunaan mendapatkan nilai yang sama ), sebaliknya
hanya mengembalikan nilai masukannya.

Alasan undefs dapat digunakan untuk melanggar keamanan memori saat ini adalah karena mereka dapat secara ajaib mengubah nilai antar penggunaan: khususnya, antara pemeriksaan batas dan aritmatika penunjuk berikutnya. Jika rustc menambahkan pembekuan setelah setiap cast berbahaya, Anda hanya akan mendapatkan nilai yang tidak diketahui tetapi berperilaku baik. Dari segi kinerja, pembekuan pada dasarnya gratis di sini, karena tentu saja instruksi mesin yang sesuai dengan cetakan menghasilkan nilai tunggal, bukan nilai yang berfluktuasi; bahkan jika pengoptimal merasa ingin menduplikasi instruksi cast karena alasan tertentu, tindakan tersebut harus aman dilakukan karena hasil input out-of-range biasanya deterministik pada arsitektur tertentu.

... Tapi bukan deterministik lintas arsitektur, jika ada yang bertanya-tanya. x86 mengembalikan 0x80000000 untuk semua input yang buruk; ARM jenuh untuk input di luar jangkauan dan (jika saya membaca pseudocode ini dengan benar) mengembalikan 0 untuk NaN. Jadi, jika tujuannya adalah untuk menghasilkan hasil yang deterministik dan platform-independen , itu tidak cukup hanya menggunakan fp-to-int intrinsik platform; setidaknya di ARM Anda juga perlu memeriksa status register untuk pengecualian. Ini mungkin memiliki beberapa overhead dalam dirinya sendiri, dan tentu saja mencegah autovectorization jika tidak menggunakan intrinsik. Bergantian, saya kira Anda dapat secara eksplisit menguji nilai dalam kisaran menggunakan operasi perbandingan reguler dan kemudian menggunakan float-to-int biasa. Kedengarannya jauh lebih bagus untuk pengoptimal…

as konversi tidak pernah panik saat ini

Pada titik tertentu kami mengubah + menjadi panik (pada mode debug). Saya tidak akan terkejut melihat as panik dalam kasus-kasus yang sebelumnya UB.

Jika kita peduli tentang memeriksa (yang mana yang harus kita lakukan), maka kita harus menghentikan as (adakah kasus penggunaan di mana itu satu-satunya pilihan yang baik?) Atau setidaknya menyarankan untuk tidak menggunakannya, dan memindahkan orang ke hal-hal seperti TryFrom dan TryInto sebagai gantinya, itulah yang kami katakan akan kami lakukan ketika diputuskan untuk meninggalkan as apa adanya. Saya tidak merasa bahwa kasus yang dibahas secara kualitatif berbeda, secara abstrak , dari kasus di mana as sudah ditentukan untuk tidak melakukan pemeriksaan apa pun. Perbedaannya hanya pada praktiknya pelaksanaan untuk kasus-kasus tersebut saat ini belum lengkap dan memiliki UB. Dunia di mana Anda tidak dapat mengandalkan as melakukan pemeriksaan (karena untuk sebagian besar jenis, tidak), dan Anda tidak dapat mengandalkannya untuk tidak panik (karena untuk beberapa jenis, itu akan), dan itu tidak konsisten, dan kami masih belum mencela sepertinya yang terburuk dari semuanya bagi saya.

Jadi, saya pikir pada titik ini @jorendorff pada dasarnya menyebutkan apa yang menurut saya merupakan rencana terbaik :

  • as akan memiliki beberapa perilaku deterministik;
  • kami akan memilih perilaku berdasarkan kombinasi dari seberapa masuk akal perilaku tersebut, dan seberapa efisien perilaku tersebut

Dia menyebutkan tiga kemungkinan. Saya pikir pekerjaan yang tersisa adalah menyelidiki kemungkinan-kemungkinan itu - atau setidaknya menyelidiki salah satunya. Artinya, benar-benar terapkan, dan cobalah untuk merasakan betapa "lambat" atau "cepat" itu.

Apakah ada orang di luar sana yang merasa termotivasi untuk mencobanya? Saya akan memberi tag ini sebagai E-help-wanted dengan harapan dapat menarik perhatian seseorang. (@ oli-obk?)

Eh, saya lebih suka tidak membayar harga untuk konsistensi lintas platform : / Ini sampah, saya tidak peduli sampah apa yang keluar (namun pernyataan debug akan sangat membantu).

Saat ini semua fungsi pembulatan / pemotongan di Rust sangat lambat (pemanggilan fungsi dengan implementasi yang sangat tepat), jadi as adalah cara terakhir saya untuk melakukan retasan untuk pembulatan float cepat.

Jika Anda ingin menghasilkan as apa pun selain cvttss2si , tambahkan juga alternatif stabil yang hanya itu.

@pornel ini bukan hanya UB dari jenis teoretis di mana hal-hal baik-baik saja jika Anda mengabaikannya, ini memiliki implikasi dunia nyata. Saya telah mengekstrak # 41799 dari contoh kode dunia nyata.

@ est31 Saya setuju bahwa membiarkannya sebagai UB adalah salah, tetapi saya telah melihat freeze diusulkan sebagai solusi untuk UB. AFAIK yang menjadikannya nilai deterministik yang ditentukan, Anda tidak bisa mengatakan yang mana. Perilaku itu baik-baik saja bagi saya.

Jadi saya akan baik-baik saja jika misalnya u128::MAX as f32 deterministik menghasilkan 17.5 pada x86, dan 999.0 pada x86-64, dan -555 pada ARM.

freeze tidak akan menghasilkan nilai yang ditentukan, deterministik, dan tidak ditentukan. Hasilnya masih "pola bit apa pun yang disukai kompilator", dan ini hanya konsisten di seluruh penggunaan operasi yang sama. Ini mungkin mengelak dari contoh penghasil UB yang telah dikumpulkan orang-orang di atas, tetapi tidak akan memberikan ini:

u128 :: MAX sebagai f32 secara deterministik diproduksi 17.5 pada x86, dan 999.0 pada x86-64, dan -555 pada ARM.

Misalnya, jika LLVM memperhatikan bahwa u128::MAX as f32 meluap dan menggantinya dengan freeze poison , penurunan yang valid dari fn foo() -> f32 { u128::MAX as f32 } pada x86_64 mungkin seperti ini:

foo:
  ret

(yaitu, kembalikan saja apa pun yang terakhir disimpan di register pengembalian)

Saya melihat. Itu masih dapat diterima untuk penggunaan saya (untuk kasus di mana saya mengharapkan nilai di luar kisaran, saya melakukan penjepitan sebelumnya. Di mana saya mengharapkan nilai dalam kisaran, tetapi tidak, maka saya tidak akan mendapatkan hasil yang benar apa pun yang terjadi) .

Saya tidak punya masalah dengan float cast di luar jangkauan mengembalikan nilai sewenang-wenang selama nilainya dibekukan sehingga tidak dapat menyebabkan perilaku tidak terdefinisi lebih lanjut.

Apakah sesuatu seperti freeze tersedia di LLVM? Saya pikir itu adalah konstruksi teoritis murni.

@nikomatsakis Saya belum pernah melihatnya digunakan seperti itu (tidak seperti poison ) - ini adalah perubahan yang direncanakan dari poison / undef.

freeze sekali tidak ada di LLVM hari ini. Ini baru diusulkan ( Makalah PLDI ini versi mandiri, tapi juga banyak dibahas di milis). Proposal tersebut tampaknya mendapat dukungan yang cukup besar, tetapi tentu saja itu bukan jaminan akan diadopsi, apalagi diadopsi pada waktu yang tepat. (Menghapus tipe pointee dari tipe pointer telah diterima selama bertahun-tahun dan masih belum selesai.)

Apakah kita ingin membuka RFC agar diskusi lebih luas tentang perubahan yang diajukan di sini? IMO, apa pun yang berpotensi memengaruhi kinerja as akan diperdebatkan, tetapi akan menjadi perdebatan ganda jika kita tidak memberi orang kesempatan untuk membuat suaranya didengar.

Saya seorang pengembang Julia, dan saya telah mengikuti masalah ini untuk sementara waktu, karena kami berbagi backend LLVM yang sama dan memiliki masalah yang serupa. Jika itu menarik, inilah yang telah kami selesaikan (dengan perkiraan waktu untuk satu fungsi di mesin saya):

  • unsafe_trunc(Int64, x) memetakan langsung ke LLVM intrinsik yang sesuai fptosi (1,5 ns)
  • trunc(Int64, x) melempar pengecualian untuk nilai di luar jangkauan (3 ns)
  • convert(Int64, x) melempar pengecualian untuk nilai di luar rentang atau non-integer (6 ns)

Juga, saya telah bertanya di milis tentang membuat perilaku tidak terdefinisi sedikit lebih jelas, tetapi tidak menerima tanggapan yang sangat menjanjikan.

@bstrie Saya baik-baik saja dengan RFC, tapi saya pikir pasti akan berguna untuk memiliki data! Namun komentar

Saya telah bermain-main dengan semantik JS (modulo @jorendorff disebutkan) dan semantik Java yang tampaknya menjadi kolom "saturasi". Jika tautan tersebut kedaluwarsa, itu JS dan Java .

Saya juga menyiapkan implementasi cepat saturasi di Rust yang menurut saya (?) Benar. Dan mendapat beberapa nomor patokan juga. Menariknya, saya melihat bahwa penerapan penjenuhan 2-3x lebih lambat daripada intrinsik, yang berbeda dari apa yang ditemukan @simonbyrne dengan hanya 2x lebih lambat.

Saya tidak sepenuhnya yakin bagaimana menerapkan semantik "mod" di Rust ...

Namun, bagi saya, tampaknya jelas bahwa kita memerlukan banyak metode f32::as_u32_unchecked() dan semacamnya bagi mereka yang membutuhkan kinerja.

tampaknya jelas bahwa kita memerlukan metode f32::as_u32_unchecked() dan semacamnya bagi mereka yang membutuhkan kinerja.

Itu mengecewakan - atau maksud Anda varian yang aman tetapi ditentukan oleh implementasi?

Apakah tidak ada opsi untuk implementasi yang didefinisikan sebagai default cepat?

@eddyb Saya berpikir bahwa kami hanya akan memiliki unsafe fn as_u32_unchecked(self) -> u32 pada f32 dan semacam itu yang merupakan analog langsung dengan apa as hari ini.

Saya tentu tidak akan mengklaim bahwa penerapan Rust yang saya tulis adalah optimal, tetapi saya mendapat kesan bahwa ketika membaca thread ini determinisme dan keamanan lebih penting daripada kecepatan dalam konteks ini sebagian besar waktu. Palka pelarian unsafe diperuntukkan bagi mereka yang berada di sisi lain pagar.

Jadi tidak ada varian murah yang bergantung pada platform? Saya menginginkan sesuatu yang cepat, memberikan nilai yang tidak ditentukan saat berada di luar batas dan aman. Saya tidak ingin UB untuk beberapa masukan dan saya pikir itu terlalu berbahaya untuk penggunaan umum, jika kita bisa berbuat lebih baik.

Sejauh yang saya ketahui, di sebagian besar jika tidak semua platform, cara kanonik untuk mengimplementasikan konversi ini melakukan sesuatu ke input di luar jangkauan yang bukan UB. Tetapi LLVM tampaknya tidak memiliki cara untuk memilih opsi itu (apa pun itu) daripada UB. Jika kita bisa meyakinkan pengembang LLVM untuk memperkenalkan intrinsik yang menghasilkan hasil "tidak ditentukan tetapi bukan undef / poison " pada input di luar jangkauan, kita bisa menggunakannya.

Tetapi saya memperkirakan bahwa seseorang di utas ini harus menulis RFC yang meyakinkan (pada daftar llvm-dev), mendapatkan persetujuan, dan menerapkannya (di backend yang kami pedulikan, dan dengan implementasi fall-back untuk yang lain target). Mungkin lebih mudah daripada meyakinkan llvm-dev untuk membuat cast yang ada bukan-UB (karena ini pertanyaan langkah samping seperti "apakah ini akan membuat program C dan C ++ lebih lambat"), tetapi masih tidak terlalu mudah.

Untuk berjaga-jaga jika Anda akan memilih di antara ini:

Saturasi (nilai di luar rentang menjadi IntType :: max_value () / min_value ())
Modulo (nilai di luar rentang diperlakukan seolah-olah dengan mengonversi menjadi bigint terlebih dahulu, kemudian memotong)

Hanya saturasi IMO yang masuk akal di sini, karena presisi absolut floating point dengan cepat turun saat nilainya menjadi besar, jadi pada titik tertentu modulo akan menjadi sesuatu yang tidak berguna seperti semua nol.

Saya menandai ini sebagai E-needs-mentor dan menandainya dengan WG-compiler-middle karena tampaknya periode impl mungkin waktu yang tepat untuk menyelidiki hal ini lebih lanjut! Catatan saya yang ada

@nikomat

IIRC LLVM berencana untuk akhirnya mengimplementasikan freeze , yang seharusnya memungkinkan kita untuk berurusan dengan UB dengan melakukan freeze .

Hasil saya sejauh ini: https://gist.github.com/s3bk/4bdfbe2acca30fcf587006ebb4811744

Varian _array menjalankan loop 1024 nilai.
_cast: x as i32
_clip: x.min (MAX) .max (MIN) sebagai i32
_panic: panik jika x di luar batas
_zero: menyetel hasil ke nol jika di luar batas

test bench_array_cast       ... bench:       1,840 ns/iter (+/- 37)
test bench_array_cast_clip  ... bench:       2,657 ns/iter (+/- 13)
test bench_array_cast_panic ... bench:       2,397 ns/iter (+/- 20)
test bench_array_cast_zero  ... bench:       2,671 ns/iter (+/- 19)
test bench_cast             ... bench:           2 ns/iter (+/- 0)
test bench_cast_clip        ... bench:           2 ns/iter (+/- 0)
test bench_cast_panic       ... bench:           2 ns/iter (+/- 0)
test bench_cast_zero        ... bench:           2 ns/iter (+/- 0)

Mungkin Anda tidak perlu membulatkan hasil menjadi integer untuk operasi individual. Jelas pasti ada beberapa perbedaan di balik 2 ns / iter ini. Atau benar-benar seperti ini, _exactly_ 2 ns untuk semua 4 varian?

@ sp-1234 Saya ingin tahu apakah itu sebagian dioptimalkan.

@ sp-1234 Terlalu cepat untuk diukur. Tolok ukur non-array pada dasarnya tidak berguna.
Jika Anda memaksa fungsi nilai tunggal menjadi fungsi melalui #[inline(never)] , Anda mendapatkan 2ns vs 3ns.

@ ariel
Saya memiliki beberapa reservasi tentang freeze . Jika saya mengerti dengan benar, undef dibekukan masih dapat berisi nilai sembarang, itu tidak akan berubah di antara penggunaan. Dalam praktiknya, kompilator mungkin akan menggunakan kembali register atau slot tumpukan.

Namun ini berarti bahwa sekarang kita dapat membaca memori yang tidak diinisialisasi dari kode aman. Ini bisa menyebabkan data rahasia bocor, seperti Heartbleed. Masih bisa diperdebatkan apakah ini benar-benar dianggap UB dari sudut pandang Rust, tetapi jelas terlihat tidak diinginkan.

Saya menjalankan benchmark @ s3bk secara lokal. Saya dapat mengonfirmasi bahwa versi skalar dioptimalkan sepenuhnya, dan asm untuk varian array juga terlihat dioptimalkan dengan baik secara mencurigakan: misalnya, loop di-vectorisasi, yang bagus tetapi menyulitkan untuk mengekstrapolasi kinerja ke kode skalar.

Sayangnya, mengirim spam black_box sepertinya tidak membantu. Saya melihat asm melakukan pekerjaan yang berguna, tetapi menjalankan benchmark masih secara konsisten memberikan 0ns untuk tolok ukur skalar (kecuali cast_zero , yang menunjukkan 1ns). Saya melihat @alexcrichton melakukan perbandingan 100 kali dalam tolok ukur mereka, jadi saya mengadopsi peretasan yang sama. Saya sekarang melihat angka-angka ini ( kode sumber ):

test bench_cast             ... bench:          53 ns/iter (+/- 0)
test bench_cast_clip        ... bench:         164 ns/iter (+/- 1)
test bench_cast_panic       ... bench:         172 ns/iter (+/- 2)
test bench_cast_zero        ... bench:         100 ns/iter (+/- 0)

Tolok ukur array terlalu bervariasi bagi saya untuk mempercayainya. Yah, sejujurnya, saya skeptis terhadap infrastruktur pembandingan test , terutama setelah melihat angka-angka di atas dibandingkan dengan flat 0ns yang saya dapatkan sebelumnya. Selain itu, bahkan hanya 100 iterasi black_box(x); (sebagai baseline) membutuhkan 34ns, yang membuatnya semakin sulit untuk menafsirkan angka-angka itu dengan andal.

Dua hal yang perlu diperhatikan:

  • Meskipun tidak menangani NaN secara khusus (mengembalikan -inf alih-alih 0?), Implementasi cast_clip tampaknya lebih lambat daripada pemeran saturasi @alexcrichton (perhatikan bahwa menjalankan dan saya memiliki waktu yang kira-kira sama untuk as cast, 53-54ns).
  • Tidak seperti hasil array @ s3bk , saya melihat cast_panic lebih lambat dari cast yang dicentang lainnya. Saya juga melihat perlambatan yang lebih besar pada benchmark array. Mungkin hal-hal ini sangat bergantung pada detail mikroarsitektur dan / atau mood pengoptimal?

Sebagai catatan, saya telah mengukur dengan rustc 1.21.0-nightly (d692a91fa 2017-08-04), -C opt-level=3 , pada i7-6700K di bawah beban ringan.


Sebagai kesimpulan, saya menyimpulkan bahwa sejauh ini tidak memiliki data yang dapat diandalkan dan bahwa mendapatkan data yang lebih andal tampaknya sulit. Selain itu, saya sangat meragukan bahwa aplikasi nyata apa pun menghabiskan bahkan 1% dari waktu jam dindingnya untuk operasi ini. Oleh karena itu, saya akan menyarankan untuk bergerak maju dengan menerapkan cetakan as jenuh di rustc , di belakang bendera -Z , dan kemudian menjalankan beberapa tolok ukur non-buatan dengan dan tanpa tanda ini untuk menentukan dampak pada realistis aplikasi.

Sunting: Saya juga akan merekomendasikan untuk menjalankan tolok ukur tersebut pada berbagai arsitektur (misalnya, termasuk ARM) dan mikroarsitektur, jika memungkinkan.

Saya akui saya tidak begitu paham dengan karat, tetapi saya pikir baris ini secara halus salah: std::i32::MAX (2 ^ 31-1) tidak dapat direpresentasikan sebagai Float32, jadi std::i32::MAX as f32 akan menjadi dibulatkan ke nilai terwakili terdekat (2 ^ 31). Jika nilai ini digunakan sebagai argumen x , hasilnya secara teknis tidak ditentukan. Mengganti dengan ketidaksetaraan yang ketat harus memperbaiki kasus ini.

Ya, kami pernah mengalami masalah itu di Servo sebelumnya. Solusi terakhir adalah melemparkan ke f64 dan kemudian menjepit.

Ada solusi lain tetapi cukup rumit dan karat tidak mengekspos API yang bagus untuk menangani ini dengan baik.

menggunakan 0x7FFF_FF80i32 sebagai batas atas dan -0x8000_0000i32 akan menyelesaikan masalah ini tanpa mentransmisikan ke f64.
edit: gunakan nilai yang benar.

Saya pikir yang Anda maksud 0x7fff_ff80 , tetapi hanya menggunakan ketidaksetaraan yang ketat mungkin akan membuat maksud kode lebih jelas.

seperti di x < 0x8000_0000u32 as f32 ? Itu mungkin ide yang bagus.

Saya memikirkan semua opsi deterministik yang disarankan, penjepitan secara genetis paling berguna karena saya pikir itu tetap sering dilakukan. Jika jenis konversi benar-benar didokumentasikan menjadi jenuh, penjepitan manual menjadi tidak diperlukan.

Saya hanya sedikit khawatir tentang penerapan yang disarankan karena itu tidak menerjemahkan dengan benar ke instruksi mesin dan sangat bergantung pada percabangan. Percabangan membuat kinerja bergantung pada pola data tertentu. Dalam kasus pengujian yang diberikan di atas semuanya terlihat (secara komparatif) cepat karena cabang yang sama selalu diambil dan prosesor memiliki data prediksi cabang yang baik dari banyak iterasi loop sebelumnya. Dunia nyata mungkin tidak akan terlihat seperti itu. Selain itu, percabangan melukai kemampuan kompilator untuk memvektorisasi kode. Saya tidak setuju dengan pendapat @rkruppe , bahwa operasi tersebut juga tidak boleh diuji dalam kombinasi dengan vektorisasi. Vektorisasi penting dalam kode kinerja tinggi dan kemampuan untuk melakukan vektorisasi cetakan sederhana pada arsitektur umum harus menjadi persyaratan penting.

Untuk alasan yang diberikan di atas, saya bermain-main dengan versi alternatif tanpa cabang dan aliran data yang berorientasi pada cast @alexcrichton dengan semantik saturasi dan perbaikan @simonbyrne . Saya menerapkannya untuk u16 , i16 dan i32 karena semuanya harus mencakup kasus yang sedikit berbeda yang menghasilkan kinerja yang berbeda-beda.

Hasil:

test i16_bench_array_cast       ... bench:          99 ns/iter (+/- 2)
test i16_bench_array_cast_clip  ... bench:         197 ns/iter (+/- 3)
test i16_bench_array_cast_clip2 ... bench:         113 ns/iter (+/- 3)
test i16_bench_cast             ... bench:          76 ns/iter (+/- 1)
test i16_bench_cast_clip        ... bench:         218 ns/iter (+/- 25)
test i16_bench_cast_clip2       ... bench:         148 ns/iter (+/- 4)
test i16_bench_rng_cast         ... bench:       1,181 ns/iter (+/- 17)
test i16_bench_rng_cast_clip    ... bench:       1,952 ns/iter (+/- 27)
test i16_bench_rng_cast_clip2   ... bench:       1,287 ns/iter (+/- 19)

test i32_bench_array_cast       ... bench:         114 ns/iter (+/- 1)
test i32_bench_array_cast_clip  ... bench:         200 ns/iter (+/- 3)
test i32_bench_array_cast_clip2 ... bench:         128 ns/iter (+/- 3)
test i32_bench_cast             ... bench:          74 ns/iter (+/- 1)
test i32_bench_cast_clip        ... bench:         168 ns/iter (+/- 3)
test i32_bench_cast_clip2       ... bench:         189 ns/iter (+/- 3)
test i32_bench_rng_cast         ... bench:       1,184 ns/iter (+/- 13)
test i32_bench_rng_cast_clip    ... bench:       2,398 ns/iter (+/- 41)
test i32_bench_rng_cast_clip2   ... bench:       1,349 ns/iter (+/- 19)

test u16_bench_array_cast       ... bench:          99 ns/iter (+/- 1)
test u16_bench_array_cast_clip  ... bench:         136 ns/iter (+/- 3)
test u16_bench_array_cast_clip2 ... bench:         105 ns/iter (+/- 3)
test u16_bench_cast             ... bench:          76 ns/iter (+/- 2)
test u16_bench_cast_clip        ... bench:         184 ns/iter (+/- 7)
test u16_bench_cast_clip2       ... bench:         110 ns/iter (+/- 0)
test u16_bench_rng_cast         ... bench:       1,178 ns/iter (+/- 22)
test u16_bench_rng_cast_clip    ... bench:       1,336 ns/iter (+/- 26)
test u16_bench_rng_cast_clip2   ... bench:       1,207 ns/iter (+/- 21)

Tes dijalankan pada CPU Intel Haswell i5-4570 dan Rust 1.22.0-nightly.
clip2 adalah implementasi tanpa cabang baru. Ini setuju dengan clip pada semua 2 ^ 32 kemungkinan nilai input f32.

Untuk tolok ukur rng , nilai input acak yang sering digunakan untuk kasus yang berbeda. Ini mengungkap biaya kinerja _extreme_ (kira-kira 10 kali biaya normal !!!) yang terjadi jika prediksi cabang gagal. Saya pikir sangat _sangat_ penting untuk mempertimbangkan hal ini. Ini juga bukan kinerja dunia nyata rata-rata, tetapi ini masih merupakan kasus yang mungkin dan beberapa aplikasi akan mencapai ini. Orang-orang akan mengharapkan pemain f32 memiliki kinerja yang konsisten.

Perbandingan Assmbly pada x86: https://godbolt.org/g/AhdF71
Versi tanpa cabang memetakan dengan sangat baik ke instruksi minss / maxss.

Sayangnya saya tidak dapat membuat godbolt menghasilkan perakitan ARM dari Rust, tetapi berikut adalah perbandingan metode ARM dengan Clang: https://godbolt.org/g/s7ronw
Tanpa dapat menguji kode dan mengetahui banyak tentang ARM: Ukuran kode tampaknya juga lebih kecil dan LLVM sebagian besar menghasilkan vmax / vmin yang terlihat menjanjikan. Mungkin LLVM akhirnya bisa diajari untuk melipat sebagian besar kode menjadi satu instruksi?

@ActuallyaDeviloper Hasil asm dan benchmark terlihat sangat bagus! Lebih lanjut, kode tanpa cabang seperti milik Anda mungkin lebih mudah dibuat dalam rustc daripada persyaratan bersarang dari solusi lain (sebagai catatan, saya berasumsi kita ingin membuat IR inline daripada memanggil fungsi item lang). Terima kasih banyak telah menulis ini.

Saya punya pertanyaan tentang u16_cast_clip2 : sepertinya tidak menangani NaN ?! Ada komentar yang berbicara tentang NaN, tetapi saya yakin fungsinya akan melewati NaN tanpa modifikasi dan mencoba mentransmisikannya ke f32 (dan bahkan jika tidak, itu akan menghasilkan salah satu nilai batas daripada 0 ).

PS: Agar jelas, saya tidak mencoba menyiratkan bahwa tidak penting apakah pemeran dapat vektorisasi. Jelas penting jika kode di sekitarnya sebaliknya dapat di-vektorisasi. Tetapi kinerja skalar juga penting, karena vektorisasi seringkali tidak berlaku, dan tolok ukur yang saya komentari tidak membuat pernyataan apa pun tentang kinerja skalar. Karena tertarik, sudahkah Anda memeriksa asm dari tolok ukur *array* untuk melihat apakah mereka masih vektorisasi dengan implementasi Anda?

@rkruppe Anda benar, saya tidak sengaja menukar sisi if dan melupakannya. f32 as u16 happend untuk melakukan hal yang benar dengan memotong 0x8000 bagian atas menjauh, jadi pengujian juga tidak menangkapnya. Saya memperbaiki masalah sekarang dengan menukar cabang lagi dan menguji semua metode dengan if (y.is_nan()) { panic!("NaN"); } kali ini.

Saya memperbarui posting saya sebelumnya. Kode x86 tidak berubah secara signifikan sama sekali tetapi sayangnya perubahan tersebut menghentikan LLVM dari menghasilkan vmax dalam kasus u16 ARM untuk beberapa alasan. Saya berasumsi ini ada hubungannya dengan beberapa detail tentang penanganan NaN dari instruksi ARM itu atau mungkin itu batasan LLVM.

Untuk alasan ini berhasil, perhatikan bahwa nilai batas bawah sebenarnya adalah 0 untuk nilai unsigned. Jadi NaN dan batas bawah bisa ditangkap pada saat bersamaan.

Versi array adalah vektorisasi.
Godbolt: https://godbolt.org/g/HnmsSV

Re: ARM asm , saya yakin alasan vmax tidak digunakan lagi adalah karena ia mengembalikan NaN jika salah satu operandnya adalah NaN . Kode ini masih tanpa cabang, meskipun, itu hanya menggunakan gerakan berpredikat ( vmovgt , mengacu pada hasil dari vcmp dengan 0).

Untuk alasan ini berhasil, perhatikan bahwa nilai batas bawah sebenarnya adalah 0 untuk nilai unsigned. Jadi NaN dan batas bawah bisa ditangkap pada saat bersamaan.

Ohhh, benar. Bagus.

Saya akan menyarankan untuk bergerak maju dengan menerapkan saturating as casts di rustc, di belakang bendera -Z

Saya telah menerapkan ini dan akan mengajukan PR setelah saya juga memperbaiki # 41799 dan memiliki lebih banyak tes.

45134 telah menunjukkan jalur kode yang saya lewatkan (generasi ekspresi konstan LLVM - ini terpisah dari evaluasi konstan rustc sendiri). Saya akan menggulirkan perbaikan untuk itu ke dalam PR yang sama, tetapi itu akan memakan waktu lebih lama.

@rkruppe Anda harus berkoordinasi dengan @ oli-obk sehingga miri berakhir dengan perubahan yang sama.

Permintaan tarik naik: # 45205

45205 telah digabungkan, jadi siapa pun sekarang dapat (yah, mulai malam berikutnya) mengukur dampak kinerja dari kejenuhan dengan melewatkan -Z saturating-float-casts melalui RUSTFLAGS . [1] Pengukuran seperti itu akan sangat berharga untuk memutuskan bagaimana melanjutkan dengan masalah ini.

[1] Sebenarnya, ini tidak akan mempengaruhi bagian non-generik, non- #[inline] dari pustaka standar, jadi agar 100% akurat Anda ingin membangun std secara lokal dengan Xargo. Namun, saya tidak berharap akan ada banyak kode yang terpengaruh oleh ini (berbagai sifat konversi menyiratkan adalah #[inline] , misalnya).

@rkruppe Saya sarankan untuk memulai laman internal / pengguna untuk mengumpulkan data, dengan nada yang sama seperti https://internals.rust-lang.org/t/help-us-benchmark-incremental-compilation/6153/ (kami juga bisa tautkan orang ke itu, daripada beberapa komentar acak di pelacak masalah kami)

@rkruppe Anda harus membuat masalah pelacakan. Diskusi ini sudah dibagi menjadi dua masalah. Itu tidak baik!

@Gankro Ya, saya setuju, tetapi mungkin perlu beberapa hari sebelum saya menemukan waktu untuk menulis posting itu dengan benar, jadi saya pikir saya akan meminta umpan balik dari orang-orang yang berlangganan masalah ini untuk sementara waktu.

@ est31 H. Meskipun -Z flag menutupi kedua arah cast (yang mungkin merupakan kesalahan, dalam retrospeksi), tampaknya tidak mungkin kita akan mengaktifkan keduanya pada saat yang bersamaan, dan ada sedikit tumpang tindih antara keduanya dalam hal apa yang harus dilakukan. dibahas (misalnya, masalah ini bergantung pada kinerja kejenuhan, sedangkan di # 41799 disepakati tentang solusi yang tepat).
Ini adalah sedikit konyol yang benchmark terutama ditargetkan pada masalah ini juga akan mengukur dampak dari perbaikan untuk # 41.799, tapi itu bisa paling menyebabkan overreporting regresi kinerja, jadi aku semacam baik saja dengan itu. (Tetapi jika ada yang termotivasi untuk membagi -Z flag menjadi dua, silakan.)

Saya telah mempertimbangkan masalah pelacakan untuk tugas menghapus bendera setelah masa manfaatnya habis, tetapi saya tidak melihat perlunya menggabungkan diskusi yang terjadi di sini dan di # 41799.

Saya telah menyusun posting internal: https://gist.github.com/Gankro/feab9fb0c42881984caf93c7ad494ebd

Jangan ragu untuk menyalinnya, atau cukup beri saya catatan agar saya dapat mempostingnya. (perhatikan saya agak bingung tentang perilaku const fn )

Satu berita menarik tambahan adalah bahwa biaya konversi float-> int spesifik untuk implementasi saat ini, daripada menjadi fundamental. Pada x86, cvtss2si cvttss2si mengembalikan 0x80000000 dalam kasus terlalu rendah, terlalu tinggi, dan nan, jadi seseorang dapat mengimplementasikan -Zsaturating-float-casts dengan cvtss2si cvttss2si diikuti oleh kode khusus dalam kasus 0x80000000, jadi ini bisa jadi hanya satu cabang pembanding dan yang dapat diprediksi dalam kasus umum. Di ARM, vcvt.s32.f32 sudah memiliki semantik -Zsaturating-float-casts . LLVM saat ini tidak mengoptimalkan pemeriksaan ekstra dalam kedua kasus tersebut.

@Tokopedia

Luar biasa, terima kasih banyak! Saya meninggalkan beberapa catatan tentang intinya. Setelah membaca ini, saya ingin mencoba memisahkan gips u128-> f32 dari bendera -Z. Hanya untuk menghilangkan peringatan yang mengganggu tentang bendera yang menutupi dua fitur ortogonal.

(Saya telah mengajukan # 45900 untuk memfokuskan kembali flag -Z sehingga hanya mencakup masalah float-> int)

Alangkah baiknya jika kita bisa mendapatkan implementasi khusus platform ala @sunfishcode (setidaknya untuk x86) sebelum meminta pembandingan massal. Seharusnya tidak terlalu sulit.

Masalahnya adalah bahwa LLVM saat ini tidak menyediakan cara untuk melakukan ini, sejauh yang saya tahu, kecuali mungkin dengan inline asm yang tidak perlu saya rekomendasikan untuk rilis.

Saya telah memperbarui draf untuk mencerminkan diskusi (pada dasarnya merobek penyebutan sebaris u128 -> f32 ke bagian tambahan di akhir).

@sunfishcode Apakah Anda yakin? Bukankah llvm.x86.sse.cvttss2si intrinsik yang Anda cari?

Berikut ini tautan taman bermain yang menggunakannya:

https://play.rust-lang.org/?gist=33cf9e0871df2eb2475b845af4f1b574&version=nightly

Dalam mode rilis, float_to_int_with_intrinsic dan float_to_int_with_as keduanya dikompilasi menjadi satu instruksi. (Dalam mode debug, float_to_int_with_intrinsic menyia-nyiakan beberapa instruksi yang membuat nol menjadi tinggi, tetapi tidak terlalu buruk.)

Ia bahkan tampaknya melakukan pelipatan konstan dengan benar. Sebagai contoh,

float_to_int_with_intrinsic(42.0)

menjadi

movl    $42, %eax

Tapi nilai di luar jangkauan,

float_to_int_with_intrinsic(42.0e33)

tidak terlipat:

cvttss2si   .LCPI2_0(%rip), %eax

(Idealnya ini akan dilipat menjadi 0x80000000 konstan, tapi itu bukan masalah besar. Yang penting adalah tidak menghasilkan undef.)

Oh keren. Sepertinya itu akan berhasil!

Sangat menyenangkan mengetahui bahwa kita, bagaimanapun juga, memiliki cara untuk mengembangkan cvttss2si . Namun, saya tidak setuju bahwa jelas lebih baik mengubah implementasi untuk menggunakannya sebelum kita memanggil tolok ukur:

Kebanyakan orang akan melakukan benchmark pada x86, jadi jika kita kasus khusus x86, kita akan mendapatkan lebih sedikit data tentang implementasi umum, yang masih akan digunakan pada sebagian besar target lainnya. Memang, sulit untuk menyimpulkan apa pun tentang arsitektur lain, tetapi penerapan yang sepenuhnya berbeda membuatnya tidak mungkin.

Kedua, jika kita mengumpulkan tolok ukur sekarang, dengan solusi "sederhana", dan menemukan bahwa tidak ada regresi kinerja dalam kode nyata (dan memang itulah yang saya harapkan), maka kita bahkan tidak perlu bersusah payah mencoba optimalkan jalur kode ini lebih jauh.

Akhirnya, saya bahkan tidak yakin membangun di cvttss2si akan lebih cepat dari yang kita miliki sekarang (meskipun di ARM, hanya menggunakan instruksi yang sesuai jelas lebih baik):

  • Anda memerlukan perbandingan untuk mengetahui bahwa konversi menghasilkan 0x80000000, dan jika demikian, Anda masih memerlukan perbandingan lain (dari nilai masukan) untuk mengetahui apakah Anda harus mengembalikan int :: MIN atau int :: MAX. Dan jika itu adalah tipe integer yang ditandatangani, saya tidak melihat bagaimana menghindari perbandingan ketiga untuk membedakan NaN. Jadi dalam kasus terburuk:

    • Anda tidak menyimpan dalam jumlah membandingkan / memilih

    • Anda memperdagangkan perbandingan float untuk perbandingan int, yang mungkin bagus untuk inti OoO (jika Anda mengalami hambatan pada FU yang dapat melakukan perbandingan, yang tampaknya relatif besar jika), tetapi perbandingan itu juga bergantung pada float -> perbandingan int, sementara perbandingan dalam implementasi saat ini semuanya independen, jadi masih jauh dari jelas bahwa ini adalah kemenangan.

  • Vektorisasi mungkin menjadi lebih sulit atau tidak mungkin. Saya tidak berharap bahwa loop vectorizer menangani intrinsik ini sama sekali.
  • Perlu juga dicatat bahwa (AFAIK) strategi ini hanya berlaku untuk beberapa tipe integer. f32 -> u8, misalnya, akan membutuhkan perbaikan tambahan pada hasil, yang membuat strategi ini jelas tidak menguntungkan. Saya tidak begitu yakin tipe mana yang terpengaruh oleh ini (misalnya, saya tidak tahu apakah ada instruksi untuk f32 -> u32), tetapi aplikasi yang hanya menggunakan tipe tersebut tidak akan mendapat manfaat sama sekali.
  • Anda dapat melakukan solusi percabangan dengan hanya satu perbandingan di jalur bahagia (sebagai lawan dari dua atau tiga perbandingan, dan dengan demikian bercabang, seperti solusi sebelumnya). Namun, seperti yang dikatakan @ActuallyaDeviloper sebelumnya, percabangan mungkin tidak diinginkan: Performa sekarang menjadi lebih bergantung pada beban kerja dan bergantung pada prediksi cabang.

Apakah aman untuk mengasumsikan bahwa kita akan membutuhkan unsafe fn as_u32_unchecked(self) -> u32 dan teman-teman terlepas dari apa yang ditunjukkan oleh pembandingan? Apa jalan potensial lainnya akan seseorang memiliki jika mereka berakhir mengamati perlambatan?

@bstrie Saya pikir akan lebih masuk akal, dalam kasus seperti itu, untuk melakukan sesuatu seperti memperluas sintaks ke as <type> [unchecked] dan membutuhkan unchecked hanya ada di unsafe konteks.

Seperti yang saya lihat, hutan _unchecked berfungsi sebagai varian dari as casting akan menjadi kutukan, baik sejauh intuisi berjalan, dan ketika datang untuk menghasilkan dokumentasi yang bersih dan dapat digunakan.

@ssokolow Menambahkan sintaksis harus selalu menjadi pilihan terakhir, terutama jika semua ini dapat dilakukan hanya dengan sepuluh fungsi hafalan. Bahkan memiliki foo.as_unchecked::<u32>() generik akan lebih disukai daripada perubahan sintaksis (dan bikeshed tanpa henti yang terjadi secara bersamaan), terutama karena kita harus mengurangi, bukan meningkatkan, jumlah hal yang unsafe dibuka.

Titik. Turbofish tergelincir di benak saya ketika mempertimbangkan opsi dan, di belakang, saya tidak benar-benar menembak semua silinder malam ini, jadi saya seharusnya lebih berhati-hati dalam mengomentari keputusan desain.

Meskipun demikian, rasanya salah jika memanggang jenis tujuan menjadi nama fungsi ... tidak elegan dan berpotensi menjadi beban evolusi bahasa di masa mendatang. Turbofish terasa seperti pilihan yang lebih baik.

Metode generik dapat didukung oleh set baru UncheckedFrom / UncheckedInto dengan metode unsafe fn , bergabung dengan From / Into dan TryFrom / TryInto koleksi.

@bstrie Salah satu solusi alternatif untuk orang-orang yang kodenya menjadi lebih lambat dapat menggunakan intrinsik (misalnya, via stdsimd) untuk mengakses instruksi perangkat keras yang mendasarinya. Saya berpendapat sebelumnya bahwa ini memiliki kelemahan untuk pengoptimal - vektorisasi otomatis kemungkinan menderita, dan LLVM tidak dapat mengeksploitasinya mengembalikan undef pada input di luar jangkauan - tetapi itu memang menawarkan cara untuk melakukan cast tanpa pekerjaan tambahan apa pun pada waktu berjalan. Saya tidak dapat memutuskan apakah ini cukup baik, tetapi tampaknya paling masuk akal.

Beberapa catatan tentang konversi di set instruksi x86:

SSE2 sebenarnya relatif terbatas dalam operasi konversi yang diberikannya kepada Anda. Kamu punya:

  • Keluarga CVTTSS2SI dengan register 32-bit: mengubah single float menjadi i32
  • Keluarga CVTTSS2SI dengan register 64-bit: mengonversi single float menjadi i64 (hanya x86-64)
  • Keluarga CVTTPS2PI: mengubah dua float menjadi dua i32s

Masing-masing memiliki varian untuk f32 dan f64 (serta varian yang bulat alih-alih memotong, tapi itu tidak berguna di sini).

Tetapi tidak ada untuk integer yang tidak ditandatangani, tidak ada untuk ukuran yang lebih kecil dari 32, dan jika Anda menggunakan 32-bit x86, tidak ada untuk 64-bit. Ekstensi set instruksi selanjutnya menambahkan lebih banyak fungsionalitas, tetapi tampaknya hampir tidak ada yang mengkompilasi untuk itu.

Akibatnya, perilaku yang ada ('tidak aman'):

  • Untuk mengonversi ke u32, kompiler mengonversi ke i64 dan memotong bilangan bulat yang dihasilkan. (Ini menghasilkan perilaku aneh untuk nilai di luar kisaran, tapi itu UB jadi siapa yang peduli.)
  • Untuk mengonversi menjadi 16-bit atau 8-bit, kompiler mengkonversi ke i64 atau i32 dan memotong integer yang dihasilkan.
  • Untuk mengonversi ke u64, penyusun menghasilkan banyak instruksi. Untuk f32 hingga u64 GCC dan LLVM menghasilkan yang setara dengan:
fn f32_to_u64(f: f32) -> u64 {
    const CUTOFF: f32 = 0x8000000000000000 as f32; // 2^63 exactly
    if !(f >= CUTOFF) { // less, or NaN
        // just use the signed conversion
        f as i64 as u64
    } else {
        0x8000000000000000u64 + ((f - CUTOFF) as i64 as u64)
    }
}

Fakta menyenangkan yang tidak terkait: Pembuatan kode "Convert-than-truncate" adalah penyebab kesalahan " alam semesta paralel " di Super Mario 64. Kode deteksi tabrakan pertama kali menggunakan instruksi MIPS untuk mengubah koordinat f32 menjadi i32, kemudian memotong ke i16; dengan demikian, koordinat yang sesuai dengan i16 tetapi tidak pada 'bungkus' i32, misalnya pergi ke koordinat 65536.0 memberi Anda deteksi tabrakan sebesar 0,0.

Bagaimanapun, kesimpulannya:

  • "Uji untuk 0x80000000 dan miliki penangan khusus" hanya berfungsi untuk konversi ke i32 dan i64.
  • Untuk konversi ke u32, u / i16, dan u / i8, bagaimanapun, "uji apakah keluaran terpotong / tanda diperpanjang berbeda dari aslinya" adalah padanan. (Ini akan mengambil kedua bilangan bulat yang berada dalam kisaran untuk konversi asli tetapi di luar kisaran untuk jenis akhir, dan 0x8000000000000000, indikator bahwa float adalah NaN atau di luar kisaran untuk konversi asli.)
  • Tetapi biaya cabang dan banyak kode tambahan untuk kasus itu mungkin berlebihan. Mungkin tidak masalah jika cabang dapat dihindari.
  • @ActuallyaDeviloper 's minss / maxss pendekatan berbasis tidak begitu buruk! Bentuk minimal,
minss %xmm2, %xmm1
maxss %xmm3, %xmm1
cvttss2si %rax, %xmm1

hanya tiga instruksi (yang memiliki ukuran kode dan throughput / latency yang layak) dan tidak ada cabang.

Namun:

  • Versi Pure-Rust membutuhkan tes ekstra untuk NaN. Untuk konversi ke 32-bit atau lebih kecil, yang dapat dihindari dengan menggunakan intrinsik, dengan menggunakan 64-bit cvttss2si dan memotong hasilnya. Jika input bukan NaN, min / max memastikan bahwa integer tidak diubah oleh pemotongan. Jika inputnya adalah NaN, integernya adalah 0x8000000000000000 yang dipotong menjadi 0.
  • Saya tidak memasukkan biaya memuat 2147483647.0 dan -2148473648.0 ke dalam register, biasanya masing-masing satu langkah dari memori.
  • Untuk f32, 2147483647.0 tidak dapat direpresentasikan dengan tepat, jadi itu tidak benar-benar berfungsi: Anda perlu pemeriksaan lagi. Itu membuat segalanya menjadi lebih buruk. Ditto untuk f64 ke u / i64, tetapi f64 ke u / i32 tidak memiliki masalah ini.

Saya menyarankan kompromi antara dua pendekatan:

  • Untuk f32 / f64 ke u / i16 dan u / i8, dan f64 ke u / i32, gunakan min / max + truncation, seperti di atas, misalnya:
    let f = if f > 32767.0 { 32767.0 } else { f };
    let f = if f < -32768.0 { -32768.0 } else { f };
    cvttss2si(f) as i16

(Untuk u / i16 dan u / i8, konversi awal bisa ke i32; untuk f64 ke u / i32, harus ke i64.)

  • Untuk f32 / 64 hingga u32,
    let r = cvttss2si64(f) as u32;
    if f >= 4294967296.0 { 4294967295 } else { r }

hanya beberapa instruksi dan tidak ada cabang:

    cvttss2si   %xmm0, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movl    $-1, %eax
    cmovbl  %ecx, %eax
  • Untuk f32 / 64 hingga i64, mungkin
    let r = cvttss2si64(f);
    if f >= 9223372036854775808. {
        9223372036854775807 
    } else if f != f {
        0
    } else {
        r
    }

Ini menghasilkan urutan yang lebih panjang (masih tanpa cabang):

    cvttss2si   %xmm0, %rax
    xorl    %ecx, %ecx
    ucomiss %xmm0, %xmm0
    cmovnpq %rax, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movabsq $9223372036854775807, %rax
    cmovbq  %rcx, %rax

… Tapi setidaknya kita menyimpan satu perbandingan dibandingkan dengan pendekatan naif, seolah-olah f terlalu kecil, 0x8000000000000000 sudah menjadi jawaban yang benar (yaitu i64 :: MIN).

  • Untuk f32 ke i32, tidak yakin apakah akan lebih baik jika melakukan hal yang sama seperti sebelumnya, atau hanya mengonversi ke f64 terlebih dahulu dan kemudian melakukan hal min / maks yang lebih pendek.

  • u64 adalah kekacauan yang tidak ingin saya pikirkan. : hal

Di https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 seseorang melaporkan perlambatan yang signifikan dan terukur pada encoding JPEG dengan kotak gambar. Saya telah meminimalkan program sehingga mandiri dan sebagian besar berfokus pada bagian-bagian yang terkait dengan perlambatan: https://gist.github.com/rkruppe/4e7972a209f74654ebd872eb4bc57722 (program ini menunjukkan ~ 15% perlambatan bagi saya dengan kejenuhan gips).

Perhatikan bahwa castnya adalah f32-> u8 ( rgb_to_ycbcr ) dan f32-> i32 ( encode_rgb , loop "Quantization") dalam proporsi yang sama. Sepertinya semua input berada dalam jangkauan, yaitu, saturasi tidak pernah benar-benar masuk, tetapi dalam kasus f32-> u8 ini hanya dapat diverifikasi dengan menghitung minimum dan maksimum polinominal dan memperhitungkan kesalahan pembulatan, yang mana banyak yang harus ditanyakan. Gips f32-> i32 lebih jelas dalam kisaran untuk i32, tetapi hanya karena elemen self.tables bukan nol, yang (rupanya?) Tidak semudah itu untuk ditampilkan oleh pengoptimal, terutama di program aslinya. tl; dr: Cek saturasi ada untuk tinggal, satu-satunya harapan adalah membuatnya lebih cepat.

Saya juga menyodok beberapa LLVM IR - tampaknya satu-satunya perbedaan adalah perbandingan dan pemilihan dari pemeran yang jenuh. Pandangan sekilas menunjukkan asm memiliki instruksi yang sesuai dan tentu saja lebih banyak nilai hidup (yang mengarah ke lebih banyak tumpahan).

@comex Apakah menurut Anda gips f32-> u8 dan f32-> i32 dapat dibuat lebih cepat secara terukur dengan CVTTSS2SI?

Pembaruan kecil, mulai dari rustc 1.28.0-nightly (952f344cd 2018-05-18) , tanda -Zsaturating-float-casts masih menyebabkan kode di https://github.com/rust-lang/rust/issues/10184#issuecomment -345479698 menjadi ~ 20 % lebih lambat di x86_64. Artinya LLVM 6 tidak mengubah apapun.

| Bendera | Waktu |
| ------- | -------: |
| -Copt-level = 3 -Ctarget-cpu = native | 325.699 ns / iter (+/- 7.607) |
| -Copt-level = 3 -Ctarget-cpu = native -Zsaturating-float-casts | 386.962 ns / iter (+/- 11.601)
(19% lebih lambat) |
| -Tingkat koptik = 3 | 331.521 ns / iter (+/- 14.096) |
| -Copt-level = 3 -Zsaturating-float-cast | 413.572 ns / iter (+/- 19.183)
(25% lebih lambat) |

@kennytm Apakah kami mengharapkan LLVM 6 mengubah sesuatu? Apakah mereka mendiskusikan peningkatan tertentu yang akan menguntungkan kasus penggunaan ini? Jika ya, berapa nomor tiketnya?

@insanitybit It ... sepertinya masih buka ...?

image

Baik, tidak tahu apa yang saya lihat. Terima kasih!

@rkruppe tidak kami memastikan bahwa float to int cast tidak lagi UB di LLVM
(dengan mengubah dokumen)?

Pada 20 Jul 2018 4:31 AM, "Colin" [email protected] menulis:

Baik, tidak tahu apa yang saya lihat.

-
Anda menerima ini karena Anda berlangganan utas ini.
Balas email ini secara langsung, lihat di GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406462053 ,
atau bisu
utasnya
https://github.com/notifications/unsubscribe-auth/AApc0v3rJHhZMD7Kv7RC8xkGOiIhkGB1ks5uITMHgaJpZM4BJ45C
.

@nagisa Mungkin Anda memikirkan f32::from_bits(v: u32) -> f32 (dan juga f64 )? Dulu melakukan normalisasi NaN tetapi sekarang hanya transmute .

Masalah ini adalah tentang as konversi yang mencoba mendekati nilai numerik.

@nagisa Anda mungkin berpikir tentang float-> float cast, lihat # 15536 ​​dan https://github.com/rust-lang-nursery/nomicon/pull/65.

Ah, ya, itu mengapung untuk mengapung.

Pada Jum, 20 Juli 2018, 12:24 Robin Kruppe [email protected] menulis:

@nagisa https://github.com/nagisa Anda mungkin berpikir tentang float-> float
cast, lihat # 15536 https://github.com/rust-lang/rust/issues/15536 dan
rust-lang-nursery / nomicon # 65
https://github.com/rust-lang-nursery/nomicon/pull/65 .

-
Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406542903 ,
atau nonaktifkan utasnya
https://github.com/notifications/unsubscribe-auth/AApc0gA24Hz8ndnYhRXCyacd3HdUSZjYks5uIaHegaJpZM4BJ45C
.

Catatan rilis LLVM 7 menyebutkan sesuatu:

Optimalisasi gips floating-point ditingkatkan. Hal ini dapat menyebabkan hasil yang mengejutkan untuk kode yang mengandalkan perilaku tidak terdefinisi dari cast yang meluap. Pengoptimalan dapat dinonaktifkan dengan menentukan atribut fungsi: "strict-float-cast-overflow" = "false". Atribut ini dapat dibuat dengan opsi clang -fno-strict-float-cast-overflow. Pembersih kode dapat digunakan untuk mendeteksi pola yang terpengaruh. Opsi dentang untuk mendeteksi masalah ini saja adalah -fsanitize = float-cast-overflow:

Apakah itu ada hubungannya dengan masalah ini?

Kita seharusnya tidak peduli apa yang LLVM lakukan untuk pemain yang melimpah, selama itu bukan perilaku tidak terdefinisi yang tidak aman. Akibatnya bisa jadi sampah asalkan tidak menimbulkan perilaku tidak sehat.

Apakah itu ada hubungannya dengan masalah ini?

Tidak juga. UB tidak berubah, LLVM semakin agresif mengeksploitasinya, yang pada praktiknya lebih mudah terpengaruh olehnya, tetapi masalah kesehatan tidak berubah. Secara khusus, atribut baru tidak menghapus UB atau memengaruhi pengoptimalan apa pun yang ada sebelum LLVM 7.

@rkruppe karena ingin tahu, apakah ini sudah jatuh di pinggir jalan? Sepertinya https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 berjalan cukup baik dan implementasinya tidak memiliki terlalu banyak bug. Tampaknya sedikit penurunan kinerja selalu diharapkan, tetapi mengompilasi dengan benar tampaknya seperti pertukaran yang berharga.

Apakah ini hanya menunggu untuk didorong melintasi garis finis? Atau apakah ada pemblokir lain yang dikenal?

Sebagian besar saya telah terganggu / sibuk dengan hal-hal lain, tetapi regresi x0.82 dalam pengkodean RBG JPEG tampaknya lebih dari "kecil", pil yang agak pahit untuk ditelan (meskipun meyakinkan bahwa jenis beban kerja lainnya tampaknya tidak terpengaruh) . Ini tidak cukup parah sehingga saya keberatan untuk mengaktifkan saturasi secara default, tetapi cukup sehingga saya ragu untuk mendorongnya sendiri sebelum kami mencoba "juga menyediakan fungsi konversi yang lebih cepat daripada saturasi tetapi dapat menghasilkan sampah (aman) "Opsi yang dibahas sebelumnya. Saya belum mendapatkan itu, dan tampaknya tidak ada orang lain yang juga, jadi ini telah gagal.

Oke keren terima kasih untuk pembaruan @rkruppe! Saya ingin tahu apakah sebenarnya ada penerapan opsi sampah aman? Saya dapat membayangkan kami dengan mudah memberikan sesuatu seperti unsafe fn i32::unchecked_from_f32(...) dan semacamnya, tetapi sepertinya Anda berpikir bahwa itu seharusnya fungsi yang aman . Apakah itu mungkin dengan LLVM hari ini?

Belum ada freeze tetapi dimungkinkan untuk menggunakan rakitan inline untuk mengakses instruksi arsitektur target untuk mengonversi float menjadi integer (dengan fallback ke misalnya, menjenuhkan as ). Meskipun hal ini dapat menghambat beberapa pengoptimalan, mungkin cukup baik untuk memperbaiki sebagian besar regresi di beberapa tolok ukur.

Fungsi unsafe yang menjaga UB bahwa masalah ini tentang (dan dikodekan dengan cara yang sama seperti as sekarang) adalah opsi lain, tetapi yang jauh kurang menarik, saya ' Saya lebih suka fungsi yang aman jika dapat menyelesaikan pekerjaan.

Ada juga ruang yang signifikan untuk peningkatan dalam urutan float-to-int saturasi yang aman . LLVM hari ini tidak memiliki apa pun secara khusus untuk ini, tetapi jika solusi asm inline ada di atas meja, tidak akan sulit untuk melakukan sesuatu seperti ini:

     cvttsd2si %xmm0, %eax   # x86's cvttsd2si returns 0x80000000 on overflow and invalid cases
     cmp $1, %eax            # a compact way to test whether %eax is equal to 0x80000000
     jno ok
     ...  # slow path: check for and handle overflow and invalid cases
ok:

yang seharusnya jauh lebih cepat daripada yang sekarang dilakukan Rustc .

Ok saya hanya ingin memastikan untuk klarifikasi, terima kasih! Saya pikir solusi asm inline tidak dapat diterapkan sebagai default karena akan menghambat optimasi lain terlalu banyak, tetapi saya belum mencobanya sendiri. Saya pribadi lebih suka kita menutup lubang yang tidak sehat ini dengan mendefinisikan beberapa perilaku yang masuk akal (seperti pemeran yang tepat hari ini). Jika perlu, kami selalu dapat mempertahankan penerapan cepat / tidak sehat hari ini sebagai fungsi yang tidak aman, dan dalam batas waktu yang diberikan sumber daya tak terbatas, kami bahkan dapat secara drastis meningkatkan default dan / atau menambahkan fungsi konversi khusus lainnya (seperti konversi aman di luar batas tidak) bukan UB tapi hanya pola bit sampah)

Apakah orang lain akan menentang strategi seperti itu? Apakah menurut kami hal ini tidak cukup penting untuk diperbaiki untuk sementara?

Saya pikir perakitan inline harus dapat ditoleransi untuk cvttsd2si (atau instruksi serupa) khususnya karena asm inline itu tidak akan mengakses memori atau memiliki efek samping, jadi itu hanya kotak hitam buram yang dapat dihapus jika tidak digunakan dan tidak sangat menghambat optimasi di sekitarnya, LLVM tidak bisa bernalar tentang internal dan nilai hasil asm inline. Bagian terakhir itulah mengapa saya akan skeptis tentang misalnya menggunakan asm inline untuk urutan kode @sunfishcode menyarankan untuk saturasi: pemeriksaan yang diperkenalkan untuk saturasi kadang-kadang dapat dihapus hari ini jika mereka berlebihan, tetapi cabang di blok asm sebaris bisa ' t disederhanakan.

Apakah orang lain akan menentang strategi seperti itu? Apakah menurut kami hal ini tidak cukup penting untuk diperbaiki untuk sementara?

Saya tidak keberatan membalik saturasi sekarang dan mungkin menambahkan alternatif nanti, saya hanya tidak ingin menjadi orang yang harus menyusun konsensus untuk itu dan membenarkannya kepada pengguna yang kodenya semakin lambat 😅

Saya telah memulai beberapa pekerjaan untuk mengimplementasikan intrinsik untuk float jenuh ke cast int di LLVM: https://reviews.llvm.org/D54749

Jika itu berhasil, ini akan memberikan cara overhead yang relatif rendah untuk mendapatkan semantik yang menjenuhkan.

Bagaimana cara mereproduksi perilaku yang tidak terdefinisi ini? Saya mencoba contoh di komentar tetapi hasilnya 255 , yang menurut saya OK:

println!("{}", 1.04E+17 as u8);

Perilaku tidak terdefinisi tidak dapat diamati dengan andal dengan cara itu, terkadang hal itu memberi Anda apa yang Anda harapkan tetapi dalam situasi yang lebih kompleks rusak.

Singkatnya, mesin pembuat kode (LLVM) yang kami gunakan diizinkan untuk berasumsi bahwa ini tidak terjadi, dan karenanya dapat menghasilkan kode yang buruk jika mengandalkan asumsi ini.

@ AaronM04 contoh perilaku tidak terdefinisi yang dapat direproduksi telah diposting di reddit hari ini:

fn main() {
    let a = 360.0f32;
    println!("{}", a as u8);

    let a = 360.0f32 as u8;
    println!("{}", a);

    println!("{}", 360.0f32 as u8);
}

(lihat taman bermain )

Saya berasumsi bahwa komentar terakhir dimaksudkan untuk @ AaronM04 , mengacu pada komentar mereka sebelumnya .

"Oh, itu cukup mudah."

  • @pengen_lucu , 2014

Maaf, saya telah membaca dengan cermat semua sejarah 6 tahun tentang niat baik ini. Tapi, sungguh, 6 tahun panjang dari 10 !!! Seandainya itu forum politisi, orang akan mengantisipasi beberapa sabotase yang berkobar di sekitar sini.

Jadi, tolong, adakah yang bisa menjelaskan, dengan kata-kata sederhana, apa yang membuat proses mencari solusi lebih menarik daripada solusi itu sendiri?

Karena itu lebih sulit daripada yang terlihat pada awalnya dan membutuhkan perubahan LLVM.

Oke, tapi bukan Tuhan yang membuat LLVM ini pada minggu kedua, dan bergerak ke arah yang sama mungkin butuh 15 tahun lagi untuk menyelesaikan masalah mendasar ini.

Sungguh, saya tidak memiliki perhatian untuk menyakiti seseorang, dan saya baru mengenal infrastruktur Rust untuk membantu tiba-tiba tetapi ketika saya mengetahui tentang kasus ini saya hanya tertegun.

Pelacak masalah ini untuk mendiskusikan bagaimana memecahkan masalah ini, dan menyatakan yang sudah jelas tidak membuat kemajuan ke arah itu. Jadi, jika Anda ingin membantu memecahkan masalah atau memiliki beberapa informasi baru untuk disumbangkan, silakan lakukan, tetapi jika tidak, komentar Anda tidak akan secara ajaib membuat perbaikan muncul. :)

Saya pikir asumsi bahwa ini memerlukan perubahan LLVM terlalu dini.

Saya pikir kami dapat melakukannya dalam bahasa dengan biaya kinerja mimimal. Apakah itu perubahan yang merusak * * ya tetapi itu bisa dilakukan, dan harus dilakukan.

Saya pergi ke solusi akan mendefinisikan float ke int cast sebagai unsafe kemudian memberikan beberapa fungsi pembantu di lib standar untuk memberikan hasil terikat dalam Result Jenis.

Ini adalah perbaikan yang tidak seksi dan merupakan perubahan yang mengganggu, tetapi pada akhirnya itulah yang harus dikodekan oleh setiap pengembang untuk bekerja di sekitar UB yang sudah ada. Ini adalah pendekatan karat yang benar.

Terima kasih @RalfJung , sudah membuat saya mengerti. Saya tidak punya niat untuk menghina siapa pun atau mencemooh proses curah pendapat yang produktif. Menjadi baru dalam karat, memang benar, tidak banyak yang bisa saya lakukan. Namun demikian, ini membantu saya dan mungkin orang lain, yang mencoba masuk ke dalam karat, mempelajari lebih lanjut tentang kekurangannya yang belum terpecahkan dan membuat hasil yang relevan: apakah perlu untuk menggali lebih dalam atau lebih baik memilih yang lain untuk saat ini. Tetapi saya senang bahwa penghapusan "komentar saya yang tidak berguna" akan jauh lebih mudah.

Seperti disebutkan sebelumnya di utas, ini perlahan tapi pasti diperbaiki dengan Cara Benar dengan memperbaiki llvm untuk mendukung semantik yang diperlukan, seperti yang telah disepakati tim terkait sejak lama.

Tidak ada lagi yang benar-benar dapat ditambahkan ke diskusi ini.

https://reviews.llvm.org/D54749

@nikic Sepertinya kemajuan di sisi LLVM terhenti, dapatkah Anda memberikan pembaruan singkat jika memungkinkan? Terima kasih.

Dapatkah pemeran jenuh diimplementasikan sebagai fungsi pustaka yang dapat dipilih oleh pengguna, jika mereka bersedia mengambil beberapa regresi pref untuk mendapatkan suara? Saya membaca implementasi kompiler tetapi tampaknya cukup halus:

https://github.com/rust-lang/rust/blob/625451e376bb2e5283fc4741caa0a3e8a2ca4d54/src/librustc_codegen_ssa/mir/rvalue.rs#L774 -L901

Kita bisa mengekspos intrinsik yang menghasilkan LLVM IR untuk saturasi (apakah itu IR kode terbuka saat ini atau llvm.fpto[su]i.sat di masa mendatang) terlepas dari flag -Z . Itu tidak sulit sama sekali.

Namun, saya khawatir apakah itu tindakan terbaik. Ketika (if?) Saturasi menjadi semantik default dari as cast, API seperti itu menjadi mubazir. Tampaknya juga tidak menyenangkan untuk memberi tahu pengguna bahwa mereka harus memilih sendiri apakah mereka menginginkan kesehatan atau kinerja, meskipun itu hanya sementara.

Pada saat yang sama, situasi saat ini jelas lebih buruk. Jika kita berpikir tentang menambahkan API perpustakaan, saya memperingatkan lebih dan lebih untuk hanya mengaktifkan saturasi secara default dan menawarkan unsafe intrinsik yang memiliki UB di NaN dan angka di luar jangkauan (dan lebih rendah ke polos fpto[su]i ). Itu pada dasarnya masih menawarkan pilihan yang sama, tetapi default ke kesehatan, dan API baru kemungkinan tidak akan menjadi mubazir di masa depan.

Beralih ke suara secara default terdengar bagus. Saya pikir kita dapat menawarkan intrinsik dengan malas atas permintaan daripada dari awal. Juga, akankah const eval melakukan saturasi juga dalam kasus ini? (cc @RalfJung @eddyb @ oli-obk)

Const eval kita sudah melakukan saturasi dan telah melakukannya selama berabad-abad, saya pikir bahkan sebelum miri (saya ingat dengan jelas mengubahnya di evaluator berbasis llvm::Constant ).

@rkruppe Luar biasa! Karena Anda sudah familiar dengan kode yang dipermasalahkan, apakah Anda ingin menjadi ujung tombak dalam mengganti default?

@tokopedia

Kita bisa mengekspos intrinsik yang menghasilkan IR LLVM untuk saturasi

Ini mungkin perlu 10 atau 12 intrinsik terpisah, untuk setiap kombinasi jenis sumber dan tujuan.

@Sentril

Beralih ke suara secara default terdengar bagus. Saya pikir kita dapat menawarkan intrinsik dengan malas atas permintaan daripada dari awal.

Saya berasumsi bahwa tidak seperti di komentar lain, "intrinsik" dalam komentar Anda berarti sesuatu yang akan memiliki regresi pref lebih sedikit ketika as melakukan saturasi.

Menurut saya ini bukan pendekatan yang baik untuk menangani regresi signifikan yang diketahui . Untuk beberapa pengguna, hilangnya kinerja mungkin menjadi masalah nyata, sementara algoritme mereka memastikan input selalu dalam jangkauan. Jika mereka tidak berlangganan utas ini, mereka mungkin hanya menyadari bahwa mereka terpengaruh ketika perubahan mencapai saluran Stabil. Pada saat itu mereka mungkin terjebak selama 6 hingga 12 minggu bahkan jika kami mendapatkan API yang tidak aman segera setelah diminta.

Saya lebih suka kita mengikuti pola yang sudah ditetapkan untuk peringatan deprecation: hanya lakukan pengalihan di Nightly setelah alternatif tersedia di Stable untuk beberapa waktu.

Ini mungkin perlu 10 atau 12 intrinsik terpisah, untuk setiap kombinasi jenis sumber dan tujuan.

Baik, Anda mengerti, tapi saya tidak melihat relevansinya? Biarlah 30 intrinsik, masih sepele untuk menambahkannya. Namun pada kenyataannya, lebih mudah untuk memiliki satu intrinsik generik tunggal yang digunakan oleh pembungkus tipis N. Jumlahnya juga tidak berubah jika kita memilih opsi "buat as suara dan perkenalkan unsafe cast API".

Menurut saya ini bukan pendekatan yang baik untuk menangani regresi signifikan _diketahui_. Untuk beberapa pengguna, hilangnya kinerja mungkin menjadi masalah nyata, sementara algoritme mereka memastikan input selalu dalam jangkauan. Jika mereka tidak berlangganan utas ini, mereka mungkin hanya menyadari bahwa mereka terpengaruh ketika perubahan mencapai saluran Stabil. Pada saat itu mereka mungkin terjebak selama 6 hingga 12 minggu bahkan jika kami mendapatkan API yang tidak aman segera setelah diminta.

+1

Saya tidak yakin apakah prosedur untuk peringatan penghentian (hanya tidak berlaku lagi setiap malam setelah penggantian stabil) diperlukan karena tampaknya kurang penting untuk tetap bebas kinerja-regresi di semua saluran rilis daripada tetap bebas peringatan di semua saluran rilis , tetapi sekali lagi, menunggu 12 minggu lagi pada dasarnya adalah kesalahan pembulatan dengan sudah berapa lama masalah ini terjadi.

Kami juga dapat membiarkan -Zsaturating-float-casts sekitar (hanya mengubah default) yang berarti setiap pengguna malam masih dapat memilih keluar dari cange untuk sementara waktu.

(Ya, jumlah intrinsik hanyalah detail implementasi dan tidak dimaksudkan sebagai argumen untuk atau menentang apa pun.)

@rkruppe Saya tidak bisa mengklaim telah dicerna semua komentar di sini, tapi saya di bawah kesan bahwa LLVM sekarang memang memiliki instruksi pembekuan, yang item menghalangi "jalan terpendek" untuk menghilangkan UB sini, kan?

Meskipun saya kira freeze sangat baru sehingga mungkin tidak tersedia di versi LLVM kita sendiri, bukan? Namun, sepertinya sesuatu yang harus kita eksplorasi dalam pengembangan, mungkin selama paruh pertama tahun 2020?

Nominasi untuk diskusi pada pertemuan penyusun-T, untuk mencoba mendapatkan konsensus kasar tentang jalur yang kita inginkan pada saat ini.

Menggunakan freeze masih bermasalah meskipun untuk semua alasan yang dirujuk di sini . Saya tidak yakin seberapa realistis masalah seperti itu dengan penggunaan freeze untuk cast ini, tetapi pada prinsipnya hal itu berlaku. Pada dasarnya, perkirakan freeze untuk mengembalikan sampah acak atau kunci rahasia Anda, apa pun yang lebih buruk. (Saya membaca ini secara online di suatu tempat dan sangat suka memiliki ringkasan.: D)

Dan bagaimanapun, bahkan mengembalikan sampah acak tampaknya agak buruk untuk pemeran as . Masuk akal untuk memiliki operasi yang lebih cepat untuk kecepatan di mana diperlukan, mirip dengan unchecked_add , tetapi membuat default tersebut tampaknya sangat bertentangan dengan semangat Rust.

@SimonSapin Anda mengusulkan pendekatan yang berlawanan terlebih dahulu (default ke semantik tidak sehat / "aneh" dan menyediakan metode suara eksplisit); Saya tidak tahu dari komentar Anda nanti apakah menurut Anda default ke kesehatan (setelah periode transisi yang sesuai) juga masuk akal / lebih baik?

@pramugari_id

Saya mendapat kesan bahwa LLVM sekarang memang memiliki instruksi pembekuan, yang merupakan item yang memblokir "jalur terpendek" untuk menghilangkan UB di sini, bukan?

Ada beberapa peringatan. Yang terpenting, bahkan jika semua yang kami pedulikan adalah menyingkirkan UB dan kami memperbarui LLVM yang dibundel untuk menyertakan freeze (yang dapat kami lakukan kapan saja), kami mendukung beberapa versi lama (kembali ke LLVM 6 di momen) dan kami memerlukan beberapa implementasi fallback bagi mereka untuk benar-benar menyingkirkan UB untuk semua pengguna.

Kedua, tentu saja, adalah pertanyaan apakah yang kita pedulikan hanya "bukan UB" saja. Secara khusus, saya ingin menyoroti lagi bahwa freeze(fptosi %x) berperilaku sangat berlawanan dengan intuisi: ini non-deterministik dan dapat mengembalikan hasil yang berbeda (bahkan yang diambil dari memori sensitif seperti yang dikatakan @RalfJung ) setiap kali dijalankan. Saya tidak ingin memperdebatkan hal ini lagi sekarang tetapi perlu dipertimbangkan dalam rapat jika kami lebih suka melakukan sedikit lebih banyak pekerjaan untuk menjadikan saturasi sebagai default dan konversi yang tidak dicentang (tidak aman atau freeze -menggunakan) opsi non-default.

@RalfJung Posisi saya adalah bahwa as sebaiknya dihindari sepenuhnya terlepas dari masalah ini, karena dapat memiliki semantik yang sangat berbeda (memotong, menjenuhkan, membulatkan,…) tergantung pada jenis input dan output, dan itu tidak selalu jelas saat membaca kode. (Bahkan yang terakhir dapat disimpulkan dengan foo as _ .) Jadi saya memiliki draf pra-RFC untuk mengusulkan berbagai metode konversi yang dinamai secara eksplisit yang mencakup kasus yang dilakukan as hari ini (dan mungkin lebih) .

Saya pikir as seharusnya tidak memiliki UB, karena dapat digunakan di luar unsafe . Mengembalikan sampah juga tidak terdengar bagus. Tapi kita mungkin harus memiliki beberapa jenis mitigasi / transisi / alternatif untuk kasus regresi kinerja yang diketahui yang disebabkan oleh pemeran yang jenuh. Saya hanya bertanya tentang implementasi pustaka pemeran jenuh agar tidak memblokir draf RFC ini pada transisi itu.

@Septianjoko_

Posisi saya adalah bahwa yang terbaik dihindari sepenuhnya terlepas dari masalah ini, karena ini dapat memiliki semantik yang sangat berbeda (pemotongan, penjenuhan, pembulatan,…)

Sepakat. Tapi itu tidak terlalu membantu kami dalam masalah ini.

(Juga, saya senang Anda bekerja untuk menghasilkan as tidak diperlukan. Menantikan itu.: D)

Saya pikir harusnya tidak ada UB, karena bisa digunakan di luar tidak aman. Mengembalikan sampah juga tidak terdengar bagus. Tapi kita mungkin harus memiliki beberapa jenis mitigasi / transisi / alternatif untuk kasus regresi kinerja yang diketahui yang disebabkan oleh pemeran yang jenuh. Saya hanya bertanya tentang implementasi pustaka pemeran jenuh agar tidak memblokir draf RFC ini pada transisi itu.

Jadi kita tampaknya setuju bahwa keadaan akhir harus float-to-int as saturates? Saya senang dengan rencana transisi apa pun selama itu adalah tujuan akhir yang kita jalani.

Menurut saya, tujuan akhir itu bagus.

Menurut saya ini bukan pendekatan yang baik untuk menangani regresi signifikan _diketahui_. Untuk beberapa pengguna, hilangnya kinerja mungkin menjadi masalah nyata, sementara algoritme mereka memastikan input selalu dalam jangkauan. Jika mereka tidak berlangganan utas ini, mereka mungkin hanya menyadari bahwa mereka terpengaruh ketika perubahan mencapai saluran Stabil. Pada saat itu mereka mungkin terjebak selama 6 hingga 12 minggu bahkan jika kami mendapatkan API yang tidak aman segera setelah diminta.

Dalam pandangan saya, ini tidak akan menjadi akhir dunia jika para pengguna menunggu dengan memutakhirkan rustc mereka selama 6-12 minggu itu - mereka mungkin tidak memerlukan apa pun dari rilis yang akan datang dalam kedua kasus tersebut, atau perpustakaan mereka mungkin memiliki kendala MSRV untuk menegakkan.

Sementara itu, pengguna, yang juga tidak berlangganan utas mungkin mendapatkan kesalahan kompilasi seperti halnya mereka akan kehilangan kinerja. Mana yang harus kita prioritaskan? Kami memberikan jaminan tentang stabilitas dan kami memberikan jaminan tentang keselamatan - tetapi sepengetahuan saya, tidak ada jaminan yang diberikan tentang kinerja (mis. RFC 1122 tidak menyebutkan kinerja sama sekali).

Saya lebih suka kita mengikuti pola yang sudah ditetapkan untuk peringatan deprecation: hanya lakukan pengalihan di Nightly setelah alternatif tersedia di Stable untuk beberapa waktu.

Dalam kasus peringatan deprecation, konsekuensi menunggu dengan deprecation sampai ada alternatif yang stabil bukanlah, setidaknya sejauh yang saya tahu, lubang kesehatan selama masa tunggu. (Selain itu, sementara intrinsik dapat diberikan di sini, dalam kasus umum, kami mungkin tidak dapat menawarkan alternatif secara masuk akal saat memperbaiki lubang kesehatan. Jadi menurut saya, memiliki alternatif yang stabil tidak dapat menjadi persyaratan yang sulit.)

Baik, Anda mengerti, tapi saya tidak melihat relevansinya? Biarlah 30 intrinsik, masih sepele untuk menambahkannya. Namun pada kenyataannya, lebih mudah untuk memiliki satu intrinsik generik tunggal yang digunakan oleh pembungkus tipis N. Jumlahnya juga tidak berubah jika kita memilih opsi "buat as suara dan perkenalkan unsafe cast API".

Tidakkah intrinsik generik tunggal itu memerlukan implementasi terpisah dalam kompilator untuk contoh monomorfik spesifik 12/30 itu?

Mungkin sepele untuk menambahkan intrinsik ke kompiler, karena LLVM telah melakukan sebagian besar pekerjaan, tetapi itu juga jauh dari biaya penuh. Selain itu, ada implementasi di Miri, Cranelift, serta pekerjaan akhir yang diperlukan dalam spesifikasi. Jadi saya tidak berpikir kita harus menambahkan intrinsik jika seseorang membutuhkannya.

Namun saya tidak menentang untuk mengekspos lebih banyak intrinsik, tetapi jika seseorang membutuhkannya, mereka harus membuat proposal (misalnya sebagai PR dengan beberapa deskripsi yang rumit), dan membenarkan penambahan dengan beberapa angka pembandingan atau semacamnya.

Kami juga dapat membiarkan -Zsaturating-float-casts sekitar (hanya mengubah default) yang berarti setiap pengguna malam masih dapat memilih keluar dari cange untuk sementara waktu.

Ini tampaknya baik-baik saja bagi saya, tetapi saya menyarankan untuk mengganti nama bendera menjadi -Zunsaturating-float-casts untuk menghindari perubahan semantik menjadi tidak sehat bagi mereka yang sudah menggunakan bendera ini.

@Sentril

Tidakkah intrinsik generik tunggal itu memerlukan implementasi terpisah dalam kompilator untuk contoh monomorfik spesifik 12/30 itu?

Tidak, sebagian besar implementasi dapat dan sudah digunakan bersama oleh parametrizing pada lebar bit sumber dan tujuan. Hanya beberapa bit yang membutuhkan perbedaan huruf besar / kecil. Hal yang sama berlaku untuk implementaton di miri dan kemungkinan besar juga untuk implementasi lain dan spesifikasi.

(Sunting: agar jelas, pembagian ini dapat terjadi meskipun ada N intrinsik yang berbeda, tetapi satu intrinsik generik memotong plat boiler yang diperlukan per-intrinsik.)

Jadi saya tidak berpikir kita harus menambahkan intrinsik jika seseorang membutuhkannya.

Namun saya tidak menentang untuk mengekspos lebih banyak intrinsik, tetapi jika seseorang membutuhkannya, mereka harus membuat proposal (misalnya sebagai PR dengan beberapa deskripsi yang rumit), dan membenarkan penambahan dengan beberapa angka pembandingan atau semacamnya. Saya tidak berpikir itu seharusnya menghalangi perbaikan lubang kesehatan.

Kami sudah memiliki beberapa nomor pembanding. Kita tahu dari panggilan untuk tolok ukur sejak lama bahwa encoding JPEG menjadi lebih lambat secara signifikan pada x86_64 dengan cast yang jenuh. Seseorang dapat menjalankannya kembali tetapi saya merasa yakin memprediksi bahwa ini tidak berubah (meskipun tentu saja angka spesifik tidak akan identik) dan tidak melihat alasan mengapa masa depan berubah pada bagaimana saturasi diterapkan (seperti beralih ke asm inline atau LLVM intrinsics @nikic bekerja pada) akan secara fundamental mengubahnya. Meskipun sulit untuk memastikan masa depan, tebakan saya adalah bahwa satu-satunya cara yang masuk akal untuk mengembalikan kinerja itu adalah dengan menggunakan sesuatu yang menghasilkan kode tanpa pemeriksaan jangkauan, seperti konversi unsafe atau sesuatu yang menggunakan freeze .

Oke jadi dari bilangan benchmarking yang ada, sepertinya ada keinginan aktif untuk kata intrinsik tersebut. Jika demikian, saya akan mengusulkan rencana tindakan berikut:

  1. Secara bersamaan:

    • Perkenalkan intrinsik yang diekspos setiap malam melalui fungsi #[unstable(...)] .

    • Hapus -Zsaturating-float-casts dan perkenalkan -Zunsaturating-float-casts .

    • Alihkan default ke fungsi -Zsaturating-float-casts .

  2. Kami menstabilkan intrinsik setelah beberapa waktu; kita bisa sedikit mempercepat.
  3. Hapus -Zunsaturating-float-casts setelah beberapa saat.

Kedengarannya bagus. Kecuali bahwa intrinsik adalah detail implementasi dari beberapa API publik, mungkin metode pada f32 dan f64 . Mereka bisa berupa:

  • Metode yang bersifat umum (dengan parameter untuk tipe pengembalian bilangan bulat konversi), secara opsional di pendahuluan
  • Metode yang melekat dengan sifat pendukung (mirip dengan str::parse dan FromStr ) untuk mendukung jenis pengembalian yang berbeda
  • Beberapa metode inheren non-generik dengan jenis target dalam namanya

Ya, maksud saya mengekspos intrinsik melalui metode atau semacamnya.

Beberapa metode inheren non-generik dengan jenis target dalam namanya

Ini terasa seperti hal yang biasa kami lakukan - ada yang keberatan dengan opsi ini?

Apakah itu? Saya merasa bahwa ketika memiliki nama jenis (tanda tangan) sebagai bagian dari nama metode, itu ad-hoc "salah satu jenis" konversi (seperti Vec::as_slice dan [T]::to_vec ) , atau rangkaian konversi yang perbedaannya bukan jenisnya (seperti to_ne_bytes , to_be_bytes , to_le_bytes ). Tetapi bagian dari motivasi untuk std::convert adalah untuk menghindari lusinan metode terpisah seperti u8::to_u16 , u8::to_u32 , u8::to_u64 , dll.

Saya bertanya-tanya apakah ini akan secara alami dapat digeneralisasikan ke suatu sifat mengingat bahwa metode harus unsafe fn . Jika kita menambahkan metode inheren, maka Anda selalu dapat mendelegasikannya ke implementasi sifat dan yang lainnya.

Tampaknya aneh bagi saya untuk menambahkan ciri untuk konversi yang tidak aman, tetapi saya rasa mungkin Simon sedang memikirkan fakta bahwa kita mungkin memerlukan metode berbeda untuk setiap kombinasi titik mengambang dan jenis bilangan bulat (mis. f32::to_u8_unsaturated , f32::to_u16_unsaturated , dll).

Bukan untuk mempertimbangkan utas panjang yang belum saya baca dalam ketidaktahuan total, tetapi apakah itu diinginkan atau cukup untuk memiliki misalnya f32::to_integer_unsaturated yang diubah menjadi u32 atau sesuatu? Apakah ada pilihan yang jelas untuk jenis target untuk konversi tidak aman?

Memberikan konversi yang tidak aman hanya untuk i32 / u32 (misalnya) sepenuhnya mengecualikan semua jenis bilangan bulat yang rentang nilainya tidak terlalu kecil, dan terkadang memang diperlukan. Mengecilkan (turun ke u8, seperti dalam encoding JPEG) juga sering dibutuhkan tetapi dapat ditiru dengan mengonversi ke tipe integer yang lebih luas dan memotong dengan as (yang murah meskipun biasanya tidak gratis).

Tapi kami tidak bisa hanya memberikan konversi ke ukuran bilangan bulat terbesar. Itu tidak selalu didukung secara asli (karenanya, lambat) dan pengoptimalan tidak dapat memperbaikinya: tidak tepat untuk mengoptimalkan "ubah ke int besar, lalu potong" menjadi "ubah ke int yang lebih kecil secara langsung" karena yang terakhir memiliki UB (di LLVM IR) / hasil yang berbeda (pada level kode mesin, pada sebagian besar arsitektur) dalam kasus di mana hasil konversi asli akan melilit saat pemotongan.

Perhatikan bahwa bahkan mengecualikan bilangan bulat 128 bit secara pragmatis dan berfokus pada bilangan bulat 64 bit akan tetap buruk untuk target 32 ​​bit umum.

Saya baru dalam percakapan ini tetapi tidak dalam pemrograman. Saya ingin tahu mengapa orang berpikir konversi jenuh dan mengubah NaN menjadi nol adalah perilaku default yang wajar. Saya memahami bahwa Java melakukan ini (meskipun membungkus tampaknya jauh lebih umum), tetapi tidak ada nilai integer yang NaN benar-benar dapat dikatakan sebagai konversi yang benar. Demikian pula, mengonversi 1000000.0 menjadi 65535 (u16), misalnya, tampaknya salah. Tidak ada u16 yang jelas merupakan jawaban yang benar. Setidaknya, saya tidak melihatnya sebagai yang lebih baik daripada perilaku saat ini untuk mengubahnya menjadi 16960, yang setidaknya merupakan perilaku yang dibagikan dengan C / C ++, C #, go, dan lainnya, dan dengan demikian setidaknya agak tidak mengejutkan.

Berbagai orang telah mengomentari kesamaan dengan pemeriksaan luapan, dan saya setuju dengan mereka. Ini juga mirip dengan pembagian bilangan bulat dengan nol. Saya pikir konversi yang tidak valid harus panik seperti aritmatika yang tidak valid. Mengandalkan NaN -> 0 dan 1000000.0 -> 65535 (atau 16960) tampaknya sama rawan kesalahannya dengan mengandalkan integer overflow atau hipotetis n / 0 == 0. Ini adalah jenis hal yang seharusnya menghasilkan kesalahan secara default. (Dalam rilis build, karat dapat menghilangkan pemeriksaan kesalahan, seperti halnya dengan aritmatika integer.) Dan dalam kasus yang jarang terjadi ketika Anda _ ingin_ mengonversi NaN menjadi nol atau memiliki saturasi floating point, Anda harus memilih ke dalamnya, seperti Anda harus memilih overflow integer.

Sedangkan untuk kinerja, sepertinya kinerja umum tertinggi akan datang dari melakukan konversi biasa dan mengandalkan kesalahan perangkat keras. Baik x86 maupun ARM, misalnya, meningkatkan pengecualian perangkat keras saat konversi floating-point-to-integer tidak dapat direpresentasikan dengan benar (termasuk kasus NaN dan di luar rentang). Solusi ini tanpa biaya kecuali untuk konversi yang tidak valid, kecuali saat mengonversi langsung dari tipe floating-point ke bilangan bulat kecil dalam pembuatan debug - kasus yang jarang terjadi - yang seharusnya masih relatif murah. (Pada perangkat keras teoretis yang tidak mendukung pengecualian ini, maka dapat diemulasikan dalam perangkat lunak, tetapi sekali lagi hanya dalam pembuatan debug.) Saya membayangkan bahwa pengecualian perangkat keras persis seperti cara mendeteksi pembagian integer dengan nol diterapkan hari ini. Saya melihat banyak pembicaraan tentang LLVM, jadi mungkin Anda dibatasi di sini, tetapi akan sangat disayangkan untuk memiliki emulasi perangkat lunak di setiap konversi floating point bahkan dalam rilis build untuk memberikan perilaku alternatif yang meragukan untuk konversi yang tidak valid secara inheren.

@admilazz Kami dibatasi oleh apa yang dapat dilakukan LLVM, dan saat ini LLVM tidak mengekspos metode untuk mengonversi float menjadi integer secara efisien tanpa risiko perilaku yang tidak ditentukan.

Saturasi adalah karena bahasa menentukan as cast agar selalu berhasil, jadi kami tidak dapat mengubah operator menjadi panik.

Demikian pula, mengonversi 1000000.0 menjadi 65535 (u16), misalnya, tampaknya salah. Tidak ada u16 yang jelas merupakan jawaban yang benar. Setidaknya, saya tidak melihatnya sebagai yang lebih baik dari perilaku saat ini untuk mengubahnya menjadi 16960,

Itu tidak jelas bagi saya jadi saya pikir ada baiknya menunjukkan: 16960 adalah hasil dari mengubah 1000000.0 menjadi bilangan bulat yang cukup lebar, kemudian memotong untuk menjaga 16 bit yang lebih rendah.

Ini ~ bukan opsi yang telah disarankan sebelumnya di utas ini, dan ini ~ (Sunting: Saya salah di sini, maaf saya tidak menemukannya) juga bukan perilaku saat ini. Perilaku saat ini di Rust adalah bahwa konversi float-to-integer di luar jangkauan adalah Perilaku Tidak Terdefinisi. Dalam prakteknya hal ini seringkali mengarah pada nilai sampah, pada prinsipnya dapat menyebabkan kesalahan kompilasi dan kerentanan. Utas ini adalah tentang memperbaikinya. Ketika saya menjalankan program di bawah ini di Rust 1.39.0 saya mendapatkan nilai yang berbeda setiap saat:

fn main() {
    dbg!(1000000.0 as u16);
}

Taman bermain . Contoh keluaran:

[src/main.rs:2] 1000000.0 as u16 = 49072

Saya pribadi berpikir pemotongan seperti integer tidak lebih baik atau lebih buruk dari saturasi, keduanya salah secara numerik untuk nilai di luar kisaran. Konversi yang sempurna ada tempatnya, asalkan deterministik dan bukan UB. Anda mungkin sudah tahu dari algoritme Anda bahwa nilai berada dalam kisaran, atau mungkin tidak peduli dengan kasus seperti itu.

Saya pikir kita juga harus menambahkan API konversi yang salah yang mengembalikan Result , tetapi saya masih perlu menyelesaikan penulisan draf itu sebelum RFC :)

Semantik "ubah menjadi bilangan bulat matematika, lalu potong menjadi lebar target" atau "sampul" telah disarankan sebelumnya di utas ini (https://github.com/rust-lang/rust/issues/10184#issuecomment-299229143). Saya tidak terlalu menyukainya:

  • Saya pikir itu sedikit kurang masuk akal daripada saturasi. Saturasi umumnya tidak memberikan hasil yang masuk akal untuk angka yang jauh di luar jangkauan, tetapi:

    • itu berperilaku lebih bijaksana daripada sampul ketika angka sedikit di luar jangkauan (misalnya karena kesalahan pembulatan terakumulasi). Sebaliknya, gips yang membungkus dapat memperkuat kesalahan pembulatan ringan dalam perhitungan float hingga kesalahan semaksimal mungkin dalam domain integer.

    • itu agak umum digunakan dalam pemrosesan sinyal digital, jadi setidaknya ada beberapa aplikasi yang sebenarnya diinginkan. Sebaliknya, saya tidak tahu satu algoritma pun yang mendapat manfaat dari semantik sampul.

  • AFAIK satu-satunya alasan untuk memilih semantik sampul adalah keefektifan emulasi perangkat lunak, tetapi bagi saya ini tampak seperti asumsi yang belum terbukti. Saya akan senang terbukti salah tetapi sekilas sepintas tampaknya memerlukan rangkaian instruksi ALU yang begitu panjang (ditambah cabang untuk menangani infinitas dan NaN secara terpisah) sehingga saya tidak merasa jelas salah satu akan jelas lebih baik untuk itu. kinerja dari yang lain.
  • Sementara pertanyaan tentang apa yang harus dilakukan untuk NaN adalah masalah yang buruk untuk setiap konversi ke integer, saturasi setidaknya tidak memerlukan casing khusus (baik dalam semantik maupun di sebagian besar implementasi) untuk tak terbatas. Tapi sebagai kesimpulan, apa padanan integer +/- infinity seharusnya? JavaScript mengatakan itu 0, dan saya kira jika kita membuat as panik di NaN maka itu juga bisa panik pada tak terbatas, tapi bagaimanapun ini sepertinya akan membuat sampul lebih sulit untuk dibuat cepat daripada melihat angka normal dan denormal sendiri akan menyarankan.

Saya menduga bahwa sebagian besar kode yang diregresikan oleh semantik saturasi untuk konversi akan lebih baik menggunakan SIMD. Jadi, meskipun disayangkan, perubahan ini tidak akan mencegah penulisan kode berkinerja tinggi (terutama jika intrinsik dengan semantik berbeda disediakan), dan bahkan mungkin mendorong beberapa proyek ke arah implementasi yang lebih cepat (jika kurang portabel).

Jika demikian, beberapa regresi kinerja kecil tidak boleh digunakan sebagai pembenaran untuk menghindari penutupan lubang yang sehat.

https://github.com/rust-lang/rust/pull/66841 menambahkan unsafe fn metode yang mengonversi dengan fptoui dan fptosi dari LLVM, untuk kasus-kasus di mana nilainya diketahui berada dalam jangkauan dan jenuh adalah regresi kinerja yang dapat diukur.

Setelah itu saya pikir tidak apa-apa untuk mengganti default untuk as (dan mungkin menambahkan bendera -Z untuk menyisih?), Meskipun itu mungkin keputusan resmi tim Lang.

Setelah mendarat, saya pikir tidak apa-apa untuk mengganti default untuk as (dan mungkin menambahkan bendera -Z untuk menyisih?), Meskipun itu mungkin keputusan resmi tim Lang.

Jadi kami (tim bahasa, dengan orang-orang yang ada di sana setidaknya) membahas ini di https://github.com/rust-lang/lang-team/blob/master/minutes/2019-11-21.md dan kami pikir menambahkan intrinsik baru + menambahkan -Zunsaturated-float-casts akan menjadi langkah pertama yang baik.

Saya pikir akan lebih baik untuk mengganti default sebagai bagian dari itu atau segera setelahnya, mungkin dengan FCP jika perlu.

Saya berasumsi bahwa dengan intrinsik baru yang Anda maksudkan seperti https://github.com/rust-lang/rust/pull/66841

Apa yang dimaksud dengan menambahkan -Z unsaturated-float-casts tanpa mengubah default? Terima itu sebagai no-op daripada memancarkan "error: opsi debugging tidak dikenal"?

Saya berasumsi bahwa yang Anda maksud dengan intrinsik baru adalah sesuatu seperti # 66841

Ya 👍 - terima kasih telah menjadi ujung tombak itu.

Apa yang dimaksud dengan menambahkan -Z unsaturated-float-casts tanpa mengubah default? Terima itu sebagai no-op daripada memancarkan "error: opsi debugging tidak dikenal"?

Ya pada dasarnya. Sebagai alternatif, kami menghapus -Z saturated-float-casts untuk mendukung -Z unsaturated-float-casts dan mengganti default secara langsung, tetapi itu akan mengarah ke hasil yang sama dengan PR yang lebih sedikit.

Saya benar-benar tidak mengerti saran "tak jenuh". Jika tujuannya hanya untuk memberikan kenop untuk menyisih dari default baru, lebih mudah untuk hanya mengubah default dari bendera yang ada dan tidak melakukan apa-apa lagi. Jika tujuannya adalah untuk memilih nama baru yang lebih jelas tentang trade-off (tidak sehat), maka "unsaturating" sangat buruk - saya akan menyarankan nama yang menyertakan "unsafe" atau "UB" atau sejenisnya kata menakutkan, misalnya -Z fix-float-cast-ub .

unchecked adalah istilah dengan beberapa preseden dalam nama API.

@admilazz Kami dibatasi oleh apa yang dapat dilakukan LLVM, dan saat ini LLVM tidak mengekspos metode untuk mengonversi float menjadi integer secara efisien tanpa risiko perilaku yang tidak ditentukan.

Namun sepertinya Anda hanya dapat menambahkan pemeriksaan waktu proses di build debug, seperti yang Anda lakukan untuk overflow integer.

AFAIK satu-satunya alasan untuk memilih semantik sampul adalah efisiensi emulasi perangkat lunak

Saya tidak berpikir kita harus memilih baik sampul atau saturasi, karena keduanya salah, tetapi sampul setidaknya memiliki keuntungan menjadi metode yang digunakan oleh banyak bahasa yang mirip dengan rust: C / C ++, C #, go, mungkin D, dan tentunya lebih, dan juga menjadi perilaku karat saat ini (setidaknya kadang-kadang). Yang mengatakan, menurut saya "panik pada konversi tidak valid (mungkin hanya dalam build debug)" adalah hal yang ideal, seperti yang kita lakukan untuk overflow integer dan aritmatika tidak valid seperti pembagian dengan nol.

(Menariknya, saya mendapatkan 16960 di taman bermain . Tetapi saya melihat dari contoh lain yang diposting bahwa terkadang karat melakukannya secara berbeda ...)

Saturasi adalah karena bahasa yang didefinisikan sebagai cast agar selalu berhasil, jadi kami tidak dapat mengubah operator menjadi panik.

Mengubah apa yang dievaluasi oleh operasi sudah merupakan perubahan yang menghancurkan, sejauh kita peduli dengan hasil orang yang sudah melakukan ini. Perilaku tidak panik ini juga bisa berubah.

Saya kira jika kita membuat panik di NaN maka itu juga bisa panik hingga tak terbatas, tapi bagaimanapun ini sepertinya akan membuat sampul lebih sulit untuk dibuat dengan cepat.

Jika hanya diperiksa di build debug, seperti integer overflow, saya rasa kita bisa mendapatkan yang terbaik dari kedua dunia: konversi dijamin benar (dalam build debug), kesalahan pengguna lebih mungkin ditangkap, Anda dapat ikut serta hingga perilaku aneh seperti sampul dan / atau kejenuhan jika Anda suka, dan kinerjanya sebaik mungkin.

Juga, tampaknya aneh untuk mengontrol hal ini melalui saklar baris perintah. Itu palu besar. Tentunya perilaku yang diinginkan dari konversi di luar rentang bergantung pada spesifikasi algoritme, jadi itu adalah sesuatu yang harus dikontrol pada basis per konversi. Saya menyarankan f.to_u16_sat () dan f.to_u16_wrap () atau serupa dengan opt-in, dan tidak memiliki opsi baris perintah yang mengubah semantik kode. Itu akan menyulitkan untuk mencampur dan mencocokkan potongan kode yang berbeda, dan Anda tidak dapat memahami apa yang dilakukan sesuatu dengan membacanya ...

Dan, jika benar-benar tidak dapat diterima untuk menjadikan "panik jika tidak valid" sebagai perilaku default, alangkah baiknya memiliki metode intrinsik yang mengimplementasikannya tetapi hanya melakukan pemeriksaan validitas dalam pembuatan debug sehingga kita dapat memastikan konversi kita benar dalam (luas mayoritas?) kasus ketika kami berharap mendapatkan jumlah yang sama setelah konversi, tetapi tanpa membayar penalti apa pun dalam build rilis.

Menariknya, saya mendapatkan 16960 di taman bermain.

Beginilah cara kerja Undefined Behavior: bergantung pada formulasi yang tepat dari program dan versi kompiler yang tepat serta tanda kompilasi yang tepat, Anda mungkin mendapatkan perilaku deterministik, atau nilai sampah yang berubah pada setiap proses, atau kesalahan kompilasi. Kompiler diperbolehkan melakukan apapun.

sampul setidaknya memiliki keuntungan menjadi metode yang digunakan oleh banyak bahasa yang mirip dengan rust: C / C ++, C #, go, mungkin D, dan tentunya lebih,

Benarkah? Setidaknya tidak di C dan C ++, mereka memiliki Perilaku Tidak Terdefinisi yang sama dengan Rust. Ini bukan kebetulan, kami menggunakan LLVM yang utamanya dibangun untuk clang yang mengimplementasikan C dan C ++. Apakah Anda yakin tentang C # dan pergi?

Standar C11 https://port70.net/~nsz/c/c11/n1570.html#6.3.1.4

Ketika nilai terbatas dari tipe real floating diubah ke tipe integer selain _Bool, bagian pecahan akan dibuang (yaitu, nilai dipotong menuju nol). Jika nilai bagian integral tidak dapat diwakili oleh tipe integer, perilaku tidak ditentukan.

Operasi yang tersisa dilakukan ketika nilai tipe integer diubah menjadi tipe unsigned tidak perlu dilakukan ketika nilai tipe floating nyata diubah menjadi tipe unsigned. Jadi, kisaran nilai mengambang nyata portabel adalah (-1, Utype_MAX + 1).

Standar C ++ 17 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf#section.7.10

Nilai pr dari tipe titik-mengambang dapat diubah menjadi nilai pr dari tipe bilangan bulat. Konversi memotong; yaitu, bagian pecahan dibuang. Perilaku tidak ditentukan jika nilai yang terpotong tidak dapat direpresentasikan dalam tipe tujuan.

C # referensi https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions

Saat Anda mengonversi nilai ganda atau float menjadi tipe integral, nilai ini dibulatkan menuju nol ke nilai integral terdekat. Jika nilai integral yang dihasilkan berada di luar rentang tipe tujuan, hasilnya bergantung pada konteks pemeriksaan luapan. Dalam konteks yang dicentang, OverflowException dilemparkan, sedangkan dalam konteks yang tidak dicentang, hasilnya adalah nilai yang tidak ditentukan dari tipe tujuan.

Jadi bukan UB, hanya "nilai tidak ditentukan".

@admilazz Ada perbedaan besar antara ini dan overflow integer: overflow integer tidak diinginkan tetapi didefinisikan dengan baik . Gips floating point adalah perilaku yang tidak ditentukan .

Apa yang Anda minta mirip dengan mematikan pemeriksaan Vec bounds dalam mode rilis, tetapi itu akan salah karena itu akan memungkinkan perilaku yang tidak ditentukan.

Mengizinkan perilaku tidak terdefinisi dalam kode aman tidak dapat diterima, meskipun itu hanya terjadi dalam mode rilis. Jadi, perbaikan apa pun harus diterapkan pada mode rilis dan debug.

Tentu saja mungkin untuk memiliki perbaikan yang lebih ketat dalam mode debug, tetapi perbaikan untuk mode rilis masih harus didefinisikan dengan baik.

@admilazz Ada perbedaan besar antara ini dan integer overflow: integer overflow tidak diinginkan tetapi didefinisikan dengan baik. Gips floating point adalah perilaku yang tidak ditentukan.

Tentu, tetapi utas ini adalah tentang mendefinisikan perilaku. Jika itu didefinisikan sebagai menghasilkan "nilai yang tidak ditentukan dari jenis tujuan", seperti dalam spesifikasi C # yang direferensikan dengan baik oleh Amanieu di atas, maka itu tidak akan lagi ditentukan (dengan cara yang berbahaya). Anda tidak dapat dengan mudah menggunakan sifat overflow integer yang terdefinisi dengan baik dalam program praktis karena masih akan panik dalam build debug. Demikian pula, nilai yang dihasilkan oleh cast yang tidak valid dalam build rilis tidak perlu dapat diprediksi atau sangat berguna karena program secara praktis tidak dapat menggunakannya jika panik dalam build debug. Hal ini sebenarnya memberikan cakupan maksimum pada compiler untuk pengoptimalan, sedangkan memilih perilaku seperti saturasi akan membatasi compiler dan dapat menjadi lebih lambat secara signifikan pada perangkat keras tanpa instruksi konversi saturasi asli. (Dan itu tidak seperti saturasi yang jelas benar.)

Apa yang Anda minta mirip dengan mematikan pemeriksaan batas Vec dalam mode rilis, tetapi itu salah karena itu akan memungkinkan perilaku yang tidak ditentukan. Mengizinkan perilaku tidak terdefinisi dalam kode aman tidak dapat diterima ...

Tidak semua perilaku tidak terdefinisi sama. Perilaku tidak terdefinisi hanya berarti terserah pada pelaksana kompilator untuk memutuskan apa yang akan terjadi. Selama tidak ada cara untuk melanggar jaminan keamanan karat dengan melemparkan pelampung ke int, maka menurut saya itu tidak sama dengan mengizinkan orang untuk menulis ke lokasi memori sewenang-wenang. Meskipun demikian, tentu saja saya setuju bahwa ini harus didefinisikan dalam arti dijamin aman meskipun tidak selalu dapat diprediksi.

Benarkah? Setidaknya tidak di C dan C ++, mereka memiliki Perilaku Tidak Terdefinisi yang sama dengan Rust ... Apakah Anda yakin tentang C # dan pergi?

Cukup adil. Saya tidak membaca semua spesifikasi mereka; Saya baru saja menguji berbagai kompiler. Anda benar bahwa mengatakan "semua kompiler yang saya coba melakukannya dengan cara ini" berbeda dengan mengatakan "spesifikasi bahasa menetapkannya seperti ini". Tapi saya tidak mendukung overflow, hanya menunjukkan bahwa itu tampaknya yang paling umum. Saya benar-benar memperdebatkan untuk memiliki konversi yang 1) melindungi dari hasil yang "salah" seperti 1000000.0 menjadi 65535 atau 16960 untuk alasan yang sama seperti kami melindungi dari integer overflow - kemungkinan besar ini adalah bug sehingga pengguna harus ikut serta , dan 2) memungkinkan kinerja maksimum dalam versi rilis.

Tidak semua perilaku tidak terdefinisi sama. Perilaku tidak terdefinisi hanya berarti terserah pada pelaksana kompilator untuk memutuskan apa yang akan terjadi. Selama tidak ada cara untuk melanggar jaminan keamanan karat dengan melemparkan pelampung ke int, maka menurut saya itu tidak sama dengan mengizinkan orang untuk menulis ke lokasi memori sewenang-wenang. Meskipun demikian, tentu saja saya setuju bahwa itu harus didefinisikan: ditentukan, tetapi tidak harus dapat diprediksi.

Perilaku tidak terdefinisi berarti pengoptimal (yang disediakan oleh pengembang LLVM yang berfokus pada C dan C ++) bebas berasumsi bahwa hal itu tidak akan pernah terjadi dan mengubah kode berdasarkan asumsi tersebut, termasuk menghapus potongan kode yang hanya dapat dijangkau dengan melewatkan cast yang tidak ditentukan atau, sebagai contoh ini menunjukkan, dengan asumsi bahwa tugas harus telah disebut, meskipun sebenarnya tidak, karena memohon kode yang disebut tanpa terlebih dahulu menyebutnya akan perilaku undefined.

Bahkan jika itu yang wajar untuk membuktikan bahwa menyusun optimasi yang berbeda melewati tidak menghasilkan perilaku muncul berbahaya, para pengembang LLVM tidak akan membuat usaha sadar untuk melestarikan itu.

Saya berpendapat bahwa semua perilaku tidak terdefinisi adalah sama atas dasar itu.

Sekalipun masuk akal untuk membuktikan bahwa menyusun jalur pengoptimalan yang berbeda tidak menghasilkan perilaku muncul yang berbahaya, pengembang LLVM tidak akan melakukan upaya sadar apa pun untuk melestarikannya.

Yah, sangat disayangkan bahwa LLVM mengganggu desain karat dengan cara ini, tetapi saya baru saja membaca beberapa referensi instruksi LLVM dan menyebutkan operasi "pembekuan" yang disebutkan di atas ("... yang lainnya adalah menunggu LLVM menambahkan pembekuan konsep… ") yang akan mencegah perilaku tidak terdefinisi di tingkat LLVM. Apakah karat terkait dengan versi lama LLVM? Jika tidak, kita bisa menggunakannya. Dokumentasi mereka tidak jelas tentang perilaku sebenarnya.

Jika argumennya undef atau poison, 'freeze' mengembalikan nilai tipe 'ty' yang sewenang-wenang, tetapi tetap. Jika tidak, instruksi ini adalah no-op dan mengembalikan argumen input. Semua penggunaan nilai yang dikembalikan oleh instruksi 'freeze' yang sama dijamin untuk selalu mengamati nilai yang sama, sementara instruksi 'freeze' yang berbeda dapat menghasilkan nilai yang berbeda.

Saya tidak tahu apa yang mereka maksud dengan "nilai tetap" atau "instruksi 'membekukan' yang sama". Saya pikir idealnya itu akan dikompilasi menjadi no-op dan memberikan integer yang tidak dapat diprediksi, tetapi kedengarannya seperti itu mungkin melakukan sesuatu yang mahal. Adakah yang mencoba operasi pembekuan ini?

Nah, sangat disayangkan LLVM merusak desain karat dengan cara ini

Bukan hanya pengembang LLVM yang menulis pengoptimal. Itu bahwa, bahkan jika pengembang rustc menulis pengoptimalan, menggoda dengan ketidaktentuan secara inheren merupakan senjata yang sangat besar karena sifat-sifat yang muncul dari pengoptimal merangkai. Otak manusia sama sekali tidak berevolusi untuk "mengetahui besarnya potensi kesalahan pembulatan" ketika pembulatan yang dimaksud adalah perilaku yang muncul yang dibangun oleh pengoptimalan rangkaian.

Saya tidak akan setuju dengan Anda di sana. :-) Saya berharap instruksi "pembekuan" LLVM ini memberikan cara tanpa biaya untuk menghindari perilaku yang tidak ditentukan ini.

Hal itu telah dibahas di atas dan kesimpulannya adalah bahwa meskipun casting-kemudian-pembekuan didefinisikan sebagai perilaku, itu sama sekali bukan perilaku yang wajar . Dalam mode rilis, cast tersebut akan mengembalikan hasil arbitrer untuk input di luar batas (dalam kode yang sepenuhnya aman). Itu bukan semantik yang bagus untuk sesuatu yang terlihat polos seperti as .

Semantik IMO seperti itu akan menjadi desain bahasa buruk yang sebaiknya kita hindari.

Posisi saya adalah bahwa as sebaiknya dihindari sepenuhnya terlepas dari masalah ini, karena ini dapat memiliki semantik yang sangat berbeda (memotong, menjenuhkan, membulatkan,…) tergantung pada jenis masukan dan keluaran, dan itu tidak selalu jelas ketika membaca kode. (Bahkan yang terakhir dapat disimpulkan dengan foo as _ .) Jadi saya memiliki draf pra-RFC untuk mengusulkan berbagai metode konversi yang dinamai secara eksplisit yang mencakup kasus yang dilakukan as hari ini (dan mungkin lebih) .

Saya menyelesaikan draf itu! https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Setiap umpan balik sangat diterima, tapi tolong berikan di utas internal daripada di sini.

Dalam mode rilis, cast tersebut akan mengembalikan hasil arbitrer untuk input di luar batas (dalam kode yang sepenuhnya aman). Itu bukan semantik yang bagus untuk sesuatu yang tampak polos seperti.

Maaf berulang kali, tapi menurut saya argumen yang sama ini berlaku untuk integer overflow. Jika Anda mengalikan beberapa angka dan hasilnya meluap, Anda akan mendapatkan hasil yang sangat salah yang hampir pasti akan membatalkan kalkulasi yang Anda coba lakukan, tetapi panik dalam debug build dan oleh karena itu bug kemungkinan besar akan tertangkap. Saya akan mengatakan bahwa konversi numerik yang memberikan hasil yang sangat salah juga harus panik karena ada kemungkinan yang sangat tinggi itu mewakili bug dalam kode pengguna. (Kasus ketidakakuratan floating-point tipikal sudah ditangani. Jika kalkulasi menghasilkan 65535,3, maka sudah valid untuk mengubahnya menjadi u16. Untuk mendapatkan konversi di luar batas, biasanya Anda memerlukan bug dalam kode Anda, dan jika saya memiliki bug saya ingin diberi tahu sehingga saya bisa memperbaikinya.)

Kemampuan rilis build untuk memberikan hasil yang sewenang-wenang tetapi ditentukan untuk konversi tidak valid juga memungkinkan performa maksimum, yang menurut saya penting, untuk sesuatu yang fundamental seperti konversi numerik. Selalu jenuh memiliki dampak kinerja yang signifikan, menyembunyikan bug, dan jarang membuat perhitungan yang secara tidak terduga memberikan hasil yang benar.

Maaf berulang kali, tapi menurut saya argumen yang sama ini berlaku untuk integer overflow. Jika Anda mengalikan beberapa angka dan hasilnya melimpah, Anda akan mendapatkan hasil yang sangat salah yang hampir pasti akan membatalkan kalkulasi yang Anda coba lakukan.

Kami tidak berbicara tentang perkalian, kami berbicara tentang pemeran. Dan ya, hal yang sama berlaku untuk overflow integer: cast int-to-int tidak pernah panik, bahkan saat meluap. Ini karena as , secara desain, tidak pernah panik, bahkan dalam pembuatan debug. Menyimpang dari ini untuk cast floating point sangat mengejutkan dan berbahaya paling buruk, karena kebenaran dan keamanan pada kode yang tidak aman dapat bergantung pada operasi tertentu yang tidak panik.

Jika Anda ingin berargumen bahwa desain as cacat karena memberikan konversi yang sempurna antara tipe-tipe di mana konversi yang tepat tidak selalu memungkinkan, saya pikir sebagian besar dari kita akan setuju. Tapi itu sepenuhnya di luar cakupan utas ini, yaitu tentang memperbaiki konversi float-to-int di dalam kerangka kerja as cast yang ada . Ini harus sempurna, tidak boleh panik, bahkan dalam build debug. Jadi tolong usulkan beberapa semantik yang masuk akal (tidak melibatkan freeze ), non-panik untuk pemain float-to-int, atau coba untuk memulai diskusi baru tentang mendesain ulang as untuk membuat panik ketika castnya lossy (dan melakukannya secara konsisten untuk cast int-to-int dan float-to-int) - tetapi yang terakhir di luar topik dalam masalah ini, jadi buka utas baru (pra-gaya RFC) untuk itu.

Bagaimana kalau kita mulai dengan hanya menerapkan semantik freeze sekarang untuk memperbaiki UB, dan kemudian kita dapat memiliki waktu di dunia untuk menyetujui semantik apa yang sebenarnya kita inginkan karena semantik apa pun yang kita pilih akan kompatibel dengan freeze semantik.

Bagaimana kalau kita mulai dengan hanya mengimplementasikan freeze semantics _now_ untuk memperbaiki UB, dan kemudian kita dapat memiliki waktu di seluruh dunia untuk menyetujui semantik apa yang sebenarnya kita inginkan karena semantik apa pun yang kita pilih akan kompatibel dengan freeze semantik.

  1. Panik tidak kompatibel dengan pembekuan, jadi kami harus menolak setidaknya semua proposal yang melibatkan panik. Beralih dari UB ke panik kurang jelas tidak sesuai, meskipun seperti dibahas di atas ada beberapa alasan lain untuk tidak membuat as panik.
  2. Seperti yang saya tulis sebelumnya ,
    > kami mendukung beberapa versi lama (kembali ke LLVM 6 saat ini) dan kami memerlukan beberapa implementasi fallback agar mereka benar-benar menyingkirkan UB untuk semua pengguna.

Saya setuju dengan @RalfJung bahwa membuat as sangat tidak diinginkan, tetapi selain itu saya rasa poin yang dibuat @admilazz ini jelas tidak benar:

(Kasus ketidakakuratan floating-point tipikal sudah ditangani. Jika kalkulasi menghasilkan 65535,3, maka sudah valid untuk mengubahnya menjadi u16. Untuk mendapatkan konversi di luar batas, biasanya Anda memerlukan bug dalam kode Anda, dan jika saya memiliki bug saya ingin diberi tahu sehingga saya bisa memperbaikinya.)

Untuk f32-> u16 mungkin benar bahwa Anda memerlukan kesalahan pembulatan yang luar biasa besar untuk keluar dari rentang u16 hanya dari kesalahan pembulatan, tetapi untuk konversi dari f32 ke bilangan bulat 32 bit itu tidak begitu jelas benar. i32::MAX tidak dapat direpresentasikan persis dalam f32, angka terdekat yang dapat direpresentasikan adalah 47 dari i32::MAX . Jadi jika Anda memiliki kalkulasi yang secara matematis menghasilkan angka hingga i32::MAX , kesalahan apa pun> = 1 ULP dari nol akan membuat Anda keluar batas. Dan itu menjadi jauh lebih buruk setelah kami mempertimbangkan float presisi rendah (IEEE 754 binary16, atau non-standar bfloat16).

Kami tidak berbicara tentang perkalian, kami berbicara tentang pemeran

Nah, konversi floating-point ke integer digunakan hampir secara eksklusif dalam konteks yang sama dengan perkalian: kalkulasi numerik, dan menurut saya ada paralel yang berguna dengan perilaku integer overflow.

Dan ya, hal yang sama berlaku untuk overflow integer: cast int-to-int tidak pernah panik, bahkan ketika mereka meluap ... Menyimpang dari ini untuk cast floating point sangat mengejutkan dan berbahaya paling buruk, karena kebenaran dan keamanan pada kode yang tidak aman dapat bergantung pada operasi tertentu yang tidak panik.

Saya berpendapat bahwa ketidakkonsistenan di sini dibenarkan oleh praktik umum dan tidak akan terlalu mengejutkan. Memotong dan memotong bilangan bulat dengan shift, mask, dan cast - secara efektif menggunakan cast sebagai bentuk bitwise AND plus perubahan ukuran - sangat umum dan memiliki sejarah panjang dalam pemrograman sistem. Itu adalah sesuatu yang saya lakukan setidaknya beberapa kali seminggu. Tetapi selama lebih dari 30 tahun terakhir, saya tidak ingat pernah berharap mendapatkan hasil yang masuk akal dari mengubah NaN, Infinity, atau nilai floating-point di luar jangkauan menjadi integer. (Setiap contoh yang saya ingat telah menjadi bug dalam perhitungan yang menghasilkan nilai.) Jadi saya tidak berpikir kasus integer -> cast integer dan floating-point -> cast integer harus diperlakukan sama. Meskipun demikian, saya dapat memahami bahwa beberapa keputusan telah ditetapkan sebelumnya.

tolong ... usulkan beberapa semantik yang wajar (tidak melibatkan pembekuan) dan tidak panik untuk cast float-to-int

Nah, proposal saya adalah:

  1. Jangan gunakan sakelar kompilasi global yang memengaruhi perubahan signifikan pada semantik. (Saya berasumsi -Zsaturating-float-casts adalah parameter baris perintah atau serupa.) Kode yang bergantung pada perilaku saturasi, katakanlah, akan rusak jika dikompilasi tanpanya. Agaknya kode dengan ekspektasi berbeda tidak dapat digabungkan bersama dalam proyek yang sama. Harus ada beberapa cara lokal untuk kalkulasi untuk menentukan semantik yang diinginkan, mungkin sesuatu seperti pra-RFC ini .
  2. Buat as pemeran memiliki kinerja maksimum secara default, seperti yang diharapkan dari pemeran.

    • Saya pikir ini harus dilakukan melalui pembekuan pada versi LLVM yang mendukungnya dan semantik konversi lainnya pada versi LLVM yang tidak (mis. Pemotongan, saturasi, dll). Saya berharap klaim 'pembekuan dapat membocorkan nilai dari memori sensitif' adalah murni hipotesis. (Atau, jika y = freeze(fptosi(x)) hanya menyisakan y tidak berubah, sehingga membocorkan memori yang tidak diinisialisasi, yang dapat diperbaiki dengan membersihkan y terlebih dahulu.)

    • Jika as akan relatif lambat secara default (misalnya karena jenuh), berikan beberapa cara untuk mendapatkan kinerja maksimum (misalnya metode - tidak aman jika perlu - yang menggunakan freeze).

  1. Jangan gunakan sakelar kompilasi global yang memengaruhi perubahan signifikan pada semantik. (Saya berasumsi -Zsaturating-float-casts adalah parameter baris perintah atau serupa.)

Untuk lebih jelasnya, saya tidak berpikir ada yang tidak setuju. Bendera ini hanya pernah diusulkan sebagai alat jangka pendek untuk mengukur dan menangani regresi kinerja dengan lebih mudah sementara perpustakaan diperbarui untuk memperbaiki regresi tersebut.

Untuk f32-> u16 mungkin benar bahwa Anda memerlukan kesalahan pembulatan yang luar biasa besar untuk keluar dari rentang u16 hanya dari kesalahan pembulatan, tetapi untuk konversi dari f32 ke bilangan bulat 32 bit itu tidak begitu jelas benar. i32 :: MAX tidak dapat direpresentasikan persis dalam f32, angka terdekat yang dapat direpresentasikan adalah 47 off dari i32 :: MAX. Jadi jika Anda memiliki perhitungan yang secara matematis menghasilkan angka hingga i32 :: MAX, kesalahan apa pun> = 1 ULP jauh dari nol akan membuat Anda keluar batas

Ini sedikit keluar dari topik, tetapi katakanlah Anda memiliki algoritme hipotetis yang seharusnya secara matematis menghasilkan f32 hingga 2 ^ 31-1 (tetapi _not_ harus menghasilkan 2 ^ 31 atau lebih tinggi, kecuali mungkin karena kesalahan pembulatan). Sepertinya sudah cacat.

  1. Saya pikir i32 paling dekat yang dapat direpresentasikan sebenarnya 127 di bawah i32 :: MAX, jadi bahkan di dunia yang sempurna tanpa ketidaktepatan floating-point, algoritme yang Anda harapkan menghasilkan nilai hingga 2 ^ 31-1 sebenarnya hanya dapat menghasilkan (legal ) nilai hingga 2 ^ 31-128. Mungkin itu sudah bug. Saya tidak yakin masuk akal untuk membicarakan kesalahan yang diukur dari 2 ^ 31-1 ketika angka itu tidak mungkin untuk direpresentasikan. Anda harus meleset sebanyak 64 dari angka terdekat yang dapat diwakili (dengan mempertimbangkan pembulatan) untuk keluar dari batas. Memang, persentase itu tidak seberapa jika Anda mendekati 2 ^ 32.
  2. Anda tidak boleh mengharapkan diskriminasi nilai yang terpisah 1 (yaitu 2 ^ 31-1 tetapi tidak 2 ^ 31) ketika nilai terdekat yang dapat direpresentasikan berjarak 128. Lebih lanjut, hanya 3,5% dari i32s yang dapat direpresentasikan sebagai f32 (dan <2% dari u32s). Anda tidak bisa mendapatkan kisaran semacam itu sambil juga memiliki presisi semacam itu dengan f32. Algoritme terdengar seperti menggunakan alat yang salah untuk pekerjaan itu.

Saya kira setiap algoritma praktis yang melakukan apa yang Anda gambarkan akan sangat terkait dengan bilangan bulat. Misalnya, jika Anda mengonversi i32 acak ke f32 dan sebaliknya, ini bisa gagal jika di atas i32 :: MAX-64. Tapi itu menurunkan presisi Anda dan saya tidak tahu mengapa Anda melakukan hal seperti itu. Hampir semua perhitungan i32 -> f32 -> i32 yang mengeluarkan rentang i32 penuh dapat diekspresikan lebih cepat dan lebih akurat dengan matematika integer, dan jika tidak, ada f64.

Bagaimanapun, meskipun saya yakin mungkin untuk menemukan beberapa kasus di mana algoritme yang melakukan konversi di luar batas akan diperbaiki oleh saturasi, menurut saya hal itu jarang terjadi - cukup jarang sehingga kami tidak memperlambat _semua_ konversi untuk mengakomodasi mereka . Dan, saya berpendapat bahwa algoritme semacam itu mungkin masih cacat dan harus diperbaiki. Dan jika algoritme tidak dapat diperbaiki, algoritme selalu dapat melakukan pemeriksaan batas sebelum konversi yang mungkin di luar batas (atau memanggil fungsi konversi jenuh). Dengan cara itu biaya untuk membatasi hasil dibayarkan hanya jika diperlukan.

PS Selamat Thanksgiving yang terlambat untuk semua orang.

Untuk lebih jelasnya, saya tidak berpikir ada yang tidak setuju. Bendera ini hanya pernah diusulkan sebagai alat jangka pendek ...

Saya terutama mengacu pada proposal untuk mengganti -Zsaturated-float-cast dengan -Zunsaturated-float-cast. Bahkan jika saturasi menjadi default, flag seperti -Zunsaturated-float-casts tampaknya buruk untuk kompatibilitas, tetapi jika itu juga dimaksudkan untuk sementara, oke, tidak masalah. :-)

Bagaimanapun, saya yakin semua orang berharap saya telah cukup banyak bicara tentang masalah ini - termasuk saya sendiri. Saya tahu tim Rust secara tradisional berusaha menyediakan banyak cara untuk melakukan sesuatu sehingga orang dapat membuat pilihan mereka sendiri antara kinerja dan keselamatan. Saya telah membagikan perspektif saya dan percaya bahwa kalian akan menemukan solusi yang baik pada akhirnya. Hati hati!

Saya berasumsi bahwa -Zunsaturated-float-casts hanya akan ada sementara, dan akan dihapus di beberapa titik. Bahwa itu adalah opsi -Z (hanya tersedia di Nightly) daripada -C menyarankannya, setidaknya.

Untuk apa nilainya, saturasi dan UB bukan satu-satunya pilihan. Kemungkinan lain adalah mengubah LLVM untuk menambahkan varian fptosi yang menggunakan perilaku overflow asli CPU - yaitu, perilaku overflow tidak akan portabel di seluruh arsitektur, tetapi akan didefinisikan dengan baik pada arsitektur tertentu ( misalnya mengembalikan 0x80000000 pada x86), dan itu tidak akan pernah mengembalikan racun atau memori yang tidak diinisialisasi. Bahkan jika default menjadi jenuh, alangkah baiknya jika itu sebagai pilihan. Lagipula, meskipun cast jenuh memiliki overhead yang melekat pada arsitektur yang bukan perilaku defaultnya, "lakukan apa yang dilakukan CPU" hanya memiliki overhead jika hal itu menghambat beberapa pengoptimalan compiler tertentu. Saya tidak yakin, tetapi saya menduga bahwa pengoptimalan apa pun yang diaktifkan dengan memperlakukan float-to-int overflow karena UB adalah niche dan tidak berlaku untuk sebagian besar kode.

Yang mengatakan, satu masalah mungkin jika arsitektur memiliki beberapa instruksi float-to-int yang mengembalikan nilai berbeda saat overflow. Dalam kasus ini, compiler yang memilih satu atau yang lain akan mempengaruhi perilaku yang dapat diamati, yang tidak menjadi masalah dengan sendirinya, tetapi mungkin menjadi masalah jika satu fptosi diduplikasi dan dua salinan akhirnya berperilaku berbeda. Tetapi saya tidak yakin apakah perbedaan semacam ini benar-benar ada pada arsitektur populer mana pun. Dan masalah yang sama berlaku untuk pengoptimalan floating-point lainnya, termasuk kontraksi floating-point ...

const fn (miri) telah memilih perilaku cast jenuh sejak Rust 1.26 meskipun (dengan asumsi kita ingin hasil CTFE dan RTFE konsisten) (sebelum 1.26 cast waktu kompilasi yang meluap kembali 0)

const fn g(a: f32) -> i32 {
    a as i32
}

const Q: i32 = g(1e+12);

fn main() {
    println!("{}", Q); // always 2147483647
    println!("{}", g(1e+12)); // unspecified value, but always 2147483647 in miri
}

Miri / CTFE menggunakan metode apfloat to_u128 / to_i128 untuk melakukan konversi. Tetapi saya tidak yakin apakah ini adalah jaminan yang stabil - terutama karena tampaknya telah berubah sebelumnya (yang tidak kami sadari saat menerapkan hal itu di Miri).

Saya pikir kita bisa menyesuaikan ini dengan codegen apa pun yang akhirnya diambil. Tetapi fakta bahwa apfloat LLVM (di mana versi Rust adalah port langsung) menggunakan saturasi adalah indikator yang baik bahwa ini adalah semacam "default yang wajar".

Salah satu solusi untuk perilaku yang dapat diamati adalah dengan memilih secara acak salah satu metode yang tersedia pada waktu pembuatan kompilator atau biner yang dihasilkan.
Kemudian memiliki fungsi seperti a.saturating_cast::<i32>() untuk pengguna yang membutuhkan perilaku tertentu.

@dns_dns

Kata "secara acak" akan berlawanan dengan upaya untuk mendapatkan build yang dapat direproduksi dan, jika dapat diprediksi dalam versi kompiler, Anda tahu seseorang akan memutuskan bergantung padanya untuk tidak berubah.

IMO apa yang dijelaskan @comex (bukan novel untuk utas ini IIRC, semua yang lama adalah baru lagi) ini adalah pilihan terbaik berikutnya jika kita tidak ingin kejenuhan. Perhatikan bahwa kami bahkan tidak memerlukan perubahan LLVM untuk mengujinya, kami dapat menggunakan asm inline (pada arsitektur di mana instruksi tersebut ada).

Yang mengatakan, satu masalah mungkin jika arsitektur memiliki beberapa instruksi float-to-int yang mengembalikan nilai berbeda saat overflow. Dalam kasus ini, compiler yang memilih satu atau yang lain akan mempengaruhi perilaku yang dapat diamati, yang tidak menjadi masalah dengan sendirinya, tetapi mungkin menjadi masalah jika satu fptosi diduplikasi dan dua salinan akhirnya berperilaku berbeda.

IMO non-determinisme seperti itu akan melepaskan hampir semua keuntungan praktis dibandingkan dengan freeze . Jika kita melakukan ini, kita harus memilih satu instruksi per arsitektur dan menaatinya, baik untuk determinisme dan agar program benar-benar dapat mengandalkan perilaku instruksi ketika itu masuk akal bagi mereka. Jika hal ini tidak mungkin dilakukan pada beberapa arsitektur, maka kami dapat kembali ke implementasi perangkat lunak (tetapi seperti yang Anda katakan, ini sepenuhnya hipotetis).

Ini paling mudah jika kita tidak mendelegasikan keputusan ini ke LLVM tetapi mengimplementasikan operasi dengan asm inline sebagai gantinya. Yang secara tersembunyi juga akan jauh lebih mudah daripada mengubah LLVM untuk menambahkan intrinsik baru dan menurunkannya di setiap backend.

@tokopedia

[...] Yang secara rahasia juga akan jauh lebih mudah daripada mengubah LLVM untuk menambahkan intrinsik baru dan menurunkannya di setiap backend.

Selain itu, LLVM tidak terlalu senang tentang intrinsik dengan semantik yang bergantung pada target:

Namun, jika Anda ingin pemeran ditentukan dengan baik, Anda harus menentukan perilakunya. "Lakukan sesuatu dengan cepat" bukanlah definisi yang sebenarnya, dan saya tidak percaya bahwa kita harus memberikan perilaku yang bergantung pada target pada konstruksi yang tidak bergantung pada target.

https://groups.google.com/forum/m/#!msg/llvm -dev / cgDFaBmCnDQ / CZAIMj4IBAA

Aku akan tag ulang # 10184 sebagai semata-mata T-lang: Saya pikir masalah yang harus diselesaikan ada pilihan semantik tentang apa float as int sarana

(yaitu apakah kita ingin membiarkannya memiliki semantik panik atau tidak, apakah kita bersedia untuk membiarkannya memiliki spesifikasi bawah berbasis freeze atau tidak, dll)

ini adalah pertanyaan yang lebih baik ditujukan untuk tim T-lang, bukan T-compiler, setidaknya untuk diskusi awal, IMO

Baru saja mengalami masalah ini menghasilkan hasil yang _irreproducible antara run_ bahkan tanpa kompilasi ulang. Operator as tampaknya mengambil beberapa sampah dari memori dalam kasus seperti itu.

Saya sarankan untuk sepenuhnya melarang penggunaan as untuk "float as int" dan mengandalkan metode pembulatan tertentu sebagai gantinya. Alasan: as tidak merugikan untuk tipe lain.

Penalaran: karena tidak merugikan untuk jenis lainnya.

Apakah itu?

Berdasarkan Rust Book saya dapat berasumsi bahwa itu lossless hanya dalam kasus-kasus tertentu (yaitu dalam kasus di mana From<X> ditentukan untuk tipe Y), yaitu Anda dapat mentransmisikan u8 menjadi u32 menggunakan From , tetapi tidak sebaliknya.

Yang saya maksud dengan "tidak rugi" adalah memberikan nilai-nilai yang cukup kecil agar pas. Contoh: 1_u64 as u8 tidak lossy, jadi u8 as u64 as u8 tidak lossy. Untuk float tidak ada definisi sederhana dari "cocok" karena 20000000000000000000000000000_u128 as f32 tidak lossy sedangkan 20000001_u32 as f32 adalah, jadi baik float as int atau int as float tidak lossless.

256u64 as u8 adalah kerugian.

Tapi <anything>_u8 as u64 as u8 bukan.

Saya pikir kehilangan adalah normal dan diharapkan dengan pemain, dan bukan masalah. Memotong integer dengan cast (misalnya u32 as u8 ) adalah operasi umum dengan makna yang dipahami dengan baik yang konsisten di semua bahasa mirip C yang saya ketahui (setidaknya pada arsitektur yang menggunakan representasi integer komplemen dua, yang mana pada dasarnya semuanya hari ini). Konversi floating-point yang valid (yaitu di mana bagian integral sesuai dengan tujuan) juga memiliki semantik yang dipahami dengan baik dan disepakati. 1.6 as u32 lossy, tetapi semua bahasa mirip C yang saya ketahui setuju bahwa hasilnya harus 1. Kedua kasus tersebut mengalir keluar dari konsensus di antara produsen perangkat keras tentang bagaimana konversi tersebut harus bekerja dan konvensi di C -seperti bahasa yang ditransmisikan harus berkinerja tinggi, jenis operator "Saya tahu apa yang saya lakukan".

Jadi saya tidak berpikir kita harus menganggap itu bermasalah dengan cara yang sama seperti konversi floating-point yang tidak valid, karena itu tidak memiliki semantik yang disepakati dalam bahasa seperti C atau dalam perangkat keras (tetapi biasanya menghasilkan status kesalahan atau pengecualian perangkat keras) dan hampir selalu menunjukkan bug (menurut pengalaman saya) dan oleh karena itu biasanya tidak ada dalam kode yang benar.

Baru saja mengalami masalah ini menghasilkan hasil yang tidak dapat direproduksi di antara proses bahkan tanpa kompilasi ulang. Operator as tampaknya mengambil beberapa sampah dari memori dalam kasus seperti itu.

Secara pribadi saya pikir itu baik-baik saja selama itu hanya terjadi ketika konversi tidak valid dan tidak memiliki efek samping selain menghasilkan nilai sampah. Jika Anda benar-benar membutuhkan konversi yang tidak valid dalam sebuah kode, Anda dapat menangani kasus tidak valid itu sendiri dengan semantik apa pun yang menurut Anda seharusnya ada.

dan tidak memiliki efek samping selain menghasilkan nilai sampah

Efek sampingnya adalah, nilai sampah berasal dari suatu tempat di memori dan mengungkapkan beberapa data (mungkin sensitif). Mengembalikan nilai "acak" yang dihitung hanya dari float itu sendiri akan baik-baik saja, tetapi perilaku saat ini tidak.

Konversi floating-point yang valid (yaitu di mana bagian integral sesuai dengan tujuan) juga memiliki semantik yang dipahami dengan baik dan disepakati.

Apakah ada kasus penggunaan konversi float-to-int yang tidak disertai dengan eksplisit trunc() , round() , floor() atau ceil() ? Strategi pembulatan as ini "tidak ditentukan", membuat as hampir tidak dapat digunakan untuk nomor yang tidak dibulatkan. Saya percaya bahwa dalam banyak kasus, orang yang menulis x as u32 sebenarnya menginginkan x.round() as u32 .

Saya pikir kehilangan adalah normal dan diharapkan dengan pemain, dan bukan masalah.

Saya setuju, tetapi hanya jika kerugian mudah ditebak. Untuk bilangan bulat, kondisi konversi lossy sudah jelas. Untuk pelampung mereka tidak jelas. Mereka adalah lossless untuk beberapa angka yang sangat besar tetapi lossy untuk beberapa angka yang lebih kecil, bahkan jika mereka bulat. Preferensi pribadi saya adalah memiliki dua operator berbeda untuk konversi lossy dan lossless untuk menghindari terjadinya konversi lossy karena kesalahan, tetapi saya juga baik-baik saja dengan hanya satu operator asalkan saya dapat mengetahui apakah itu lossy atau tidak.

Efek sampingnya adalah, nilai sampah berasal dari suatu tempat di memori dan mengungkapkan beberapa data (mungkin sensitif).

Saya berharap itu hanya meninggalkan tujuan tidak berubah atau apa pun, tetapi jika itu benar-benar masalah, itu bisa menjadi nol terlebih dahulu.

Apakah ada kasus penggunaan konversi float-to-int yang tidak disertai dengan trunc (), round (), floor () atau ceil () eksplisit? Strategi pembulatan saat ini dari as adalah "tidak ditentukan", membuatnya hampir tidak dapat digunakan untuk nomor yang tidak dibulatkan.

Jika strategi pembulatan benar-benar tidak ditentukan maka itu akan mengejutkan saya, dan saya setuju bahwa operator hampir tidak berguna kecuali Anda sudah memberikannya bilangan bulat. Saya berharap itu memotong ke nol.

Saya percaya bahwa dalam banyak kasus, orang yang menulis x as u32 sebenarnya menginginkan x.round() as u32 .

Saya kira itu tergantung pada domainnya, tetapi saya mengharapkan x.trunc() as u32 juga cukup umum.

Saya setuju, tetapi hanya jika kerugian mudah ditebak.

Saya sangat setuju. Apakah 1.6 as u32 menjadi 1 atau 2 tidak boleh tidak ditentukan, misalnya.

https://doc.rust-lang.org/nightly/reference/expressions/operator-expr.html#type -cast-expression

Casting dari float ke integer akan membulatkan float menuju nol
CATATAN: saat ini ini akan menyebabkan Perilaku Tidak Terdefinisi jika nilai yang dibulatkan tidak dapat diwakili oleh jenis bilangan bulat target. Ini termasuk Inf dan NaN. Ini adalah bug dan akan diperbaiki.

Tautan catatan di sini.

Pembulatan nilai-nilai yang "cocok" didefinisikan dengan baik, bukan itu masalahnya. Utas ini sudah panjang, alangkah baiknya tidak menganggapnya berspekulasi tentang fakta-fakta yang sudah ditetapkan dan didokumentasikan. Terima kasih.

Yang masih harus diputuskan adalah bagaimana mendefinisikan f as $Int dalam kasus berikut:

  • f.trunc() > $Int::MAX (termasuk positif tak terbatas)
  • f.trunc() < $Int::MIN (termasuk tak terbatas negatif)
  • f.is_nan()

Salah satu opsi yang sudah diterapkan dan tersedia di Nightly dengan tanda compiler -Z saturating-casts adalah mendefinisikan mereka untuk mengembalikan masing-masing: $Int::MAX , $Int::MIN , dan nol. Tapi masih mungkin memilih beberapa perilaku lain.

Pandangan saya adalah bahwa perilaku tersebut pasti harus deterministik dan mengembalikan beberapa nilai bilangan bulat (daripada panik misalnya), tetapi nilai pastinya tidak terlalu penting dan pengguna yang peduli dengan kasus ini sebaiknya menggunakan metode konversi yang saya usulkan secara terpisah kepada kita. tambahkan: https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Saya kira itu tergantung pada domainnya, tetapi saya berharap x.trunc() as u32 juga cukup umum.

Benar. Secara umum, x.anything() as u32 , kemungkinan besar round() , tetapi bisa juga trunc() , floor() , ceil() . Hanya x as u32 tanpa menentukan prosedur pembulatan konkret kemungkinan besar merupakan kesalahan.

Pandangan saya adalah bahwa perilaku tersebut pasti harus deterministik dan mengembalikan beberapa nilai integer (daripada panik misalnya), tetapi nilai pastinya tidak terlalu penting

Saya pribadi baik-baik saja bahkan dengan nilai "tidak ditentukan" asalkan tidak bergantung pada apa pun kecuali mengambang itu sendiri dan, yang paling penting, tidak mengekspos isi register dan memori yang tidak terkait.

Satu opsi yang sudah diterapkan dan tersedia di Nightly dengan tanda compiler -Z saturating-casts adalah dengan mendefinisikan mereka untuk mengembalikan masing-masing: $Int::MAX , $Int::MIN , dan nol. Tapi masih mungkin memilih beberapa perilaku lain.

Perilaku yang saya harapkan untuk f.trunc() > $Int::MAX dan f.trunc() < $Int::MIN adalah sama seperti ketika bilangan titik mengambang imajiner diubah menjadi bilangan bulat berukuran tak terbatas dan kemudian bit signifikan terendah yang dikembalikan ( seperti dalam konversi jenis integer). Secara teknis ini akan menjadi beberapa bit signifikan yang bergeser ke kiri tergantung pada eksponen (untuk bilangan positif, bilangan negatif membutuhkan inversi sesuai dengan komplemen keduanya).

Jadi misalnya saya mengharapkan angka yang sangat besar untuk dikonversi menjadi 0 .

Tampaknya lebih sulit / lebih sewenang-wenang untuk menentukan ke mana infinity dan NaN dikonversi.

@CryZe jadi jika saya membacanya dengan benar, itu cocok dengan -Z saturating-casts (dan apa yang sudah diterapkan Miri)?

@RalfJah Benar.

Luar biasa, saya akan menyalin https://github.com/WebAssembly/testsuite/blob/master/conversions.wast (dengan jebakan diganti dengan hasil yang ditentukan) ke rangkaian pengujian Miri. :)

@RalfJung Harap perbarui ke versi terbaru

@sunfishcode terima kasih telah memperbarui! Saya harus menerjemahkan tes ke Rust jadi saya masih harus mengganti banyak hal. ;)

Apakah pengujian _sat berbeda dalam hal nilai yang diuji? (EDIT: ada komentar di sana yang mengatakan nilainya sama.) Untuk pemeran yang jenuh Rust, saya mengambil banyak dari nilai-nilai ini dan menambahkannya di https://github.com/rust-lang/miri/pull/1321. Saya terlalu malas untuk melakukannya untuk mereka semua ... tapi menurut saya ini berarti tidak ada yang perlu diubah saat ini dengan file yang diperbarui.

Untuk intrinsik UB, perangkap di sisi wasm kemudian harus menjadi tes gagal-kompilasi di Miri saya pikir.

Nilai masukan semuanya sama, satu-satunya perbedaan adalah bahwa operator _sat memiliki nilai keluaran yang diharapkan pada masukan di mana operator perangkap mengharapkan perangkap.

Tes untuk Miri (dan juga mesin Rust CTFE) telah ditambahkan di https://github.com/rust-lang/miri/pull/1321. Saya secara lokal memeriksa bahwa rustc -Zmir-opt-level=0 -Zsaturating-float-casts juga lolos tes dalam file itu.
Saya sekarang juga menerapkan intrinsik yang tidak dicentang di Miri, lihat https://github.com/rust-lang/miri/pull/1325.

Saya telah memposting https://github.com/rust-lang/rust/pull/71269#issuecomment -615537137 yang mendokumentasikan keadaan saat ini seperti yang saya pahami dan bahwa PR juga bergerak untuk menstabilkan perilaku bendera -Z yang jenuh.

Mengingat panjang utas ini, saya pikir jika orang merasa bahwa saya melewatkan sesuatu dalam komentar itu, saya akan mengarahkan komentar ke PR, atau, jika kecil, silakan ping saya di Zulip atau Discord (simulacrum) dan saya bisa perbaiki segalanya untuk menghindari kebisingan yang tidak perlu pada utas PR.

Saya berharap seseorang di tim bahasa kemungkinan akan segera memulai proposal FCP pada PR tersebut, dan menggabungkannya akan secara otomatis menutup masalah ini :)

Apakah ada rencana untuk konversi yang diperiksa? Sesuatu seperti fn i32::checked_from(f64) -> Result<i32, DoesntFit> ?

Anda harus mempertimbangkan apa yang seharusnya dikembalikan i32::checked_from(4.5) .

Apakah halaman ini membantu?
0 / 5 - 0 peringkat