GCC는 0일 때도 x/x를 1로 최적화: 정의되지 않은 동작과 상수 접기 이해하기
개발자가 x 값을 읽고 x / x를 출력하는 C++ 프로그램을 작성했을 때, x = 0이면 나누기 0 오류로 프로그램이 충돌할 것으로 예상했다. 그러나 GCC는 예외 없이 조용히 1을 출력했다. 이는 상수 접기(정적 최적화) 때문인데, 컴파일러가 x / x를 고정된 값 1로 변환해 런타임에서의 x 값은 무시한다.
예제 코드 (실행 확인용)
#include <iostream>
int main()
{
int x;
std::cin >> x;
std::cout << x / x << std::endl;
}
x = 0인 경우, SIGFPE나 예외가 발생하지 않고 프로그램은 조용히 1을 출력한다. -O0 옵션을 사용해도 이 현상은 바뀌지 않으며, 최저 수준의 최적화에서도 상수 접기가 발생한다.
GCC에서 상수 접기 작동 방식
컴파일러는 대수적 항등식 x / x = 1을 x ≠ 0인 경우에 적용한다. 생성된 머신 코드에는 실제로 나눗셈 명령어가 전혀 없고, 단지 1을 로드하는 mov 명령어만 존재한다. 이는 일반적인 최적화지만, x가 0일 가능성에 대한 검사를 하지 않는다는 점에서 문제다.
GCC는 C++ 표준에 따라 나누기 0이 정의되지 않은 동작(UB, Undefined Behavior)이라고 판단한다. 표준이 결과를 명시하지 않았기 때문에, 컴파일러는 x ≠ 0이라는 가정을 하고 표현식을 접기한다.
컴파일러 비교
- GCC/Clang:
x / x를1로 접기, 실제 나눗셈 명령어 생성 안 함. - MSVC: 실제 나눗셈 명령어 생성,
x = 0시 예외 발생.
두 방식 모두 C++ 표준에 부합하며, 정의되지 않은 동작은 컴파일러에게 큰 자유를 부여한다.
수학적 함정: 잘못된 단순화
x / x를 1로 단순화하는 것은 x ≠ 0일 때만 수학적으로 타당하다. 컴파일러는 이 규칙을 무시하며, 고전적인 오류를 반복한다. 바로 1 = 2를 '증명'하려는 논리적 오류다:
Let a = b
Then a² = ab
...
(a-b)(a+b) = b(a-b) ← (a-b) = 0으로 나누는 순간, 숨겨진 오류
...
2 = 1
여기서 (a-b)를 암묵적으로 약분하면서 0인지 확인하지 않는다. GCC도 마찬가지로, 정의되지 않은 동작을 이유로 x ≠ 0이라고 가정하지만, 이 가정은 안전하지 않다.
정의되지 않은 동작이 컴파일러에게 허락하는 것들
정의되지 않은 동작이 존재하면 컴파일러는 다음과 같은 최적화를 할 수 있다:
- 나눗셈 연산 생성 (하드웨어 예외 유발)
- 전체 코드 블록 제거 (불가능한 경로로 간주)
- 표현식을 상수로 교체
- 해당 분기 무시
핵심 원칙: 정의되지 않은 동작은 발생하지 않는다. 컴파일러는 어떤 정의되지 않은 동작도 발생하지 않을 것이라 가정하고 최적화를 수행한다. 성능은 높아지지만, 예측 가능성은 떨어진다.
GCC가 0에서 충돌하게 강제하는 방법
volatile 키워드를 사용하면 컴파일러가 가정을 하지 못하게 된다:
#include <iostream>
int main()
{
int tmp;
std::cin >> tmp;
volatile int x = tmp;
std::cout << x / x << std::endl;
}
volatile은 상수 접기를 차단한다. 컴파일러는 이제 실제 나눗셈 코드를 생성하므로, x = 0일 때 예상대로 SIGFPE가 발생한다.
핵심 요약
- GCC는
x/x = 1을 상수 접기로 처리하여, 런타임에x가 0이어도1을 출력한다. 이는 정의되지 않은 동작 덕분이다. volatile을 사용하면 최적화를 막고 예측 가능한 충돌을 보장한다.- C++ 표준은 정의되지 않은 동작에 대해 컴파일러에게 넓은 선택권을 부여한다. MSVC의 접근법은 더 수학적으로 일관성 있다.
- 다양한 컴파일러에서 에지 케이스를 반드시 테스트해야 한다. 최적화는 정의되지 않은 동작을 감추기 쉽다.
- 정의되지 않은 동작은 버그가 아니라, 강력한 변환을 허용하는 라이선스다.
개발에 미치는 영향
이 현상은 정의되지 않은 동작의 위험성을 잘 보여준다. 컴파일러는 오류를 경고하지 않고, 의미를 조용히 변경한다. 중급 이상 개발자라면, 0이 등장할 수 있는 표현식에 주의해야 한다:
- 일반 코드에서는
x/x를 피하라. - 여러 컴파일러에서 에지 케이스로 테스트하라.
- 신뢰할 수 있는 동작을 위해
volatile이나 명시적if (x != 0)체크를 사용하라.
C++ 표준은 성능과 안전성 사이에서 정의되지 않은 동작을 통해 균형을 잡는다. 중요한 논쟁점은: 나누기 0을 정의된 동작(예: 트랩)으로 만들면 좋지 않겠는가?
— Editorial Team
아직 댓글이 없습니다.