Mengapa volatile
dibutuhkan C? Untuk apa ini digunakan? Apa yang akan dilakukannya?
Mengapa volatile
dibutuhkan C? Untuk apa ini digunakan? Apa yang akan dilakukannya?
Jawaban:
Volatile memberitahu compiler untuk tidak mengoptimalkan apa pun yang ada hubungannya dengan variabel volatile.
Setidaknya ada tiga alasan umum untuk menggunakannya, semua situasi yang melibatkan di mana nilai variabel dapat berubah tanpa tindakan dari kode yang terlihat: Ketika Anda berinteraksi dengan perangkat keras yang mengubah nilai itu sendiri; ketika ada thread lain yang berjalan yang juga menggunakan variabel; atau ketika ada penangan sinyal yang dapat mengubah nilai variabel.
Katakanlah Anda memiliki sedikit perangkat keras yang dipetakan ke dalam RAM di suatu tempat dan yang memiliki dua alamat: port perintah dan port data:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Sekarang Anda ingin mengirim beberapa perintah:
void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Terlihat mudah, tetapi bisa gagal karena kompiler bebas mengubah urutan penulisan data dan perintah. Ini akan menyebabkan gadget kecil kami mengeluarkan perintah dengan nilai data sebelumnya. Lihat juga lingkaran tunggu selagi sibuk. Yang itu akan dioptimalkan. Compiler akan mencoba menjadi pintar, membaca nilai isbusy hanya sekali dan kemudian masuk ke infinite loop. Bukan itu yang Anda inginkan.
Cara menyiasatinya adalah dengan menyatakan gadget penunjuk sebagai volatile. Dengan cara ini kompiler dipaksa untuk melakukan apa yang Anda tulis. Itu tidak dapat menghapus tugas memori, itu tidak bisa cache variabel dalam register dan itu tidak bisa mengubah urutan tugas baik:
Ini adalah versi yang benar:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
volatile
di C sebenarnya muncul untuk tujuan tidak caching nilai-nilai variabel secara otomatis. Ini akan memberitahu kompiler untuk tidak men-cache nilai variabel ini. Jadi itu akan menghasilkan kode untuk mengambil nilai dari volatile
variabel yang diberikan dari memori utama setiap kali bertemu dengannya. Mekanisme ini digunakan karena sewaktu-waktu nilainya dapat dimodifikasi oleh OS atau interupsi apa pun. Jadi menggunakan volatile
akan membantu kita mengakses nilai baru setiap waktu.
volatile
adalah untuk memungkinkan kompiler untuk mengoptimalkan kode sambil tetap memungkinkan programmer untuk mencapai semantik yang akan dicapai tanpa optimasi tersebut. Para penulis Standar berharap bahwa implementasi kualitas akan mendukung semantik apa pun yang berguna mengingat platform target dan bidang aplikasi mereka, dan tidak berharap bahwa penulis kompiler akan berusaha menawarkan semantik kualitas terendah yang sesuai dengan Standar dan tidak 100% bodoh (perhatikan bahwa penulis Standar secara eksplisit mengakui dalam alasan ...
Penggunaan lain untuk volatile
penangan sinyal. Jika Anda memiliki kode seperti ini:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Compiler diperbolehkan untuk melihat loop body tidak menyentuh quit
variabel dan mengubah loop menjadi while (true)
loop. Bahkan jika quit
variabel diatur pada penangan sinyal untuk SIGINT
dan SIGTERM
; kompiler tidak memiliki cara untuk mengetahui hal itu.
Namun, jika quit
variabel tersebut dideklarasikanvolatile
, kompiler dipaksa untuk memuatnya setiap waktu, karena dapat dimodifikasi di tempat lain. Inilah yang Anda inginkan dalam situasi ini.
quit
, kompiler dapat mengoptimalkannya menjadi loop konstan, dengan asumsi bahwa tidak ada cara untuk quit
diubah di antara iterasi. NB: Ini bukan pengganti yang baik untuk pemrograman threadsafe sebenarnya.
volatile
atau penanda lain, ia akan menganggap bahwa tidak ada yang di luar loop memodifikasi variabel begitu memasuki loop, bahkan jika itu adalah variabel global.
extern int global; void fn(void) { while (global != 0) { } }
dengan gcc -O3 -S
dan lihat file rakitan yang dihasilkan, di mesin saya ya movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
, yaitu, infinite loop jika global tidak nol. Kemudian coba tambahkan volatile
dan lihat perbedaannya.
volatile
memberitahu kompiler bahwa variabel Anda dapat diubah dengan cara lain, daripada kode yang mengaksesnya. misalnya, mungkin lokasi memori yang dipetakan I / O. Jika ini tidak ditentukan dalam kasus seperti itu, beberapa akses variabel dapat dioptimalkan, misalnya, isinya dapat disimpan dalam register, dan lokasi memori tidak dibaca kembali lagi.
Lihat artikel ini oleh Andrei Alexandrescu, " volatile - Best Friend Programmer Multithreaded "
Kata kunci yang mudah menguap dirancang untuk mencegah optimisasi kompiler yang mungkin membuat kode salah di hadapan peristiwa asinkron tertentu. Misalnya, jika Anda mendeklarasikan variabel primitif sebagai volatile , kompiler tidak diizinkan untuk men-cache-nya dalam register - optimasi umum yang akan menjadi bencana jika variabel itu dibagi di antara banyak utas. Jadi aturan umumnya adalah, jika Anda memiliki variabel tipe primitif yang harus dibagi di antara banyak utas, deklarasikan variabel-variabel tersebut tidak stabil . Tetapi Anda sebenarnya dapat melakukan lebih banyak hal dengan kata kunci ini: Anda dapat menggunakannya untuk menangkap kode yang tidak aman, dan Anda dapat melakukannya pada waktu kompilasi. Artikel ini menunjukkan bagaimana hal itu dilakukan; solusinya melibatkan smart pointer sederhana yang juga membuatnya mudah untuk membuat serial bagian penting dari kode.
Artikel ini berlaku untuk keduanya C
dan C++
.
Juga lihat artikel " C ++ dan Perils of Double-Checked Locking " oleh Scott Meyers dan Andrei Alexandrescu:
Jadi ketika berhadapan dengan beberapa lokasi memori (mis. Port memori yang dipetakan atau memori yang dirujuk oleh ISR [Interrupt Service Routines]), beberapa optimasi harus ditunda. volatile ada untuk menentukan perlakuan khusus untuk lokasi tersebut, khususnya: (1) konten dari variabel volatil adalah "tidak stabil" (dapat berubah dengan cara yang tidak diketahui oleh kompiler), (2) semua menulis ke data volatil adalah "dapat diamati" sehingga mereka harus dijalankan dengan religius, dan (3) semua operasi pada data yang mudah menguap dieksekusi dalam urutan di mana mereka muncul dalam kode sumber. Dua aturan pertama memastikan membaca dan menulis yang tepat. Yang terakhir memungkinkan implementasi protokol I / O yang mencampur input dan output. Ini adalah informasi informal yang dijamin volatilitas C dan C ++.
volatile
tidak menjamin atomicity.
Penjelasan sederhana saya adalah:
Dalam beberapa skenario, berdasarkan logika atau kode, kompiler akan melakukan optimalisasi variabel yang dianggapnya tidak berubah. Kata volatile
kunci mencegah variabel dioptimalkan.
Sebagai contoh:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
Dari kode di atas, kompiler mungkin berpikir usb_interface_flag
didefinisikan sebagai 0, dan bahwa dalam loop sementara itu akan menjadi nol selamanya. Setelah optimasi, kompiler akan memperlakukannya while(true)
sepanjang waktu, menghasilkan loop tak terbatas.
Untuk menghindari skenario semacam ini, kami menyatakan flag sebagai volatile, kami mengatakan kepada kompiler bahwa nilai ini dapat diubah oleh antarmuka eksternal atau modul program lainnya, yaitu, tolong jangan optimalkan. Itulah use case untuk volatile.
Penggunaan marginal untuk volatile adalah sebagai berikut. Katakanlah Anda ingin menghitung turunan numerik dari suatu fungsi f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Masalahnya adalah bahwa x+h-x
umumnya tidak sama dengan h
karena kesalahan pembulatan. Pikirkan tentang hal ini: ketika Anda mengurangi angka yang sangat dekat, Anda kehilangan banyak digit signifikan yang dapat merusak perhitungan derivatif (pikirkan 1,00001 - 1). Solusi yang mungkin bisa dilakukan
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
tetapi tergantung pada platform Anda dan sakelar kompiler, baris kedua dari fungsi itu dapat dihilangkan oleh kompiler yang mengoptimalkan secara agresif. Jadi, Anda yang menulis
volatile double hh = x + h;
hh -= x;
untuk memaksa kompiler membaca lokasi memori yang mengandung hh, kehilangan peluang optimasi akhirnya.
h
atau hh
dalam formula turunan? Ketika hh
dihitung, rumus terakhir menggunakannya seperti yang pertama, tanpa perbedaan. Mungkin seharusnya begitu (f(x+h) - f(x))/hh
?
h
dan hh
adalah yang hh
terpotong ke beberapa kekuatan negatif dari dua oleh operasi x + h - x
. Dalam hal ini, x + hh
dan x
berbeda persis oleh hh
. Anda juga dapat mengambil formula Anda, itu akan memberikan hasil yang sama, karena x + h
dan x + hh
sama (itu adalah penyebut yang penting di sini).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? tanpa menggunakan volatile.
-ffast-math
atau setara.
Ada dua kegunaan. Ini secara khusus lebih sering digunakan dalam pengembangan tertanam.
Compiler tidak akan mengoptimalkan fungsi yang menggunakan variabel yang didefinisikan dengan kata kunci yang mudah menguap
Volatile digunakan untuk mengakses lokasi memori yang tepat dalam RAM, ROM, dll. Ini digunakan lebih sering untuk mengontrol perangkat yang dipetakan memori, mengakses register CPU dan menemukan lokasi memori tertentu.
Lihat contoh dengan daftar perakitan. Re: Penggunaan Kata Kunci C "volatile" dalam Pengembangan Tertanam
Volatile juga berguna, ketika Anda ingin memaksa kompiler untuk tidak mengoptimalkan urutan kode tertentu (misalnya untuk menulis tolok ukur mikro).
Saya akan menyebutkan skenario lain di mana volatil penting.
Misalkan Anda memetakan file memori untuk I / O yang lebih cepat dan file itu dapat berubah di belakang layar (mis. File tersebut tidak ada di hard drive lokal Anda, tetapi sebaliknya dilayani melalui jaringan oleh komputer lain).
Jika Anda mengakses data file yang dipetakan memori melalui pointer ke objek yang tidak mudah menguap (pada tingkat kode sumber), maka kode yang dihasilkan oleh kompiler dapat mengambil data yang sama beberapa kali tanpa Anda menyadarinya.
Jika data itu berubah, program Anda mungkin menggunakan dua atau lebih versi data yang berbeda dan masuk ke kondisi tidak konsisten. Hal ini tidak hanya menyebabkan perilaku program yang salah secara logis, tetapi juga lubang keamanan yang dapat dieksploitasi jika program memproses file yang tidak dipercaya atau file dari lokasi yang tidak terpercaya.
Jika Anda peduli dengan keamanan, dan Anda harus melakukannya, ini adalah skenario penting untuk dipertimbangkan.
volatile berarti penyimpanan cenderung berubah kapan saja dan diubah tetapi sesuatu di luar kendali program pengguna. Ini berarti bahwa jika Anda mereferensikan variabel, program harus selalu memeriksa alamat fisik (mis. Input fifo yang dipetakan), dan tidak menggunakannya dengan cara di-cache.
Wiki mengatakan segalanya tentang volatile
:
Dan kernel Linux doc juga membuat notasi yang bagus tentang volatile
:
Menurut pendapat saya, Anda tidak harus berharap terlalu banyak dari volatile
. Untuk mengilustrasikannya, lihat contoh di jawaban yang sangat dipilih oleh Nils Pipenbrinck .
Saya akan mengatakan, teladannya tidak cocok untuk volatile
. volatile
hanya digunakan untuk:
mencegah kompiler membuat optimasi yang berguna dan diinginkan . Ini bukan apa-apa tentang thread thread, akses atom atau bahkan urutan memori.
Dalam contoh itu:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
yang gadget->data = data
sebelumnya gadget->command = command
hanya hanya dijamin dalam kode dikompilasi oleh compiler. Pada waktu berjalan, prosesor masih dapat menata ulang data dan penugasan perintah, terkait dengan arsitektur prosesor. Perangkat keras bisa mendapatkan data yang salah (misalkan gadget dipetakan ke perangkat keras I / O). Penghalang memori diperlukan antara data dan penugasan perintah.
volatile
menurunkan kinerja tanpa alasan. Adapun apakah itu cukup, itu akan tergantung pada aspek-aspek lain dari sistem yang programmer mungkin tahu lebih banyak tentang daripada kompiler. Di sisi lain, jika prosesor menjamin bahwa instruksi untuk menulis ke alamat tertentu akan mem-flush cache CPU tetapi kompiler tidak memberikan cara untuk mem-flush variabel cache yang tidak diketahui CPU, pembilasan cache tidak akan berguna.
Dalam bahasa yang dirancang oleh Dennis Ritchie, setiap akses ke objek apa pun, selain objek otomatis yang alamatnya tidak diambil, akan berperilaku seolah-olah itu menghitung alamat objek dan kemudian membaca atau menulis penyimpanan di alamat itu. Ini membuat bahasa ini sangat kuat, tetapi peluang optimasi sangat terbatas.
Walaupun mungkin mungkin untuk menambahkan kualifikasi yang akan mengundang kompiler untuk mengasumsikan bahwa objek tertentu tidak akan diubah dengan cara yang aneh, asumsi seperti itu akan sesuai untuk sebagian besar objek dalam program C, dan itu akan memiliki tidak praktis untuk menambahkan kualifikasi ke semua objek yang anggapan seperti itu akan sesuai. Di sisi lain, beberapa program perlu menggunakan beberapa objek yang asumsi seperti itu tidak berlaku. Untuk mengatasi masalah ini, Standar mengatakan bahwa kompilator dapat mengasumsikan bahwa objek yang tidak dideklarasikan tidak volatile
akan memiliki nilainya diamati atau diubah dengan cara yang berada di luar kendali kompiler, atau akan berada di luar pemahaman kompiler yang masuk akal.
Karena berbagai platform mungkin memiliki cara berbeda di mana objek dapat diamati atau dimodifikasi di luar kendali kompiler, sudah sepantasnya bahwa kompiler berkualitas untuk platform tersebut harus berbeda dalam penanganan volatile
semantik yang tepat . Sayangnya, karena Standar gagal untuk menyarankan bahwa kompiler berkualitas yang dimaksudkan untuk pemrograman tingkat rendah pada platform harus menangani volatile
dengan cara yang akan mengenali setiap dan semua efek yang relevan dari operasi baca / tulis tertentu pada platform itu, banyak kompiler gagal melakukan jadi dengan cara yang membuatnya lebih sulit untuk memproses hal-hal seperti latar belakang I / O dengan cara yang efisien tetapi tidak dapat dipecah oleh "optimisasi" kompiler.
Secara sederhana, ini memberitahu kompiler untuk tidak melakukan optimasi pada variabel tertentu. Variabel yang dipetakan ke register perangkat dimodifikasi secara tidak langsung oleh perangkat. Dalam hal ini, volatile harus digunakan.
Volatile dapat diubah dari luar kode yang dikompilasi (misalnya, suatu program dapat memetakan variabel volatil ke register yang dipetakan memori.) Kompiler tidak akan menerapkan optimasi tertentu untuk kode yang menangani variabel volatil - misalnya, ia tidak akan t memuatnya ke dalam register tanpa menuliskannya ke memori. Ini penting ketika berhadapan dengan register perangkat keras.
Seperti yang disarankan oleh banyak orang di sini, penggunaan populer kata kunci yang mudah menguap adalah untuk melewatkan pengoptimalan variabel yang mudah menguap.
Keuntungan terbaik yang terlintas dalam pikiran, dan layak disebutkan setelah membaca tentang volatile adalah - untuk mencegah tergulingnya variabel dalam kasus a longjmp
. Lompatan non-lokal.
Apa artinya ini?
Ini berarti bahwa nilai terakhir akan dipertahankan setelah Anda melakukan stack unwinding , untuk kembali ke beberapa frame stack sebelumnya; biasanya dalam kasus beberapa skenario yang salah.
Karena itu akan keluar dari ruang lingkup pertanyaan ini, saya tidak akan membahas detail setjmp/longjmp
sini, tetapi perlu membaca tentang hal itu; dan bagaimana fitur volatilitas dapat digunakan untuk mempertahankan nilai terakhir.