Melihat kode ini:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
Entri array mana yang diperbarui? 0 atau 2?
Apakah ada bagian dalam spesifikasi C yang menunjukkan prioritas operasi dalam kasus khusus ini?
c
language-lawyer
order-of-execution
Jiminion
sumber
sumber
clang
sehingga kode ini memicu IMHO peringatan.Jawaban:
Orde Operan Kiri dan Kanan
Untuk melakukan penugasan dalam
arr[global_var] = update_three(2)
, implementasi C harus mengevaluasi operan dan, sebagai efek samping, memperbarui nilai yang disimpan dari operan kiri. C 2018 6.5.16 (yaitu tentang penugasan) paragraf 3 memberitahu kita tidak ada urutan di operan kiri dan kanan:Ini berarti implementasi C bebas untuk menghitung lvalue
arr[global_var]
terlebih dahulu (dengan menghitung lvalue, kami bermaksud mencari tahu apa yang dimaksud dengan ungkapan ini), kemudian mengevaluasiupdate_three(2)
, dan akhirnya menetapkan nilai yang terakhir ke yang sebelumnya; atau untuk mengevaluasiupdate_three(2)
dulu, lalu menghitung nilai, lalu menugaskan yang pertama ke yang terakhir; atau untuk mengevaluasi nilai danupdate_three(2)
dalam beberapa cara yang dicampur dan kemudian menetapkan nilai yang tepat untuk nilai kiri.Dalam semua kasus, penugasan nilai ke nilai harus datang terakhir, karena 6.5.16 3 juga mengatakan:
Pelanggaran Sequencing
Beberapa mungkin merenungkan tentang perilaku tidak terdefinisi karena menggunakan
global_var
dan memperbaruinya secara terpisah yang melanggar 6.5 2, yang mengatakan:Cukup umum bagi banyak praktisi C bahwa perilaku ekspresi seperti
x + x++
tidak didefinisikan oleh standar C karena mereka berdua menggunakan nilaix
dan secara terpisah memodifikasinya dalam ekspresi yang sama tanpa diurutkan. Namun, dalam hal ini, kami memiliki panggilan fungsi, yang menyediakan beberapa urutan.global_var
digunakanarr[global_var]
dan diperbarui dalam pemanggilan fungsiupdate_three(2)
.6.5.2.2 10 memberitahu kita ada titik sekuens sebelum fungsi dipanggil:
Di dalam fungsi,
global_var = val;
adalah ekspresi penuh , dan begitu juga3
direturn 3;
, per 6.8 4:Lalu ada titik berurutan antara dua ekspresi ini, sekali lagi per 6,8 4:
Dengan demikian, implementasi C dapat mengevaluasi
arr[global_var]
pertama dan kemudian melakukan pemanggilan fungsi, dalam hal ini ada titik urutan di antara mereka karena ada satu sebelum pemanggilan fungsi, atau dapat mengevaluasiglobal_var = val;
dalam pemanggilan fungsi dan kemudianarr[global_var]
, dalam hal ini ada titik urutan di antara mereka karena ada satu setelah ekspresi penuh. Jadi perilakunya tidak ditentukan — salah satu dari kedua hal itu dapat dievaluasi terlebih dahulu — tetapi tidak terdefinisi.sumber
Hasil di sini tidak ditentukan .
Sementara urutan operasi dalam ekspresi, yang menentukan bagaimana subekspresi dikelompokkan, didefinisikan dengan baik, urutan evaluasi tidak ditentukan. Dalam hal ini berarti
global_var
dapat dibaca terlebih dahulu atau ajakan untukupdate_three
terjadi lebih dulu, tetapi tidak ada cara untuk mengetahui yang mana.Tidak ada perilaku tidak terdefinisi di sini karena panggilan fungsi memperkenalkan titik urutan, seperti halnya setiap pernyataan dalam fungsi termasuk yang memodifikasi
global_var
.Untuk memperjelas, standar C mendefinisikan perilaku yang tidak terdefinisi dalam bagian 3.4.3 sebagai:
dan mendefinisikan perilaku yang tidak ditentukan dalam bagian 3.4.4 sebagai:
Standar menyatakan bahwa urutan evaluasi argumen fungsi tidak ditentukan, yang dalam hal ini berarti bahwa
arr[0]
akan diatur ke 3 atauarr[2]
diatur ke 3.sumber
Saya mencoba dan mendapat entri 0 diperbarui.
Namun menurut pertanyaan ini: akankah sisi kanan ekspresi selalu dievaluasi terlebih dahulu
Urutan evaluasi tidak ditentukan dan tidak dilakukan. Jadi saya pikir kode seperti ini harus dihindari.
sumber
Karena tidak masuk akal untuk memancarkan kode untuk suatu tugas sebelum Anda memiliki nilai untuk ditugaskan, kebanyakan kompiler C pertama-tama akan memancarkan kode yang memanggil fungsi dan menyimpan hasilnya di suatu tempat (daftar, susun, dll.), Kemudian mereka akan memancarkan kode yang menulis nilai ini ke tujuan akhirnya dan oleh karena itu mereka akan membaca variabel global setelah diubah. Mari kita sebut ini "tatanan alami", tidak didefinisikan oleh standar apa pun tetapi oleh logika murni.
Namun dalam proses optimasi, kompiler akan mencoba menghilangkan langkah menengah menyimpan sementara nilai di suatu tempat dan mencoba untuk menulis hasil fungsi secara langsung ke tujuan akhir dan dalam kasus itu, mereka sering harus membaca indeks terlebih dahulu , misalnya ke register, untuk dapat langsung memindahkan hasil fungsi ke array. Ini dapat menyebabkan variabel global dibaca sebelum diubah.
Jadi pada dasarnya ini adalah perilaku yang tidak terdefinisi dengan properti yang sangat buruk sehingga kemungkinan hasilnya akan berbeda, tergantung pada apakah optimasi dilakukan dan seberapa agresif optimasi ini. Adalah tugas Anda sebagai pengembang untuk menyelesaikan masalah tersebut dengan mengode:
atau coding:
Sebagai aturan praktis yang baik: Kecuali jika variabel global
const
(atau tidak tetapi Anda tahu bahwa tidak ada kode yang akan mengubahnya sebagai efek samping), Anda tidak boleh menggunakannya secara langsung dalam kode, seperti di lingkungan multi-threaded, bahkan ini dapat didefinisikan:Karena kompiler dapat membacanya dua kali dan utas lainnya dapat mengubah nilai di antara keduanya. Namun, sekali lagi, pengoptimalan pasti akan menyebabkan kode hanya membacanya sekali, jadi Anda mungkin lagi memiliki hasil berbeda yang sekarang juga tergantung pada waktu utas lainnya. Dengan demikian Anda akan memiliki sakit kepala yang jauh lebih sedikit jika Anda menyimpan variabel global ke variabel stack sementara sebelum digunakan. Perlu diingat jika kompiler berpikir ini aman, kemungkinan besar akan mengoptimalkan bahkan itu jauh dan alih-alih menggunakan variabel global secara langsung, jadi pada akhirnya, itu mungkin tidak membuat perbedaan dalam kinerja atau penggunaan memori.
(Untuk berjaga-jaga jika seseorang bertanya mengapa ada orang yang melakukan
x + 2 * x
alih - alih3 * x
- pada beberapa penambahan CPU sangat cepat dan begitu juga perkalian dengan kekuatan dua karena kompiler akan mengubahnya menjadi bit shift (2 * x == x << 1
), namun perkalian dengan angka sewenang-wenang bisa sangat lambat , jadi alih-alih mengalikannya dengan 3, Anda mendapatkan kode yang jauh lebih cepat dengan menggeser bit x dengan 1 dan menambahkan x ke hasilnya - dan bahkan trik itu dilakukan oleh kompiler modern jika Anda mengalikan 3 dan mengaktifkan optimasi agresif kecuali target modern CPU di mana multiplikasi sama cepatnya dengan penambahan sejak saat itu triknya akan memperlambat perhitungan.)sumber
3 * x
menjadi dua bacaan x. Mungkin membaca x sekali dan kemudian melakukan metode x + 2 * x pada register itu dibaca x kelanguage-lawyer
, di mana bahasa tersebut memiliki "makna yang sangat istimewa" untuk undefined , Anda hanya akan menimbulkan kebingungan dengan tidak menggunakan definisi bahasa.Sunting global: maaf kawan, saya semua bersemangat dan menulis banyak omong kosong. Hanya kakek tua mengoceh.
Saya ingin percaya bahwa C telah dihindarkan, tetapi sayangnya sejak C11 telah disejajarkan dengan C ++. Rupanya, mengetahui apa yang akan dilakukan oleh kompiler dengan efek samping dalam ekspresi sekarang harus menyelesaikan sedikit teka-teki matematika yang melibatkan urutan sebagian urutan kode berdasarkan "terletak sebelum titik sinkronisasi".
Saya kebetulan telah merancang dan menerapkan beberapa sistem tertanam waktu-nyata yang kritis pada masa K&R (termasuk pengontrol mobil listrik yang dapat mengirim orang menabrak tembok terdekat jika mesin tidak disimpan dalam pengawasan, sebuah industri 10 ton robot yang dapat menekan orang ke bubur kertas jika tidak diperintahkan dengan benar, dan lapisan sistem yang, meskipun tidak berbahaya, akan memiliki beberapa lusin prosesor menyedot bus data mereka kering dengan overhead sistem kurang dari 1%).
Saya mungkin terlalu pikun atau bodoh untuk mendapatkan perbedaan antara tidak ditentukan dan tidak ditentukan, tapi saya pikir saya masih memiliki ide yang cukup bagus tentang apa yang dimaksud dengan eksekusi secara bersamaan dan akses data. Menurut pendapat saya, informasi tentang C ++ dan sekarang orang-orang C dengan bahasa peliharaan mereka mengambil alih masalah sinkronisasi adalah mimpi pipa yang mahal. Entah Anda tahu apa eksekusi serentak itu, dan Anda tidak memerlukan alat-alat ini, atau tidak, dan Anda akan melakukan kebaikan secara umum untuk tidak mencoba mengacaukannya.
Semua truk dengan abstraksi penghalang memori yang memikat ini hanya disebabkan oleh keterbatasan sementara dari sistem cache multi-CPU, yang semuanya dapat dengan aman dienkapsulasi dalam objek sinkronisasi OS umum seperti, misalnya, variabel dan variabel kondisi C ++ penawaran.
Biaya enkapsulasi ini hanyalah penurunan satu menit dalam kinerja dibandingkan dengan apa yang dapat dicapai oleh penggunaan instruksi CPU spesifik berbutir halus.
Kata
volatile
kunci (atau a#pragma dont-mess-with-that-variable
untuk semua saya, sebagai pemrogram sistem, perawatan) sudah cukup untuk memberitahu kompiler untuk berhenti memesan kembali akses memori. Kode optimal dapat dengan mudah diproduksi dengan arahan asm langsung untuk menaburkan driver tingkat rendah dan kode OS dengan instruksi khusus CPU ad hoc. Tanpa pengetahuan mendalam tentang bagaimana perangkat keras yang mendasarinya (sistem cache atau antarmuka bus) bekerja, Anda terikat untuk menulis kode yang tidak berguna, tidak efisien atau salah.Penyesuaian satu menit
volatile
kata kunci dan Bob akan menjadi semua orang kecuali paman programmer tingkat rendah yang paling keras. Alih-alih itu, geng matematika C ++ biasa memiliki hari lapangan merancang abstraksi lain yang tidak bisa dipahami, menghasilkan kecenderungan khas mereka untuk merancang solusi mencari masalah yang tidak ada dan salah mengartikan definisi bahasa pemrograman dengan spesifikasi kompiler.Hanya kali ini perubahan diperlukan untuk merusak aspek fundamental C juga, karena "hambatan" ini harus dihasilkan bahkan dalam kode C tingkat rendah untuk bekerja dengan baik. Itu, antara lain, menimbulkan malapetaka dalam definisi ekspresi, tanpa penjelasan atau justifikasi apa pun.
Sebagai kesimpulan, fakta bahwa kompiler dapat menghasilkan kode mesin yang konsisten dari potongan C yang absurd ini hanyalah konsekuensi yang jauh dari cara orang-orang C ++ mengatasi potensi ketidakkonsistenan sistem cache di akhir tahun 2000-an.
Itu membuat kekacauan yang mengerikan dari satu aspek mendasar dari C (definisi ekspresi), sehingga sebagian besar programmer C - yang tidak peduli tentang sistem cache, dan memang begitu - sekarang dipaksa mengandalkan guru untuk menjelaskan perbedaan antara
a = b() + c()
dana = b + c
.Mencoba menebak apa yang akan terjadi pada array yang tidak menguntungkan ini adalah kehilangan waktu dan usaha. Terlepas dari apa yang akan dibuat oleh kompiler, kode ini secara patologis salah. Satu-satunya hal yang bertanggung jawab untuk dilakukan adalah mengirimkannya ke tempat sampah.
Secara konseptual, efek samping selalu dapat dipindahkan dari ekspresi, dengan upaya sepele membiarkan modifikasi terjadi sebelum atau setelah evaluasi, dalam pernyataan terpisah.
Jenis kode menyebalkan ini mungkin dibenarkan pada tahun 80-an, ketika Anda tidak dapat mengharapkan kompiler untuk mengoptimalkan apa pun. Tapi sekarang kompiler telah lama menjadi lebih pintar dari kebanyakan programmer, yang tersisa hanyalah sepotong kode buruk.
Saya juga gagal memahami pentingnya debat yang tidak ditentukan / tidak ditentukan ini. Entah Anda bisa mengandalkan kompiler untuk menghasilkan kode dengan perilaku yang konsisten atau Anda tidak bisa. Apakah Anda menyebutnya tidak terdefinisi atau tidak spesifik, sepertinya ini adalah titik perdebatan.
Menurut pendapat saya yang bisa dibilang informasi, C sudah cukup berbahaya dalam kondisi K&R-nya. Evolusi yang bermanfaat adalah menambahkan langkah-langkah keamanan akal sehat. Sebagai contoh, dengan menggunakan alat analisis kode canggih ini, spesifikasi memaksa kompiler untuk mengimplementasikan setidaknya menghasilkan peringatan tentang kode gila, alih-alih diam-diam menghasilkan kode yang berpotensi tidak dapat diandalkan hingga ekstrem.
Tetapi sebaliknya orang-orang memutuskan, misalnya, untuk menentukan urutan evaluasi tetap di C ++ 17. Sekarang setiap perangkat lunak imbecile secara aktif dihasut untuk menempatkan efek samping dalam kodenya secara sengaja, berdasarkan kepastian bahwa kompiler baru akan dengan bersemangat menangani kebingungan dengan cara deterministik.
K&R adalah salah satu keajaiban dunia komputasi. Untuk dua puluh dolar Anda mendapatkan spesifikasi bahasa yang komprehensif (saya telah melihat satu orang menulis kompiler lengkap hanya menggunakan buku ini), sebuah manual referensi yang sangat baik (daftar isi biasanya akan mengarahkan Anda dalam beberapa halaman jawaban untuk jawaban Anda). pertanyaan), dan buku teks yang akan mengajarkan Anda untuk menggunakan bahasa dengan cara yang masuk akal. Lengkap dengan alasan, contoh dan kata-kata bijak peringatan tentang berbagai cara Anda dapat menyalahgunakan bahasa untuk melakukan hal-hal yang sangat, sangat bodoh.
Menghancurkan warisan itu hanya karena sedikit sekali keuntungan bagiku bagaikan pemborosan yang kejam bagiku. Tetapi sekali lagi saya mungkin gagal memahami intinya sepenuhnya. Mungkin beberapa jenis jiwa dapat mengarahkan saya ke arah contoh kode C baru yang mengambil keuntungan signifikan dari efek samping ini?
sumber
0,expr,0
.