it-swarm-vi.tech

Tại sao không ổn định tồn tại?

Từ khóa volatile làm gì? Trong C++, nó giải quyết vấn đề gì?

Trong trường hợp của tôi, tôi chưa bao giờ cố ý cần nó.

201
theschmitzer

volatile là cần thiết nếu bạn đang đọc từ một vị trí trong bộ nhớ, giả sử, một quy trình/thiết bị hoàn toàn riêng biệt/bất cứ điều gì có thể ghi vào.

Tôi đã từng làm việc với ram cổng kép trong một hệ thống đa bộ xử lý thẳng C. Chúng tôi đã sử dụng một phần cứng được quản lý giá trị 16 bit như một semaphore để biết khi nào anh chàng kia đã hoàn thành. Về cơ bản chúng tôi đã làm điều này:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

Không có volatile, trình tối ưu hóa xem vòng lặp là vô dụng (Chàng trai không bao giờ đặt giá trị! Anh ta bị loại bỏ mã đó!) Và mã của tôi sẽ tiếp tục mà không có được semaphore, gây ra vấn đề về sau.

247
Doug T.

volatile là cần thiết khi phát triển các hệ thống nhúng hoặc trình điều khiển thiết bị, nơi bạn cần đọc hoặc ghi một thiết bị phần cứng được ánh xạ bộ nhớ. Nội dung của một thanh ghi thiết bị cụ thể có thể thay đổi bất cứ lúc nào, vì vậy bạn cần từ khóa volatile để đảm bảo rằng các truy cập đó không được trình biên dịch tối ưu hóa.

78
ChrisN

Một số bộ xử lý có các thanh ghi dấu phẩy động có độ chính xác hơn 64 bit (ví dụ: 32 bit x86 không có SSE, xem bình luận của Peter). Theo cách đó, nếu bạn chạy một số thao tác trên các số có độ chính xác kép, bạn thực sự nhận được câu trả lời có độ chính xác cao hơn so với việc bạn cắt ngắn mỗi kết quả trung gian thành 64 bit.

Điều này thường rất tuyệt, nhưng điều đó có nghĩa là tùy thuộc vào cách trình biên dịch được gán các thanh ghi và tối ưu hóa, bạn sẽ có kết quả khác nhau cho cùng một hoạt động trên cùng một đầu vào chính xác. Nếu bạn cần sự nhất quán thì bạn có thể buộc mỗi thao tác quay trở lại bộ nhớ bằng cách sử dụng từ khóa dễ bay hơi.

Nó cũng hữu ích cho một số thuật toán không có ý nghĩa đại số nhưng giảm lỗi dấu phẩy động, chẳng hạn như phép cộng Kahan. Đại số là một nop, vì vậy nó thường sẽ được tối ưu hóa không chính xác trừ khi một số biến trung gian không ổn định.

68
tfinniga

Từ một bài viết "Dễ bay hơi như một lời hứa" của Dan Saks:

(...) một đối tượng dễ bay hơi là một đối tượng có giá trị có thể thay đổi tự phát. Đó là, khi bạn khai báo một đối tượng là không ổn định, bạn đang nói với trình biên dịch rằng đối tượng có thể thay đổi trạng thái mặc dù không có câu lệnh nào trong chương trình xuất hiện để thay đổi nó. "

Dưới đây là các liên kết đến ba bài viết của anh ấy về từ khóa volatile:

45
MikeZ

Bạn PHẢI sử dụng biến động khi thực hiện cấu trúc dữ liệu không khóa. Mặt khác, trình biên dịch là miễn phí để tối ưu hóa quyền truy cập vào biến, điều này sẽ thay đổi ngữ nghĩa.

Nói cách khác, volility báo cho trình biên dịch truy cập vào biến này phải tương ứng với thao tác đọc/ghi bộ nhớ vật lý.

Ví dụ: đây là cách InterlockedIncrement được khai báo trong API Win32:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);
23
Frederik Slijkerman

Một ứng dụng lớn mà tôi đã sử dụng để làm việc vào đầu những năm 1990 có xử lý ngoại lệ dựa trên C sử dụng setjmp và longjmp. Từ khóa dễ bay hơi là cần thiết cho các biến có giá trị cần được bảo toàn trong khối mã được dùng làm mệnh đề "bắt", kẻo các vars đó được lưu trữ trong các thanh ghi và bị longjmp xóa sạch.

10
Jeff Doar

Trong Tiêu chuẩn C, một trong những nơi sử dụng volatile là với bộ xử lý tín hiệu. Thực tế, trong Tiêu chuẩn C, tất cả những gì bạn có thể làm một cách an toàn trong trình xử lý tín hiệu là sửa đổi biến volatile sig_atomic_t Hoặc thoát nhanh. Thật vậy, AFAIK, đây là nơi duy nhất trong Tiêu chuẩn C yêu cầu sử dụng volatile để tránh hành vi không xác định.

ISO/IEC 9899: 2011 §7.14.1.1 Hàm signal

¶5 Nếu tín hiệu xảy ra khác với kết quả của việc gọi hàm abort hoặc raise, hành vi không được xác định nếu trình xử lý tín hiệu đề cập đến bất kỳ đối tượng nào có thời gian lưu trữ tĩnh hoặc luồng không phải là một đối tượng nguyên tử không khóa khác bằng cách gán giá trị cho một đối tượng được khai báo là volatile sig_atomic_t hoặc trình xử lý tín hiệu gọi bất kỳ hàm nào trong thư viện chuẩn ngoài hàm abort, _Exit, Hàm quick_exit Hoặc hàm signal với đối số đầu tiên bằng số tín hiệu tương ứng với tín hiệu gây ra sự gọi của trình xử lý. Hơn nữa, nếu một cuộc gọi đến hàm signal như vậy dẫn đến trả về SIG_ERR, giá trị của errno là không xác định.252)

252) Nếu bất kỳ tín hiệu nào được tạo bởi bộ xử lý tín hiệu không đồng bộ, hành vi không được xác định.

Điều đó có nghĩa là trong Tiêu chuẩn C, bạn có thể viết:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

và không nhiều khác.

POSIX nhẹ nhàng hơn rất nhiều về những gì bạn có thể làm trong trình xử lý tín hiệu, nhưng vẫn còn những hạn chế (và một trong những hạn chế là thư viện I/O tiêu chuẩn - printf() et al - không thể được sử dụng một cách an toàn ).

10
Jonathan Leffler

Bên cạnh việc sử dụng nó như dự định, dễ bay hơi được sử dụng trong (siêu mẫu) lập trình. Nó có thể được sử dụng để ngăn ngừa quá tải ngẫu nhiên, vì thuộc tính dễ bay hơi (như const) tham gia vào độ phân giải quá tải.

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

Đây là hợp pháp; cả hai quá tải đều có khả năng gọi được và làm gần như nhau. Diễn viên trong quá tải volatile là hợp pháp vì chúng ta biết thanh sẽ không vượt qua T dù sao đi nữa. Tuy nhiên, phiên bản volatile hoàn toàn tệ hơn, do đó, không bao giờ được chọn ở độ phân giải quá tải nếu không có biến động f.

Lưu ý rằng mã không bao giờ thực sự phụ thuộc vào truy cập bộ nhớ volatile.

7
MSalters

Tôi đã sử dụng nó trong các bản dựng gỡ lỗi khi trình biên dịch khăng khăng tối ưu hóa một biến mà tôi muốn có thể nhìn thấy khi bước qua mã.

7
indentation

Phát triển cho một nhúng, tôi có một vòng lặp kiểm tra một biến có thể được thay đổi trong một trình xử lý ngắt. Không có "biến động", vòng lặp sẽ trở thành noop - theo như trình biên dịch có thể nói, biến không bao giờ thay đổi, vì vậy nó tối ưu hóa việc kiểm tra đi.

Điều tương tự sẽ áp dụng cho một biến có thể được thay đổi trong một luồng khác trong môi trường truyền thống hơn, nhưng ở đó chúng ta thường thực hiện các cuộc gọi đồng bộ hóa, vì vậy trình biên dịch không quá miễn phí với tối ưu hóa.

7
Arkadiy
  1. bạn phải sử dụng nó để thực hiện spinlocks cũng như một số cấu trúc dữ liệu không khóa (tất cả?)
  2. sử dụng nó với các hoạt động/hướng dẫn nguyên tử
  3. đã giúp tôi một lần khắc phục lỗi của trình biên dịch (mã được tạo sai trong quá trình tối ưu hóa)
6
Mladen Janković

Từ khóa volatile nhằm ngăn chặn trình biên dịch áp dụng bất kỳ tối ưu hóa nào trên các đối tượng có thể thay đổi theo những cách mà trình biên dịch không thể xác định được.

Các đối tượng được khai báo là volatile được bỏ qua khỏi tối ưu hóa vì các giá trị của chúng có thể được thay đổi bởi mã bên ngoài phạm vi của mã hiện tại bất cứ lúc nào. Hệ thống luôn đọc giá trị hiện tại của một đối tượng volatile từ vị trí bộ nhớ thay vì giữ giá trị của nó trong thanh ghi tạm thời tại điểm được yêu cầu, ngay cả khi một lệnh trước đó yêu cầu một giá trị từ cùng một đối tượng.

Hãy xem xét các trường hợp sau

1) Các biến toàn cục được sửa đổi bởi một thói quen dịch vụ ngắt ngoài phạm vi.

2) Biến toàn cục trong một ứng dụng đa luồng.

Nếu chúng tôi không sử dụng vòng loại dễ bay hơi, các vấn đề sau có thể phát sinh

1) Mã có thể không hoạt động như mong đợi khi bật tối ưu hóa.

2) Mã có thể không hoạt động như mong đợi khi ngắt được kích hoạt và sử dụng.

Dễ bay hơi: Một người bạn tốt nhất của lập trình viên

https://en.wikipedia.org/wiki/Voliverse_ (computer_programming)

4
roottraveller

Chương trình của bạn dường như hoạt động ngay cả khi không có từ khóa volatile? Có lẽ đây là lý do:

Như đã đề cập trước đây, từ khóa volatile giúp cho các trường hợp như

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

Nhưng dường như hầu như không có tác dụng gì khi một chức năng bên ngoài hoặc phi tuyến được gọi. Ví dụ.:

while( *p!=0 ) { g(); }

Sau đó có hoặc không có volatile gần như cùng một kết quả được tạo.

Miễn là g() hoàn toàn có thể được nội tuyến, trình biên dịch có thể thấy mọi thứ đang diễn ra và do đó có thể tối ưu hóa. Nhưng khi chương trình thực hiện cuộc gọi đến nơi trình biên dịch không thể nhìn thấy Điều gì đang xảy ra, trình biên dịch sẽ không an toàn khi đưa ra bất kỳ giả định nào nữa. Do đó trình biên dịch sẽ tạo mã luôn đọc trực tiếp từ bộ nhớ.

Nhưng hãy cẩn thận trong ngày, khi hàm của bạn g() trở thành nội tuyến (do thay đổi rõ ràng hoặc do sự thông minh của trình biên dịch/trình liên kết) thì mã của bạn có thể bị hỏng nếu bạn quên volatile từ khóa!

Do đó, tôi khuyên bạn nên thêm từ khóa volatile ngay cả khi chương trình của bạn dường như không hoạt động. Nó làm cho ý định rõ ràng hơn và mạnh mẽ hơn đối với những thay đổi trong tương lai.

2
Joachim

Trong những ngày đầu của C, trình biên dịch sẽ diễn giải tất cả các hành động đọc và ghi giá trị như các hoạt động của bộ nhớ, được thực hiện theo trình tự giống như các lần đọc và ghi xuất hiện trong mã. Hiệu quả có thể được cải thiện đáng kể trong nhiều trường hợp nếu trình biên dịch được cung cấp một lượng tự do nhất định để đặt hàng lại và hợp nhất các hoạt động, nhưng có một vấn đề với điều này. Ngay cả các thao tác thường được chỉ định theo một thứ tự nhất định chỉ vì cần chỉ định chúng theo thứ tự một số, và do đó, lập trình viên đã chọn một trong nhiều lựa chọn thay thế tốt như nhau, điều đó không phải luôn luôn như vậy. Đôi khi, điều quan trọng là các hoạt động nhất định xảy ra theo một trình tự cụ thể.

Chính xác các chi tiết của trình tự là quan trọng sẽ thay đổi tùy thuộc vào nền tảng và trường ứng dụng đích. Thay vì cung cấp kiểm soát đặc biệt chi tiết, Tiêu chuẩn đã chọn một mô hình đơn giản: nếu một chuỗi các truy cập được thực hiện với các giá trị không đủ điều kiện volatile, trình biên dịch có thể sắp xếp lại và hợp nhất chúng khi thấy phù hợp. Nếu một hành động được thực hiện với giá trị đủ điều kiện volatile-, thì việc triển khai chất lượng sẽ cung cấp bất kỳ đảm bảo đặt hàng bổ sung nào có thể được yêu cầu bằng cách nhắm mục tiêu vào nền tảng và trường ứng dụng dự định của nó, mà không phải sử dụng cú pháp không chuẩn .

Thật không may, thay vì xác định những gì đảm bảo các lập trình viên sẽ cần, nhiều trình biên dịch đã chọn thay thế để đưa ra các đảm bảo tối thiểu được ủy quyền theo Tiêu chuẩn. Điều này làm cho volatile ít hữu ích hơn mức cần thiết. Ví dụ, trên gcc hoặc clang, một lập trình viên cần thực hiện một "mutex off-hand" cơ bản [một trong đó một nhiệm vụ đã thu được và phát hành một mutex sẽ không thực hiện lại cho đến khi nhiệm vụ khác đã thực hiện] phải thực hiện một trong bốn điều:

  1. Đặt việc mua lại và phát hành mutex trong một chức năng mà trình biên dịch không thể nội tuyến và nó không thể áp dụng Tối ưu hóa toàn bộ chương trình.

  2. Đủ điều kiện tất cả các đối tượng được bảo vệ bởi mutex là volatile-- một cái gì đó không cần thiết nếu tất cả các truy cập xảy ra sau khi có được mutex và trước khi phát hành nó.

  3. Sử dụng mức tối ưu hóa 0 để buộc trình biên dịch tạo mã như thể tất cả các đối tượng không đủ điều kiện registervolatile.

  4. Sử dụng chỉ thị cụ thể gcc.

Ngược lại, khi sử dụng trình biên dịch chất lượng cao hơn, phù hợp hơn cho lập trình hệ thống, chẳng hạn như icc, người ta sẽ có một tùy chọn khác:

  1. Đảm bảo rằng một volatile- ghi đủ điều kiện được thực hiện ở mọi nơi cần có hoặc phát hành.

Để có được một "mutex off-hand" cơ bản đòi hỏi phải đọc volatile (để xem nó đã sẵn sàng chưa) và không nên yêu cầu viết volatile (phía bên kia sẽ không cố gắng lấy lại nó cho đến khi nó được trả lại) nhưng phải thực hiện một ghi volatile vô nghĩa vẫn tốt hơn bất kỳ tùy chọn nào có sẵn dưới gcc hoặc clang.

2
supercat

Bên cạnh thực tế là từ khóa dễ bay hơi được sử dụng để báo cho trình biên dịch không tối ưu hóa quyền truy cập vào một số biến (có thể được sửa đổi bởi một luồng hoặc một thói quen gián đoạn), nó cũng có thể được sử dụng để loại bỏ một số lỗi trình biên dịch - CÓ, nó có thể là ---.

Ví dụ, tôi đã làm việc trên một nền tảng nhúng là trình biên dịch đã đưa ra một số giả định sai về giá trị của một biến. Nếu mã không được tối ưu hóa, chương trình sẽ chạy ổn. Với việc tối ưu hóa (điều thực sự cần thiết vì đây là một thói quen quan trọng), mã sẽ không hoạt động chính xác. Giải pháp duy nhất (mặc dù không chính xác lắm) là khai báo biến 'bị lỗi' là không ổn định.

2
INS

Một cách sử dụng tôi nên nhắc bạn là, trong hàm xử lý tín hiệu, nếu bạn muốn truy cập/sửa đổi một biến toàn cục (ví dụ: đánh dấu nó là exit = true), bạn phải khai báo biến đó là 'dễ bay hơi'.

1
bugs king