(A + B + C) ≠ (A + C + B) 및 컴파일러 재정렬


108

두 개의 32 비트 정수를 추가하면 정수 오버 플로우가 발생할 수 있습니다.

uint64_t u64_z = u32_x + u32_y;

32 비트 정수 중 하나가 먼저 캐스트되거나 64 비트 정수에 추가되는 경우 이러한 오버플로를 피할 수 있습니다.

uint64_t u64_z = u32_x + u64_a + u32_y;

그러나 컴파일러가 추가 순서를 변경하기로 결정한 경우 :

uint64_t u64_z = u32_x + u32_y + u64_a;

정수 오버플로가 계속 발생할 수 있습니다.

컴파일러가 이러한 재정렬을 수행 할 수 있습니까? 아니면 결과 불일치를 인식하고 표현식 순서를 그대로 유지할 수 있다고 믿을 수 있습니까?


15
uint32_t값 이 더해진 것처럼 보이기 때문에 실제로 정수 오버플로를 표시 하지 않습니다. 오버플로하지 않고 래핑합니다. 이것들은 다른 행동이 아닙니다.
마틴 보너 모니카 지원

5
C ++ 표준의 1.9 섹션을 참조하면 질문에 직접 답할 수 있습니다 (당신의 것과 거의 똑같은 예제도 있습니다).
Holt

3
@Tal : 다른 사람들이 이미 언급했듯이 정수 오버플로가 없습니다. 서명되지 않은 것은 래핑하도록 정의되어 있으며 서명 된 경우 정의되지 않은 동작이므로 코 데몬을 포함한 모든 구현이 수행됩니다.
이 사이트에 대한 너무 정직

5
@ 탈 : 말도 안돼! 이미 쓴 : 표준은 매우 명확하고 서명과 그 표준으로-의 UB 한, 포장, 그게 가능한 것 (포화하지가 필요합니다.
너무 정직이 사이트

15
@rustyx : 래핑 또는 오버플로 라고 부르든간에 ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0결과는 0이지만 (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)결과는 이며이 0x100000000두 값은 동일하지 않습니다. 따라서 컴파일러가 해당 변환을 적용 할 수 있는지 여부는 중요합니다. 하지만 표준은 부호없는 정수가 아닌 부호있는 정수에 대해서만 "오버플로"라는 단어를 사용합니다.
Steve Jessop

답변:


84

옵티마이 저가 이러한 재정렬을 수행하면 여전히 C 사양에 바인딩되어 있으므로 이러한 재정렬은 다음과 같습니다.

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

이론적 해석:

우리는

uint64_t u64_z = u32_x + u64_a + u32_y;

추가는 왼쪽에서 오른쪽으로 수행됩니다.

정수 승격 규칙은 원래 표현식의 첫 번째 추가에서 u32_x로 승격되도록 지정 uint64_t합니다. 두 번째 추가에서u32_y 도 승격됩니다 uint64_t.

그래서, C 규격을 준수하기 위해, 어떤 최적화를 촉진해야한다 u32_xu32_y64 부호 값을 비트. 이것은 캐스트를 추가하는 것과 같습니다. (실제 최적화는 C 수준에서 수행되지 않지만 우리가 이해하는 표기법이므로 C 표기법을 사용합니다.)


좌파가 아닌가? (u32_x + u32_t) + u64_a 아닌가요?
쓸모없는

12
@ 쓸모없는 : Klas는 모든 것을 64 비트로 캐스팅했습니다. 이제 순서는 전혀 차이가 없습니다. 컴파일러는 연관성을 따를 필요가 없으며 마치 그랬던 것처럼 정확히 동일한 결과를 생성하면됩니다.
gnasher729

2
OP의 코드가 그렇게 평가 될 것 같지만 사실이 아닙니다.
쓸모없는

@Klas- 이런 일이 발생하고 코드 샘플에 정확히 얼마나 도착 했는지 설명해 주 시겠습니까?
rustyx

1
@rustyx 설명이 필요했습니다. 추가하도록 밀어 주셔서 감사합니다.
Klas Lindbäck

28

컴파일러는 as if 규칙 아래에서만 순서를 변경할 수 있습니다. 즉, 재정렬이 항상 지정된 주문과 동일한 결과를 제공하는 경우 허용됩니다. 그렇지 않으면 (귀하의 예에서와 같이) 아닙니다.

예를 들어 다음 표현식이 주어지면

i32big1 - i32big2 + i32small

크지 만 유사한 것으로 알려진 두 값을 뺀 다음 다른 작은 값 을 더하기 위해 신중하게 구성되어 있습니다 (따라서 오버플로 방지), 컴파일러는 다음과 같이 재정렬하도록 선택할 수 있습니다.

(i32small - i32big2) + i32big1

대상 플랫폼이 문제를 방지하기 위해 랩-라운드가있는 2- 보체 산술을 사용하고 있다는 사실에 의존합니다. (컴파일러가 레지스터에 대해 눌려져 i32small있고 레지스터에 이미 있는 경우 이러한 재정렬이 적절할 수 있습니다 .)


OP의 예제는 서명되지 않은 유형을 사용합니다. i32big1 - i32big2 + i32small서명 된 유형을 의미합니다. 추가적인 우려가 작용합니다.
chux - 분석 재개 모니카

@chux 물론입니다. 내가하려는 요점은 비록 (i32small-i32big2) + i32big1UB를 유발할 수 있기 때문에 쓸 수는 없지만 컴파일러는 컴파일러가 동작이 올바르다 고 확신 할 수 있도록 효과적으로 재배치 할 수 있다는 것입니다.
마틴 보너 모니카 지원

3
@chux : as-if 규칙에 따라 컴파일러 재정렬에 대해 이야기하고 있기 때문에 UB와 같은 추가 문제는 발생하지 않습니다. 특정 컴파일러는 자체 오버플로 동작을 알고있는 이점을 활용할 수 있습니다.
MSalters

16

C, C ++ 및 Objective-C에는 "as if"규칙이 있습니다. 컴파일러는 적합한 프로그램이 차이를 알 수없는 한 원하는대로 수행 할 수 있습니다.

이러한 언어에서 a + b + c는 (a + b) + c와 동일하게 정의됩니다. 이것과 예를 들어 a + (b + c) 사이의 차이점을 알 수 있다면 컴파일러는 순서를 변경할 수 없습니다. 차이를 알 수 없다면 컴파일러는 순서를 자유롭게 변경할 수 있지만 차이를 알 수 없기 때문에 괜찮습니다.

귀하의 예에서 b = 64 비트, a 및 c 32 비트를 사용하면 컴파일러가 (b + a) + c 또는 (b + c) + a를 평가할 수 있습니다. 왜냐하면 차이점을 말할 수 없기 때문에 차이를 알 수 있기 때문에 (a + c) + b가 아닙니다.

즉, 컴파일러는 코드가해야하는 것과 다르게 동작하도록 만드는 작업을 수행 할 수 없습니다. 생성 할 것이라고 생각하거나 생성해야한다고 생각하는 코드를 생성 할 필요는 없지만 코드 정확한 결과를 제공합니다.


그러나 큰 경고가 있습니다. 컴파일러는 정의되지 않은 동작 (이 경우 오버플로)이 없다고 가정 할 수 있습니다. 이는 오버플로 검사 if (a + 1 < a)를 최적화 할 수있는 방법과 유사합니다 .
csiz

7
@csiz ... 서명 된 변수. 부호없는 변수는 잘 정의 된 오버플로 의미 체계 (랩 어라운드)를 갖습니다.
Gavin S. Yancey

7

표준 에서 인용 :

[참고 : 연산자는 실제로 연관성 또는 교환 성인 경우에만 일반적인 수학적 규칙에 따라 다시 그룹화 할 수 있습니다 .7 예를 들어 다음 단편에서 int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

표현식 문은 다음과 정확히 동일하게 작동합니다.

a = (((a + 32760) + b) + 5);

이러한 연산자의 연관성과 우선 순위 때문입니다. 따라서 합계 (a + 32760)의 결과가 다음에 b에 더 해지고 그 결과가 5에 더해져 값이 a에 할당됩니다. 오버플로로 인해 예외가 발생하고 int로 표현할 수있는 값의 범위가 [-32768, + 32767] 인 시스템에서 구현은이 식을 다음과 같이 다시 작성할 수 없습니다.

a = ((a + b) + 32765);

a와 b의 값이 각각 -32754와 -15이면 합계 a + b는 예외를 생성하지만 원래 표현식은 그렇지 않기 때문입니다. 식을 다음과 같이 다시 작성할 수도 없습니다.

a = ((a + 32765) + b);

또는

a = (a + (b + 32765));

a와 b의 값은 각각 4와 -8 또는 -17과 12 일 수 있기 때문입니다. 그러나 오버플로가 예외를 생성하지 않고 오버플로 결과를 되돌릴 수있는 시스템에서는 위의 식 문을 사용할 수 있습니다. 동일한 결과가 발생하기 때문에 위의 방법 중 하나로 구현에 의해 다시 작성됩니다. — 끝 참고]


4

컴파일러가 이러한 재정렬을 수행 할 수 있습니까? 아니면 결과 불일치를 인식하고 표현식 순서를 그대로 유지할 수 있다고 믿을 수 있습니까?

컴파일러는 동일한 결과를 제공하는 경우에만 재정렬 할 수 있습니다. 여기에서는 관찰했듯이 그렇지 않습니다.


원하는 경우 std::common_type추가 하기 전에 모든 인수를 승격하는 함수 템플릿을 작성할 수 있습니다. 이것은 안전하고 인수 순서 나 수동 캐스팅에 의존하지 않지만 꽤 투박합니다.


명시 적 캐스팅을 사용해야한다는 것을 알고 있지만 이러한 캐스팅이 실수로 생략되었을 때의 컴파일러 동작을 알고 싶습니다.
Tal

1
내가 말했듯이 명시 적 캐스팅없이 : 왼쪽 추가가 통합 프로모션없이 먼저 수행되므로 래핑이 적용됩니다. 그 추가 의 결과 ( 랩핑되었을 수 있음)는 가장 오른쪽 값에 추가하기 위해 승격됩니다 uint64_t.
쓸모없는

as-if 규칙에 대한 설명이 완전히 잘못되었습니다. 예를 들어 C 언어는 추상 기계에서 어떤 작업이 발생해야하는지 지정합니다. "as-if"규칙은 아무도 그 차이를 알 수없는 한 원하는 모든 것을 절대적으로 할 수 있도록합니다.
gnasher729

즉, 결과가 표시된 왼쪽 연관성 및 산술 변환 규칙에 의해 결정된 것과 동일하면 컴파일러가 원하는 모든 작업을 수행 할 수 있습니다.
쓸모없는

1

의 비트 폭에 따라 다릅니다 unsigned/int.

아래 2는 동일하지 않습니다 ( unsigned <= 32비트 일 때 ). u32_x + u32_y0이됩니다.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

그들은 동일합니다 ( unsigned >= 34비트 때 ). 정수 프로모션 발생 u32_x + u32_y 64 비트 수학에서 덧셈이 발생했습니다. 순서는 무관합니다.

UB ( unsigned == 33비트 때 )입니다. 정수 승격으로 인해 부호있는 33 비트 수학에서 덧셈이 발생했으며 부호있는 오버플로는 UB입니다.

컴파일러가 이러한 재정렬을 할 수 있습니까 ...?

(32 비트 수학) : 재주문 예, 그러나 동일한 결과가 발생해야하므로 재주문 OP가 제안 하는 것은 아닙니다 . 아래는 동일합니다

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... 그들이 결과 불일치를 인식하고 표현 순서를 그대로 유지할 수 있다고 믿을 수 있습니까?

예를 믿으십시오. 그러나 OP의 코딩 목표는 명확하지 않습니다. u32_x + u32_y캐리가 기여 해야합니까 ? OP가 그 기여를 원하면 코드는

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

하지만

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