Tại sao volatile
cần thiết trong C? Cái này được dùng để làm gì? Nó sẽ làm gì?
Tại sao volatile
cần thiết trong C? Cái này được dùng để làm gì? Nó sẽ làm gì?
Câu trả lời:
Biến động nói với trình biên dịch không tối ưu hóa bất cứ điều gì phải làm với biến dễ bay hơi.
Có ít nhất ba lý do phổ biến để sử dụng nó, tất cả đều liên quan đến các tình huống trong đó giá trị của biến có thể thay đổi mà không cần hành động từ mã hiển thị: Khi bạn giao diện với phần cứng tự thay đổi giá trị; khi có một luồng khác đang chạy cũng sử dụng biến đó; hoặc khi có một bộ xử lý tín hiệu có thể thay đổi giá trị của biến.
Giả sử bạn có một phần cứng nhỏ được ánh xạ vào RAM ở đâu đó và có hai địa chỉ: cổng lệnh và cổng dữ liệu:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Bây giờ bạn muốn gửi một số lệnh:
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;
}
Nhìn có vẻ dễ dàng, nhưng nó có thể thất bại vì trình biên dịch có thể tự do thay đổi thứ tự ghi dữ liệu và lệnh. Điều này sẽ khiến tiện ích nhỏ của chúng tôi đưa ra các lệnh với giá trị dữ liệu trước đó. Cũng hãy xem sự chờ đợi trong khi vòng lặp bận rộn. Điều đó sẽ được tối ưu hóa. Trình biên dịch sẽ cố gắng khéo léo, đọc giá trị của isbusy chỉ một lần và sau đó đi vào một vòng lặp vô hạn. Đó không phải là những gì bạn muốn.
Cách để giải quyết vấn đề này là khai báo tiện ích con trỏ là không ổn định. Bằng cách này, trình biên dịch buộc phải làm những gì bạn đã viết. Nó không thể loại bỏ các bài tập bộ nhớ, nó không thể lưu các biến trong bộ đăng ký và nó cũng không thể thay đổi thứ tự của các bài tập:
Đây là phiên bản chính xác:
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
trong C thực sự ra đời với mục đích không lưu trữ các giá trị của biến tự động. Nó sẽ báo cho trình biên dịch không lưu trữ giá trị của biến này. Vì vậy, nó sẽ tạo mã để lấy giá trị của volatile
biến đã cho từ bộ nhớ chính mỗi lần nó gặp nó. Cơ chế này được sử dụng bởi vì bất cứ lúc nào giá trị có thể được sửa đổi bởi HĐH hoặc bất kỳ ngắt. Vì vậy, sử dụng volatile
sẽ giúp chúng tôi truy cập vào giá trị mỗi lần.
volatile
là để các trình biên dịch có thể tối ưu hóa mã trong khi vẫn cho phép các lập trình viên đạt được ngữ nghĩa sẽ đạt được mà không cần tối ưu hóa như vậy. Các tác giả của Tiêu chuẩn dự kiến rằng việc triển khai chất lượng sẽ hỗ trợ bất kỳ ngữ nghĩa nào hữu ích với các nền tảng mục tiêu và trường ứng dụng của họ và không hy vọng rằng các tác giả biên dịch sẽ tìm cách cung cấp ngữ nghĩa chất lượng thấp nhất phù hợp với Tiêu chuẩn và không 100% ngu ngốc (lưu ý rằng các tác giả của Tiêu chuẩn nhận ra rõ ràng trong lý do ...
Một cách sử dụng khác volatile
là xử lý tín hiệu. Nếu bạn có mã như thế này:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Trình biên dịch được phép thông báo thân vòng lặp không chạm vào quit
biến và chuyển đổi vòng lặp thành while (true)
vòng lặp. Ngay cả khi quit
biến được đặt trên bộ xử lý tín hiệu cho SIGINT
và SIGTERM
; trình biên dịch không có cách nào để biết điều đó.
Tuy nhiên, nếu quit
biến được khai báo volatile
, trình biên dịch buộc phải tải nó mỗi lần, bởi vì nó có thể được sửa đổi ở nơi khác. Đây chính xác là những gì bạn muốn trong tình huống này.
quit
, trình biên dịch có thể tối ưu hóa nó thành một vòng lặp không đổi, giả sử rằng không có cách nào quit
để thay đổi giữa các lần lặp. NB: Đây không hẳn là một sự thay thế tốt cho lập trình luồng an toàn thực tế.
volatile
hoặc các dấu hiệu khác, nó sẽ cho rằng không có gì bên ngoài vòng lặp sửa đổi biến đó một khi nó đi vào vòng lặp, ngay cả khi đó là biến toàn cục.
extern int global; void fn(void) { while (global != 0) { } }
với gcc -O3 -S
và xem xét các tập tin lắp ráp kết quả, trên máy tính của tôi nó movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
, đó là một vòng lặp vô hạn nếu toàn cầu không bằng không. Sau đó thử thêm volatile
và xem sự khác biệt.
volatile
báo cho trình biên dịch rằng biến của bạn có thể được thay đổi bằng các phương tiện khác, ngoài mã đang truy cập vào nó. ví dụ: nó có thể là một vị trí bộ nhớ được ánh xạ I / O. Nếu điều này không được chỉ định trong các trường hợp như vậy, một số truy cập biến có thể được tối ưu hóa, ví dụ: nội dung của nó có thể được giữ trong một thanh ghi và vị trí bộ nhớ không được đọc lại.
Xem bài viết này của Andrei Alexandrescu, " Người bạn tốt nhất của lập trình viên đa luồng "
Các biến động từ khóa được đưa ra để ngăn chặn tối ưu hóa trình biên dịch mà có thể làm cho mã không chính xác trong sự hiện diện của sự kiện không đồng bộ nhất định. Ví dụ: nếu bạn khai báo một biến nguyên thủy là dễ bay hơi , trình biên dịch không được phép lưu vào bộ đệm đó trong một thanh ghi - một tối ưu hóa phổ biến sẽ là thảm họa nếu biến đó được chia sẻ giữa nhiều luồng. Vì vậy, quy tắc chung là, nếu bạn có các biến kiểu nguyên thủy phải được chia sẻ giữa nhiều luồng, hãy khai báo các biến đó biến động. Nhưng bạn thực sự có thể làm nhiều hơn với từ khóa này: bạn có thể sử dụng nó để bắt mã không an toàn cho chuỗi và bạn có thể làm như vậy trong thời gian biên dịch. Bài viết này cho thấy nó được thực hiện như thế nào; giải pháp liên quan đến một con trỏ thông minh đơn giản cũng giúp dễ dàng tuần tự hóa các phần quan trọng của mã.
Bài viết áp dụng cho cả C
và C++
.
Cũng xem bài viết " C ++ và những hiểm họa của khóa kiểm tra hai lần " của Scott Meyers và Andrei Alexandrescu:
Vì vậy, khi xử lý một số vị trí bộ nhớ (ví dụ: cổng được ánh xạ bộ nhớ hoặc bộ nhớ được tham chiếu bởi ISR [Các tuyến dịch vụ ngắt], một số tối ưu hóa phải bị treo. dễ bay hơi tồn tại để chỉ định xử lý đặc biệt cho các vị trí đó, cụ thể: (1) nội dung của biến dễ bay hơi là "không ổn định" (có thể thay đổi bằng phương tiện không xác định đối với trình biên dịch), (2) tất cả ghi vào dữ liệu dễ bay hơi là "có thể quan sát được" phải được thực hiện một cách tôn giáo và (3) tất cả các hoạt động trên dữ liệu dễ bay hơi được thực hiện theo trình tự xuất hiện trong mã nguồn. Hai quy tắc đầu tiên đảm bảo đọc và viết đúng. Cái cuối cùng cho phép thực hiện các giao thức I / O trộn lẫn đầu vào và đầu ra. Đây là những gì không chính thức đảm bảo tính dễ bay hơi của C và C ++.
volatile
không đảm bảo tính nguyên tử.
Giải thích đơn giản của tôi là:
Trong một số tình huống, dựa trên logic hoặc mã, trình biên dịch sẽ thực hiện tối ưu hóa các biến mà nó cho rằng không thay đổi. Các volatile
từ khóa ngăn chặn một biến được tối ưu hóa.
Ví dụ:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
Từ đoạn mã trên, trình biên dịch có thể nghĩ usb_interface_flag
được định nghĩa là 0 và trong vòng lặp while nó sẽ bằng 0 mãi mãi. Sau khi tối ưu hóa, trình biên dịch sẽ coi nó như while(true)
mọi lúc, dẫn đến một vòng lặp vô hạn.
Để tránh các loại kịch bản này, chúng tôi tuyên bố cờ là không ổn định, chúng tôi đang nói với trình biên dịch rằng giá trị này có thể được thay đổi bởi giao diện bên ngoài hoặc mô-đun chương trình khác, tức là không tối ưu hóa nó. Đó là trường hợp sử dụng cho dễ bay hơi.
Một sử dụng cận biên cho dễ bay hơi là sau đây. Giả sử bạn muốn tính đạo hàm số của hàm f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Vấn đề là x+h-x
thường không bằng h
do lỗi vòng. Hãy nghĩ về nó: khi bạn trừ các số rất gần, bạn sẽ mất rất nhiều chữ số có nghĩa có thể làm hỏng tính toán của đạo hàm (nghĩ 1,00001 - 1). Một cách giải quyết có thể là
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
nhưng tùy thuộc vào nền tảng và trình chuyển đổi trình biên dịch của bạn, dòng thứ hai của chức năng đó có thể bị xóa bởi trình biên dịch tối ưu hóa mạnh mẽ. Vì vậy, bạn viết thay
volatile double hh = x + h;
hh -= x;
để buộc trình biên dịch đọc vị trí bộ nhớ chứa hh, từ bỏ cơ hội tối ưu hóa cuối cùng.
h
hoặc hh
trong công thức phái sinh là gì? Khi hh
được tính công thức cuối cùng sử dụng nó như công thức đầu tiên, không có sự khác biệt. Có lẽ nó nên (f(x+h) - f(x))/hh
?
h
và hh
được hh
cắt bớt thành một số sức mạnh tiêu cực của hai bởi hoạt động x + h - x
. Trong trường hợp này, x + hh
và x
khác nhau chính xác bởi hh
. Bạn cũng có thể lấy công thức của mình, nó sẽ cho kết quả tương tự, vì x + h
và x + hh
bằng nhau (nó là mẫu số quan trọng ở đây).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? mà không sử dụng dễ bay hơi.
-ffast-math
hoặc tương đương.
Có hai công dụng. Chúng được sử dụng đặc biệt thường xuyên hơn trong phát triển nhúng.
Trình biên dịch sẽ không tối ưu hóa các hàm sử dụng các biến được xác định bằng từ khóa dễ bay hơi
Dễ bay hơi được sử dụng để truy cập các vị trí bộ nhớ chính xác trong RAM, ROM, v.v ... Điều này được sử dụng thường xuyên hơn để kiểm soát các thiết bị ánh xạ bộ nhớ, truy cập các thanh ghi CPU và định vị các vị trí bộ nhớ cụ thể.
Xem ví dụ với danh sách lắp ráp. Re: Sử dụng từ khóa "dễ bay hơi" trong phát triển nhúng
Tôi sẽ đề cập đến một kịch bản khác trong đó các chất bay hơi là quan trọng.
Giả sử bạn lập bản đồ bộ nhớ cho một tệp I / O nhanh hơn và tệp đó có thể thay đổi phía sau hậu trường (ví dụ: tệp không nằm trên ổ cứng cục bộ của bạn, nhưng thay vào đó được phục vụ qua mạng bởi một máy tính khác).
Nếu bạn truy cập dữ liệu của tệp ánh xạ bộ nhớ thông qua các con trỏ tới các đối tượng không bay hơi (ở cấp mã nguồn), thì mã được trình biên dịch tạo ra có thể tìm nạp cùng một dữ liệu nhiều lần mà bạn không biết về nó.
Nếu dữ liệu đó thay đổi, chương trình của bạn có thể trở nên sử dụng hai hoặc nhiều phiên bản dữ liệu khác nhau và rơi vào trạng thái không nhất quán. Điều này có thể dẫn đến không chỉ hành vi không chính xác về mặt logic của chương trình mà còn dẫn đến các lỗ hổng bảo mật có thể khai thác trong đó nếu nó xử lý các tệp hoặc tệp không tin cậy từ các vị trí không tin cậy.
Nếu bạn quan tâm đến bảo mật, và bạn nên, đây là một kịch bản quan trọng cần xem xét.
không ổn định có nghĩa là bộ lưu trữ có thể thay đổi bất cứ lúc nào và được thay đổi nhưng có gì đó nằm ngoài sự kiểm soát của chương trình người dùng. Điều này có nghĩa là nếu bạn tham chiếu biến, chương trình phải luôn kiểm tra địa chỉ vật lý (nghĩa là fifo đầu vào được ánh xạ) và không sử dụng nó theo cách được lưu trong bộ nhớ cache.
Wiki nói mọi thứ về volatile
:
Và tài liệu của nhân Linux cũng tạo ra một ký hiệu tuyệt vời về volatile
:
Theo tôi, bạn không nên mong đợi quá nhiều từ volatile
. Để minh họa, hãy xem ví dụ trong câu trả lời được bình chọn cao của Nils Pipenbrinck .
Tôi sẽ nói, ví dụ của anh ấy không phù hợp volatile
. volatile
chỉ được sử dụng để:
ngăn trình biên dịch thực hiện tối ưu hóa hữu ích và mong muốn . Không có gì về chủ đề an toàn, truy cập nguyên tử hoặc thậm chí thứ tự bộ nhớ.
Trong ví dụ đó:
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;
}
các gadget->data = data
trước gadget->command = command
chỉ duy nhất được đảm bảo trong mã biên soạn bởi trình biên dịch. Trong thời gian chạy, bộ xử lý vẫn có thể sắp xếp lại dữ liệu và gán lệnh, liên quan đến kiến trúc bộ xử lý. Phần cứng có thể nhận được dữ liệu sai (giả sử tiện ích được ánh xạ tới I / O phần cứng). Hàng rào bộ nhớ là cần thiết giữa dữ liệu và gán lệnh.
volatile
làm giảm hiệu suất mà không có lý do. Về việc nó có đủ hay không, điều đó sẽ phụ thuộc vào các khía cạnh khác của hệ thống mà lập trình viên có thể biết nhiều hơn trình biên dịch. Mặt khác, nếu bộ xử lý đảm bảo rằng một lệnh ghi vào một địa chỉ nhất định sẽ xóa bộ đệm CPU nhưng trình biên dịch không cung cấp cách nào để xóa các biến được lưu trong bộ nhớ cache mà CPU không biết gì, việc xóa bộ đệm sẽ vô dụng.
Trong ngôn ngữ do Dennis Ritchie thiết kế, mọi quyền truy cập vào bất kỳ đối tượng nào, ngoài các đối tượng tự động không có địa chỉ, sẽ hoạt động như thể nó tính toán địa chỉ của đối tượng và sau đó đọc hoặc ghi lưu trữ tại địa chỉ đó. Điều này làm cho ngôn ngữ rất mạnh mẽ, nhưng cơ hội tối ưu hóa bị hạn chế nghiêm trọng.
Mặc dù có thể thêm một vòng loại mời một trình biên dịch để cho rằng một đối tượng cụ thể sẽ không bị thay đổi theo những cách kỳ lạ, nhưng một giả định như vậy sẽ phù hợp với đại đa số các đối tượng trong các chương trình C, và nó sẽ có không thực tế để thêm một vòng loại cho tất cả các đối tượng mà giả định đó sẽ phù hợp. Mặt khác, một số chương trình cần sử dụng một số đối tượng mà giả định đó sẽ không giữ được. Để giải quyết vấn đề này, Standard nói rằng các trình biên dịch có thể cho rằng các đối tượng không được khai báovolatile
sẽ không quan sát hoặc thay đổi giá trị của chúng theo những cách nằm ngoài sự kiểm soát của trình biên dịch, hoặc nằm ngoài sự hiểu biết của trình biên dịch hợp lý.
Bởi vì các nền tảng khác nhau có thể có những cách khác nhau trong đó các đối tượng có thể được quan sát hoặc sửa đổi bên ngoài sự kiểm soát của trình biên dịch, điều phù hợp là các trình biên dịch chất lượng cho các nền tảng đó sẽ khác nhau trong cách xử lý chính xác volatile
ngữ nghĩa của chúng . Thật không may, vì Tiêu chuẩn không đề xuất rằng các trình biên dịch chất lượng dành cho lập trình cấp thấp trên nền tảng nên xử lý volatile
theo cách sẽ nhận ra bất kỳ và tất cả các tác động có liên quan của một hoạt động đọc / ghi cụ thể trên nền tảng đó, nhiều trình biên dịch không thực hiện được vì vậy theo những cách khiến cho việc xử lý những thứ như I / O nền trở nên khó khăn hơn theo cách hiệu quả nhưng không thể bị phá vỡ bởi "tối ưu hóa" của trình biên dịch.
Nói một cách đơn giản, nó báo cho trình biên dịch không thực hiện bất kỳ tối ưu hóa nào trên một biến cụ thể. Các biến được ánh xạ tới thanh ghi thiết bị được sửa đổi gián tiếp bởi thiết bị. Trong trường hợp này, dễ bay hơi phải được sử dụng.
Một biến động có thể được thay đổi từ bên ngoài mã được biên dịch (ví dụ: chương trình có thể ánh xạ biến dễ bay hơi sang thanh ghi ánh xạ bộ nhớ.) Trình biên dịch sẽ không áp dụng một số tối ưu hóa nhất định cho mã xử lý biến dễ bay hơi - ví dụ: nó đã thắng ' t tải nó vào một thanh ghi mà không ghi nó vào bộ nhớ. Điều này rất quan trọng khi làm việc với các thanh ghi phần cứng.
Như được đề xuất đúng bởi nhiều người ở đây, cách sử dụng phổ biến của từ khóa biến động là bỏ qua việc tối ưu hóa biến dễ bay hơi.
Ưu điểm tốt nhất xuất hiện trong đầu và đáng được đề cập sau khi đọc về biến động là - để ngăn ngừa sự quay trở lại của biến trong trường hợp a longjmp
. Một bước nhảy không cục bộ.
Điều đó có nghĩa là gì?
Điều đó đơn giản có nghĩa là giá trị cuối cùng sẽ được giữ lại sau khi bạn hủy xếp chồng , để trở về một số khung ngăn xếp trước đó; thông thường trong trường hợp một số kịch bản sai lầm.
Vì nó nằm ngoài phạm vi của câu hỏi này, tôi không đi sâu vào chi tiết setjmp/longjmp
ở đây, nhưng nó đáng để đọc về nó; và làm thế nào tính năng biến động có thể được sử dụng để giữ lại giá trị cuối cùng.