이 부동 소수점 최적화가 허용됩니까?


90

나는 float큰 정수를 정확하게 표현하는 능력을 잃는 곳을 확인하려고 노력했습니다 . 그래서이 작은 조각을 썼습니다.

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

이 코드는 clang을 제외한 모든 컴파일러에서 작동하는 것 같습니다. Clang은 간단한 무한 루프를 생성합니다. Godbolt .

허용됩니까? 그렇다면 QoI 문제입니까?


@geza 결과 번호를 듣고 싶습니다!
나다

5
gcc-Ofast대신 컴파일하는 경우 동일한 무한 루프 최적화를 수행 하므로 최적화 gcc가 안전하지 않은 것으로 간주되지만 할 수 있습니다.
12345ieee

3
g ++는 또한 무한 루프를 생성하지만 내부에서 작업을 최적화하지는 않습니다. 당신은 그것이 그 자체와 ucomiss xmm0,xmm0비교 (float)i하는 것을 볼 수 있습니다 . 그것이 당신의 C ++ 소스가 당신이 생각한 바를 의미하지 않는다는 첫 단서였습니다. 이 루프를 인쇄 / 반환 할 수 있다고 주장 16777216하십니까? 어떤 컴파일러 / 버전 / 옵션이 있었습니까? 컴파일러 버그이기 때문입니다. gcc jnp는 루프 분기 ( godbolt.org/z/XJYWeu ) 로 코드를 올바르게 최적화합니다 . 피연산자 != 가 NaN이 아닌 한 계속 루프를 수행합니다 .
Peter Cordes

4
특히, GCC가 안전하지 않은 부동 소수점 최적화를 적용하여 Clang과 동일한 코드를 생성 할 수 있도록 -ffast-math하여 암시 적으로 활성화 된 옵션입니다 -Ofast. MSVC는 똑같은 방식으로 작동합니다.를 사용하지 않으면 /fp:fast무한 루프를 생성하는 코드 묶음이 생성됩니다. 를 사용 /fp:fast하면 단일 jmp명령을 내 보냅니다. 안전하지 않은 FP 최적화를 명시 적으로 설정하지 않으면 이러한 컴파일러가 NaN 값에 관한 IEEE 754 요구 사항에 걸려 넘어 진다고 가정합니다. Clang이 실제로하지 않는 것은 오히려 흥미 롭습니다. 정적 분석기가 더 좋습니다. @ 12345ieee
코디 회색

1
@geza : 코드가 의도 한대로 수행 한 경우의 수학적 값이의 수학적 값과 (float) i다른 i경우 결과 ( return문에 반환 된 값 )는 16,777,216이 아니라 16,777,217이됩니다.
에릭 Postpischil

답변:


49

@Angew는 지적!=연산자는 양쪽에 동일한 유형을 필요로한다. (float)i != i결과적으로 RHS가 떠 다니는 것을 촉진하므로 (float)i != (float)i.


g ++는 또한 무한 루프를 생성하지만 내부에서 작업을 최적화하지는 않습니다. 당신은 함께 INT-> 부동 소수점 변환 볼 수 있습니다 cvtsi2ss및 수행 ucomiss xmm0,xmm0비교 (float)i자체. (이것은 C ++ 소스가 @Angew의 대답이 설명하는 것처럼 생각한 것을 의미하지 않는다는 첫 번째 단서였습니다.)

x != xxNaN 이기 때문에 "정렬되지 않은"경우에만 true 입니다. ( INFINITYIEEE 수학에서 자신과 동일하지만 NaN은 그렇지 않습니다. NAN == NAN거짓, NAN != NAN참).

gcc7.4 및 이전 버전 jnp은 루프 분기 ( https://godbolt.org/z/fyOhW1 ) 로 코드를 올바르게 최적화합니다 . 피연산자 x != x 가 NaN이 아닌 한 계속 루프를 수행합니다 . (gcc8 이상은 또한 je루프의 브레이크 아웃을 확인 하여 NaN이 아닌 입력에 대해 항상 참이라는 사실을 기반으로 최적화에 실패합니다.) x86 FP는 순서가 지정되지 않은 상태에서 세트 PF를 비교합니다.


그리고 BTW는 clang의 최적화도 안전함 을 의미 합니다 . 단지 (float)i != (implicit conversion to float)i동일한 것으로 CSE 해야 i -> float하며 가능한 범위에서 NaN이 아님을 증명해야합니다 int.

(이 루프가 signed-overflow UB에 도달한다는 점을 감안할 때, ud2불법 명령을 포함하여 문자 그대로 원하는 asm을 방출 하거나 루프 본문이 실제로 무엇인지에 관계없이 빈 무한 루프 를 방출 할 수 있습니다.) 그러나 signed-overflow UB를 무시합니다. ,이 최적화는 여전히 100 % 합법적입니다.


GCC 는 부호있는 정수 오버플로를 잘 정의 하더라도-fwrapv 루프 본문을 최적화하지 못합니다 (2의 보수 순환으로 ). https://godbolt.org/z/t9A8t_

활성화 -fno-trapping-math해도 도움이되지 않습니다. (GCC의 기본은 불행하게도 활성화
-ftrapping-math에도 불구하고 그것의 GCC의 구현 / 버그를 파괴한다 .) 예외 가능성에 합리적인 아니에요 폭로와 INT-> 부동 소수점 변환이 때문에, (정확히 표현하기에 너무 큰 숫자) 예외 부정확 한 FP를 일으킬 수 있습니다 루프 본체를 최적화하십시오. ( 16777217부정확 한 예외가 마스크 해제되면 float 로 변환 하면 관찰 가능한 부작용이 발생할 수 있기 때문 입니다.)

그러나 -O3 -fwrapv -fno-trapping-math를 사용하면이를 빈 무한 루프로 컴파일하지 않는 것이 100 % 최적화를 놓쳤습니다. 가 없으면 #pragma STDC FENV_ACCESS ON마스킹 된 FP 예외를 기록하는 고정 플래그의 상태는 코드의 관찰 가능한 부작용이 아닙니다. 아니오 int-> float변환은 NaN을 초래할 x != x수 있으므로 사실 일 수 없습니다.


이러한 컴파일러는 모두 IEEE 754 단 정밀도 (binary32) float및 32 비트 를 사용하는 C ++ 구현에 최적화되어 int있습니다.

버그가 잡힌(int)(float)i != i 루프는 좁은 16 비트와 C ++ 구현에 UB있을 것입니다 int및 / 또는 넓은 float당신이 때렸어 때문에, 서명 정수 오버 플로우 UB A와 정확하게 표현할 수없는했던 첫 번째 정수에 도달하기 전에 float.

그러나 다른 구현 정의 선택 세트 아래의 UB는 x86-64 System V ABI로 gcc 또는 clang과 같은 구현을 위해 컴파일 할 때 부정적인 결과를 초래하지 않습니다.


BTW, 에서 정의 된 FLT_RADIXand 에서이 루프의 결과를 정적으로 계산할 수 있습니다 . 또는 적어도 이론적으로 는 Posit / unum과 같은 다른 종류의 실수 표현보다는 IEEE 플로트 모델에 실제로 적합 하다면 할 수 있습니다 .FLT_MANT_DIG<climits>float

ISO C ++ 표준이 float동작 에 대해 얼마나 잘하고 있는지, 고정 너비 지수 및 유효 필드를 기반으로하지 않은 형식이 표준을 준수하는지 여부는 확실하지 않습니다.


댓글에서 :

@geza 결과 번호를 듣고 싶습니다!

@nada : 16777216입니다

이 루프를 인쇄 / 반환 할 수 있다고 주장 16777216하십니까?

업데이트 : 해당 댓글이 삭제되었으므로 삭제되지 않은 것 같습니다. 아마도 OP는 float32 비트로 정확하게 표현할 수없는 첫 번째 정수 앞의 인용 부호 일 것 float입니다. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values 즉,이 버그가있는 코드로 확인하려는 내용.

버그 수정 버전은 물론 16777217그 이전의 값보다는 정확하게 표현할 수 없는 첫 번째 정수인 print 합니다.

(높은 부동 소수점 값은 모두 정확한 정수이지만 유효 폭보다 높은 지수 값에 대해 2, 4, 8 등의 배수입니다. 더 높은 정수 값이 많이 표시 될 수 있지만 마지막 자리에는 1 단위가 있습니다. (유효 값의)는 1보다 크므로 연속 된 정수가 아닙니다. 가장 큰 유한 float은 2 ^ 128 바로 아래이며 짝수에 비해 너무 큽니다 int64_t.)

컴파일러가 원래 루프를 종료하고 인쇄하면 컴파일러 버그입니다.


3
@SombreroChicken : 아니요, 먼저 전자 공학을 배웠습니다 (아빠가 누워 있던 일부 교과서에서 그는 물리학 교수였습니다), 그다음에는 디지털 로직을 배웠고 그 후 CPU / 소프트웨어에 들어갔습니다. : P 저는 항상 처음부터 사물을 이해하는 것을 좋아했습니다. 또는 더 높은 수준에서 시작하면 수준에서 사물이 작동하는 방식 / 왜에 영향을 미치는 수준 아래에 대해 적어도 무언가를 배우고 싶습니다. 에 대해 생각. (.. 예를 들어, 방법은 ASM의 작품이 차례로 물리 + 수학에서 오는 CPU 아키텍처 물건 어떤 CPU의 설계 제약 / 의해 영향을 최적화하는 방법)
피터 코르

1
GCC는를 사용해도 최적화 할 수 frapw없지만 GCC 10 -ffinite-loops은 이와 같은 상황을 위해 설계 되었다고 확신합니다 .
MCCCS

64

내장 연산자 !=는 피연산자가 동일한 유형이어야하며 필요한 경우 승격 및 변환을 사용하여이를 달성합니다. 즉, 조건은 다음과 같습니다.

(float)i != (float)i

절대 실패해서는 안되며 결국 코드가 오버플 i로되어 프로그램에 Undefined Behaviour를 제공합니다. 따라서 모든 동작이 가능합니다.

확인하려는 항목을 올바르게 확인하려면 결과를 int다음으로 다시 캐스팅해야합니다 .

if ((int)(float)i != i)

8
@ Džuris UB입니다. 이 없다 아무도 명확한 결과는. 컴파일러는 UB로만 끝날 수 있음을 인식하고 루프를 완전히 제거하기로 결정할 수 있습니다.
Fund Monica의 소송

4
@opa 의미 static_cast<int>(static_cast<float>(i))합니까? reinterpret_cast거기에 명백한 UB
Caleth

6
@NicHartley : (int)(float)i != iUB라는 건가요? 어떻게 결론을 내릴까요? 예, 구현 정의 속성 에 따라 다르지만 ( floatIEEE754 binary32 일 필요가 없기 때문에 ) 주어진 구현에서 float모든 양의 int값을 정확하게 표현할 수 없는 한 잘 정의 되어 있으므로 부호있는 정수 오버플로 UB를 얻습니다. ( en.cppreference.com/w/cpp/types/climits이것을 정의 FLT_RADIX하고 FLT_MANT_DIG결정합니다). 일반 인쇄 구현 정의 것들에서처럼 std::cout << sizeof(int)UB ... 아니다
피터 코르

2
@Caleth : reinterpret_cast<int>(float)정확히 UB가 아니라 구문 오류 / 잘못된 형식입니다. 그 구문 이 (잘 정의 된) int대체 방법으로 float의 유형 -punning을 허용한다면 좋을 것 memcpy같지만 reinterpret_cast<>포인터 유형에서만 작동한다고 생각합니다.
Peter Cordes

2
@Peter Just for NaN x != x은 사실입니다. coliru에서 라이브를 참조하십시오 . C에서도.
Deduplicator
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.