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


222

Không những gì volatiletừ khóa 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ó.


Dưới đây là một cuộc thảo luận thú vị về sự biến động liên quan đến mẫu Singleton: aristeia.com/Papers/DDJ_Jul_Aug_2004_Vvised.pdf
Chessguy

3
Có một kỹ thuật hấp dẫn làm cho trình biên dịch của bạn phát hiện các điều kiện chủng tộc có thể phụ thuộc rất nhiều vào từ khóa dễ bay hơi, bạn có thể đọc về nó tại http://www.ddj.com/cpp/184403766 .
Neno Ganchev

Đây là một tài nguyên tốt với một ví dụ về thời điểm volatilecó thể được sử dụng một cách hiệu quả, kết hợp với nhau theo các thuật ngữ giáo dân khá. Liên kết: ấn
hóa được tối ưu hóa

Tôi sử dụng nó để khóa mã miễn phí / khóa kiểm tra kép
paulm

Đối với tôi, volatilehữu ích hơn friendtừ khóa.
acegs

Câu trả lời:


268

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

Nếu không volatile, trình tối ưu hóa sẽ 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.


Trong trường hợp này, điều gì sẽ xảy ra nếu uint16_t* volatile semPtrđược viết thay thế? Điều này sẽ đánh dấu con trỏ là dễ bay hơi (thay vì giá trị được trỏ đến), để kiểm tra chính con trỏ, ví dụ semPtr == SOME_ADDRcó thể không được tối ưu hóa. Tuy nhiên, điều này cũng ngụ ý một giá trị nhọn dễ bay hơi. Không?
Zyl

@Zyl Không, không. Trong thực tế, những gì bạn đề xuất có khả năng những gì sẽ xảy ra. Nhưng về mặt lý thuyết, người ta có thể kết thúc với một trình biên dịch tối ưu hóa quyền truy cập vào các giá trị vì nó quyết định rằng không có giá trị nào trong số đó được thay đổi. Và nếu bạn có nghĩa là không ổn định để áp dụng cho giá trị chứ không phải con trỏ, bạn sẽ bị lừa. Một lần nữa, không thể, nhưng tốt hơn là sai lầm khi làm việc đúng, hơn là lợi dụng hành vi xảy ra với công việc ngày nay.
iheanyi

1
@Doug T. Một lời giải thích tốt hơn là thế này
machineaddict

3
@cquilguy nó không quyết định sai. Nó đã được khấu trừ chính xác dựa trên thông tin được đưa ra. Nếu bạn không đánh dấu thứ gì đó dễ bay hơi, trình biên dịch sẽ tự do cho rằng nó không dễ bay hơi . Đó là những gì trình biên dịch làm khi tối ưu hóa mã. Nếu có nhiều thông tin hơn, cụ thể là dữ liệu nói trên thực tế không ổn định, thì lập trình viên phải có trách nhiệm cung cấp thông tin đó. Những gì bạn đang tuyên bố bởi một trình biên dịch lỗi thực sự chỉ là lập trình xấu.
iheanyi

1
@cquilguy không, chỉ vì từ khóa dễ bay hơi xuất hiện một lần không có nghĩa là mọi thứ đột nhiên biến động. Tôi đã đưa ra một kịch bản trong đó trình biên dịch thực hiện đúng và đạt được một kết quả trái ngược với những gì lập trình viên mong đợi. Giống như "phân tích cú pháp nhất" không phải là dấu hiệu của lỗi trình biên dịch, trường hợp này cũng không xảy ra.
iheanyi

82

volatilelà 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 volatiletừ khóa để đảm bảo rằng các truy cập đó không được trình biên dịch tối ưu hóa.


9
Điều này không chỉ hợp lệ cho các hệ thống nhúng mà còn cho tất cả sự phát triển trình điều khiển thiết bị.
Mladen Janković

Lần duy nhất tôi từng cần nó trên một chiếc xe buýt 8 bit của ISA, nơi bạn đọc cùng một địa chỉ hai lần - trình biên dịch có lỗi và bỏ qua nó (đầu Zortech c ++)
Martin Beckett

Dễ bay hơi rất hiếm khi đủ để kiểm soát các thiết bị bên ngoài. Ngữ nghĩa của nó là sai đối với MMIO hiện đại: bạn phải làm cho quá nhiều đối tượng biến động và làm tổn thương tối ưu hóa. Nhưng MMIO hiện đại hoạt động như bộ nhớ bình thường cho đến khi một lá cờ được thiết lập nên không cần thiết. Nhiều trình điều khiển không bao giờ sử dụng dễ bay hơi.
tò mò

69

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ư tổng hợp 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.


5
Khi bạn tính các đạo hàm số, nó cũng hữu ích, để đảm bảo x + h - x == h bạn xác định hh = x + h - x là biến động để có thể tính toán một delta thích hợp.
Alexandre C.

5
+1, thực sự theo kinh nghiệm của tôi đã có một trường hợp khi các phép tính dấu phẩy động tạo ra các kết quả khác nhau trong Gỡ lỗi và Phát hành, do đó, các bài kiểm tra đơn vị được viết cho một cấu hình đã thất bại cho một cấu hình khác. Chúng tôi đã giải quyết nó bằng cách khai báo một biến dấu phẩy động volatile doublethay vì chỉ double, vì vậy để đảm bảo rằng nó được cắt từ độ chính xác của FPU thành độ chính xác 64 bit (RAM) trước khi tiếp tục tính toán thêm. Các kết quả rất khác nhau do sự phóng đại của lỗi dấu phẩy động.
Serge Rogatch

Định nghĩa của bạn về "hiện đại" là một chút tắt. Chỉ mã x86 32 bit tránh SSE / SSE2 bị ảnh hưởng bởi điều này và nó không "hiện đại" ngay cả 10 năm trước. MIPS / ARM / POWER đều có các thanh ghi phần cứng 64 bit, và x86 với SSE2 cũng vậy. Việc triển khai C ++ x86-64 luôn sử dụng SSE2 và trình biên dịch cũng có các tùy chọn muốn g++ -mfpmath=ssesử dụng nó cho x86 32 bit. Bạn có thể sử dụng gcc -ffloat-stoređể buộc làm tròn mọi nơi ngay cả khi sử dụng x87 hoặc bạn có thể đặt độ chính xác x87 thành mantissa 53 bit: Randomascii.wordpress.com/2012/03/21/ Lỗi .
Peter Cordes

Nhưng vẫn có câu trả lời tốt, đối với gen-x87 lỗi thời, bạn có thể sử dụng volatileđể buộc làm tròn ở một vài nơi cụ thể mà không mất lợi ích ở mọi nơi.
Peter Cordes

1
Hay tôi nhầm lẫn không chính xác với không nhất quán?
Chipster

49

Từ một bài viết "Biến động 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ề volatiletừ khóa:


23

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

Bạn tuyệt đối KHÔNG cần phải khai báo một biến dễ bay hơi để có thể sử dụng InterlockedIncrement.
tò mò

Câu trả lời này đã lỗi thời khi C ++ 11 cung cấp std::atomic<LONG>để bạn có thể viết mã không khóa an toàn hơn mà không gặp vấn đề gì về việc tải thuần / cửa hàng thuần túy được tối ưu hóa, hoặc sắp xếp lại, hoặc bất cứ điều gì khác.
Peter Cordes

10

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

Trong Tiêu chuẩn C, một trong những nơi để sử dụng volatilelà với bộ xử lý tín hiệu. Trong 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 một volatile sig_atomic_tbiến hoặc thoát nhanh chóng. Thật vậy, AFAIK, đây là nơi duy nhất trong Tiêu chuẩn C volatileyêu cầu sử dụng để tránh hành vi không xác định.

ISO / IEC 9899: 2011 §7.14.1.1 signalChức năng

¶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 aborthoặc raisehàm, hành vi không được xác định nếu trình xử lý tín hiệu tham chiếu đế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à đối tượng nguyên tử không khóa ngoài việc gán giá trị đến một đối tượng được khai báo là volatile sig_atomic_thoặ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 aborthàm, _Exithàm, quick_exithàm hoặc signalhàm có đối số thứ nhất bằng số tín hiệu tương ứng với tín hiệu gây ra sự gọi của xử lý. Hơn nữa, nếu một lệnh gọi như vậy dẫn đến signalkết quả trả về SIG_ERR, giá trị của errnolà 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).


7

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

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

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 volatiletình trạng quá tải là hợp pháp vì chúng ta biết rằng thanh sẽ không vượt qua bất ổn T. Cácvolatile phiên bản 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 volatiletruy cập bộ nhớ.


Bạn có thể vui lòng giải thích về điều này với một ví dụ? Nó thực sự giúp tôi hiểu rõ hơn. Cảm ơn!
batbrat

" Diễn viên trong tình trạng quá tải dễ bay hơi " Một diễn viên là một chuyển đổi rõ ràng. Đó là một cấu trúc SYNTAX. Nhiều người làm cho sự nhầm lẫn đó (ngay cả các tác giả tiêu chuẩn).
tò mò

6
  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)

5
Bạn nên sử dụng một thư viện, nội tại trình biên dịch hoặc mã lắp ráp nội tuyến. Dễ bay hơi là không đáng tin cậy.
Zan Lynx

1
Cả 1 và 2 đều sử dụng các hoạt động nguyên tử, nhưng không ổn định không cung cấp ngữ nghĩa nguyên tử và việc triển khai nguyên tử cụ thể theo nền tảng sẽ đáp ứng nhu cầu sử dụng dễ bay hơi, vì vậy, đối với 1 và 2, tôi không đồng ý với những điều này.

Ai nói bất cứ điều gì về việc cung cấp ngữ nghĩa nguyên tử dễ bay hơi? Tôi đã nói rằng bạn cần phải sử dụng các hoạt động nguyên tử dễ bay hơi với các hoạt động nguyên tử và nếu bạn không nghĩ rằng đó là sự thật về các tuyên bố về các hoạt động lồng vào nhau của API win32 (anh chàng này cũng đã giải thích điều này trong câu trả lời của mình)
Mladen Janković 17/211

4

Các volatiletừ khóa được thiết kế để ngăn chặn sự biên dịch từ áp dụng bất kỳ optimisations trên các đối tượng có thể thay đổi theo những cách mà không thể được xác định bởi trình biên dịch.

Các đối tượng được khai báo là volatileđược bỏ qua khỏi tối ưu hóa vì giá trị của chúng có thể được thay đổi bằng 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 volatileđối tượng 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 đây

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: Bạn thân của lập trình viên

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


Liên kết bạn đã đăng rất lỗi thời và không phản ánh các thực tiễn tốt nhất hiện tại.
Tim Seguine

2

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Ó được ---.

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.


3
Đó là một giả định sai lầm cho rằng trình biên dịch không tối ưu hóa quyền truy cập vào các chất bay hơi. Các tiêu chuẩn không biết gì về tối ưu hóa. Trình biên dịch được yêu cầu phải tôn trọng những gì tiêu chuẩn ra lệnh, nhưng có thể tự do thực hiện bất kỳ tối ưu hóa nào không can thiệp vào hành vi thông thường.
Ga cuối

3
Theo kinh nghiệm của tôi, 99,9% tất cả các "lỗi" tối ưu hóa trong gcc arm là lỗi của người lập trình. Không có ý tưởng nếu điều này áp dụng cho câu trả lời này. Chỉ là một câu chuyện về chủ đề chung
odinthenerd

@Terminus " Đó là một giả định sai lầm khi cho rằng trình biên dịch không tối ưu hóa quyền truy cập vào các chất bay hơi " Nguồn?
tò mò

2

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

Như đã đề cập trước đây, volatiletừ khóa 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ó volatilekết quả gần như giống nhau được tạo ra.

Miễn là g () có thể được hoàn toàn 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 một nơi mà trình biên dịch không thể nhìn thấy những gì đang diễn ra, thì trình biên dịch sẽ không an toàn để đư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 g () của bạn 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 / liên kết) thì mã của bạn có thể bị hỏng nếu bạn quên volatiletừ khóa!

Vì vậy, tôi khuyên bạn nên thêm volatiletừ khóa 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.


Lưu ý rằng một hàm có thể có mã của nó được nội tuyến trong khi vẫn tạo tham chiếu (được giải quyết tại thời điểm liên kết) cho hàm phác thảo; đây sẽ là trường hợp của hàm đệ quy được nội tuyến một phần. Một hàm cũng có thể có "nội tuyến" ngữ nghĩa của trình biên dịch, đó là trình biên dịch giả định các tác dụng phụ và kết quả nằm trong các tác dụng phụ có thể có và kết quả có thể theo mã nguồn của nó, trong khi vẫn không nội tuyến. Điều này dựa trên "Quy tắc một định nghĩa hiệu quả" trong đó nêu rõ rằng tất cả các định nghĩa của một thực thể sẽ tương đương hiệu quả (nếu không giống hệt nhau).
tò mò

Có thể tránh được việc đặt nội tuyến một cuộc gọi (hoặc "nội tuyến" ngữ nghĩa của nó) bởi một chức năng mà trình biên dịch có thể nhìn thấy được (ngay cả tại thời điểm liên kết với tối ưu hóa toàn cầu) có thể bằng cách sử dụng một volatilecon trỏ hàm đủ điều kiện:void (* volatile fun_ptr)() = fun; fun_ptr();
tò mò

2

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 hoạt động thường được chỉ định theo một thứ tự nhất định chỉ vì cần phải chỉ định chúng theo một số thứ tự, 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 mục tiêu và trường ứng dụng. 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 volatilegiá trị đủ tiêu chuẩn, 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 theo tiêu chuẩn bắt buộc theo Tiêu chuẩn. Điều này làm cho volatileít hữu ích hơn nó nên được. 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à - một volatilecá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 các trình biên dịch để tạo ra mã như thể tất cả các đối tượng mà không đủ điều kiện registerđược volatile.

  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. Hãy chắc chắn rằng một bản volatileghi đủ tiêu chuẩ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 volatileđọc (để xem nó đã sẵn sàng chưa) và cũng không cần phải volatileviết (bên kia sẽ không cố gắng lấy lại cho đến khi nó được trả lại) mà phải thực hiện một volatilebài viết 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.


1

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

Tất cả các câu trả lời là tuyệt vời. Nhưng trên hết, tôi muốn chia sẻ một ví dụ.

Dưới đây là một chương trình cpp nhỏ:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if(x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

Bây giờ, hãy tạo tập hợp mã ở trên (và tôi sẽ chỉ dán các phần của tập hợp có liên quan ở đây):

Lệnh tạo lắp ráp:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

Và lắp ráp:

main:
.LFB1594:
    subq    $40, %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    $5, x(%rip)  #, x
    addq    $40, %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Bạn có thể thấy trong hội đồng mà mã lắp ráp không được tạo sprintfvì trình biên dịch giả định rằng nó xsẽ không thay đổi bên ngoài chương trình. Và tương tự là trường hợp với whilevòng lặp. whilevòng lặp đã bị loại bỏ hoàn toàn do tối ưu hóa vì trình biên dịch đã xem nó như một mã vô dụng và do đó được gán trực tiếp 5cho x(xem movl $5, x(%rip)).

Vấn đề xảy ra khi điều gì xảy ra nếu một quy trình / phần cứng bên ngoài sẽ thay đổi giá trị của xmột nơi nào đó giữa x = 8;if(x == 8). Chúng tôi hy vọng elsekhối sẽ hoạt động nhưng không may là trình biên dịch đã cắt bỏ phần đó.

Bây giờ, để giải quyết việc này, trong assembly.cpp, chúng ta hãy thay đổi int x;để volatile int x;và nhanh chóng xem mã lắp ráp tạo ra:

main:
.LFB1594:
    subq    $104, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:9:     if(x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if(x == 8)
    cmpl    $8, %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    $1000, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    $5, %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    $1, %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    $5, %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    $104, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Ở đây bạn có thể thấy rằng các mã lắp ráp cho sprintf, printfwhilevòng lặp đã được tạo ra. Ưu điểm là nếu xbiến được thay đổi bởi một số chương trình hoặc phần cứng bên ngoài, sprintfmột phần của mã sẽ được thực thi. Và whilevòng lặp tương tự có thể được sử dụng cho chờ đợi bận rộn bây giờ.


0

Các câu trả lời khác đã đề cập đến việc tránh một số tối ưu hóa để:

  • sử dụng các thanh ghi ánh xạ bộ nhớ (hoặc "MMIO")
  • viết trình điều khiển thiết bị
  • cho phép gỡ lỗi chương trình dễ dàng hơn
  • làm cho tính toán dấu phẩy động mang tính quyết định hơn

Dễ bay hơi là điều cần thiết bất cứ khi nào bạn cần một giá trị xuất hiện từ bên ngoài và không thể đoán trước và tránh tối ưu hóa trình biên dịch dựa trên giá trị đã biết và khi kết quả không thực sự được sử dụng nhưng bạn cần tính toán hoặc sử dụng nhưng bạn muốn tính toán nó nhiều lần cho điểm chuẩn và bạn cần tính toán để bắt đầu và kết thúc tại các điểm chính xác.

Một giá trị đọc không ổn định giống như một thao tác đầu vào (như scanfhoặc sử dụng cin): giá trị dường như đến từ bên ngoài chương trình, do đó, bất kỳ tính toán nào có phụ thuộc vào giá trị cần phải bắt đầu sau nó .

Một ghi dễ bay hơi giống như một hoạt động đầu ra (như printfhoặc sử dụng cout): giá trị dường như được truyền đạt bên ngoài chương trình, vì vậy nếu giá trị phụ thuộc vào một tính toán, nó cần phải được hoàn thành trước đó .

Vì vậy, một cặp đọc / ghi dễ bay hơi có thể được sử dụng để chế ngự điểm chuẩn và làm cho phép đo thời gian có ý nghĩa .

Nếu không có biến động, tính toán của bạn có thể được trình biên dịch bắt đầu trước đó, vì không có gì ngăn cản việc sắp xếp lại các tính toán với các chức năng như đo thời gian .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.