Mengapa compiler C ++ tidak mengoptimalkan tugas boolean bersyarat ini sebagai tugas tanpa syarat?

117

Pertimbangkan fungsi berikut:

void func(bool& flag)
{
    if(!flag) flag=true;
}

Menurut saya, jika flag memiliki nilai boolean yang valid, ini akan setara dengan pengaturan tanpa syarat true, seperti ini:

void func(bool& flag)
{
    flag=true;
}

Namun baik gcc maupun clang tidak mengoptimalkannya dengan cara ini - keduanya menghasilkan yang berikut pada -O3tingkat pengoptimalan:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

Pertanyaan saya adalah: apakah hanya karena kodenya terlalu kasus khusus untuk dirawat untuk dioptimalkan, atau adakah alasan bagus mengapa pengoptimalan seperti itu tidak diinginkan, mengingat itu flagbukan referensi volatile? Tampaknya satu-satunya alasan yang mungkin adalah bahwa flagentah bagaimana bisa memiliki nilai non- trueatau falsetanpa perilaku yang tidak terdefinisi pada saat membacanya, tetapi saya tidak yakin apakah ini mungkin.

Ruslan
sumber
8
Apakah Anda memiliki bukti bahwa ini adalah "pengoptimalan"?
David Schwartz
1
@ 200_success Saya tidak berpikir bahwa menempatkan baris kode dengan markup yang tidak berfungsi sebagai judul adalah hal yang baik. Jika Anda menginginkan judul yang lebih spesifik, boleh saja tetapi pilih kalimat bahasa Inggris , dan coba hindari kode di dalamnya (misalnya, mengapa kompiler tidak mengoptimalkan penulisan bersyarat ke penulisan tanpa syarat ketika mereka dapat membuktikannya setara? Atau serupa). Juga karena backticks tidak dirender, jangan gunakan mereka dalam judul bahkan jika Anda menggunakan kode.
Bakuriu
2
@Ruslan, meskipun tampaknya tidak melakukan pengoptimalan ini untuk fungsi itu sendiri, ketika dapat menyebariskan kode, tampaknya melakukannya untuk versi sebaris. Seringkali hanya menghasilkan konstanta waktu kompilasi 1yang digunakan. godbolt.org/g/swe0tc
Evan Teran

Jawaban:

102

Ini dapat berdampak negatif pada kinerja program karena pertimbangan koherensi cache . Menulis ke flagsetiap kali func()dipanggil akan mengotori baris cache yang memuatnya. Ini akan terjadi terlepas dari fakta bahwa nilai yang ditulis sama persis dengan bit yang ditemukan di alamat tujuan sebelum penulisan.


EDIT

hvd telah memberikan alasan bagus lainnya yang mencegah pengoptimalan seperti itu. Ini adalah argumen yang lebih meyakinkan terhadap pengoptimalan yang diusulkan, karena dapat mengakibatkan perilaku tidak terdefinisi, sedangkan jawaban (asli) saya hanya membahas aspek kinerja.

Setelah sedikit refleksi lagi, saya dapat mengusulkan satu contoh lagi mengapa kompiler harus sangat dilarang - kecuali mereka dapat membuktikan bahwa transformasi aman untuk konteks tertentu - dari memperkenalkan penulisan tanpa syarat. Pertimbangkan kode ini:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

Dengan penulisan tanpa syarat dalam func()hal ini pasti memicu perilaku tidak terdefinisi (menulis ke memori hanya-baca akan menghentikan program, bahkan jika efek penulisan sebaliknya akan menjadi tanpa operasi).

Leon
sumber
7
Ini juga dapat berdampak positif pada kinerja karena Anda menyingkirkan cabang. Jadi menurut saya kasus khusus ini tidak bermakna untuk dibahas tanpa sistem yang sangat spesifik.
Lundin
3
Ketetapan perilaku @Yakk tidak dipengaruhi oleh platform target. Mengatakan bahwa itu akan menghentikan program itu tidak benar, tetapi UB sendiri dapat memiliki konsekuensi yang jauh, termasuk setan hidung.
John Dvorak
16
@Yakk Itu tergantung pada apa yang dimaksud dengan "memori hanya-baca". Tidak, ini bukan dalam chip ROM, tetapi sering kali di bagian yang dimuat ke halaman yang tidak mengaktifkan akses tulis, dan Anda akan mendapatkan misalnya sinyal SIGSEGV atau pengecualian STATUS_ACCESS_VIOLATION saat Anda mencoba menulisnya.
Random832
5
"ini pasti memicu perilaku yang tidak terdefinisi". Tidak. Perilaku tak terdefinisi adalah properti mesin abstrak. Ini adalah apa yang dikatakan kode yang menentukan apakah UB ada. Kompiler tidak dapat menyebabkannya (meskipun jika buggy, kompilator dapat menyebabkan program berperilaku tidak benar).
Eric M Schmidt
7
Ini adalah casting-away dari constuntuk meneruskan ke sebuah fungsi yang dapat mengubah data yang merupakan sumber dari perilaku yang tidak ditentukan, bukan penulisan tanpa syarat. Dokter, sakit saat saya melakukan ini ....
Spencer
48

Selain dari jawaban Leon tentang kinerja:

Misalkan flagadalah true. Misalkan dua utas terus memanggil func(flag). Fungsi seperti yang tertulis, dalam hal ini, tidak menyimpan apa pun ke flag, jadi ini harus aman untuk thread. Dua utas mengakses memori yang sama, tetapi hanya untuk membacanya. Menyetel tanpa syarat flagke trueberarti dua utas berbeda akan menulis ke memori yang sama. Ini tidak aman, ini tidak aman meskipun data yang ditulis identik dengan data yang sudah ada.


sumber
9
Saya pikir ini adalah hasil dari penerapan [intro.races]/21.
Griwes
10
Sangat menarik. Jadi saya membaca ini sebagai: Kompilator tidak pernah diizinkan untuk "mengoptimalkan" dalam operasi tulis di mana mesin abstrak tidak akan memilikinya.
Martin Ba
3
@Martin Kebanyakan begitu. Tetapi jika kompiler dapat membuktikan bahwa itu tidak masalah, misalnya karena dapat membuktikan bahwa tidak ada thread lain yang mungkin memiliki akses ke variabel tertentu, maka itu mungkin baik-baik saja.
13
Ini hanya tidak aman jika sistem yang ditargetkan oleh compiler membuatnya tidak aman . Saya tidak pernah mengembangkan sistem di mana menulis 0x01ke byte yang sudah 0x01menyebabkan perilaku "tidak aman". Pada sistem dengan akses memori kata atau dword itu akan; tetapi pengoptimal harus menyadari hal ini. Pada PC atau OS ponsel modern, tidak ada masalah yang terjadi. Jadi ini bukan alasan yang sah.
Yakk - Adam Nevraumont
4
@Yakk Sebenarnya, dengan berpikir lebih jauh, menurut saya ini benar, bahkan untuk prosesor biasa. Saya pikir Anda benar ketika CPU dapat menulis ke memori secara langsung, tetapi misalkan flagada di halaman copy-on-write. Sekarang, di tingkat CPU, perilaku mungkin ditentukan (kesalahan halaman, biarkan OS menanganinya), tetapi di tingkat OS, mungkin masih belum ditentukan, bukan?
13

Saya tidak yakin tentang perilaku C ++ di sini, tetapi di C memori mungkin berubah karena jika memori berisi nilai bukan nol selain 1, itu akan tetap tidak berubah dengan pemeriksaan, tetapi diubah ke 1 dengan pemeriksaan.

Tetapi karena saya tidak terlalu fasih dalam C ++, saya tidak tahu apakah situasi ini mungkin.

glglgl
sumber
Apakah ini masih benar tentang _Bool?
Ruslan
5
Di C, jika memori berisi nilai yang menurut ABI tidak valid untuk tipenya, maka itu adalah representasi trap, dan membaca representasi trap adalah perilaku yang tidak ditentukan. Dalam C ++, ini hanya bisa terjadi saat membaca objek yang tidak diinisialisasi, dan membaca objek yang tidak diinisialisasi yaitu UB. Tetapi jika Anda dapat menemukan ABI yang mengatakan bahwa nilai bukan nol valid untuk tipe bool/ _Booldan mean true, maka dalam ABI tertentu itu, Anda mungkin benar.
1
@Ruslan Dengan kompiler yang menggunakan Itanium ABI, dan pada prosesor ARM, C _Booldan C ++ booladalah tipe yang sama, atau tipe kompatibel yang mengikuti aturan yang sama. Dengan MSVC, mereka memiliki ukuran dan keselarasan yang sama, tetapi tidak ada pernyataan resmi tentang apakah mereka menggunakan aturan yang sama.
Waktu Justin - Kembalikan Monica
1
@JustinTime: C <stdbool.h>menyertakan typedef _Bool bool; Dan ya, pada x86 (setidaknya di Sistem V ABI), bool/ _Boolharus 0 atau 1, dengan bit atas byte dihapus. Saya rasa penjelasan ini tidak masuk akal.
Peter Cordes
1
@JustinTime: Itu benar, saya seharusnya hanya menunjukkan bahwa itu pasti memiliki semantik yang sama di semua ragam x86 dari System V ABI, yang menjadi pokok pertanyaan ini. (Saya tahu karena arg pertama untuk funcditeruskan dalam RDI, sedangkan Windows akan menggunakan RDX).
Peter Cordes