실제로 다른 컴파일러가 int x = ++ i + ++ i;의 다른 값을 계산하는 이유는 무엇입니까?


165

이 코드를 고려하십시오.

int i = 1;
int x = ++i + ++i;

이 코드가 컴파일된다고 가정하면 컴파일러가이 코드에 대해 무엇을 할 수 있을지 추측 할 수 있습니다.

  1. 둘 다 ++i반환 2되어 x=4.
  2. 하나 ++i는 반환 2하고 다른 하나 는 반환 3하여 x=5.
  3. 둘 다 ++i반환 3되어 x=6.

나에게는 두 번째가 가장 가능성이 높습니다. 둘 개 중 하나의 ++사업자로 실행 i = 1i증가하고, 결과가 2반환됩니다. 이어서 제 ++연산자로 실행 i = 2i증가되고, 그 결과는 3리턴된다. 다음 23주고 함께 추가됩니다 5.

그러나 Visual Studio에서이 코드를 실행 한 결과 6. 나는 컴파일러를 더 잘 이해하기 위해 노력하고 있으며, 어떤 결과로 이어질 수 있는지 궁금합니다 6. 내 유일한 추측은 코드가 "내장 된"동시성으로 실행될 수 있다는 것입니다. 두 ++연산자가 호출되었으며, 각각 i다른 연산자가 반환되기 전에 증가한 다음 둘 다를 반환했습니다 3. 이것은 호출 스택에 대한 나의 이해와 모순되며 설명이 필요합니다.

어떤 (합리적인) 일이 결과 또는 C++결과로 이어질 수 있습니까?46

노트

이 예제는 Bjarne Stroustrup의 Programming : Principles and Practice using C ++ (C ++ 14)에서 정의되지 않은 동작의 예제로 나타났습니다.

계피의 의견을 참조하십시오 .


5
C 사양은 실제로 이전 / 사후 증가 작업과 비교하여 =의 오른쪽에있는 작업 또는 평가의 순서를 다루지 않고 왼쪽에만 있습니다.
Cristobol Polychronopolis 20.06.05

2
Stroustrup의 책에서이 예를 얻은 경우 질문에 인용 할 것을 권장합니다 (답변 중 하나에 대한 주석에서 언급 됨).
Daniel R. Collins

4
@philipxy 제안 된 중복은이 질문의 중복이 아닙니다. 질문이 다릅니다. 중복 제안에 대한 답변은이 질문에 대한 답변이 아닙니다. 귀하가 제안한 중복의 답변은이 질문에 대한 수락 된 (또는 높은 투표) 답변의 중복이 아닙니다. 내 질문을 잘못 읽은 것 같습니다. 나는 당신이 그것을 다시 읽고 투표를 닫을 것을 재고 할 것을 제안합니다.
계피

3
@philipxy "답변은 컴파일러가 무엇이든 할 수 있다고 말합니다 ..."내 질문에 대답하지 않습니다. "그들은 당신의 질문이 다르다고 생각하더라도 그것은 단지 그 질문에 대한 변형이라는 것을 보여줍니다."뭐? "당신이 C ++의 버전을주지는 않지만"내 C ++의 버전은 내 질문과 관련이 없습니다. "그러므로 성명서가있는 전체 프로그램은 무엇이든 할 수 있습니다."나는 알고 있지만 내 질문은 특정 행동에 관한 것이었다. "귀하의 의견은 답변 내용을 반영하지 않습니다." 내 의견은 내 질문의 내용을 반영하므로 다시 읽어야합니다.
계피

2
제목에 답하기 위해; UB는 bahavioir가 정의되지 않았 음을 의미하기 때문입니다. 역사 전반에 걸쳐 서로 다른 시간에 서로 다른 아키텍처에 대해 서로 다른 사람들이 만든 여러 컴파일러, 라인 외부에 색상을 지정하고 실제 구현 을 요청하면 사양 외부에이 부분 에 무언가 를 넣어야 했기 때문에 사람들이 정확히 그렇게했습니다. 그들 중 다른 크레용을 사용했습니다. 따라서 고색 창연 한 격언, UB에 의존하지 않는
토비

답변:


200

컴파일러는 코드를 가져 와서 매우 간단한 명령어로 분할 한 다음 최적이라고 생각하는 방식으로 다시 결합하고 정렬합니다.

코드

int i = 1;
int x = ++i + ++i;

다음 지침으로 구성됩니다.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

그러나 이것이 내가 작성한 방식대로 번호가 매겨진 목록 임에도 불구하고 여기 에는 몇 가지 순서 종속성이 있습니다. 1-> 2-> 3-> 4-> 5-> 10-> 11 및 1-> 6-> 7- > 8-> 9-> 10-> 11은 상대적인 순서를 유지해야합니다. 그 외에 컴파일러는 자유롭게 재정렬 할 수 있으며 중복성을 제거 할 수 있습니다.

예를 들어 다음과 같이 목록을 주문할 수 있습니다.

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

컴파일러가 이것을 할 수있는 이유는 무엇입니까? 증분의 부작용에 대한 순서가 없기 때문입니다. 그러나 이제 컴파일러는 단순화 할 수 있습니다. 예를 들어 4에 데드 스토어가 있습니다. 값은 즉시 덮어 쓰여집니다. 또한 tmp2와 tmp4는 실제로 동일합니다.

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

이제 tmp1로 할 수있는 모든 것은 죽은 코드입니다. 절대 사용되지 않습니다. 그리고 i를 다시 읽는 것도 제거 할 수 있습니다.

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

이 코드는 훨씬 더 짧습니다. 옵티마이 저는 행복합니다. 내가 한 번만 증가했기 때문에 프로그래머는 그렇지 않습니다. 죄송합니다.

컴파일러가 대신 할 수있는 다른 작업을 살펴 ​​보겠습니다. 원래 버전으로 돌아 갑시다.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

컴파일러는 다음과 같이 재정렬 할 수 있습니다.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

그리고 내가 두 번 읽 혔다는 것을 다시 확인하고 그중 하나를 제거하십시오.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

좋지만 더 나아갈 수 있습니다. tmp1을 재사용 할 수 있습니다.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

그런 다음 6에서 i를 다시 읽지 않아도됩니다.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

이제 4는 죽은 상점입니다.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

이제 3과 7을 하나의 명령어로 병합 할 수 있습니다.

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

마지막 임시 제거 :

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

이제 Visual C ++가 제공하는 결과를 얻을 수 있습니다.

두 최적화 경로 모두에서 아무 작업도 수행하지 않은 경우 지침이 제거되지 않는 한 중요한 순서 종속성이 유지되었습니다.


36
현재 이것은 시퀀싱 을 언급하는 유일한 답변입니다 .
PM 2Ring

3
-1이 답변이 명확하지 않다고 생각합니다. 관찰 된 결과는 컴파일러 최적화에 전혀 의존하지 않습니다 (내 대답 참조).
Daniel R. Collins

3
이것은 읽기-수정-쓰기 작업을 가정합니다. 유비쿼터스 x86과 같은 일부 CPU에는 원자 증분 작업이있어 ​​상황이 더욱 복잡해집니다.
Mark

6
@philipxy "표준에는 개체 코드에 대해 말할 것이 없습니다." 표준은이 스 니펫의 동작에 대해 말할 것도 없습니다. UB입니다. 그것이 질문의 전제입니다. OP는 실제로 컴파일러가 다른 이상한 결과를 얻을 수있는 이유를 알고 싶었습니다. 또한 내 대답은 객체 코드에 대해 아무것도 말하지 않습니다.
Sebastian Redl

5
@philipxy 귀하의 이의를 이해하지 못합니다. 언급했듯이 문제는 C ++ 표준이 아니라 UB가있을 때 컴파일러가 수행 할 수있는 작업에 대한 것입니다. 가상 컴파일러가 코드를 변환하는 방법을 탐색 할 때 객체 코드를 사용하는 것이 왜 부적절합니까? 사실, 객체 코드 외에는 어떤 관련이 있습니까?
Konrad Rudolph

58

이것이 UB (OP가 암시했듯이)이지만 다음은 컴파일러가 3 개의 결과를 얻을 수있는 가상의 방법입니다. 하나의 변수 대신 x다른 int i = 1, j = 1;변수를 사용하면 세 가지 모두 동일한 올바른 결과를 제공합니다 i.

  1. 둘 다 ++ i가 2를 반환하므로 x = 4가됩니다.
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. 하나의 ++ i는 2를 반환하고 다른 하나는 3을 반환하여 x = 5를 반환합니다.
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. 둘 다 ++ i가 3을 반환하므로 x = 6이됩니다.
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
제가 바라던 것보다 더 좋은 답변입니다. 감사합니다.
계피

1
옵션 1의 경우 컴파일러는 preincrement에 대한 메모를 작성했을 수 i있습니다. 한 번만 발생할 수 있다는 것을 알면 한 번만 방출합니다. 옵션 2의 경우 코드는 대학 컴파일러 클래스 프로젝트처럼 문자 그대로 기계어 코드로 변환됩니다. 옵션 3의 경우 옵션 1과 비슷하지만 사전 증가의 복사본을 두 개 만들었습니다. 집합이 아닌 벡터를 사용해야합니다. :-)
ZAN 살쾡이

죄송 @dxiv, 내 나쁜, 나는 게시물 혼합
muru을

22

나에게는 두 번째가 가장 가능성이 높습니다.

옵션 # 4 : 둘 다 ++i동시에 발생합니다.

최신 프로세서는 흥미로운 최적화 및 병렬 코드 평가로 이동합니다. 여기에서 허용되는 경우 컴파일러가 더 빠른 코드를 계속 만드는 또 다른 방법입니다. 나는 실용적인 구현 , 컴파일러가 병렬 처리로 이동 한다고 생각 합니다.

동일한 메모리 경합으로 인해 비 결정적 동작이나 버스 오류를 일으키는 경합 상태를 쉽게 볼 수 있습니다.

내 질문은 : C ++ 컴파일러가 4 또는 결과 또는 6의 결과로 이어질 수있는 (합리적인) 일이 무엇입니까?

그것은 할 수 있지만, 거기에 포함되지 않습니다.

++i + ++i합리적인 결과를 사용 하거나 기대 하지 마십시오 .


이 대답과 @dxiv를 모두 받아 들일 수 있다면 그렇게 할 것입니다. 응답 해 주셔서 감사합니다.
계피

4
@UriRaz : 프로세서는 컴파일러의 선택에 따라 데이터 위험이 있음을 인식하지 못할 수도 있습니다. 예를 들어 컴파일러는 i두 개의 레지스터에 할당 하고 두 레지스터를 모두 증가시킨 다음 다시 쓸 수 있습니다. 프로세서는이를 해결할 방법이 없습니다. 근본적인 문제는 C ++도 최신 CPU도 엄격하게 순차적이지 않다는 것입니다. C ++에는 명시 적으로 발생 전 및 후 발생 시퀀싱이있어 기본적으로 동시 발생을 허용합니다.
MSalters

1
그러나 우리는 Visual Studio를 사용하는 OP의 경우가 아니라는 것을 알고 있습니다. x86 및 ARM을 포함한 대부분의 주류 ISA는 하나의 기계 명령어가 다음 명령어가 시작되기 전에 완전히 완료되는 완전 순차 실행 모델 측면에서 정의됩니다. 비 순차 수퍼 스칼라는 단일 스레드에 대한 환상을 유지해야합니다. (공유 메모리를 읽는 다른 스레드는 프로그램 순서대로 항목을 볼 수 있다는 보장은 없지만 OoO exec의 기본 규칙은 단일 스레드 실행을 중단하지 않는 것입니다.)
Peter Cordes

1
이것이 제가 가장 좋아하는 대답입니다. CPU 수준에서 병렬 명령 실행을 언급하는 유일한 대답이기 때문입니다. Btw는 경쟁 조건으로 인해 동일한 메모리 위치에서 뮤텍스 잠금 해제를 기다리는 동안 CPU 스레드가 중단되므로 동시성 모델에서 매우 적합하지 않다는 답변을 언급하는 것이 좋습니다. 두 번째-동일한 경쟁 조건으로 인해 실제 대답은 4또는 5,-CPU 스레드 실행 모델 / 속도에 따라 다르므로 이것이 핵심입니다.
Agnius Vasiliauskas

1
@AgniusVasiliauskas 아마도 "실제로 왜 서로 다른 컴파일러가 서로 다른 값을 계산 하는가"는 오늘날의 프로세서에 대한 단순한 관점을 고려할 때 이해하기 더 쉬운 방법을 찾고 있습니다. 그러나 컴파일러 / 프로세서 시나리오의 범위는 언급 된 몇 가지 다양한 답변보다 훨씬 큽니다. 당신의 유용한 통찰력은 또 다른 것입니다. IMO, 병렬 처리는 미래입니다. 따라서이 답변은 추상적 인 방식이기는하지만 미래가 아직 펼쳐지고 있기 때문에 이들에 초점을 맞췄습니다. IAC, 게시물이 인기를 얻고 있으며 이해하기 쉬운 답변이 최고의 보상을받습니다.
chux-Monica 복원

17

(컴파일러 최적화 또는 멀티 스레딩에 대한 입찰없이) 간단하고 직접적인 해석은 다음과 같을 것이라고 생각합니다.

  1. 증가 i
  2. 증가 i
  3. 추가 i+i

i두 번 증가 하면 값은 3이고 더하면 합계는 6입니다.

검사를 위해 이것을 C ++ 함수로 간주하십시오.

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

이제 GNU C ++ 컴파일러 (win32, gcc 버전 3.4.2 (mingw-special))의 이전 버전을 사용하여 해당 함수를 컴파일하여 얻은 어셈블리 코드는 다음과 같습니다. 여기에는 멋진 최적화 나 멀티 스레딩이 없습니다.

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

지역 변수 i는 단 한 곳의 스택에 있습니다 : address [ebp-4]. 해당 위치는 두 번 증가합니다 (어셈블리 함수의 5-8 번째 줄에서 해당 주소의 중복로드를 포함하여 eax). 그런 다음 9 ~ 10 번째 행에서 해당 값이에로드 된 eax다음에 추가됩니다 eax(즉, 현재를 계산합니다 i + i). 그런 다음 스택에 중복 복사되고 eax반환 값 (분명히 6)으로 다시 복사됩니다 .

C ++ 표준 (여기서는 이전 표준 : ISO / IEC 14882 : 1998 (E))을 살펴 보는 것이 흥미로울 수 있습니다. 이는 Expressions 섹션 5.4에 대해 다음과 같이 말합니다.

언급 된 경우를 제외하고 개별 연산자의 피연산자 및 개별 식의 하위 식에 대한 평가 순서와 부작용이 발생하는 순서는 지정되지 않습니다.

각주 포함 :

연산자의 우선 순위는 직접 지정되지 않지만 구문에서 파생 될 수 있습니다.

지정되지 않은 동작의 두 가지 예가 그 시점에 주어지며, 둘 다 증분 연산자 (그 중 하나 :)를 포함합니다 i = ++i + 1.

이제 원한다면 다음과 같이 할 수 있습니다. 정수 래퍼 클래스 (예 : Java Integer)를 만듭니다. 오버로드 함수 operator+operator++중간 값 개체를 반환합니다. ++iObj + ++iObj5를 포함하는 객체를 반환하도록 작성 하고 가져옵니다. (간결성을 위해 여기에 전체 코드를 포함하지 않았습니다.)

개인적으로 위에서 본 순서와 다른 방식으로 작업을 수행하는 잘 알려진 컴파일러의 예가 있다면 흥미로 웠습니다. 가장 간단한 구현은 inc추가 작업이 수행되기 전에 기본 유형에 대해 두 개의 어셈블리 코드 를 수행하는 것 같습니다.


2
증분 연산자는 정말 잘 정의 된 "반환"값을 가지고 있습니다
edc65

@philipxy : 나는 당신이 반대 한 구절을 제거하기 위해 대답을 편집했습니다. 이 시점에서 답변에 더 동의하거나 동의하지 않을 수 있습니다.
Daniel R. Collins

2
그것들은 "지정되지 않은 행동의 두 가지 예"가 아니며 , 표준의 다른 구절에서 발생하는 정의되지 않은 행동 , 매우 다른 짐승 의 두 가지 예입니다 . C ++ 98은 각주 예제의 텍스트에서 "unspecified"라고 말하면서 규범적인 텍스트와 모순되지만 나중에 수정되었습니다.
Cubbi

@Cubbi : 여기에 인용 된 표준의 텍스트와 각주 모두 "지정되지 않음", "직접 지정되지 않음"이라는 문구를 사용하며 정의 섹션 1.3.13의 용어와 일치하는 것 같습니다.
Daniel R. Collins

1
@philipxy : 여기에있는 많은 답변에 대해 동일한 의견을 반복 한 것을 확인했습니다. 당신의 주된 비평은 OP의 질문 자체에 대한 것이며, 그 범위는 추상적 인 표준에 관한 것이 아닙니다.
Daniel R. Collins

7

컴파일러가 할 수있는 합리적인 일은 Common Subexpression Elimination입니다. 이것은 컴파일러에서 이미 일반적인 최적화입니다. 같은 하위 표현식 (x+1)이 더 큰 표현식에서 두 번 이상 발생하면 한 번만 계산하면됩니다. 예를 a/(x+1) + b*(x+1)들어 x+1하위 표현식 에서 한 번 계산할 수 있습니다.

물론 컴파일러는 이러한 방식으로 최적화 할 수있는 하위 표현식을 알아야합니다. rand()두 번 전화 하면 두 개의 난수를 제공해야합니다. 따라서 인라인되지 않은 함수 호출은 CSE에서 면제되어야합니다. 아시다시피, 두 번의 발생을 어떻게 i++처리해야하는지에 대한 규칙이 없으므로 CSE에서 제외 할 이유가 없습니다.

그 결과는 참이있을 수 있습니다 int x = ++i + ++i;에 최적화되어 있습니다 int __cse = i++; int x = __cse << 1. (CSE, 반복 된 강도 감소)


표준은 객체 코드에 대해 말할 것이 없습니다. 이것은 언어 정의에 의해 정당화되거나 관련되지 않습니다.
philipxy

1
@philipxy : 표준에는 정의되지 않은 동작의 어떤 형태에 대해서도 말할 것이 없습니다. 그것이 질문의 전제입니다.
MSalters

7

실제로 정의되지 않은 동작을 호출합니다. "합리적"이라고 생각하는 것뿐만 아니라 모든 일 발생할 수 있으며, 종종 합리적이라고 생각하지 않는 일 발생합니다. 모든 것이 정의상 "합리적"입니다.

매우 합리적인 컴파일은 명령문을 실행하면 정의되지 않은 동작이 호출되므로 명령문을 실행할 수 없으므로 의도적으로 응용 프로그램을 충돌시키는 명령으로 변환된다는 것을 컴파일러가 관찰하는 것입니다. 그것은 매우 합리적입니다.

Downvoter : GCC는 귀하에게 강력히 동의하지 않습니다.


표준이 어떤 것을 "정의되지 않은 행동"으로 특성화 할 때, 이는 행동이 표준의 관할권 밖에 있다는 것을 의미 합니다. 표준은 관할권 밖에서 사물의 합리성을 판단하려는 시도를하지 않으며, 준수하는 구현이 부당하게 쓸모가 없을 수있는 모든 방식을 금지하려는 시도를하지 않기 때문에 표준이 특정 상황에서 요구 사항을 부과하지 않는다고해서 모든 것이 가능한 조치는 똑같이 "합리적"입니다.
supercat

6

아무 없다 합리적인 컴파일러 6의 결과를 얻기 위해 할 수있는 일이 있지만 가능하고 합법적이다. 4의 결과는 완전히 합리적이며 5 경계선의 결과는 합리적이라고 생각합니다. 그들 모두는 완벽하게 합법적입니다.

어이 기다려! 무슨 일이 일어나야하는지 분명하지 않습니까? 추가에는 두 증분의 결과가 필요하므로 분명히 먼저 발생해야합니다. 그리고 우리는 왼쪽에서 오른쪽으로 이동합니다 . 그렇게 간단하다면. 불행히도 그렇지 않습니다. 우리는 왼쪽에서 오른쪽으로 가지 않습니다 . 그게 문제입니다.

메모리 위치를 두 개의 레지스터로 읽는 (또는 동일한 리터럴에서 둘 다 초기화하여 메모리 왕복 최적화) 컴파일러가 수행 할 수있는 매우 합리적인 작업입니다. 이것은 은밀하게 두 개의 서로 다른 변수 가 있다는 효과를 가질 것입니다 . 각각의 값은 2이며, 최종적으로 4의 결과에 추가됩니다. 이것은 빠르고 효율적이며 두 가지 모두 에 부합하기 때문에 "합리적"입니다. 표준 및 코드.

마찬가지로 메모리 위치는 한 번 읽고 (또는 리터럴에서 초기화 된 변수) 한 번 증가 할 수 있으며, 그 후에 다른 레지스터의 섀도 복사본이 증가 될 수 있으며, 그 결과 2와 3이 함께 추가됩니다. 이것은 완벽하게 합법적이지만 경계선이 합리적 이라고 말하고 싶습니다 . 나는 그것이 둘 중 하나가 아니기 때문에 경계선이 합리적이라고 생각합니다. 이것은 "합리적인"최적화 된 방법도 아니고 "합리적인"정확히 pedantic 방법도 아닙니다. 다소 중간에 있습니다.

메모리 위치를 두 번 증가 (결과 값 3) 한 다음 최종 결과 6을 위해 해당 값을 자체에 추가하는 것은 합법적이지만 메모리 왕복을 수행하는 것이 정확히 효율적이지 않기 때문에 합리적이지 않습니다. 좋은 저장 전달 기능을 갖춘 프로세서에서는 저장이 거의 보이지 않아야하므로이를 수행하는 것이 "합리적"일 수 있습니다
. 컴파일러가 동일한 위치임을 "인식"하므로 증분을 선택하는 것이 좋습니다. 레지스터 내에서 두 번 값을 입력 한 다음 자체에도 추가합니다. 두 방법 모두 6의 결과를 제공합니다.

컴파일러는 표준의 표현에 따라 그러한 결과를 제공 할 수 있습니다. 비록 제가 개인적으로 6 개를 불쾌한 부서의 "fuck you"메모라고 생각 하겠지만 이는 다소 예상치 못한 일이기 때문에 (합법적이든 아니든, 항상 최소한의 놀라움을 주려고 노력하는 것은 좋은 일입니다!). 하지만 정의되지 않은 행동이 어떻게 관련되어 있는지 보면 슬프게도 "예기치 않은"에 대해 논쟁 할 수는 없습니다.

그렇다면 실제로 컴파일러에게 가지고있는 코드는 무엇입니까? clang에게 물어 보자. 우리가 친절하게 물어 보면 보여줄 것이다 -ast-dump -fsyntax-only.

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

보시다시피, 두 위치에 동일한 lvalue Var 0x2b3e430접두사가 ++적용되고이 두 개는 트리에서 동일한 노드 아래에 있습니다. 이는 시퀀싱 등에 대해 특별한 언급이없는 매우 비 특수 연산자 (+)입니다. 이것이 왜 중요한가요? 글쎄요, 계속 읽어보세요.

경고 : " 'i'에 대한 여러 개의 비 순차적 수정" . 오 오, 안 좋은 것 같네요. 무슨 뜻이에요? [basic.exec] 는 부작용과 시퀀싱에 대해 알려주고, 명시 적으로 달리 언급하지 않는 한 기본적 으로 개별 연산자의 피연산자 및 개별 식의 하위 식에 대한 평가가 순서가 지정 되지 않음을 알려줍니다 (문단 10) . 글쎄, 젠장, 그게 사실입니다. operator+달리 말한 것은 아무것도 없습니다.

그러나 우리는 이전에 시퀀싱되거나, 불확실하게 시퀀싱되거나, 시퀀싱되지 않은 것에 대해 관심이 있습니까? 어쨌든 누가 알고 싶어?

동일한 단락은 또한 순서없는 평가 가 겹칠 수 있고 동일한 메모리 위치를 참조 할 때 ( 그렇습니다 !) 잠재적으로 동시 적이 지 않을 때 동작이 정의되지 않는다는 것을 알려줍니다. 이것은 당신이 아무것도 모르고 "합리적"이라는 보장도 없다는 것을 의미하기 때문에 정말 추악 해지는 곳입니다. 불합리한 것은 실제로 완벽하게 허용되고 "합리적"입니다.


"합리적"의 사용은 "컴파일러는 무엇이든 할 수 있으며 심지어 단일 명령어 'set x to 7'을 내 보냅니다.
계피

@cinnamon 수년 전 제가 어렸을 때 경험이 없었을 때, Sun의 컴파일러 엔지니어들은 그들의 컴파일러가 당시 제가 부당하다고 생각했던 정의되지 않은 동작에 대해 절대적으로 합리적으로 코드를 생성한다고 말했습니다. 교훈을 얻었습니다.
gnasher729

표준은 객체 코드에 대해 말할 것이 없습니다. 이것은 제안 된 구현이 언어 정의에 의해 정당화되거나 관련되는 방식에 대해 조각화되고 명확하지 않습니다.
philipxy

@philipxy : 표준은 잘 구성되고 잘 정의 된 것과 그렇지 않은 것을 정의합니다. 이 Q의 경우 동작을 정의되지 않은 것으로 정의합니다. 합법적 인 것 외에도 효율적인 코드를 생성하는 컴파일러에 대한 합리적인 가정도 있습니다. 네, 당신이 옳습니다. 표준은 그것이 사실이 될 것을 요구하지 않습니다. 그럼에도 불구하고 합리적인 가정입니다.
Damon

@Damon : 표준 은 정의 된대로 처리하기 위해 모든 구현이 필요한 조치 와 정의 된대로 처리하는 데 필요 하지 않은 조치 구현을 지정 합니다 . 일부 작업은 다른 작업보다 더 넓은 범위의 의미 체계를 필요로하기 때문에 표준이 어떤 작업의 동작을 정의하지 않는다고해서이를 정의하는 구현이 그렇지 않은 작업보다 일부 작업에 더 적합하지 않다는 의미는 아닙니다. , 또는 그러한 동작을 정의하지 않는다고해서 구현을 정의하는 다른 것보다 일부 목적에 적합하지 않게 만드는 것은 아닙니다.
supercat

1

규칙은 :

이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 스칼라 객체는 표현식 평가에 의해 최대 한 번 수정 된 저장된 값을 가져야합니다. 그렇지 않으면 동작이 정의되지 않습니다.

따라서 x = 100도 가능한 유효한 결과입니다.

나에게 가장 논리적 인 결과는 6이다. 왜냐하면 우리는 i의 값을 두 번 증가시키고 그 값을 그 자체에 더하기 때문이다. "+"의 양쪽에서 계산 값을 계산하기 전에 더하기가 어렵습니다.

그러나 컴파일러 개발자는 다른 로직을 구현할 수 있습니다.


0

++ i는 lvalue를 반환하지만 i ++는 rvalue를 반환하는 것처럼 보입니다.
따라서이 코드는 괜찮습니다.

int i = 1;
++i = 10;
cout << i << endl;

이것은 아닙니다 :

int i = 1;
i++ = 10;
cout << i << endl;

위의 두 문장은 VisualC ++, GCC7.1.1, CLang 및 Embarcadero와 일치합니다.
이것이 VisualC ++ 및 GCC7.1.1의 코드가 다음 코드와 유사한 이유입니다.

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

디스 어셈블리를 볼 때 먼저 i를 증가시키고 i를 다시 작성합니다. 추가하려고 할 때 동일한 작업을 수행하고 i를 증가시키고 다시 작성합니다. 그런 다음 i를 i에 추가합니다. 나는 CLang과 Embarcadero가 다르게 행동한다는 것을 알아 차 렸습니다. 따라서 첫 번째 문과 일치하지 않습니다. 첫 번째 ++ i 후에 결과를 rvalue에 저장 한 다음 두 번째 i ++에 추가합니다.
여기에 이미지 설명 입력


"looks line an lvalue"의 문제는 컴파일러가 아니라 C ++ 표준의 관점에서 이야기하고 있다는 것입니다.
MSalters 2010-06-04

@MSalters이 문은 VisualStudio 2019, GCC7.1.1, clang 및 Embarcadero 및 첫 번째 코드와 일치합니다. 따라서 사양은 일관성이 있습니다. 그러나 두 번째 코드에서는 다르게 작동합니다. 두 번째 코드는 VisualStudio 2019 및 GCC7.1.1과 일치하지만 clang 및 Embarcadero와는 일치하지 않습니다.
armagedescu 20.06.04

3
글쎄, 당신의 대답의 첫 번째 코드는 합법적 인 C ++이므로 구현은 사양과 일치합니다. 질문과 비교할 때 "무엇인가"는 세미콜론으로 끝나므로 완전한 진술이됩니다. 이는 C ++ 표준에서 요구하지만 질문에는없는 시퀀싱을 생성합니다.
MSalters

@MSalters 동등한 의사 코드로 수행하고 싶었습니다. 그러나 나는 확실히 그것을 재구성하는 방법을 모르겠습니다
armagedescu

0

개인적으로 컴파일러가 귀하의 예제에서 6을 출력 할 것이라고 기대하지 않았을 것입니다. 귀하의 질문에 대한 훌륭하고 상세한 답변이 이미 있습니다. 짧은 버전을 시도해 보겠습니다.

기본적으로 다음과 ++i같은 맥락에서 2 단계 프로세스입니다.

  1. 값 증가 i
  2. 가치 읽기 i

++i + ++i양측의 맥락 에서 추가는 표준에 따라 임의의 순서로 평가 될 수 있습니다. 즉, 두 증분은 독립적으로 간주됩니다. 또한 두 용어 사이에 종속성이 없습니다. 따라서 증가 및 읽기 i가 인터리브 될 수 있습니다. 이것은 잠재적 인 순서를 제공합니다.

  1. i왼쪽 피연산자 증분
  2. i오른쪽 피연산자 증가
  3. i왼쪽 피연산자 다시 읽기
  4. i올바른 피연산자에 대해 다시 읽어보십시오.
  5. 둘을 합산하면 6이 나온다.

자, 이것에 대해 생각하면, 표준에 따르면 6이 가장 의미가 있습니다. 결과가 4 인 경우 먼저 읽는 CPU가 필요합니다.i 인 경우 독립적으로 다음 값을 증분하고 동일한 위치에 다시 쓰는 . 기본적으로 경쟁 조건입니다. 값이 5 인 경우 임시를 도입하는 컴파일러가 필요합니다.

그러나 표준은 ++i변수를 반환하기 전에 즉 현재 코드 줄을 실제로 실행하기 전에 변수 를 증가 시킨다고 말합니다 . 합계 연산자 는 증분을 적용한 후 +합계해야합니다 i + i. C ++는 값 의미론이 아닌 변수에 대해 작업해야한다고 말하고 싶습니다. 따라서 나에게 6은 CPU의 실행 모델이 아니라 언어의 의미에 의존하기 때문에 이제 가장 의미가 있습니다.


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

stackoverflow에 오신 것을 환영합니다! 답변에 제한, 가정 또는 단순화를 제공 할 수 있습니까? 이 링크에 대답하는 방법에 대한 자세한 내용을 참조하십시오 stackoverflow.com/help/how-to-answer
우 사마 Abdulrehman에게

0

잘 그것은 컴파일러의 디자인에 따라 달라집니다. 따라서 컴파일러가 명령문을 디코딩하는 방식에 따라 대답이 달라집니다. 논리를 만드는 대신 두 개의 다른 변수 ++ x 및 ++ y를 사용하는 것이 더 나은 선택이 될 것입니다. 참고 : 출력은 업데이트 된 경우 ms Visual Studio에서 최신 버전의 언어 버전에 따라 달라집니다. 따라서 규칙이 변경되면 출력도


0

이 시도

int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4

0

이 링크 평가 순서에서 :

함수 호출 표현식에서 함수 인수의 평가 순서를 포함하여 C 연산자의 피연산자 평가 순서와 모든 표현식 내의 하위 표현식 평가 순서는 지정되지 않습니다 (아래에 명시된 경우 제외). 컴파일러는 순서에 상관없이 이들을 평가하고 동일한 표현식이 다시 평가 될 때 다른 순서를 선택할 수 있습니다 .

인용문에서 평가 순서는 C 표준에 의해 지정되지 않음이 분명합니다. 다른 컴파일러는 다른 평가 순서를 구현합니다. 컴파일러는 이러한 식을 어떤 순서로든 자유롭게 평가할 수 있습니다. 이것이 다른 컴파일러가 질문에 언급 된 표현에 대해 다른 출력을 제공하는 이유입니다.

그러나 하위 표현식 Exp1과 Exp2 사이에 시퀀스 포인트 가 있으면 Exp1의 값 계산과 부작용 모두 Exp2의 모든 값 계산과 부작용 이전에 시퀀싱됩니다.


이 인용문과 귀하의 진술은 질문에 대한 답변과 어떤 관련이 있습니까? (수사적.) 어쨌든 주어진 코드는 여기 다른 곳에서 설명한 것처럼 정의되지 않은 동작을 가지고 있으므로 귀하의 요점은 적용되지 않습니다. 또한 요청자는 코드가 정의되지 않은 경우 구현의 어떤 측면이 어떤 일로 이어지는 지 (불량한) 질문을 시도합니다. 또한 그들은 질문을 명확히하지 않고 컴파일러가 할 수있는 "합리적"이라는 사실을 받아들이지 않을 것입니다. 또한이 게시물은 이미 여기에있는 게시물에 아무것도 추가하지 않습니다.
philipxy

귀하의 의견은 내 문제를 다루지 않습니다. 코드가 정의되지 않았고 지정되지 않았 음을 포함합니다.
philipxy

따옴표는 정의되지 않음을 언급하지 않고 지정되지 않음을 언급합니다. 끝났어.
philipxy

예, 그것은 구분되지 않았습니다. 그래서 다른 컴파일러는 다른 평가 순서를 구현합니다. 이것이 다른 컴파일러에 대해 다른 출력을 얻는 이유입니다.
Krishna Kanth Yenumula


-4

실제로 정의되지 않은 동작을 호출합니다. "합리적"이라고 생각하는 것뿐만 아니라 모든 일 발생할 수 있으며, 종종 합리적이라고 생각하지 않는 일 발생합니다. 모든 것이 정의상 "합리적"입니다.

매우 합리적인 컴파일은 컴파일러가 명령문을 실행하면 정의되지 않은 동작을 호출하므로 명령문을 실행할 수 없으므로 의도적으로 응용 프로그램을 충돌시키는 명령으로 변환됩니다. 그것은 매우 합리적입니다. 결국, 컴파일러는이 충돌이 결코 일어날 수 없다는 것을 알고 있습니다.


1
나는 당신이 질문을 오해했다고 믿습니다. 질문은 특정 결과 (x = 4, 5 또는 6의 결과)로 이어질 수있는 일반적인 또는 특정 행동에 관한 것입니다. "합리적"이라는 단어의 사용이 마음에 들지 않으면 위의 의견으로 안내합니다. " '합리적'의 사용은 단지 '컴파일러가 무엇이든 할 수 있습니다. ""x를 7로 설정 "" "이라는 단일 명령을 내 보냅니다. 일반적인 생각을 유지하는 질문에 대해 더 나은 표현이 있다면 저는 그것에 대해 열려 있습니다. 또한 답변을 다시 게시 한 것으로 보입니다.
계피

2
그들은 모두 매우 유사하기 때문에 당신이 개 답변 중 하나를 삭제 제안
MM
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.