volatile
C에 왜 필요한가요? 무엇을 위해 사용됩니까? 그것은 무엇을 할 것인가?
volatile
C에 왜 필요한가요? 무엇을 위해 사용됩니까? 그것은 무엇을 할 것인가?
답변:
휘발성은 컴파일러에게 휘발성 변수와 관련된 것을 최적화하지 말라고 지시합니다.
변수를 사용하는 최소한 3 가지 일반적인 이유가 있습니다. 여기에는 변수 코드의 값이 보이는 코드에서 조치없이 변경 될 수있는 상황이 포함됩니다. 값 자체를 변경하는 하드웨어와 인터페이스 할 때; 변수를 사용하는 다른 스레드가 실행 중일 때; 또는 변수 값을 변경할 수있는 신호 처리기가있을 때.
RAM에 매핑되어 있고 명령 포트와 데이터 포트의 두 가지 주소가있는 작은 하드웨어가 있다고 가정 해 봅시다.
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
이제 몇 가지 명령을 보내려고합니다.
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;
}
쉬운 것처럼 보이지만 컴파일러가 데이터와 명령이 작성된 순서를 자유롭게 변경할 수 있기 때문에 실패 할 수 있습니다. 이로 인해 작은 가젯이 이전 데이터 값으로 명령을 실행하게됩니다. 또한 통화 중 루프 대기 중을 살펴보십시오. 그 중 하나가 최적화됩니다. 컴파일러는 영리하려고 노력하고 isbusy의 값을 한 번만 읽은 다음 무한 루프로 들어갑니다. 그것은 당신이 원하는 것이 아닙니다.
이 문제를 해결하는 방법은 포인터 가젯을 일시적으로 선언하는 것입니다. 이렇게하면 컴파일러가 작성한 것을 강제로 수행합니다. 메모리 할당을 제거 할 수 없으며 레지스터의 변수를 캐시 할 수 없으며 할당 순서도 변경할 수 없습니다.
이것은 올바른 버전입니다.
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
C에서 실제로 변수 값을 자동으로 캐싱하지 않기 위해 존재했습니다. 컴파일러에게이 변수의 값을 캐시하지 않도록 지시합니다. 따라서 주어진 volatile
변수 의 값을 메인 메모리에서 발견 할 때마다 가져 오는 코드를 생성 합니다. 이 메커니즘은 언제라도 OS 또는 인터럽트에 의해 값을 수정할 수 있기 때문에 사용됩니다. 따라서를 사용 volatile
하면 매번 새로운 가치에 접근 할 수 있습니다.
volatile
프로그래머가 코드를 최적화하는 동시에 프로그래머가 이러한 최적화없이 달성 할 수있는 의미를 달성 할 수 있도록하는 것이 목적 이었습니다. 표준 작성자는 품질 구현이 대상 플랫폼 및 응용 프로그램 필드에서 유용한 의미를 지원할 것으로 기대했으며 컴파일러 작성자는 표준을 준수하고 100 %가 아닌 최저 품질 의미를 제공 할 것으로 기대하지 않았습니다. 바보 (이 표준의 저자는 이론적 근거에서 명시 적으로 인식합니다 ...
다른 용도로 volatile
는 신호 처리기가 있습니다. 이런 코드가 있다면 :
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
컴파일러는 루프 바디가 quit
변수를 건드리지 않고 루프를 루프로 변환하는 것을 알 수 while (true)
있습니다. 짝수 경우 quit
변수에 대한 신호 처리기에 설정 SIGINT
하고 SIGTERM
; 컴파일러는 그것을 알 방법이 없습니다.
그러나 quit
변수가 선언 volatile
되면 컴파일러는 다른 곳에서 수정할 수 있으므로 매번 변수 를로드해야합니다. 이것이 바로이 상황에서 원하는 것입니다.
quit
가정하면 컴파일러는 상수 루프로 최적화 할 수 있다고 가정합니다. quit
반복간에 변경 될 수있는 방법이 없습니다 . 주의 : 이것은 실제 쓰레드 세이프 프로그래밍을 대신 할만한 좋은 것은 아닙니다.
volatile
또는 다른 마커가없는 경우 전역 변수 인 경우에도 루프 외부로 들어가면 루프에 들어간 변수를 수정하는 것으로 가정합니다.
extern int global; void fn(void) { while (global != 0) { } }
과 gcc -O3 -S
그 결과 어셈블리 파일에서 보면, 그것은 않는 내 컴퓨터에 movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
즉, 전역이 0이 아닌 경우 무한 루프입니다. 그런 다음 추가 volatile
하고 차이점을 확인하십시오.
Andrei Alexandrescu의이 기사를 참조하십시오. " 휘발성-다중 스레드 프로그래머의 가장 친한 친구 "
휘발성 키워드는 특정 비동기 이벤트의 존재에 잘못된 코드를 렌더링 할 수있는 컴파일러 최적화를 방지하기 위해 고안되었다. 예를 들어, 기본 변수를 volatile 로 선언 하면 컴파일러는 레지스터에 변수 를 캐시 할 수 없습니다. 변수가 여러 스레드간에 공유되는 경우 비참한 일반적인 최적화입니다. 따라서 일반적인 규칙은 여러 스레드간에 공유해야하는 기본 유형의 변수가있는 경우 해당 변수를 휘발성으로 선언하는 것입니다. 그러나 실제로이 키워드를 사용하면 더 많은 작업을 수행 할 수 있습니다.이 키워드를 사용하여 스레드로부터 안전하지 않은 코드를 포착 할 수 있으며 컴파일 타임에 수행 할 수 있습니다. 이 기사는 어떻게 수행되는지 보여줍니다. 이 솔루션에는 간단한 스마트 포인터가 포함되어있어 중요한 코드 섹션을 쉽게 직렬화 할 수 있습니다.
이 기사는 모두 적용 C
하고 C++
.
Scott Meyers와 Andrei Alexandrescu의 " C ++ 및 이중 검사 잠금 위험 "기사도 참조하십시오 .
따라서 일부 메모리 위치 (예 : ISR (Interrupt Service Routines)에서 참조하는 메모리 매핑 포트 또는 메모리)를 처리 할 때는 일부 최적화를 일시 중단해야합니다. 휘발성은 이러한 위치에 대한 특별한 처리를 지정하기 위해 존재합니다. 특히 : (1) 휘발성 변수의 내용이 "불안정"(컴파일러가 알 수없는 방법으로 변경할 수 있음), (2) 휘발성 데이터에 대한 모든 쓰기가 "관찰 가능"하므로 (3) 휘발성 데이터에 대한 모든 작업은 소스 코드에 나타나는 순서대로 실행됩니다. 처음 두 규칙은 올바른 읽기와 쓰기를 보장합니다. 마지막으로 입력과 출력을 혼합하는 I / O 프로토콜을 구현할 수 있습니다. 이것은 비공식적으로 C 및 C ++의 휘발성이 보장하는 것입니다.
volatile
원 자성을 보장하지는 않습니다.
내 간단한 설명은 다음과 같습니다
일부 시나리오에서는 논리 또는 코드를 기반으로 컴파일러에서 변경되지 않는 것으로 생각되는 변수를 최적화합니다. volatile
키워드 방지는 변수가 최적화된다.
예를 들면 다음과 같습니다.
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
위의 코드에서 컴파일러는 usb_interface_flag
0으로 정의되고 while 루프에서는 영원히 0이된다고 생각할 수 있습니다. 최적화 후, 컴파일러는이를 while(true)
항상 처리 하여 무한 루프를 발생시킵니다.
이러한 종류의 시나리오를 피하기 위해 플래그를 휘발성으로 선언하고 외부 인터페이스 나 다른 프로그램 모듈에 의해이 값이 변경 될 수 있음을 컴파일러에 알리고 있습니다. 즉, 최적화하지 마십시오. 이것이 휘발성의 사용 사례입니다.
휘발성에 대한 한계 사용은 다음과 같습니다. 함수의 수치 미분을 계산한다고 가정 해보십시오 f
.
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
문제는 x+h-x
일반적으로 h
반올림 오류 로 인해 같지 않다는 것 입니다. 생각해보십시오. 매우 가까운 수를 빼면 미분 계수 계산을 망칠 수있는 많은 유효 자릿수가 손실됩니다 (1.00001-1). 가능한 해결 방법은 다음과 같습니다.
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
그러나 플랫폼 및 컴파일러 스위치에 따라 적극적으로 최적화되는 컴파일러가 해당 기능의 두 번째 줄을 지울 수 있습니다. 그래서 대신 쓰세요
volatile double hh = x + h;
hh -= x;
컴파일러가 hh를 포함하는 메모리 위치를 강제로 읽어서 최종 최적화 기회를 상실합니다.
h
또는 hh
파생 수식 사용의 차이점은 무엇입니까 ? 경우 hh
계산되는 최종 수식은 차이없이, 처음처럼 이용한다. 어쩌면 (f(x+h) - f(x))/hh
?
h
및 hh
그는 인 hh
동작에 의해 둘의 부의 파워 잘린다 x + h - x
. 이 경우 x + hh
와 x
정확히 차이 hh
. 또한 이후이 같은 결과를 제공합니다 수식을 취할 수 x + h
와 x + hh
(이 여기서 중요한 분모입니다) 동일합니다.
x1=x+h; d = (f(x1)-f(x))/(x1-x)
아닐까요? 휘발성 물질을 사용하지 않고.
-ffast-math
하거나 이와 동등한 것으로 컴파일러를 최적화하는 것이 더 허용된다고 생각합니다 .
두 가지 용도가 있습니다. 이들은 임베디드 개발에서 더 자주 사용됩니다.
컴파일러는 volatile 키워드로 정의 된 변수를 사용하는 함수를 최적화하지 않습니다
휘발성은 RAM, ROM 등의 정확한 메모리 위치에 액세스하는 데 사용됩니다. 메모리 매핑 장치를 제어하고 CPU 레지스터에 액세스하며 특정 메모리 위치를 찾는 데 더 자주 사용됩니다.
어셈블리 목록이있는 예제를 참조하십시오. Re : 임베디드 개발에서 C "휘발성"키워드 사용
휘발성은 컴파일러가 특정 코드 시퀀스를 최적화하지 않도록 (예 : 마이크로 벤치 마크 작성) 유용합니다.
휘발성 물질이 중요한 또 다른 시나리오를 언급하겠습니다.
빠른 I / O를 위해 파일을 메모리에 매핑하고 해당 파일이 장면 뒤에서 변경 될 수 있다고 가정합니다 (예 : 파일이 로컬 하드 드라이브에 있지 않지만 대신 다른 컴퓨터에 의해 네트워크를 통해 제공됨).
비 휘발성 객체 (소스 코드 수준에서)에 대한 포인터를 통해 메모리 매핑 된 파일의 데이터에 액세스하는 경우 컴파일러에서 생성 된 코드는 사용자가 알지 않고도 동일한 데이터를 여러 번 가져올 수 있습니다.
해당 데이터가 변경되면 프로그램에서 두 개 이상의 서로 다른 버전의 데이터를 사용하여 불일치 상태가 될 수 있습니다. 이로 인해 프로그램의 논리적으로 잘못된 동작뿐만 아니라 신뢰할 수없는 파일 또는 신뢰할 수없는 위치에서 파일을 처리하는 경우 악용 가능한 보안 허점이 될 수 있습니다.
보안이 중요하고 고려해야 할 경우 고려해야 할 중요한 시나리오입니다.
휘발성은 스토리지가 언제라도 변경되어 변경 될 수 있지만 사용자 프로그램이 제어 할 수없는 것임을 의미합니다. 즉, 변수를 참조하면 프로그램은 항상 실제 주소 (매핑 된 입력 fifo)를 확인하고 캐시 된 방식으로 사용해서는 안됩니다.
위키는 volatile
다음 에 관한 모든 것을 말합니다 .
그리고 리눅스 커널의 문서는 다음과 같은 훌륭한 표기법을 제시합니다 volatile
.
내 의견으로는을 너무 많이 기 대해서는 안됩니다 volatile
. 예를 들어 Nils Pipenbrinck의 투표가 많은 답변의 예를 살펴보십시오 .
나는 그의 예가 적합하지 않다고 말할 것이다 volatile
. 컴파일러가 유용하고 바람직한 최적화를하지 못하도록하기volatile
위해서만 사용됩니다
. 스레드 안전, 원자 액세스 또는 메모리 순서에 관한 것은 아닙니다.
이 예에서 :
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;
}
gadget->data = data
전에 gadget->command = command
만은 컴파일러에 의해 컴파일 된 코드에서 보장된다. 실행 시간에 프로세서는 프로세서 아키텍처와 관련하여 데이터 및 명령 할당을 여전히 재정렬 할 수 있습니다. 하드웨어가 잘못된 데이터를 얻을 수 있습니다 (가제트가 하드웨어 I / O에 매핑되어 있음). 데이터와 명령 할당간에 메모리 장벽이 필요합니다.
volatile
아무 이유없이 성능을 저하시키는 것 같습니다 . 충분한 지 여부는 프로그래머가 컴파일러보다 더 알 수있는 시스템의 다른 측면에 따라 다릅니다. 반면에 프로세서가 특정 주소에 쓰는 명령이 CPU 캐시를 플러시한다고 보장하지만 컴파일러가 레지스터 캐시 변수를 플러시 할 방법을 제공하지 않으면 CPU는 아무것도 알지 못하므로 캐시를 플러시하는 것은 쓸모가 없습니다.
Dennis Ritchie가 설계 한 언어에서 주소를 가져 오지 않은 자동 개체 이외의 모든 개체에 대한 모든 액세스는 마치 개체의 주소를 계산 한 다음 해당 주소의 저장소를 읽거나 쓴 것처럼 동작합니다. 이로 인해 언어가 매우 강력 해졌지만 최적화 기회가 크게 제한되었습니다.
컴파일러가 특정 객체가 이상한 방식으로 변경되지 않을 것이라고 가정하도록 한정자를 추가하는 것이 가능할 수도 있지만, 이러한 가정은 C 프로그램의 대다수 객체에 적합 할 것입니다. 그러한 가정이 적절한 모든 객체에 한정자를 추가하는 것은 비현실적이었다. 다른 한편으로, 일부 프로그램은 그러한 가정이 갖지 않는 일부 객체를 사용해야합니다. 이 문제를 해결하기 위해 표준에 따르면 컴파일러는 선언되지 않은 객체 volatile
의 값이 컴파일러의 제어 범위를 벗어나거나 합리적인 컴파일러의 이해 범위를 벗어난 방식으로 관찰되거나 변경되지 않을 것이라고 가정 할 수 있습니다.
다양한 플랫폼은 컴파일러의 제어 범위 밖에서 객체를 관찰하거나 수정할 수있는 방법이 서로 다를 수 있으므로 해당 플랫폼에 대한 양질의 컴파일러는 volatile
의미론을 정확하게 처리하는 것이 달라야합니다. 불행히도, 표준은 플랫폼에서 저수준 프로그래밍을위한 고품질 컴파일러가 해당 플랫폼 volatile
에서 특정 읽기 / 쓰기 작업의 모든 관련 영향을 인식하는 방식으로 처리해야한다고 제안하지 않았기 때문에 많은 컴파일러가 수행하지 못하고 있습니다. 따라서 효율적이지만 컴파일러 "최적화"에 의해 깨질 수없는 방식으로 백그라운드 I / O와 같은 것을 처리하기 어렵게 만드는 방식으로.
여기에서 많은 사람들이 올바르게 제안한 것처럼, 휘발성 키워드의 보편적 인 용도는 휘발성 변수의 최적화를 건너 뛰는 것입니다.
휘발성에 대해 읽은 후 언급 할 가치가있는 가장 좋은 장점은- 경우에 변수의 롤백 을 방지 하는 것 입니다.longjmp
입니다. 로컬이 아닌 점프.
이것은 무엇을 의미 하는가?
단순히 스택 해제 를 수행 한 후에 마지막 값이 유지됨을 의미합니다. 되어 일부 이전 스택 프레임으로 돌아갑니다. 일반적으로 잘못된 시나리오가있는 경우.
이 질문의 범위를 벗어나기 때문에 setjmp/longjmp
여기서 자세히 설명 하지는 않지만 그것에 대해 읽을 가치가 있습니다. 변동성 기능을 사용하여 마지막 값을 유지하는 방법.