컴파일러가 로컬 휘발성 변수를 최적화 할 수 있습니까? 변수로 컴파일 한 다음 반환합니다.

컴파일러가이를 최적화 할 수 있습니까 (C ++ 17 표준에 따라) :

int fn() {
    volatile int x = 0;
    return x;
}

이에?

int fn() {
    return 0;
}

그렇다면 그 이유는 무엇입니까? 그렇지 않다면 왜 안됩니까?


이 주제에 대한 몇 가지 생각이 있습니다. 현재 컴파일러 fn()는 스택에있는 지역 변수로 컴파일 한 다음 반환합니다. 예를 들어 x86-64에서 gcc는 다음을 생성합니다.

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret

이제 내가 아는 한 표준은 로컬 휘발성 변수가 스택에 있어야한다고 말하지 않습니다. 따라서이 버전도 똑같이 좋습니다.

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret

여기 edx상점 x. 하지만 이제 왜 여기서 멈추나요? edxeax둘 다 0이므로 다음 과 같이 말할 수 있습니다.

xor    eax,eax // eax is the return, and x as well
ret

그리고 fn()최적화 된 버전으로 전환 했습니다. 이 변환이 유효합니까? 그렇지 않은 경우 어떤 단계가 유효하지 않습니까?



답변

아니요. volatile객체에 대한 액세스 는 로컬과 전역 사이의 특별한 구분없이 I / O와 똑같이 관찰 가능한 동작으로 간주됩니다.

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

  • volatile객체에 대한 액세스 는 추상 기계의 규칙에 따라 엄격하게 평가됩니다.

[…]

이를 총체적으로 프로그램의 관찰 가능한 동작이라고합니다.

N3690, [intro.execution], ¶8

이것이 얼마나 정확하게 관찰되는지는 표준의 범위를 벗어나며 I / O 및 전역 volatile개체에 대한 액세스와 마찬가지로 구현 별 영역에 바로 해당 합니다. volatile“당신은 여기에서 일어나는 모든 일을 알고 있다고 생각하지만, 그렇지 않습니다. 저를 믿고 너무 똑똑하지 말고이 일을하세요. 왜냐하면 저는 당신의 프로그램에서 당신의 바이트로 제 비밀 일을하고 있기 때문입니다.” 이것은 실제로 [dcl.type.cv] ¶7에 설명되어 있습니다 :

[참고 : volatile객체의 값이 구현에서 감지 할 수없는 수단으로 변경 될 수 있으므로 객체와 관련된 적극적인 최적화를 피하기위한 구현에 대한 힌트입니다. 또한 일부 구현의 경우 volatile은 객체에 액세스하기 위해 특수 하드웨어 명령이 필요함을 나타낼 수 있습니다. 자세한 의미는 1.9를 참조하십시오. 일반적으로 volatile의 의미는 C에서와 동일하게 C ++에서 의도됩니다. — end note]


답변

이 루프는 관찰 가능한 동작이 없기 때문에 as-if 규칙에 따라 최적화 할 수 있습니다.

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

이것은 다음을 수행 할 수 없습니다.

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

두 번째 루프는 반복 할 때마다 어떤 작업을 수행하므로 루프가 O (n) 시간이 걸립니다. 상수가 무엇인지는 모르겠지만 측정 할 수 있고 (다소) 알려진 시간 동안 바쁘게 반복하는 방법이 있습니다.

표준이 휘발성 물질에 대한 액세스가 순서대로 이루어져야한다고 말하고 있기 때문에 그렇게 할 수 있습니다. 이 경우 표준이 적용되지 않는다고 컴파일러가 결정했다면 버그 보고서를 제출할 권리가 있다고 생각합니다.

컴파일러가 looped레지스터 에 넣기 로 선택하면 그것에 대해 좋은 주장이 없다고 생각합니다. 그러나 여전히 모든 루프 반복에 대해 해당 레지스터의 값을 1로 설정해야합니다.


답변

나는 volatile관찰 가능한 I / O 를 의미 하는 완전한 이해에도 불구하고 다수의 의견에 반대하기를 간청합니다 .

이 코드가있는 경우 :

{
    volatile int x;
    x = 0;
}

나는 컴파일러가 믿을 세 이하를 최적화 할 뿐만-경우 규칙 , 가정 한다 :

  1. volatile변수는 달리 외부에서 가시가되지를 통해 (그런 일이 주어진 범위에 존재하지 않기 때문에 여기에 문제가 분명하지 않습니다) 예를 들어, 포인터

  2. 컴파일러는 외부에서 액세스 할 수있는 메커니즘을 제공하지 않습니다. volatile

그 이유는 단순히 기준 # 2로 인해 차이를 관찰 할 수 없다는 것입니다.

그러나 컴파일러에서 기준 # 2가 충족되지 않을 수 있습니다 ! 컴파일러 volatile 스택 분석과 같이 “외부”에서 변수를 관찰하는 것에 대한 추가 보장을 제공하려고 할 수 있습니다 . 이러한 상황에서, 행동은 정말 이다 멀리 최적화되지 않을 수 있으므로, 관찰.

이제 질문은 다음 코드가 위와 다른 코드입니까?

{
    volatile int x = 0;
}

나는 최적화와 관련하여 Visual C ++에서 이것에 대해 다른 동작을 관찰했다고 생각하지만, 그 이유가 무엇인지 완전히 확신하지 못합니다. 초기화가 “액세스”로 간주되지 않을 수 있습니까? 잘 모르겠습니다. 관심이 있으시면 별도의 질문을 할 가치가 있지만 그렇지 않으면 위에서 설명한대로 대답이 있다고 생각합니다.


답변

이론적으로 인터럽트 핸들러는

  • 반환 주소가 fn()함수 내에 있는지 확인하십시오 . 인스 트루먼 테이션 또는 첨부 된 디버그 정보를 통해 기호 테이블 또는 소스 행 번호에 액세스 할 수 있습니다.
  • 그런 다음 x스택 포인터에서 예측 가능한 오프셋에 저장되는 값을 변경합니다 .

… 따라서 fn()0이 아닌 값 을 반환합니다.


답변

as-if 규칙과 volatile 키워드에 대한 자세한 참조를 추가 할 것 입니다. (이 페이지의 맨 아래에있는 “참조”및 “참조”를 따라 원래 사양을 추적하십시오.하지만 cppreference.com을 읽고 이해하기가 훨씬 더 쉽습니다.)

특히이 섹션을 읽어 주시기 바랍니다.

volatile 객체-유형이 volatile로 한정된 객체, 휘발성 객체의 하위 객체 또는 const-volatile 객체의 변경 가능한 하위 객체입니다. volatile로 한정된 유형의 glvalue 표현식을 통해 이루어진 모든 액세스 (읽기 또는 쓰기 작업, 멤버 함수 호출 등)는 최적화 목적 (즉, 단일 실행 스레드 내에서 휘발성)을 위해 가시적 인 부작용으로 처리됩니다. 액세스는 휘발성 액세스 이전 또는 이후에 순서가 지정된 다른 가시적 부작용으로 최적화되거나 재정렬 될 수 없습니다. 이는 휘발성 객체를 신호 처리기와의 통신에 적합하게 만들지 만 다른 실행 스레드와는 그렇지 않습니다. std :: memory_order를 참조하십시오. ). 비 휘발성 glvalue를 통해 (예 : 비 휘발성 유형에 대한 참조 또는 포인터를 통해) 휘발성 객체를 참조하려고하면 정의되지 않은 동작이 발생합니다.

따라서 volatile 키워드는 특히 glvalues 에서 컴파일러 최적화를 비활성화하는 입니다. 여기서 volatile 키워드가 영향을 미칠 수있는 유일한 것은 return x컴파일러가 나머지 함수로 원하는 모든 작업을 수행 할 수 있다는 것입니다.

컴파일러가 반환을 최적화 할 수있는 정도는이 경우 컴파일러가 x의 액세스를 최적화 할 수있는 정도에 따라 다릅니다 (아무것도 재정렬하지 않고 엄밀히 말하면 반환 표현식을 제거하지 않기 때문입니다. ,하지만 스택에 읽고 쓰는 것이므로 간소화 할 수 있어야합니다.) 그래서 제가 읽을 때 이것은 컴파일러가 최적화 할 수있는 정도의 회색 영역이며 두 가지 방법으로 쉽게 논쟁 할 수 있습니다.

참고 : 이러한 경우 항상 컴파일러가 원하는 / 필요한 것과 반대되는 작업을 수행한다고 가정합니다. 최적화를 비활성화하거나 (적어도이 모듈의 경우) 원하는 동작에 대해보다 정의 된 동작을 찾아야합니다. (이것이 유닛 테스트가 중요한 이유이기도합니다.) 결함이라고 생각되면 C ++ 개발자에게 문제를 제기해야합니다.


이 모든 것은 여전히 ​​읽기가 정말 어렵 기 때문에 내가 관련성이 있다고 생각하는 것을 포함하여 직접 읽을 수 있도록 노력하십시오.

glvalue glvalue 표현식은 lvalue 또는 xvalue입니다.

속성 :

glvalue는 lvalue에서 rvalue로, 배열에서 포인터로 또는 함수에서 포인터로 암시 적으로 변환하는 prvalue로 암시 적으로 변환 될 수 있습니다. glvalue는 다형성 일 수 있습니다. 식별하는 객체의 동적 유형이 반드시 표현식의 정적 유형은 아닙니다. glvalue는 표현식에서 허용하는 불완전한 유형을 가질 수 있습니다.


xvalue 다음 표현식은 xvalue 표현식입니다.

반환 유형이 객체에 대한 rvalue 참조 인 함수 호출 또는 오버로드 된 연산자 표현식 (예 : std :: move (x); a [n], 내장 첨자 표현식. 여기서 하나의 피연산자는 배열 rvalue입니다. am, 객체 표현식의 멤버. 여기서 a는 rvalue이고 m은 비 참조 유형의 비 정적 데이터 멤버입니다. a. * mp, 객체 표현식의 멤버에 대한 포인터. 여기서 a는 rvalue이고 mp는 데이터 멤버에 대한 포인터입니다. ㅏ ? b : c, 일부 b 및 c에 대한 삼항 조건식 (자세한 내용은 정의 참조); static_cast (x)와 같은 객체 유형에 대한 rvalue 참조에 대한 캐스트 표현식 임시 구체화 후 임시 객체를 지정하는 표현식입니다. (C ++ 17 이후) 속성 :

rvalue (아래)와 동일합니다. glvalue (아래)와 동일합니다. 특히, 모든 rvalue와 마찬가지로 xvalue는 rvalue 참조에 바인딩되며 모든 glvalue와 마찬가지로 xvalue는 다형성 일 수 있으며 비 클래스 xvalue는 cv-qualified 일 수 있습니다.


lvalue 다음 표현식은 lvalue 표현식입니다.

std :: cin 또는 std :: endl과 같이 유형에 관계없이 변수, 함수 또는 데이터 멤버의 이름. 변수의 유형이 rvalue 참조 인 경우에도 이름으로 구성된 표현식은 lvalue 표현식입니다. 반환 유형이 lvalue 참조 인 함수 호출 또는 오버로드 된 연산자 표현식 (예 : std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 또는 ++ it); a = b, a + = b, a % = b 및 기타 모든 내장 할당 및 복합 할당 표현식; ++ a 및 –a, 내장 사전 증가 및 사전 감소 표현식; * p, 내장 간접 표현식; a [n] 및 p [n], 내장 첨자 표현식, 단 a가 배열 rvalue (C ++ 11부터) 인 경우 제외. am, 객체 표현식의 멤버 (m이 멤버 열거 자 또는 비 정적 멤버 함수 인 경우 제외) 또는 a는 rvalue이고 m은 비 참조 유형의 비 정적 데이터 멤버입니다. p-> m, 포인터 표현식의 내장 멤버 (m이 멤버 열거 자 또는 비 정적 멤버 함수 인 경우 제외). a. * mp, 객체 표현식의 멤버에 대한 포인터. 여기서 a는 lvalue이고 mp는 데이터 멤버에 대한 포인터입니다. p-> * mp, 포인터 표현식의 멤버에 대한 내장 포인터. 여기서 mp는 데이터 멤버에 대한 포인터입니다. a, b, 내장 쉼표 표현식, 여기서 b는 lvalue입니다. ㅏ ? b : c, 일부 b 및 c에 대한 삼항 조건식 (예 : 둘 다 동일한 유형의 l 값이지만 자세한 내용은 정의 참조) “Hello, world!”와 같은 문자열 리터럴; lvalue 참조 유형에 대한 캐스트 표현식 (예 : static_cast (x); 함수 호출 또는 오버로드 된 연산자 표현식, 반환 유형은 함수에 대한 rvalue 참조입니다. static_cast (x)와 같은 함수 유형에 대한 rvalue 참조에 대한 캐스트 표현식. (C ++ 11 이후) 속성 :

glvalue (아래)와 동일합니다. lvalue의 주소를 사용할 수 있습니다. & ++ i 1
및 & std :: endl은 유효한 표현식입니다. 수정 가능한 lvalue는 내장 할당 및 복합 할당 연산자의 왼쪽 피연산자로 사용될 수 있습니다. lvalue는 lvalue 참조를 초기화하는 데 사용할 수 있습니다. 이렇게하면 새 이름이 식으로 식별되는 개체와 연결됩니다.


as-if 규칙

C ++ 컴파일러는 다음 사항이 적용되는 한 프로그램에 대한 모든 변경을 수행 할 수 있습니다.

1) 모든 시퀀스 포인트에서 모든 휘발성 객체의 값이 안정적입니다 (이전 평가가 완료되고 새로운 평가가 시작되지 않음) (C ++ 11까지) 1) 휘발성 객체에 대한 액세스 (읽기 및 쓰기)는 의미 체계에 따라 엄격하게 발생합니다. 발생하는 표현의. 특히 동일한 스레드의 다른 휘발성 액세스와 관련하여 순서가 변경되지 않습니다. (C ++ 11부터) 2) 프로그램 종료시 파일에 기록 된 데이터는 프로그램이 기록 된대로 실행 된 것과 동일합니다. 3) 프로그램이 입력을 기다리기 전에 대화 형 장치로 전송되는 프롬프트 텍스트가 표시됩니다. 4) ISO C pragma #pragma STDC FENV_ACCESS가 지원되고 ON으로 설정된 경우,


사양을 읽으려면 읽어야 할 사양이라고 생각합니다.

참고 문헌

C11 표준 (ISO / IEC 9899 : 2011) : 6.7.3 유형 한정자 (p : 121-123)

C99 표준 (ISO / IEC 9899 : 1999) : 6.7.3 유형 한정자 (p : 108-110)

C89 / C90 표준 (ISO / IEC 9899 : 1990) : 3.5.3 유형 한정자


답변

휘발성에 대한 포인터가 아닌 휘발성을 사용하는 지역 변수를 본 적이 없다고 생각합니다. 에서와 같이 :

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

내가 아는 휘발성의 유일한 다른 경우는 신호 처리기에 작성된 전역을 사용합니다. 거기에 관련된 포인터가 없습니다. 또는 하드웨어와 관련된 특정 주소에있는 링커 스크립트에 정의 된 기호에 액세스합니다.

최적화가 관찰 가능한 효과를 변경하는 이유를 추론하는 것이 훨씬 쉽습니다. 그러나 동일한 규칙이 로컬 휘발성 변수에 적용됩니다. 컴파일러는 x에 대한 액세스가 관찰 가능한 것처럼 동작해야하며이를 최적화 할 수 없습니다.


답변