Mengapa volatile dibutuhkan dalam C?


Jawaban:


425

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;
    }

46
Secara pribadi, saya lebih suka ukuran integer menjadi eksplisitas misalnya int8 / int16 / int32 ketika berbicara dengan perangkat keras. Jawaban yang bagus;)
tonylo

22
ya, Anda harus mendeklarasikan hal-hal dengan ukuran register tetap, tapi hei - itu hanya contoh.
Nils Pipenbrinck

69
Volatile juga diperlukan dalam kode berulir ketika Anda bermain dengan data yang tidak dilindungi konkurensi. Dan ya ada waktu yang sah untuk melakukan itu, Anda dapat misalnya menulis antrian pesan melingkar aman tanpa memerlukan perlindungan konkurensi eksplisit, tetapi akan membutuhkan volatile.
Gordon Wrigley

14
Baca spesifikasi C lebih keras. Volatile hanya memiliki perilaku yang ditentukan pada perangkat I / O yang dipetakan memori atau memori yang disentuh oleh fungsi interupsi asinkron. Ia tidak mengatakan apa - apa tentang threading, dan kompiler yang mengoptimalkan akses ke memori yang disentuh oleh banyak utas sesuai.
ephemient

17
@tolomea: sepenuhnya salah. sedih 17 orang tidak mengetahuinya. volatile bukanlah pagar memori. itu hanya terkait dengan menghindari kode elision selama optimisasi berdasarkan asumsi efek samping yang tidak terlihat .
v.oddou

188

volatiledi 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 volatilevariabel 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 volatileakan membantu kita mengakses nilai baru setiap waktu.


Menjadi ada? Bukankah ´volatile` awalnya dipinjam dari C ++? Sepertinya saya ingat ...
syntaxerror

Ini bukan volatile all about - itu juga melarang pemesanan ulang jika ditetapkan sebagai volatile ..
FaceBro

4
@FaceBro: Tujuan dari 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 ...
supercat

1
... bahwa implementasi mungkin akan sesuai tanpa kualitas yang cukup baik untuk benar-benar cocok untuk tujuan apa pun, tetapi mereka tidak menganggap perlu untuk mencegah hal itu).
supercat

1
@syntaxerror bagaimana bisa dipinjam dari C ++ ketika C lebih dari satu dekade lebih tua dari C ++ (baik pada rilis pertama dan standar pertama)?
phuclv

178

Penggunaan lain untuk volatilepenangan 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 quitvariabel dan mengubah loop menjadi while (true)loop. Bahkan jika quitvariabel diatur pada penangan sinyal untuk SIGINTdan SIGTERM; kompiler tidak memiliki cara untuk mengetahui hal itu.

Namun, jika quitvariabel tersebut dideklarasikanvolatile , kompiler dipaksa untuk memuatnya setiap waktu, karena dapat dimodifikasi di tempat lain. Inilah yang Anda inginkan dalam situasi ini.


ketika Anda mengatakan "kompiler dipaksa untuk memuatnya setiap waktu, apakah itu seperti ketika kompiler memutuskan untuk mengoptimalkan variabel tertentu dan kami tidak menyatakan variabel sebagai volatile, pada saat dijalankan variabel tertentu dimuat ke register CPU tidak dalam memori ?
Amit Singh Tomar

1
@AmitSinghTomar Artinya apa yang dikatakannya: Setiap kali kode memeriksa nilainya, ia dimuat kembali. Jika tidak, kompiler diperbolehkan untuk berasumsi bahwa fungsi yang tidak mengambil referensi ke variabel tidak dapat memodifikasinya, jadi anggap sebagai CesarB dimaksudkan bahwa loop di atas tidak diatur quit, kompiler dapat mengoptimalkannya menjadi loop konstan, dengan asumsi bahwa tidak ada cara untuk quitdiubah di antara iterasi. NB: Ini bukan pengganti yang baik untuk pemrograman threadsafe sebenarnya.
underscore_d

jika berhenti adalah variabel global, maka kompiler tidak akan mengoptimalkan loop sementara, benar?
Pierre G.

2
@PierreG. Tidak, kompilator selalu dapat berasumsi bahwa kode tersebut adalah single-threaded, kecuali dikatakan sebaliknya. Yaitu, dengan tidak adanya volatileatau penanda lain, ia akan menganggap bahwa tidak ada yang di luar loop memodifikasi variabel begitu memasuki loop, bahkan jika itu adalah variabel global.
CesarB

1
@PierreG. Ya, coba kompilasi misalnya extern int global; void fn(void) { while (global != 0) { } }dengan gcc -O3 -Sdan 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 volatiledan lihat perbedaannya.
CesarB

60

volatilememberitahu 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.


30

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 Cdan 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 ++.


Apakah standar tersebut menentukan apakah suatu bacaan dianggap sebagai 'perilaku yang dapat diamati' jika nilainya tidak pernah digunakan? Kesan saya adalah memang seharusnya begitu, tetapi ketika saya mengklaim itu ada di tempat lain, seseorang menantang saya untuk mengutip. Tampak bagi saya bahwa pada platform apa pun di mana pembacaan variabel volatil dapat berefek apa pun, kompiler harus diminta membuat kode yang melakukan setiap pembacaan yang ditunjukkan tepat satu kali; tanpa persyaratan itu, akan sulit untuk menulis kode yang menghasilkan urutan bacaan yang dapat diprediksi.
supercat

@supercat: Menurut artikel pertama, "Jika Anda menggunakan pengubah volatile pada suatu variabel, kompiler tidak akan men-cache variabel itu dalam register - setiap akses akan mengenai lokasi memori aktual dari variabel itu." Juga, di bagian §6.7.3.6 dari standar c99 dikatakan: "Objek yang memiliki tipe yang mudah menguap dapat dimodifikasi dengan cara yang tidak diketahui untuk implementasi atau memiliki efek samping yang tidak diketahui lainnya." Lebih lanjut menyiratkan bahwa variabel volatil mungkin tidak di-cache dalam register dan bahwa semua membaca dan menulis harus dijalankan dalam urutan relatif terhadap urutan poin, bahwa mereka sebenarnya dapat diamati.
Robert S. Barnes

Artikel terakhir memang menyatakan secara eksplisit bahwa membaca adalah efek samping. Yang pertama menunjukkan bahwa pembacaan tidak dapat dilakukan di luar urutan, tetapi tampaknya tidak menghalangi kemungkinan mereka dihilangkan sama sekali.
supercat

"kompiler tidak diizinkan untuk men-cache-nya di register" - Kebanyakan arsitektur RISC menggunakan mesin register, jadi setiap read-modifikasi-write harus men-cache objek dalam register. volatiletidak menjamin atomicity.
terlalu jujur ​​untuk situs ini

1
@ Elaf: Memuat sesuatu ke dalam register tidak sama dengan caching. Caching akan memengaruhi jumlah muatan atau toko atau waktu mereka.
supercat

28

Penjelasan sederhana saya adalah:

Dalam beberapa skenario, berdasarkan logika atau kode, kompiler akan melakukan optimalisasi variabel yang dianggapnya tidak berubah. Kata volatilekunci 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_flagdidefinisikan 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.


19

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-xumumnya tidak sama dengan hkarena 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.


Apa perbedaan antara menggunakan hatau hhdalam formula turunan? Ketika hhdihitung, rumus terakhir menggunakannya seperti yang pertama, tanpa perbedaan. Mungkin seharusnya begitu (f(x+h) - f(x))/hh?
Sergey Zhukov

2
Perbedaan antara hdan hhadalah yang hhterpotong ke beberapa kekuatan negatif dari dua oleh operasi x + h - x. Dalam hal ini, x + hhdan xberbeda persis oleh hh. Anda juga dapat mengambil formula Anda, itu akan memberikan hasil yang sama, karena x + hdan x + hhsama (itu adalah penyebut yang penting di sini).
Alexandre C.

3
Bukankah cara yang lebih mudah dibaca untuk menulis ini x1=x+h; d = (f(x1)-f(x))/(x1-x)? tanpa menggunakan volatile.
Sergey Zhukov

Adakah referensi yang bisa dihapus oleh kompiler?
CoffeeTableEspresso

@CoffeeTableEspresso: Tidak, maaf. Semakin saya tahu tentang floating point, semakin saya percaya bahwa kompiler hanya diperbolehkan untuk mengoptimalkannya jika secara eksplisit mengatakannya, dengan -ffast-mathatau setara.
Alexandre C.

11

Ada dua kegunaan. Ini secara khusus lebih sering digunakan dalam pengembangan tertanam.

  1. Compiler tidak akan mengoptimalkan fungsi yang menggunakan variabel yang didefinisikan dengan kata kunci yang mudah menguap

  2. 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


"Kompiler tidak akan mengoptimalkan fungsi yang menggunakan variabel yang didefinisikan dengan kata kunci yang mudah menguap" - itu salah.
terlalu jujur ​​untuk situs ini

10

Volatile juga berguna, ketika Anda ingin memaksa kompiler untuk tidak mengoptimalkan urutan kode tertentu (misalnya untuk menulis tolok ukur mikro).


10

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.


7

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.


Tidak ada kompiler yang berubah menjadi "alamat fisik dalam RAM" atau "pintas cache".
curiousguy


5

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. volatilehanya 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 = datasebelumnya gadget->command = commandhanya 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.


2
Saya akan mengatakan volatile digunakan untuk mencegah kompiler membuat optimasi yang biasanya berguna dan diinginkan. Seperti yang tertulis, sepertinya volatilemenurunkan 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.
supercat

5

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 volatileakan 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 volatilesemantik yang tepat . Sayangnya, karena Standar gagal untuk menyarankan bahwa kompiler berkualitas yang dimaksudkan untuk pemrograman tingkat rendah pada platform harus menangani volatiledengan 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.


5

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.


1
Adakah sesuatu yang baru dalam jawaban ini yang belum disebutkan sebelumnya?
slfan

3

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.


0

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.


-2

itu tidak memungkinkan kompiler untuk secara otomatis mengubah nilai variabel. variabel volatil adalah untuk penggunaan dinamis.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.