부동 소수점 값을 비교하는 것이 얼마나 위험합니까?


391

해상도 독립 좌표계로 인해 UIKit사용하는 것을 알고 CGFloat있습니다.

그러나 때마다 나는 예를 들어 있는지 확인하고자 frame.origin.x한다 0그것이 나를 아프게 느낄 수 있습니다 :

if (theView.frame.origin.x == 0) {
    // do important operation
}

하지인가 CGFloat와 비교할 때 잘못된 반응에 취약 ==, <=, >=, <, >? 부동 소수점이며 0.0000000000041예를 들어 정밀도 문제가 있습니다 .

되어 Objective-C비교하거나이 일이 발생할 때 내부적으로 처리 origin.x0으로 읽는는 비교되지 않는 0사실로?

답변:


466

우선, 부동 소수점 값은 동작에서 "임의"가 아닙니다. 정확한 비교는 많은 실제 사용에서 의미가 있습니다. 그러나 부동 소수점을 사용하려면 작동 방식을 알고 있어야합니다. 부동 소수점이 실수처럼 작동한다고 가정하면 실수로 코드가 빠르게 중단됩니다. 부동 소수점 결과에 관련하여 큰 임의의 퍼즈가 있다고 가정하면 (여기에서 대부분의 대답이 제안하는 것처럼) 처음에는 작동하는 것처럼 보이지만 큰 크기의 오류가 발생하고 모서리가 부러진 코드가 생성됩니다.

우선, 부동 소수점으로 프로그래밍하려면 다음을 읽어야합니다.

부동 소수점 산술에 대해 모든 컴퓨터 과학자가 알아야 할 사항

네, 전부 읽어보세요. 그것이 너무 많은 부담이라면, 읽을 시간이 될 때까지 계산에 정수 / 고정 소수점을 사용해야합니다. :-)

이제는 정확한 부동 소수점 비교의 가장 큰 문제는 다음과 같습니다.

  1. 값이 많이 당신이 소스, 쓰기,로 읽을 수 있다는 사실 scanf하거나 strtod, 존재하지 않는 부동 소수점 값으로하고 자동으로 가장 가까운 근사치로 변환됩니다. 이것이 demon9733의 대답이었습니다.

  2. 실제 결과를 나타내기에 충분한 정밀도가 없기 때문에 많은 결과가 반올림됩니다. 이것을 볼 수있는 쉬운 예는 추가 x = 0x1fffffe하고 y = 1수레입니다. 여기 x에서 가수 (ok)에서 24 비트의 정밀도를 y가지며 1 비트 만 있습니다. 그러나 비트를 추가하면 해당 비트가 겹치는 위치에 있지 않으므로 결과에는 25 비트의 정밀도가 필요합니다. 대신 반올림됩니다 ( 0x2000000기본 반올림 모드에서).

  3. 정확한 값을 얻기 위해 무한히 많은 장소가 필요하기 때문에 많은 결과가 반올림된다는 사실. 여기에는 1/3과 같은 합리적인 결과가 포함됩니다 (무한한 많은 장소를 차지하는 10 진수에 익숙 함)뿐만 아니라 1/10 (5는 2의 거듭 제곱이 아니기 때문에 2 진수로 무한히 많은 장소를 차지합니다), 완벽한 제곱이 아닌 것의 제곱근과 같은 비이성적 인 결과뿐만 아니라.

  4. 이중 반올림. 일부 시스템 (특히 x86)에서 부동 소수점 표현식은 공칭 유형보다 높은 정밀도로 평가됩니다. 즉, 위의 반올림 유형 중 하나가 발생하면 두 가지 반올림 단계가 시작됩니다. 먼저 결과를 고정밀 유형으로 반올림 한 다음 최종 유형으로 반올림합니다. 예를 들어, 1.49를 정수 (1)로 반올림하면 10 진수로 발생하는 것과, 소수점 이하 1 자리 (1.5)로 반올림 한 후 결과를 정수 (2)로 반올림하면 어떻게되는지를 고려하십시오. 이는 컴파일러의 동작 (특히 GCC와 같은 버그가있는 비준수 컴파일러의 경우)을 예측할 수 없기 때문에 실제로 부동 소수점에서 처리 할 수있는 가장 단순한 영역 중 하나입니다.

  5. 초월 함수 ( trig, exp, log등)의 결과 정확하게 원형을 갖도록 지정되지; 결과는 마지막 정밀도 (보통 1ulp ) 에서 한 단위 내에서 정확하도록 지정되었습니다 .

부동 소수점 코드를 작성할 때 결과가 정확하지 않은 숫자로 무엇을하고 있는지를 염두에두고 그에 따라 비교해야합니다. 종종 "엡실론"과 비교하는 것이 합리적이지만, 엡실론은 절대 상수가 아니라 비교하는 숫자크기를 기반으로해야합니다 . (절대 상수 엡실론이 작동하는 경우 부동 소수점이 아닌 고정 소수점이 작업에 적합한 도구임을 강력하게 나타냅니다!)

편집 : 특히 크기 기준 엡실론 검사는 다음과 같아야합니다.

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

어디 FLT_EPSILON에서 일정 float.h(로 교체 DBL_EPSILON를위한 doubles 또는 LDBL_EPSILON위해 long double들) 및 K당신이 당신의 계산의 누적 오류가 확실히에 의해 제한되도록 선택 상수 K마지막 장소의 단위는 (확실하지 않은 경우에는 오류가 발생했습니다 바운드 계산, K계산이 말한 것보다 몇 배 더 큼).

마지막으로,이 기능을 사용하면 FLT_EPSILON비정규에는 적합하지 않기 때문에 0 근처에서 특별한주의가 필요할 수 있습니다 . 빠른 수정은 다음과 같습니다.

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

DBL_MIN복식을 사용하는 경우에도 마찬가지 입니다.


25
fabs(x+y)( xy) 다른 부호를 가질 수 있다면 문제가됩니다 . 여전히화물 컬트 비교의 조류에 대한 좋은 대답.
Daniel Fischer

27
경우 xy다른 기호를 가지고, 그것은 문제가 아니다. 오른쪽 측면 "너무 작은"수 있지만 이후 xy다른 기호를 가지고, 그들은 동일한 어쨌든 비교해서는 안된다. (비정상적 일 정도로 작지 않은 경우에는 두 번째 경우에 따라 잡을 수 있습니다.)
R .. GitHub 중지 지원 얼음

4
"특히 GCC와 같은 버그가 있고 부적합한 컴파일러"에 대한 귀하의 진술이 궁금합니다. 실제로 GCC는 버그가 있고 적합하지 않습니까?
Nicolás Ozimica

3
이 질문에는 iOS 태그가 붙어 있기 때문에 Apple의 컴파일러 (clang 및 Apple의 gcc 빌드 모두)는 항상 FLT_EVAL_METHOD = 0을 사용했으며 과도한 정밀도를 전달하지 않는 것에 대해 완전히 엄격하게 시도한다는 점에 주목할 가치가 있습니다. 위반 사항이 발견되면 버그 보고서를 제출하십시오.
Stephen Canon

17
"먼저 부동 소수점 값은 동작에서"임의 "가 아닙니다. 정확한 비교는 많은 실제 사용에서 의미가 있습니다." -단 두 문장으로 이미 +1을 받았습니다! 부동 소수점으로 작업 할 때 사람들이 가장 혼란스럽게 생각하는 오해 중 하나입니다.
Christian Rau

36

0은 IEEE754 부동 소수점 숫자로 정확하게 표현 가능하기 때문에 (또는 내가 작업했던 다른 fp 숫자 구현을 사용하여) 0과 비교하는 것이 안전 할 것입니다. 그러나 프로그램에서과 같은 값 (예 :)을 계산 theView.frame.origin.x해야한다고 생각할만한 가치 가 있지만 계산에서 0을 보장 할 수없는 경우 물릴 수 있습니다 .

조금 명확히하기 위해 다음과 같은 계산이 필요합니다.

areal = 0.0

(언어 나 시스템이 고장 나지 않는 한) (areal == 0.0)이 true를 반환하지만

areal = 1.386 - 2.1*(0.66)

아닐 수도 있습니다.

계산에서 0이어야하는 값을 생성 할뿐 아니라 0이어야하는 값을 생성 할뿐 아니라 fp 값을 0과 비교할 수 있습니다. 필요한 정도로 확신 할 수없는 경우 , '공차 평등'의 일반적인 접근 방식을 따르는 것이 가장 좋습니다.

최악의 경우 fp 값을 부주의하게 비교하는 것은 매우 위험 할 수 있습니다. 항공 전자 공학, 무기 안내, 발전소 운영, 차량 내비게이션, 계산이 실제 세계를 충족하는 거의 모든 응용 프로그램을 생각하십시오.

앵그리 버드에게는 그렇게 위험하지 않습니다.


11
사실, 1.30 - 2*(0.65)복식이로 표현 때문에 컴파일러 구현이 754 IEEE 경우 분명히 0.0으로 평가하는 식의 완벽한 예이다 0.651.30같은 significands를 가지고 있고, 두 의한 곱셈 정확한 분명하다.
Pascal Cuoq

7
여전히이 답변을 받고 있으므로 두 번째 예제 스 니펫을 변경했습니다.
High Performance Mark

22

나는 다른 사람들과 약간 다른 대답을하고 싶습니다. 그들은 언급 한대로 귀하의 질문에 대답하기에는 훌륭하지만 아마도 당신이 알아야 할 것이거나 실제 문제가 무엇인지는 아닙니다.

그래픽의 부동 소수점은 괜찮습니다! 그러나 플로트를 직접 비교할 필요는 거의 없습니다. 왜 그렇게해야합니까? 그래픽은 플로트를 사용하여 간격을 정의합니다. 또한 float가 간격으로 정의되는지 여부도 float로 정의하면 항상 잘 정의되며 정확하거나 정확하지 않고 일관성이 있어야합니다! 모든 그래픽이 필요한 픽셀 (간격이기도합니다!)을 할당 할 수있는 한.

따라서 포인트가 [0..width [범위] 밖에 있는지 테스트하려면 괜찮습니다. 포함을 일관되게 정의해야합니다. 예를 들어 항상 내부는 (x> = 0 && x <width)로 정의하십시오. 교차점 또는 적중 테스트도 마찬가지입니다.

그러나 그래픽 좌표를 일종의 플래그로 남용하는 경우 (예를 들어, 윈도우가 도킹되었는지 여부를 확인하는 경우)이를 수행해서는 안됩니다. 대신 그래픽 표현 레이어와 분리 된 부울 플래그를 사용하십시오.


13

0이 계산 된 값이 아닌 한 (위의 답변에서 언급 한대로) 0과 비교 하는 것이 안전한 작업 일 있습니다. 그 이유는 0은 부동 소수점에서 완벽하게 표현 가능한 숫자이기 때문입니다.

완벽하게 표현 가능한 값을 말하면 2의 제곱 개념 (단일 정밀도)으로 24 비트의 범위를 얻을 수 있습니다. 따라서 1, 2, 4는 .5, .25 및 .125와 같이 완벽하게 표현할 수 있습니다. 모든 중요한 비트가 24 비트 인 한 황금색입니다. 따라서 10.625는 정확하게 회답 ​​될 수 있습니다.

이것은 훌륭하지만 압력을 받으면 빨리 떨어집니다. 두 가지 시나리오가 떠 오릅니다. 1) 계산이 수행되는 경우. sqrt (3) * sqrt (3) == 3임을 믿지 마십시오. 그것은 그렇게되지 않을 것입니다. 그리고 다른 답변 중 일부가 제안하는 것처럼 아마도 엡실론 내에 있지 않을 것입니다. 2) 2의 비 전력 (NPOT)이 관련된 경우. 따라서 이상하게 들릴지 모르지만 0.1은 이진수로 무한한 시리즈이므로 이와 같은 숫자를 사용하는 계산은 처음부터 정확하지 않습니다.

(Oh와 원래 질문은 0에 대한 비교를 언급했습니다. -0.0도 완벽하게 유효한 부동 소수점 값이라는 것을 잊지 마십시오.)


11

[ '올바른 대답'은 선택에 대한 광택 K입니다. 선택 K은 선택 하는 것과 마찬가지로 임시 VISIBLE_SHIFT이지만 선택 KVISIBLE_SHIFT디스플레이 속성에 기반하지 않기 때문에 덜 명확 합니다. 따라서 독을 선택 K하거나 선택하십시오 VISIBLE_SHIFT. 이 답변은 선택을 옹호 VISIBLE_SHIFT한 다음 선택 이 어렵다는 것을 보여줍니다. K]

정확하게 라운드 오류로 인해 논리 연산에 대해 '정확한'값 비교를 사용해서는 안됩니다. 시각적 디스플레이의 특정 위치의 경우 위치가 0.0 또는 0.0000000003인지 여부는 중요하지 않습니다. 차이는 눈에 보이지 않습니다. 따라서 논리는 다음과 같아야합니다.

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

그러나 결국 '눈에 보이지 않음'은 디스플레이 속성에 따라 다릅니다. 디스플레이를 상한으로 할 수 있다면 (가능해야합니다); 그런 다음 VISIBLE_SHIFT상한의 일부가되도록 선택하십시오 .

이제 '올바른 대답'을 바탕으로 K피킹을 살펴 보겠습니다 K. 위의 '정답'은 다음과 같이 말합니다.

K는 계산의 누적 오차가 마지막에 K 단위로 확실히 묶 이도록 선택하는 상수입니다 (그리고 오차 한계 계산이 확실하지 않은 경우 K를 계산보다 몇 배 크게 만듭니다. 해야한다고 말해)

그래서 우리는 필요합니다 K. 지고는 경우 K나의 선택보다 직관적 인 더 어렵 VISIBLE_SHIFT당신이 당신을 위해 작동을 결정할 수 있습니다. 이를 찾기 K위해 여러 가지 K값을 확인하는 테스트 프로그램을 작성하여 작동 방식을 확인할 수 있습니다. K'올바른 대답'을 사용할 수 있다면 을 선택하는 방법을 분명히해야합니다 . 아니?

우리는 '올바른 대답'세부 사항으로 사용할 것입니다.

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

K의 모든 값을 시도해 봅시다.

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

아, 1e-13을 '0'으로 설정하려면 K가 1e16 이상이어야합니다.

따라서 두 가지 옵션이 있습니다.

  1. 내가 제안한 것처럼 '엡실론'의 가치에 대한 엔지니어링 판단 을 사용하여 간단한 엡실론 계산을 수행하십시오 . 그래픽을하고 있고 '제로'는 시각적 자산 (이미지 등)을 검사하고 엡실론이 무엇인지 판단하는 것보다 '눈에 띄는 변화'를 의미합니다.
  2. 비화물 숭배 답변의 참조를 읽고 프로세스에서 박사 학위를받을 때까지 부동 소수점 계산을 시도하지 말고 직관적이지 않은 판단을 사용하여를 선택하십시오 K.

10
해상도 독립성의 한 측면은 컴파일 타임에 "가시적 시프트"가 무엇인지 확실히 알 수 없다는 것입니다. 슈퍼 HD 화면에서는 보이지 않는 것이 작은 화면에서는 분명 할 수 있습니다. 적어도 화면 크기의 기능으로 만들어야합니다. 또는 다른 이름을 지정하십시오.
Romain

1
그러나 '가시적 이동'을 선택 K하는 것은 선택하기 어렵고 직관적이지 않은 <정답>과 달리 쉽게 이해되는 디스플레이 (또는 프레임) 속성을 기반으로 합니다.
GoZoner

5

정답은 : Cocoa Touch에서 포인트를 어떻게 비교합니까?

정답 : CGPointEqualToPoint ().

다른 질문 : 두 개의 계산 된 값이 동일합니까?

대답은 여기에 게시되었습니다.

그들이 가까이 있는지 확인하는 방법? 그들이 가까운 지 확인하려면 CGPointEqualToPoint ()를 사용하지 마십시오. 그러나 그들이 가까이 있는지 확인하지 마십시오. 점이 선을 넘어서 있는지 또는 점이 구 안에 있는지 확인하는 것과 같이 현실 세계에서 의미있는 것을 수행하십시오.


4

C 표준을 마지막으로 확인했을 때 두 배의 부동 소수점 연산 (총 64 비트, 53 비트 가수)이 정밀도보다 정확해야 할 필요는 없었습니다. 그러나 일부 하드웨어는 레지스터에서 더 정밀한 연산을 수행 할 수 있으며, 요구 사항은 레지스터에로드되는 숫자의 정밀도를 넘어 하위 비트를 지우는 요구 사항을 의미하지 않는 것으로 해석되었습니다. 따라서 마지막으로 잠든 사람의 레지스터에 남은 내용에 따라 이와 같은 비교 결과가 예기치 않게 나타날 수 있습니다.

즉, 내가 볼 때마다 그것을 제거하려는 노력에도 불구하고, 내가 일하는 복장에는 gcc를 사용하여 컴파일되어 Linux에서 실행되는 많은 C 코드가 있으며, 우리는 이러한 예기치 않은 결과를 오랫동안 발견하지 못했습니다. . 나는 이것이 gcc가 우리를 위해 하위 비트를 지우고 있는지, 80 비트 레지스터가 현대 컴퓨터에서 이러한 작업에 사용되지 않거나, 표준이 변경되었는지, 또는 무엇인지 알지 못합니다. 누구나 챕터와 구절을 인용 할 수 있는지 알고 싶습니다.


1

float와 0을 비교하기 위해 이러한 코드를 사용할 수 있습니다.

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

이 경우 0.1 정확도와 비교하여 CGFloat에 충분합니다.


int보증없이 캐스팅 theView.frame.origin.x하는 경우 범위 int가 정의되지 않은 동작 (UB) 으로 이어지는 범위 내에 있거나 근처에 있습니다 (이 경우 범위의 1/100) int.
chux-복원 모니카

이처럼 정수로 변환 할 이유가 없습니다. chux가 말했듯이 범위를 벗어난 값에서 UB의 가능성이 있습니다. 일부 아키텍처에서는 부동 소수점에서 계산하는 것보다 속도가 상당히 느립니다. 마지막으로 100을 곱하면 0.1이 아닌 0.01 정밀도와 비교됩니다.
Sneftel

0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}


0

소수점 이하 자릿수를 비교하기 위해 다음 비교 기능을 사용하고 있습니다.

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

-6

올바른 것은 각 숫자를 객체로 선언 한 다음 해당 객체에 세 가지를 정의하는 것입니다. 1) 항등 연산자. 2) setAcceptableDifference 메소드. 3) 가치 자체. 두 값의 절대 차이가 허용 가능한 값으로 설정된 값보다 작 으면 항등 연산자는 true를 반환합니다.

문제에 맞게 객체를 서브 클래 싱 할 수 있습니다. 예를 들어, 1 인치와 2 인치 사이의 둥근 금속 막대는 직경이 0.0001 인치보다 작은 경우 동일한 직경으로 간주 될 수 있습니다. 따라서 매개 변수 0.0001로 setAcceptableDifference를 호출 한 다음 등식 연산자를 자신있게 사용합니다.


1
이것은 좋은 대답이 아닙니다. 먼저, "객체"전체가 문제를 해결하는 데 도움이되지 않습니다. 둘째, "평등"의 실제 구현은 실제로 올바른 것이 아닙니다.
Tom Swirly

3
톰, 아마도 "객체"에 대해 다시 생각할 것입니다. 높은 정밀도로 표현 된 실수로 평등은 거의 발생하지 않습니다. 그러나 평등에 대한 아이디어 는 자신에게 적합하다면 조정될 수 있습니다. 무시할 수있는 '대략 동일한'연산자가 있다면 더 좋을 것입니다.
John White
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.