Một định nghĩa về volatile
volatile
báo cho trình biên dịch rằng giá trị của biến có thể thay đổi mà trình biên dịch không biết. Do đó trình biên dịch không thể giả sử giá trị không thay đổi chỉ vì chương trình C dường như không thay đổi.
Mặt khác, điều đó có nghĩa là giá trị của biến có thể được yêu cầu (đọc) ở một nơi khác mà trình biên dịch không biết, do đó phải đảm bảo rằng mọi phép gán cho biến thực sự được thực hiện như một thao tác ghi.
Trường hợp sử dụng
volatile
được yêu cầu khi
- đại diện cho các thanh ghi phần cứng (hoặc I / O được ánh xạ bộ nhớ) dưới dạng các biến - ngay cả khi thanh ghi sẽ không bao giờ được đọc, trình biên dịch không được bỏ qua thao tác ghi suy nghĩ "Lập trình viên ngu ngốc. Cố gắng lưu trữ một giá trị trong một biến mà anh ấy / cô ấy sẽ không bao giờ đọc lại. Anh ấy / cô ấy thậm chí sẽ không chú ý nếu chúng tôi bỏ qua bài viết. " Ngược lại, ngay cả khi chương trình không bao giờ ghi giá trị vào biến, giá trị của nó vẫn có thể bị thay đổi bởi phần cứng.
- chia sẻ các biến giữa các bối cảnh thực hiện (ví dụ: ISR / chương trình chính) (xem câu trả lời của @ kkramo)
Ảnh hưởng của volatile
Khi một biến được khai báo volatile
, trình biên dịch phải đảm bảo rằng mọi phép gán cho nó trong mã chương trình được phản ánh trong một thao tác ghi thực tế và mỗi lần đọc trong mã chương trình sẽ đọc giá trị từ bộ nhớ (mmapped).
Đối với các biến không biến động, trình biên dịch giả định rằng nó biết nếu / khi giá trị của biến thay đổi và có thể tối ưu hóa mã theo các cách khác nhau.
Đối với một, trình biên dịch có thể giảm số lần đọc / ghi vào bộ nhớ, bằng cách giữ giá trị trong các thanh ghi CPU.
Thí dụ:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
Ở đây, trình biên dịch có thể sẽ không phân bổ RAM cho result
biến và sẽ không bao giờ lưu trữ các giá trị trung gian ở bất cứ đâu ngoài một thanh ghi CPU.
Nếu result
không ổn định, mọi lần xuất hiện result
trong mã C sẽ yêu cầu trình biên dịch thực hiện quyền truy cập vào RAM (hoặc cổng I / O), dẫn đến hiệu suất thấp hơn.
Thứ hai, trình biên dịch có thể sắp xếp lại các hoạt động trên các biến không biến động cho hiệu suất và / hoặc kích thước mã. Ví dụ đơn giản:
int a = 99;
int b = 1;
int c = 99;
có thể được đặt hàng lại để
int a = 99;
int c = 99;
int b = 1;
có thể lưu một lệnh trình biên dịch vì giá trị 99
sẽ không phải tải hai lần.
Nếu a
, b
và c
không ổn định, trình biên dịch sẽ phải phát ra các lệnh chỉ định các giá trị theo thứ tự chính xác như được đưa ra trong chương trình.
Một ví dụ kinh điển khác là như thế này:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
Nếu trong trường hợp này signal
là không volatile
, trình biên dịch sẽ 'nghĩ' while( signal == 0 )
có thể là một vòng lặp vô hạn (vì signal
sẽ không bao giờ bị thay đổi bởi mã bên trong vòng lặp ) và có thể tạo ra tương đương với
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
Cân nhắc xử lý các volatile
giá trị
Như đã nêu ở trên, một volatile
biến có thể đưa ra một hình phạt hiệu suất khi nó được truy cập thường xuyên hơn so với yêu cầu thực sự. Để giảm thiểu vấn đề này, bạn có thể "không biến động" giá trị bằng cách gán cho một biến không biến động, như
volatile uint32_t sysTickCount;
void doSysTick() {
uint32_t ticks = sysTickCount; // A single read access to sysTickCount
ticks = ticks + 1;
setLEDState( ticks < 500000L );
if ( ticks >= 1000000L ) {
ticks = 0;
}
sysTickCount = ticks; // A single write access to volatile sysTickCount
}
Điều này có thể đặc biệt có lợi trong ISR, nơi bạn muốn nhanh nhất có thể không truy cập cùng một phần cứng hoặc bộ nhớ nhiều lần khi bạn biết điều đó là không cần thiết vì giá trị sẽ không thay đổi trong khi ISR của bạn đang chạy. Điều này là phổ biến khi ISR là 'nhà sản xuất' các giá trị cho biến, như sysTickCount
trong ví dụ trên. Trên một AVR, sẽ rất khó khăn khi chức năng doSysTick()
truy cập cùng bốn byte trong bộ nhớ (bốn hướng dẫn = 8 chu kỳ CPU cho mỗi lần truy cập sysTickCount
) năm hoặc sáu lần thay vì chỉ hai lần, bởi vì lập trình viên biết rằng giá trị sẽ không được thay đổi từ một số mã khác trong khi anh ấy / cô ấy doSysTick()
chạy.
Với thủ thuật này, về cơ bản, bạn thực hiện chính xác điều tương tự trình biên dịch thực hiện đối với các biến không biến động, tức là chỉ đọc chúng từ bộ nhớ khi nó phải, giữ giá trị trong một thanh ghi một thời gian và chỉ ghi lại vào bộ nhớ khi nó phải ; nhưng lần này, bạn biết rõ hơn trình biên dịch nếu / khi đọc / ghi phải xảy ra, vì vậy bạn giải phóng trình biên dịch khỏi tác vụ tối ưu hóa này và tự thực hiện.
Hạn chế của volatile
Truy cập phi nguyên tử
volatile
không không cung cấp truy cập nguyên tử để biến nhiều từ. Đối với những trường hợp đó, bạn sẽ cần cung cấp loại trừ lẫn nhau bằng các phương tiện khác, ngoài việc sử dụng volatile
. Trên AVR, bạn có thể sử dụng ATOMIC_BLOCK
từ <util/atomic.h>
hoặc các cli(); ... sei();
cuộc gọi đơn giản . Các macro tương ứng cũng hoạt động như một rào cản bộ nhớ, điều này rất quan trọng khi nói đến thứ tự truy cập:
Lệnh thực hiện
volatile
áp đặt thứ tự thực hiện nghiêm ngặt chỉ đối với các biến dễ bay hơi khác. Điều này có nghĩa là, ví dụ
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
được đảm bảo trước tiên gán 1 cho i
và sau đó gán 2 cho j
. Tuy nhiên, nó không được đảm bảo a
sẽ được chỉ định ở giữa; trình biên dịch có thể thực hiện việc gán đó trước hoặc sau đoạn mã, về cơ bản bất cứ lúc nào cho đến lần đọc đầu tiên (hiển thị) a
.
Nếu nó không dành cho hàng rào bộ nhớ của các macro được đề cập ở trên, trình biên dịch sẽ được phép dịch
uint32_t x;
cli();
x = volatileVar;
sei();
đến
x = volatileVar;
cli();
sei();
hoặc là
cli();
sei();
x = volatileVar;
(Để hoàn thiện, tôi phải nói rằng các rào cản bộ nhớ, giống như các rào cản được ngụ ý bởi các macro sei / cli, có thể thực sự cản trở việc sử dụng volatile
, nếu tất cả các truy cập được đặt trong các rào cản này.)