"휘발성"은 멀티 코어 시스템을위한 휴대용 C 코드로 무엇인가를 보장합니까?


12

(A)에서보고 한 무리 다른 질문 답변 , 나는 인상 얻을 C에서 "휘발성"키워드가 정확히 무엇을 의미하는지에 대한 더 광범위한 합의가없는합니다.

표준 자체조차도 모든 사람이 그것이 의미 하는 바에 동의 할만큼 명확하지 않은 것 같습니다 .

다른 문제들 중 :

  1. 하드웨어와 컴파일러에 따라 다른 보증을 제공하는 것 같습니다.
  2. 컴파일러 최적화에는 영향을 주지만 하드웨어 최적화에는 영향을 미치지 않으므로 자체 런타임 최적화를 수행하는 고급 프로세서에서는 컴파일러 방지하려는 최적화를 막을 있는지 여부조차 명확하지 않습니다 . (일부 컴파일러는 일부 시스템에서 일부 하드웨어 최적화를 방지하기위한 명령어를 생성하지만 어떤 방식 으로든 표준화되지 않은 것으로 보입니다.)

문제를 요약하면, 그것은 (많이 읽고) 나타납니다 "휘발성"보장 뭔가 같은 : 값을 읽을 수 있습니다 /이 아니라 /에서 레지스터에 기록하지만, 코어의 L1 캐시에 적어도, 같은 순서로한다는 점에서 읽기 / 쓰기가 코드에 나타납니다. 그러나 레지스터에서 읽고 쓰는 것이 동일한 스레드 내에서 이미 충분하기 때문에 L1 캐시로 조정한다고해서 다른 스레드와의 조정에 대해 더 이상 보장 할 수는 없습니다. L1 캐시와 동기화하는 것이 언제 중요한지 상상할 수 없습니다.

사용 1
휘발성을 널리 사용 하는 유일하게 사용되는 휘발성은 특정 메모리 위치가 (직접 하드웨어에서) 조명을 제어하는 ​​메모리의 비트와 같이 I / O 기능에 하드웨어 매핑되는 구식 또는 내장형 시스템에서 사용되는 것으로 보입니다. 또는 키보드 키가 눌 렸는지 여부를 알려주는 메모리의 비트 (하드웨어가 키에 직접 연결했기 때문에).

멀티 코어 시스템을 포함하는 휴대용 코드에서는 "use 1"이 발생하지 않는 것 같습니다 .

USE 2
"use 1"과 크게 다르지 않은 경우 인터럽트 처리기 (조명을 제어하거나 키의 정보 저장)에 의해 언제든지 읽거나 쓸 수있는 메모리입니다. 그러나 이미이를 위해 우리는 시스템에 따라 인터럽트 핸들러 자체 메모리 캐시를 가진 다른 코어 에서 실행될 수 있으며 "휘발성"이 모든 시스템에서 캐시 일관성을 보장하지는 않는다는 문제가 있습니다.

따라서 "use 2"는 "volatile"이 제공 할 수있는 것 이상의 것으로 보입니다.

사용 3
내가 볼 수있는 유일한 확실한 다른 용도는 컴파일러가 인식하지 못하는 동일한 메모리를 가리키는 다른 변수를 통해 액세스가 잘못 최적화되는 것을 방지하는 것입니다. 그러나 이것은 사람들이 그것에 대해 이야기하지 않기 때문에 아마도 논란의 여지가 없습니다. 그리고 C 표준은 이미 "다른"포인터 (함수에 대한 다른 인수)가 동일한 항목이나 근처의 항목을 가리킬 수 있다는 것을 이미 인식하고 있으며 컴파일러가 그러한 경우에도 작동하는 코드를 생성해야한다고 이미 지정했습니다. 그러나 최신 (500 페이지!) 표준 에서이 주제를 빨리 찾을 수 없었습니다.

그래서 "사용 3"어쩌면 존재하지 않는 전혀?

따라서 내 질문 :

"휘발성"은 멀티 코어 시스템을위한 휴대용 C 코드로 무엇인가를 보장합니까?


편집-업데이트

최신 표준을 찾아 본 후에 는 대답이 최소한 매우 제한적인 것 같습니다.
1. 표준은 "volatile sig_atomic_t"특정 유형에 대한 특수 처리를 반복적으로 지정합니다. 그러나 표준에 따르면 다중 스레드 프로그램에서 신호 기능을 사용하면 정의되지 않은 동작이 발생합니다. 따라서이 사용 사례는 단일 스레드 프로그램과 해당 신호 처리기 간의 통신으로 제한됩니다.
2. 표준은 또한 setjmp / longjmp와 관련하여 "휘발성"에 대한 명확한 의미를 지정합니다. (중요한 코드 예제는 다른 질문답변에 나와 있습니다.)

보다 정확한 질문은 다음과 같습니다
. "휘발성"은 (1) 단일 스레드 프로그램이 신호 처리기에서 정보를 수신하도록 허용하거나 (2) setjmp를 허용하는 것 외에 멀티 코어 시스템을위한 휴대용 C 코드에서 무엇이든 보장합니다. setjmp와 longjmp 사이에서 수정 된 변수를 보는 코드?

이것은 여전히 ​​그렇습니다 / 아니오 질문입니다.

"yes"이면 "volatile"을 생략하면 버그가없는 이식 가능한 코드의 예를 보여줄 수 있다면 좋을 것입니다. "아니오"인 경우 멀티 코어 대상의 경우 컴파일러가이 두 가지 특별한 경우를 제외하고는 "휘발성"을 무시해도된다고 가정합니다.


3
신호는 휴대용 C에 존재합니다. 신호 처리기에 의해 업데이트되는 전역 변수는 어떻습니까? volatile프로그램에 비동기식으로 변경 될 수 있음 을 알리는 것입니다.
Nate Eldredge

2
@NateEldredge Global은 휘발성만으로는 충분하지 않습니다. 원자 적이어야합니다.
유진 Sh.

@EugeneSh .: 물론입니다. 그러나 당면한 질문은 volatile구체적으로 필요한 것입니다.
Nate Eldredge

" L1 캐시로 조정한다고해서 다른 스레드와의 조정에 대해서는 더 이상 보장 할 수 없습니다. " "L1 캐시로 조정" 이 다른 스레드 와 통신하기에 충분하지 않은 곳은 어디입니까?
curiousguy

1
아마 관련, 휘발성 비추천 C ++ 제안은 , 당신이 여기 인상 우려의 많은, 그리고 아마도 그 결과 제안 주소는 C위원회에 영향력이 될 것입니다
MM

답변:


1

문제를 요약하면 다음과 같이 "휘발성"이 보장됩니다 (많은 읽은 후). 값은 레지스터에서 / 레지스터로가 아니라 최소한 코어의 L1 캐시 로 읽히거나 쓰 입니다. 읽기 / 쓰기가 코드에 나타납니다 .

아니, 그것은 절대적으로하지 않습니다 . 그리고 이것은 MT 안전 코드의 목적으로 휘발성을 거의 쓸모 없게 만듭니다.

그렇다면 휘발성은 L1 캐시에서 이벤트를 주문하면 협력 할 수있는 일반적인 CPU (마더 보드의 멀티 코어 또는 멀티 CPU)에서 수행 해야하는 모든 것이므로 여러 스레드가 공유하는 변수에 휘발성이 좋습니다. 일반적인 예상 비용으로 가능한 C / C ++ 또는 Java 멀티 스레딩의 일반적인 구현을 가능하게하는 방식으로 (즉, 대부분의 원자 또는 비 콘텐츠 뮤텍스 작업에서 큰 비용이 아님).

그러나 휘발성은 이론적으로나 실제로 캐시에서 보장 된 순서 (또는 "메모리 가시성")를 제공 하지 않습니다 .

(참고 : 다음은 표준 문서에 대한 건전한 해석, 표준의 의도, 역사적 관행 및 컴파일러 작성자의 기대에 대한 심도있는 이해를 바탕으로합니다. 더 나은 사양 작성으로 알려지지 않았으며 여러 번 수정 된 문서의 단어를 구문 분석하는 것보다 훨씬 강력하고 신뢰할 수있는 실제 세계)

실제로 volatile은 모든 수준의 최적화에서 실행중인 프로그램에 디버그 정보를 사용할 수있는 ptrace 기능을 보장 하며 디버그 정보가 이러한 휘발성 객체에 적합하다는 사실을 보장 합니다.

  • ptrace휘발성 객체와 관련된 작업 후 시퀀스 지점에서 의미있는 중단 점을 설정하기 위해 (ptrace와 같은 메커니즘)을 사용할 수 있습니다. 정확하게 이러한 지점에서 중단 할 수 있습니다 (여러 지점을 여러 중단 점으로 설정하려는 경우에만 작동 함) C / C ++ 문은 대규모 언 롤링 루프에서와 같이 다양한 어셈블리 시작 및 끝 지점으로 컴파일 될 수 있습니다.
  • 중지 된 실행 스레드 인 동안, 모든 휘발성 객체는 표준 표현을 갖기 때문에 (각 유형에 대해 ABI를 따르는) 모든 휘발성 객체의 값을 읽을 수 있습니다. 비 휘발성 지역 변수는 비정형 표현 f.ex를 가질 수 있습니다. 쉬프트 된 표현 : 배열을 색인하기 위해 사용 된 변수는 더 쉬운 색인을 위해 개별 객체의 크기와 곱해질 수 있습니다. 또는 배열 요소에 대한 포인터로 대체 될 수 있습니다 (변수의 모든 용도가 비슷하게 변환 된 한) (dx를 du에서 정수로 변경하는 것을 생각하십시오);
  • 또한 한정된 정적 수명을 갖는 휘발성 객체가 메모리 범위 매핑 된 읽기 전용 일 수 있으므로 메모리 매핑에서 허용하는 한 해당 객체를 수정할 수도 있습니다.

휘발성 자동 변수는 엄격한 ptrace 해석보다 실질적으로 휘발성을 보장합니다. 또한 휘발성 자동 변수가 레지스터에 할당되지 않았기 때문에 스택에 주소가 있음을 보장합니다. 변수가 레지스터에 할당되는 방법을 설명하는 디버그 정보를 출력하지만 레지스터 상태를 읽고 변경하는 것은 메모리 주소에 액세스하는 것보다 약간 더 복잡합니다.

적어도 시퀀스 포인트에서 모든 변수를 휘발성으로 간주하는 전체 프로그램 디버그 기능은 컴파일러의 "제로 최적화"모드에서 제공됩니다.이 모드는 산술 단순화와 같은 사소한 최적화를 수행합니다. 모든 모드에서 최적화). 그러나 휘발성은 비 최적화보다 강력 x-x합니다. 비 휘발성 정수에 x대해서는 단순화 할 수 있지만 휘발성 객체는 그렇지 않습니다.

따라서 휘발성 수단 은 시스템 호출의 컴파일러에 의한 소스에서 이진 / 어셈블리로의 변환이 컴파일러에 의해 어떤 식 으로든 재 해석, 변경 또는 최적화되지 않는 것처럼 그대로 컴파일되도록 보장됩니다 . 라이브러리 호출은 시스템 호출 일 수도 있고 아닐 수도 있습니다. 많은 공식 시스템 기능은 실제로 라이브러리 기능으로, 얇은 중간 계층을 제공하며 일반적으로 마지막에 커널을 연기합니다. (특히 getpid커널로 갈 필요가 없으며 정보를 포함하는 OS에서 제공하는 메모리 위치를 읽을 수 있습니다.)

휘발성 상호 작용은 실제 기계의 외부 세계와의 상호 작용이며 , "추상 기계"를 따라야합니다. 프로그램 파트와 다른 프로그램 파트의 내부 상호 작용이 아닙니다. 컴파일러는 알고있는 것, 즉 내부 프로그램 부분에 대해서만 추론 할 수 있습니다.

휘발성 액세스를위한 코드 생성은 해당 메모리 위치와 가장 자연스럽게 상호 작용해야합니다. 이는 놀라운 일이 아닙니다. 즉, 일부 휘발성 액세스는 원자적일 것으로 예상됩니다long . 아키텍처에서의 표현을 읽거나 쓰는 자연스러운 방법 이 원자 적이라면 컴파일러가 생성하지 않아야 하므로 a의 읽거나 쓰는 volatile long것이 원자적일 것으로 예상 됩니다 휘발성 개체를 바이트 단위로 액세스하는 매우 비효율적 인 코드입니다 (예 :) .

아키텍처를 알고 있으면이를 확인할 수 있어야합니다. 휘발성은 컴파일러가 투명해야 함을 의미 하므로 컴파일러에 대해 아무것도 알 필요가 없습니다 .

그러나 휘발성은 메모리 작업을 수행하기 위해 특정 경우에 대해 가장 최적화 된 부품에 대한 예상 어셈블리의 방출을 강요하지 않습니다. 휘발성 시맨틱은 일반적인 경우 시맨틱을 의미합니다.

일반적인 경우는 컴파일러가 구문에 대한 정보가없는 경우 컴파일러가 수행하는 것입니다. f.ex. 동적 디스패치를 ​​통해 lvalue에서 가상 함수를 호출하는 것은 일반적인 경우이며, 컴파일 타임에 표현식으로 지정된 객체의 유형이 특정한 경우를 결정한 후 대체 프로그램을 직접 호출합니다. 컴파일러는 항상 모든 구문을 처리하는 일반적인 경우를 가지며 ABI를 따릅니다.

휘발성은 스레드를 동기화하거나 "메모리 가시성"을 제공하기 위해 특별한 작업을 수행하지 않습니다. 휘발성 은 스레드가 실행 또는 중지 된 스레드 내부, 즉 CPU 코어 내부 에서 볼 수 있는 추상 수준에서만 보장합니다 .

  • volatile은 어떤 메모리 작업이 주 RAM에 도달하는지에 대해 아무 것도 말하지 않습니다 (이러한 보증을 얻기 위해 어셈블리 명령 또는 시스템 호출로 특정 메모리 캐싱 유형을 설정할 수 있음).
  • volatile은 메모리 작업이 어떤 수준의 캐시 (L1도 아님)에 커밋되는지에 대한 보장을 제공하지 않습니다 .

두 번째 포인트 만이 휘발성이 대부분의 스레드 간 통신 문제에서 유용하지 않다는 것을 의미합니다. 첫 번째 요점은 본질적으로 CPU 외부의 하드웨어 구성 요소와 통신하지 않고 여전히 메모리 버스에있는 프로그래밍 문제와 관련이 없습니다.

스레드를 실행하는 코어의 관점에서 보장 된 동작을 제공하는 휘발성의 속성은 스레드의 실행 순서 관점에서 실행되는 해당 스레드로 전달되는 비동기 신호가 소스 코드 순서의 작업을 참조 함을 의미합니다. .

스레드에 신호를 보내려고하지 않는 한 (이전에 동의 한 중지 지점없이 현재 실행중인 스레드에 대한 정보를 통합하는 매우 유용한 방법), 휘발성은 적합하지 않습니다.


6

나는 전문가는 아니지만 cppreference.com은에 대한 꽤 좋은 정보volatile 로 보입니다 . 다음은 요점입니다.

volatile-qualified type의 lvalue 표현식을 통해 작성된 모든 액세스 (읽기 및 쓰기)는 최적화 목적으로 관찰 가능한 부작용으로 간주되며 추상 기계의 규칙에 따라 엄격하게 평가됩니다 (즉, 모든 쓰기는 다음 시퀀스 포인트 이전의 시간). 즉, 단일 실행 스레드 내에서 휘발성 액세스는 시퀀스 포인트에 의해 휘발성 액세스에서 분리 된 다른 가시적 부작용에 대해 최적화되거나 재정렬 될 수 없습니다.

또한 몇 가지 용도를 제공합니다.

휘발성의 사용

1) 정적 휘발성 객체 모델 메모리 매핑 된 I / O 포트 및 정적 const 휘발성 객체 모델 메모리 매핑 된 입력 포트 (예 : 실시간 클록)

2) sig_atomic_t 유형의 정적 휘발성 객체는 신호 처리기와 통신하는 데 사용됩니다.

3) setjmp 매크로의 호출을 포함하는 함수에 로컬 인 휘발성 변수는 longjmp 리턴 후 값을 유지하도록 보장되는 유일한 로컬 변수입니다.

4) 또한, 휘발성 변수를 사용하여 특정 형태의 최적화를 비활성화 할 수 있습니다.

물론 volatile스레드 동기화에는 유용하지 않다는 언급이 있습니다.

휘발성 변수는 스레드 간의 통신에 적합하지 않습니다. 원 자성, 동기화 또는 메모리 순서를 제공하지 않습니다. 동기화되지 않은 두 스레드의 동기화 또는 동시 수정없이 다른 스레드에 의해 수정 된 휘발성 변수의 읽기는 데이터 경쟁으로 인해 정의되지 않은 동작입니다.


2
특히, (2) 및 (3)은 휴대용 코드와 관련이 있습니다.
Nate Eldredge

2
@TED ​​도메인 이름에도 불구하고 링크는 C ++가 아닌 C에 대한 정보입니다.
David Brown

@NateEldredge longjmpC ++ 코드 에서는 거의 사용할 수 없습니다 .
curiousguy

@DavidBrown C와 C ++는 관찰 가능한 SE의 정의와 기본적으로 동일한 스레드 프리미티브를 갖습니다.
curiousguy

4

우선, volatile접근성과 그와 유사한 의미의 다른 해석과 관련하여 역사적으로 다양한 딸꾹질이있었습니다 . 이 연구를 참조하십시오 : 휘발성 물질이 잘못 컴파일되었으며 이에 대한 조치 .

이 연구에서 언급 된 다양한 문제들 외에도, 행동 volatile은 이식성이 뛰어나고, 그것들의 한 측면을 제외하고 : 그것들이 메모리 장벽 역할을 할 때 . 메모리 장벽은 코드의 연속적인 순차적 실행을 방지하기위한 메커니즘입니다. volatile메모리 장벽으로 사용 하는 것은 확실히 휴대 할 수 없습니다.

C 언어가 메모리 동작을 보장하는지 여부 volatile는 분명히 논쟁의 여지가 있지만 개인적으로는 언어가 분명하다고 생각합니다. 먼저 부작용에 대한 공식적인 정의 C17 5.1.2.3이 있습니다.

volatile객체에 액세스하거나 객체를 수정하거나 파일을 수정하거나 이러한 작업을 수행하는 함수를 호출 하면 실행 환경의 상태가 변경되는 부작용 이 있습니다.

표준은 평가 순서 (실행)를 결정하는 방법으로 시퀀싱이라는 용어를 정의합니다. 정의는 형식적이고 번거 롭습니다.

이전 에는 단일 스레드에 의해 실행 된 평가 간의 비대칭, 전 이적, 쌍별 관계가 순서화되어 있으며, 이러한 평가 중 일부 순서를 유도합니다. 두 가지 평가 A와 B가 주어지면 A가 B보다 먼저 시퀀싱되면 A의 실행이 B보다 먼저 실행됩니다. 반대로 A가 B보다 먼저 시퀀싱되면 B가 A 후에 시퀀싱됩니다 . A가 시퀀싱되지 않은 경우 B 전 또는 후에, 다음과 B는 unsequenced . 평가 A와 B는 A가 B 이전이나 이후에 시퀀싱 될 때 불확실하게 시퀀싱 되지만, 다음 중 미지정입니다 .13) 시퀀스 포인트 의 존재 표현 A와 B 사이의 평가는 A와 관련된 모든 값 계산 및 부작용이 B와 관련된 모든 값 계산 및 부작용 전에 시퀀싱된다는 것을 의미한다 (시퀀스 포인트의 요약은 부록 C에 제공됨).

위의 TL; DR은 기본적으로 A부작용을 포함 하는 표현식 이 B있는 경우 B시퀀스가 다른 경우에 다른 표현식보다 먼저 실행되어야한다는 것 A입니다.

이 부분을 통해 C 코드를 최적화 할 수 있습니다.

추상 머신에서 모든 표현식은 시맨틱에 의해 지정된대로 평가됩니다. 실제 구현에서는 값이 사용되지 않고 필요한 부작용이 발생하지 않는다고 추정 할 수있는 경우 (함수 호출 또는 휘발성 개체 액세스로 인해 발생하는 것을 포함하여) 표현식의 일부를 평가할 필요가 없습니다.

이는 프로그램이 표준이 요구하는 순서 (평가 순서 등)로 식을 평가 (실행) 할 수 있음을 의미합니다. 그러나 값을 사용하지 않는 것으로 추론 할 수 있으면 값을 평가 (실행) 할 필요가 없습니다. 예를 들어, 연산 0 * x은 평가를 수행 할 필요가 없으며 x단순히 표현식을로 바꿀 필요가 없습니다 0.

않는 변수 액세스 부작용이다. 경우에 있다는 의미 x입니다 volatile, 그것은 있어야 평가 (실행) 0 * x결과는 항상 0 최적화 될 것입니다하더라도 허용되지 않습니다.

또한 표준은 관찰 가능한 행동에 대해 말합니다.

적합한 구현에 대한 최소 요구 사항은 다음과 같습니다.

  • 휘발성 객체에 대한 액세스는 추상 기계의 규칙에 따라 엄격하게 평가됩니다.
    /-/ 이것은 프로그램 의 관찰 가능한 동작 입니다.

위의 모든 사항을 감안할 때 volatile, 작성된 C 소스의 의미가 달리 언급하는 경우 준수 구현 (컴파일러 + 기본 시스템)은 시퀀스에 따라 객체 의 액세스를 순서대로 실행하지 않을 수 있습니다 .

이것은이 예에서

volatile int x;
volatile int y;
z = x;
z = y;

모두 할당 표현 합니다 평가하고 z = x; 있어야 하기 전에 평가 z = y;. 이 두 가지 작업을 두 개의 서로 다른 비 순서 코어로 아웃소싱하는 다중 프로세서 구현은 적합하지 않습니다!

딜레마는 컴파일러가 프리 페치 캐싱 및 명령어 파이프 라이닝 등과 같은 작업에 대해 많은 것을 할 수 없다는 것입니다. 특히 OS에서 실행될 때는 그렇지 않습니다. 따라서 컴파일러는이 문제를 프로그래머에게 전달하여 메모리 장벽이 이제 프로그래머의 책임이라고 말합니다. C 표준은 분명히 컴파일러가 문제를 해결해야한다고 명시하고 있습니다.

컴파일러는 반드시 문제를 해결하는 데 신경 쓰지 않으므로 volatile메모리 장벽으로 작동하기 때문에 이식 할 수 없습니다. 구현 품질 문제가되었습니다.


@curiousguy 중요하지 않습니다.
Lundin

@curiousguy 한정자가 있거나없는 정수 유형 인 한 중요하지 않습니다.
Lundin

단순한 비 휘발성 정수라면 왜 중복 쓰기 z가 실제로 실행됩니까? (처럼 z = x; z = y;) 값은 다음 문장에서 삭제 될 것입니다.
curiousguy

@curiousguy 휘발성 변수에 대한 읽기 는 지정된 순서대로 실행되어야합니다.
Lundin

그렇다면 z실제로 두 번 할당됩니까? "읽기 실행"을 어떻게 알 수 있습니까?
curiousguy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.