C에서 왜 휘발성이 필요한가?


답변:


423

휘발성은 컴파일러에게 휘발성 변수와 관련된 것을 최적화하지 말라고 지시합니다.

변수를 사용하는 최소한 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;
    }

46
개인적으로 하드웨어와 대화 할 때 정수 크기를 명시 적으로 선호합니다 (예 : int8 / int16 / int32). 그래도 좋은 대답;)
tonylo

22
예, 고정 레지스터 크기로 물건을 선언해야하지만 이건 단지 예일뿐입니다.
Nils Pipenbrinck

69
동시성으로 보호되지 않은 데이터로 재생할 때 스레드 코드에도 휘발성이 필요합니다. 예, 유효한 시간이 있습니다. 예를 들어 명시 적 동시성 보호가 필요없는 스레드 안전 원형 메시지 대기열을 작성할 수는 있지만 휘발성이 필요합니다.
Gordon Wrigley

14
C 스펙을 더 자세히 읽으십시오. 휘발성은 메모리 매핑 된 장치 I / O 또는 비동기식 중단 기능에 의해 터치 된 메모리에서만 동작을 정의했습니다. 스레딩에 대해서는 아무 것도 말하지 않으며 여러 스레드가 접촉 한 메모리에 대한 액세스를 최적화하는 컴파일러가 적합합니다.
ephemient

17
@tolomea : 완전히 잘못되었습니다. 슬픈 17 명의 사람들은 그것을 모른다. 휘발성은 메모리 펜스가 아닙니다. 눈에 보이지 않는 부작용을 가정하여 최적화하는 동안 코드 제거피하는 것과 만 관련이 있습니다 .
v.oddou

187

volatileC에서 실제로 변수 값을 자동으로 캐싱하지 않기 위해 존재했습니다. 컴파일러에게이 변수의 값을 캐시하지 않도록 지시합니다. 따라서 주어진 volatile변수 의 값을 메인 메모리에서 발견 할 때마다 가져 오는 코드를 생성 합니다. 이 메커니즘은 언제라도 OS 또는 인터럽트에 의해 값을 수정할 수 있기 때문에 사용됩니다. 따라서를 사용 volatile하면 매번 새로운 가치에 접근 할 수 있습니다.


존재에 온? '휘발성'은 원래 C ++에서 빌리지 않았습니까? 글쎄, 나는 기억하는 것 같다 ...
syntaxerror

이것은
일시적인

4
@FaceBro : volatile프로그래머가 코드를 최적화하는 동시에 프로그래머가 이러한 최적화없이 달성 할 수있는 의미를 달성 할 수 있도록하는 것이 목적 이었습니다. 표준 작성자는 품질 구현이 대상 플랫폼 및 응용 프로그램 필드에서 유용한 의미를 지원할 것으로 기대했으며 컴파일러 작성자는 표준을 준수하고 100 %가 아닌 최저 품질 의미를 제공 할 것으로 기대하지 않았습니다. 바보 (이 표준의 저자는 이론적 근거에서 명시 적으로 인식합니다 ...
supercat

1
... 실제로 어떤 목적에도 적합하기에 충분한 품질을 유지하지 않고도 구현을 준수 할 수는 있지만이를 방지 할 필요는 없다고 생각합니다.
supercat

1
@syntaxerror C가 C ++보다 10 년이 넘었을 때 (첫 번째 릴리스와 첫 표준 모두) C ++에서 어떻게 빌릴 수 있습니까?
phuclv

178

다른 용도로 volatile는 신호 처리기가 있습니다. 이런 코드가 있다면 :

int quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

컴파일러는 루프 바디가 quit변수를 건드리지 않고 루프를 루프로 변환하는 것을 알 수 while (true)있습니다. 짝수 경우 quit변수에 대한 신호 처리기에 설정 SIGINT하고 SIGTERM; 컴파일러는 그것을 알 방법이 없습니다.

그러나 quit변수가 선언 volatile되면 컴파일러는 다른 곳에서 수정할 수 있으므로 매번 변수 를로드해야합니다. 이것이 바로이 상황에서 원하는 것입니다.


"컴파일러는 매번 강제로로드해야합니다. 컴파일러가 특정 변수를 최적화하기로 결정하고 런타임에 특정 변수가 메모리가 아닌 CPU 레지스터에로드되는 변수를 휘발성으로 선언하지 않는 것과 같습니다. ?
Amit Singh Tomar

1
@AmitSinghTomar 의미하는 바 : 코드가 값을 확인할 때마다 다시로드됩니다. 그렇지 않으면 컴파일러는 변수를 참조하지 않는 함수가 변수를 수정할 수 없다고 가정 할 수 있습니다 .CesarB가 위의 루프가 설정되지 않았다고 quit가정하면 컴파일러는 상수 루프로 최적화 할 수 있다고 가정합니다. quit반복간에 변경 될 수있는 방법이 없습니다 . 주의 : 이것은 실제 쓰레드 세이프 프로그래밍을 대신 할만한 좋은 것은 아닙니다.
underscore_d

quit이 전역 변수 인 경우 컴파일러는 while 루프를 최적화하지 않아야합니다.
Pierre G.

2
@PierreG. 아닙니다. 컴파일러는 달리 언급하지 않는 한 항상 코드가 단일 스레드 인 것으로 가정 할 수 있습니다. 즉, volatile또는 다른 마커가없는 경우 전역 변수 인 경우에도 루프 외부로 들어가면 루프에 들어간 변수를 수정하는 것으로 가정합니다.
CesarB

1
@PierreG. 예, 컴파일 예를 들어 노력 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하고 차이점을 확인하십시오.
CesarB

60

volatile컴파일러에게 변수에 액세스하는 코드 이외의 다른 방법으로 변수를 변경할 수 있음을 알려줍니다. 예를 들어, I / O 매핑 된 메모리 위치 일 수 있습니다. 이러한 경우에이를 지정하지 않으면 일부 변수 액세스를 최적화 할 수 있습니다. 예를 들어, 내용을 레지스터에 보유 할 수 있으며 메모리 위치를 다시 읽을 수 없습니다.


30

Andrei Alexandrescu의이 기사를 참조하십시오. " 휘발성-다중 스레드 프로그래머의 가장 친한 친구 "

휘발성 키워드는 특정 비동기 이벤트의 존재에 잘못된 코드를 렌더링 할 수있는 컴파일러 최적화를 방지하기 위해 고안되었다. 예를 들어, 기본 변수를 volatile 로 선언 하면 컴파일러는 레지스터에 변수 를 캐시 할 수 없습니다. 변수가 여러 스레드간에 공유되는 경우 비참한 일반적인 최적화입니다. 따라서 일반적인 규칙은 여러 스레드간에 공유해야하는 기본 유형의 변수가있는 경우 해당 변수를 휘발성으로 선언하는 것입니다. 그러나 실제로이 키워드를 사용하면 더 많은 작업을 수행 할 수 있습니다.이 키워드를 사용하여 스레드로부터 안전하지 않은 코드를 포착 할 수 있으며 컴파일 타임에 수행 할 수 있습니다. 이 기사는 어떻게 수행되는지 보여줍니다. 이 솔루션에는 간단한 스마트 포인터가 포함되어있어 중요한 코드 섹션을 쉽게 직렬화 할 수 있습니다.

이 기사는 모두 적용 C하고 C++.

Scott Meyers와 Andrei Alexandrescu의 " C ++ 및 이중 검사 잠금 위험 "기사도 참조하십시오 .

따라서 일부 메모리 위치 (예 : ISR (Interrupt Service Routines)에서 참조하는 메모리 매핑 포트 또는 메모리)를 처리 할 때는 일부 최적화를 일시 중단해야합니다. 휘발성은 이러한 위치에 대한 특별한 처리를 지정하기 위해 존재합니다. 특히 : (1) 휘발성 변수의 내용이 "불안정"(컴파일러가 알 수없는 방법으로 변경할 수 있음), (2) 휘발성 데이터에 대한 모든 쓰기가 "관찰 가능"하므로 (3) 휘발성 데이터에 대한 모든 작업은 소스 코드에 나타나는 순서대로 실행됩니다. 처음 두 규칙은 올바른 읽기와 쓰기를 보장합니다. 마지막으로 입력과 출력을 혼합하는 I / O 프로토콜을 구현할 수 있습니다. 이것은 비공식적으로 C 및 C ++의 휘발성이 보장하는 것입니다.


표준은 값이 사용되지 않는 경우 읽기가 '관찰 가능한 행동'으로 간주되는지 여부를 지정합니까? 내 인상은 그것이되어야한다는 것이지만, 내가 그것을 주장했을 때 누군가가 나에게 인용을 요구하는 것에 도전했다. 휘발성 변수의 읽기가 아마도 영향을 미칠 수있는 플랫폼에서 컴파일러는 표시된 모든 읽기를 정확하게 한 번만 수행하는 코드를 생성해야합니다. 그러한 요구가 없다면, 예측 가능한 판독 시퀀스를 생성하는 코드를 작성하는 것이 어려울 것이다.
슈퍼 캣

@supercat : 첫 번째 기사에 따르면, "변수에 휘발성 수정자를 사용하면 컴파일러는 해당 변수를 레지스터에 캐시하지 않습니다. 각 액세스는 해당 변수의 실제 메모리 위치에 도달합니다." 또한 c99 표준의 §6.7.3.6 섹션에 다음과 같이 명시되어 있습니다. "휘발성 유형의 객체는 구현에 알 수없는 방식으로 수정되거나 알 수없는 다른 부작용이있을 수 있습니다." 또한 휘발성 변수는 레지스터에 캐시되지 않을 수 있으며 모든 읽기 및 쓰기는 시퀀스 포인트에 대해 순서대로 실행되어야하며 실제로 관찰 가능해야합니다.
Robert S. Barnes

후자의 기사는 실제로 읽기가 부작용이라는 것을 명시 적으로 언급하고있다. 전자는 읽기가 순서대로 수행 될 수는 없지만, 완전히 제거 될 가능성을 배제하지는 않았다.
슈퍼 캣

"컴파일러는 레지스터에 캐시를 캐시 할 수 없습니다"-대부분의 RISC 아키텍처는 레지스터 머신이므로 읽기-수정-쓰기 레지스터의 객체를 캐시해야합니다. volatile원 자성을 보장하지는 않습니다.
이 사이트에 대해 너무 정직합니다.

1
@Olaf : 레지스터에 무언가를로드하는 것은 캐싱과 다릅니다. 캐싱은로드 또는 저장 수 또는 타이밍에 영향을줍니다.
supercat

28

내 간단한 설명은 다음과 같습니다

일부 시나리오에서는 논리 또는 코드를 기반으로 컴파일러에서 변경되지 않는 것으로 생각되는 변수를 최적화합니다. volatile키워드 방지는 변수가 최적화된다.

예를 들면 다음과 같습니다.

bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
    // execute logic for the scenario where the USB isn't connected 
}

위의 코드에서 컴파일러는 usb_interface_flag0으로 정의되고 while 루프에서는 영원히 0이된다고 생각할 수 있습니다. 최적화 후, 컴파일러는이를 while(true)항상 처리 하여 무한 루프를 발생시킵니다.

이러한 종류의 시나리오를 피하기 위해 플래그를 휘발성으로 선언하고 외부 인터페이스 나 다른 프로그램 모듈에 의해이 값이 변경 될 수 있음을 컴파일러에 알리고 있습니다. 즉, 최적화하지 마십시오. 이것이 휘발성의 사용 사례입니다.


19

휘발성에 대한 한계 사용은 다음과 같습니다. 함수의 수치 미분을 계산한다고 가정 해보십시오 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?
Sergey Zhukov

2
차이 hhh그는 인 hh동작에 의해 둘의 부의 파워 잘린다 x + h - x. 이 경우 x + hhx정확히 차이 hh. 또한 이후이 같은 결과를 제공합니다 수식을 취할 수 x + hx + hh(이 여기서 중요한 분모입니다) 동일합니다.
Alexandre C.

3
이것을 작성하는 더 읽기 쉬운 방법이 x1=x+h; d = (f(x1)-f(x))/(x1-x)아닐까요? 휘발성 물질을 사용하지 않고.
Sergey Zhukov

컴파일러가 함수의 두 번째 줄을 지울 수 있다는 참조가 있습니까?
CoffeeTableEspresso

@CoffeeTableEspresso : 아뇨, 죄송합니다. 부동 소수점에 대해 더 많이 알수록 컴파일러가 명시 적으로 말 -ffast-math하거나 이와 동등한 것으로 컴파일러를 최적화하는 것이 더 허용된다고 생각합니다 .
Alexandre C.

11

두 가지 용도가 있습니다. 이들은 임베디드 개발에서 더 자주 사용됩니다.

  1. 컴파일러는 volatile 키워드로 정의 된 변수를 사용하는 함수를 최적화하지 않습니다

  2. 휘발성은 RAM, ROM 등의 정확한 메모리 위치에 액세스하는 데 사용됩니다. 메모리 매핑 장치를 제어하고 CPU 레지스터에 액세스하며 특정 메모리 위치를 찾는 데 더 자주 사용됩니다.

어셈블리 목록이있는 예제를 참조하십시오. Re : 임베디드 개발에서 C "휘발성"키워드 사용


"컴파일러는 volatile 키워드로 정의 된 변수를 사용하는 함수를 최적화하지 않습니다."-이것은 잘못된 것입니다.
이 사이트에 대해 너무 정직합니다.


10

휘발성 물질이 중요한 또 다른 시나리오를 언급하겠습니다.

빠른 I / O를 위해 파일을 메모리에 매핑하고 해당 파일이 장면 뒤에서 변경 될 수 있다고 가정합니다 (예 : 파일이 로컬 하드 드라이브에 있지 않지만 대신 다른 컴퓨터에 의해 네트워크를 통해 제공됨).

비 휘발성 객체 (소스 코드 수준에서)에 대한 포인터를 통해 메모리 매핑 된 파일의 데이터에 액세스하는 경우 컴파일러에서 생성 된 코드는 사용자가 알지 않고도 동일한 데이터를 여러 번 가져올 수 있습니다.

해당 데이터가 변경되면 프로그램에서 두 개 이상의 서로 다른 버전의 데이터를 사용하여 불일치 상태가 될 수 있습니다. 이로 인해 프로그램의 논리적으로 잘못된 동작뿐만 아니라 신뢰할 수없는 파일 또는 신뢰할 수없는 위치에서 파일을 처리하는 경우 악용 가능한 보안 허점이 될 수 있습니다.

보안이 중요하고 고려해야 할 경우 고려해야 할 중요한 시나리오입니다.


7

휘발성은 스토리지가 언제라도 변경되어 변경 될 수 있지만 사용자 프로그램이 제어 할 수없는 것임을 의미합니다. 즉, 변수를 참조하면 프로그램은 항상 실제 주소 (매핑 된 입력 fifo)를 확인하고 캐시 된 방식으로 사용해서는 안됩니다.


"RAM의 물리적 주소"또는 "캐시 우회"를 의미하기 위해 휘발성을 사용하는 컴파일러는 없습니다.
curiousguy


5

내 의견으로는을 너무 많이 기 대해서는 안됩니다 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에 매핑되어 있음). 데이터와 명령 할당간에 메모리 장벽이 필요합니다.


2
volatile은 컴파일러가 일반적 으로 유용하고 바람직한 최적화를하지 못하게하는 데 사용됩니다 . 서면으로, volatile아무 이유없이 성능을 저하시키는 것 같습니다 . 충분한 지 여부는 프로그래머가 컴파일러보다 더 알 수있는 시스템의 다른 측면에 따라 다릅니다. 반면에 프로세서가 특정 주소에 쓰는 명령이 CPU 캐시를 플러시한다고 보장하지만 컴파일러가 레지스터 캐시 변수를 플러시 할 방법을 제공하지 않으면 CPU는 아무것도 알지 못하므로 캐시를 플러시하는 것은 쓸모가 없습니다.
supercat 2019

5

Dennis Ritchie가 설계 한 언어에서 주소를 가져 오지 않은 자동 개체 이외의 모든 개체에 대한 모든 액세스는 마치 개체의 주소를 계산 한 다음 해당 주소의 저장소를 읽거나 쓴 것처럼 동작합니다. 이로 인해 언어가 매우 강력 해졌지만 최적화 기회가 크게 제한되었습니다.

컴파일러가 특정 객체가 이상한 방식으로 변경되지 않을 것이라고 가정하도록 한정자를 추가하는 것이 가능할 수도 있지만, 이러한 가정은 C 프로그램의 대다수 객체에 적합 할 것입니다. 그러한 가정이 적절한 모든 객체에 한정자를 추가하는 것은 비현실적이었다. 다른 한편으로, 일부 프로그램은 그러한 가정이 갖지 않는 일부 객체를 사용해야합니다. 이 문제를 해결하기 위해 표준에 따르면 컴파일러는 선언되지 않은 객체 volatile의 값이 컴파일러의 제어 범위를 벗어나거나 합리적인 컴파일러의 이해 범위를 벗어난 방식으로 관찰되거나 변경되지 않을 것이라고 가정 할 수 있습니다.

다양한 플랫폼은 컴파일러의 제어 범위 밖에서 객체를 관찰하거나 수정할 수있는 방법이 서로 다를 수 있으므로 해당 플랫폼에 대한 양질의 컴파일러는 volatile의미론을 정확하게 처리하는 것이 달라야합니다. 불행히도, 표준은 플랫폼에서 저수준 프로그래밍을위한 고품질 컴파일러가 해당 플랫폼 volatile에서 특정 읽기 / 쓰기 작업의 모든 관련 영향을 인식하는 방식으로 처리해야한다고 제안하지 않았기 때문에 많은 컴파일러가 수행하지 못하고 있습니다. 따라서 효율적이지만 컴파일러 "최적화"에 의해 깨질 수없는 방식으로 백그라운드 I / O와 같은 것을 처리하기 어렵게 만드는 방식으로.


5

간단히 말해서 컴파일러는 특정 변수에 대해 최적화를하지 말라고 컴파일러에 지시합니다. 장치 레지스터에 매핑 된 변수는 장치에 의해 간접적으로 수정됩니다. 이 경우 휘발성을 사용해야합니다.


1
이 답변에 이전에 언급되지 않은 새로운 내용이 있습니까?
slfan

3

휘발성은 컴파일 된 코드 외부에서 변경 될 수 있습니다 (예 : 프로그램은 휘발성 변수를 메모리 매핑 된 레지스터에 매핑 할 수 있습니다). 컴파일러는 휘발성 변수를 처리하는 코드에 특정 최적화를 적용하지 않습니다. 메모리에 쓰지 않고 레지스터에로드합니다. 이것은 하드웨어 레지스터를 다룰 때 중요합니다.


0

여기에서 많은 사람들이 올바르게 제안한 것처럼, 휘발성 키워드의 보편적 인 용도는 휘발성 변수의 최적화를 건너 뛰는 것입니다.

휘발성에 대해 읽은 후 언급 할 가치가있는 가장 좋은 장점은- 경우에 변수의 롤백 을 방지 하는 것 입니다.longjmp 입니다. 로컬이 아닌 점프.

이것은 무엇을 의미 하는가?

단순히 스택 해제 를 수행 한 후에 마지막 값이 유지됨을 의미합니다. 되어 일부 이전 스택 프레임으로 돌아갑니다. 일반적으로 잘못된 시나리오가있는 경우.

이 질문의 범위를 벗어나기 때문에 setjmp/longjmp여기서 자세히 설명 하지는 않지만 그것에 대해 읽을 가치가 있습니다. 변동성 기능을 사용하여 마지막 값을 유지하는 방법.


-2

컴파일러가 변수 값을 자동으로 변경할 수 없습니다. 휘발성 변수는 동적으로 사용됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.