14. Floating Point

Floating point(실수) 계산은 때때로 문제가 되며, 신비스럽게 보이기도 합니다. 게다가 C 언어는 원래 floating point를 주로 쓸 목적이 아니었기 때문에 더욱 문제가 됩니다.



Q 14.1
float 타입의 변수에 3.1을 넣고 printf로 출력해보니 3.09999999가 나옵니다. 왜 그럴까요?
Answer
대부분의 컴퓨터는 실수를 표현할 때, 내부적으로 2 진수로 기록합니다. 2 진수에서는 0.1은 무리수로 (0.0001100110011...) 표현됩니다. 따라서 3.1과 같은 수는 (10 진수로 볼 때에는 유리수이지만) 2 진수에서는 유한한 수치로 표현될 수 없습니다. 여러분의 시스템에서 10 진수를 2 진수로 변환하는 루틴이 어떻게 작성되었느냐에 따라 다르겠지만, (특히 정밀도가 낮은 float을 쓸 경우) 대입한 수치와 출력 결과가 약간 다를 수 있습니다. 덧붙여 질문 [*]14.6도 참고하시기 바랍니다.



Q 14.2
제곱근을 (square root) 구하려 하는데, 이상한 값만 나옵니다.
Answer
먼저 <math.h>를 포함시켰는지 검사해 보고, 함수들이 double을 리턴하도록 선언되었는지 체크해보기 바랍니다. (atof() 함수는 <stdlib.h>에 선언되어 있음을 주의하기 바랍니다.) 덧붙여 질문 [*]14.3도 참고하시기 바랍니다.

References
[CT&P] § 4.5 pp. 65-6



Q 14.3
삼각 함수를 쓰려고 하는데 <math.h>를 포함시켰는데도 컴파일러는 “undefined: sin”이라는 컴파일 에러를 출력합니다.
Answer
수학 라이브러리를 포함시켰는지 체크하기 바랍니다. 예를 들어 UNIX 시스템에서는 `-lm' 옵션을 컴파일할 때 마지막 인자로 주어야 합니다. 덧붙여 질문 [*]13.25, [*]13.26, [*]14.2도 참고하시기 바랍니다.



Q 14.4
제가 만든 실수 계산 루틴은 매우 이상한 결과를 출력하고, 또 컴퓨터가 다르면 다른 결과가 나옵니다.
Answer
먼저 질문 [*]14.2를 읽어보기 바랍니다.

문제가 간단하지 않겠지만, 디지털 컴퓨터에서 실수 처리는 정확하지 않을 수도 있다는 것을 알아두셔야 합니다. 언더플로우(underflow)가 일어날 수도 있으며, 오차가 점점 누적될 수도 있습니다.

따라서 실수 연산이 정확하지 않다는 것을 기억하기 바라며, 같은 이유로 두 실수가 같은지 비교하는 것은 좋지 않습니다. (Don't throw haphazard “fuzz factors” in, either; 질문 [*]14.5를 참고하기 바랍니다.)

이런 문제는 꼭 C 언어에만 국한된 것이 아니고, 모든 프로그래밍 언어에서 발생할 수 있습니다. 실수에 대한 어떤 부분들은 대개 “프로세서가 어떻게 처리하느냐”에 따라 다릅니다 (덧붙여 질문 [*]11.34도 참고하시기 바랍니다.), 그렇지 않는 경우라면 적절한 기능을 수행할 수 있게 하기 위해 댓가가 큰 에뮬레이션을 사용합니다.

안타깝게도 이 문서는 이런 실수 연산에 대한 단점이나 해결책을 위한 것이 아닙니다. 좋은 수치 프로그래밍에 (numerical programming) 대한 책들이 여러분을 도와줄 겁니다. 아래 참고 문헌에 나온 책들을 보시면 좋습니다.

References
[1] § 6 pp. 115-8
[Knuth] Vol. 2 Chap. 4
[Goldberg]



Q 14.5
그럼 “아주 가까운” 두 실수를 비교하는 방법이 있을까요?
Answer
실수의 절대값이 정의에 의하면 `magnitude'에 따라 달라질 수 있으므로, 두 실수를 비교하려면 다음과 같이, 오차가 상대적으로 무시할 수 있는 작은 값인지를 검사하는 것이 좋습니다. 즉 다음과 같이 할 것이 아니라:

  double a, b;
  ...
  if (a == b)	/* WRONG */

이렇게 합니다:

  #include <math.h>

  if (fabs(a - b) <= epsilon * fabs(a))
... TODO ...

References
[Knuth] § 4.2.2 pp. 217-8



Q 14.6
수치를 반올림하는 (round) 방법을 알려주세요.
Answer
가장 간단하고 직관적인 방법은 다음과 같은 코드를 쓰는 것입니다:
  (int)(x + 0.5)
단, 이 기법은 음수에는 쓸 수 없습니다. 음수에는 다음과 같은 코드를 써야 할 것입니다:
  (int)(x < 0 ? x - 0.5 : x + 0.5)



Q 14.7
왜 C 언어에는 지수를 (exponentiation) 계산하는 연산자가 없을까요?
Answer
대부분 프로세서에는 지수를 계산하는 명령(instruction)이 없기 때문입니다. 대신 C 언어는 pow()라는 함수를 제공합니다. 이 함수는 <math.h>에 선언되어 있습니다. 크기가 작고 0보다 큰 정수를 곱하는 것이 이 함수를 쓰는 것보다 더 효과적일 수도 있습니다.

References
[C89] § 7.5.5.1
[H&S] § 17.6 p. 393



Q 14.8
제 시스템의 <math.h>에는 매크로 M_PI가 정의되어 있지 않습니다.
Answer
이 매크로 상수는 (이 매크로의 값은 pi이며, 정밀도는 각 시스템에 따라 다릅니다) 표준이 아닙니다. `pi' 값이 필요하다면, 여러분이 직접 정의하거나 4 * atan(1.0)으로 계산해야 합니다.
References
[PCS] § 13 p. 237



Q 14.9
IEEE NaN과 같은 특수한 값을 검사하려면 어떻게 하면 되죠?
Answer
고품질의 IEEE 실수 구현 방법을 제공하는 대부분의 시스템에서는 (미리 정의된 상수와 isnan()과 같은 함수를 제공하며, 이들은 표준이 아닙니다. 또 <math.h><ieee.h>, <nan.h>에 선언되어 있을 수 있습니다.) 이런 값들을 다루기 위한 함수들을 제공합니다. 그리고 이런 기능들을 지금 표준화하려고 하고 있답니다. NaN을 검사하는 가장 거칠고(crude) 간단한 방법은 다음과 같습니다.

  #define isnan(x)	((x) != (x))
IEEE를 생각하지 않은 컴파일러는 이런 테스트를 최적화 단계에서 없애버릴 수도 있습니다14.1.

[C9X]는 isnan(), fpclassify() 등과 다른 종류 함수들을 제공합니다.

또 하나의 방법은 이 실수를 sprintf()와 같은 루틴을 써서 포맷화시켜 보는 것입니다. 많은 시스템이 이런 경우 “NaN”이나 “Inf”와 같은 문자열을 만들어 줄 겁니다.

덧붙여 질문 [*]19.39도 참고하시기 바랍니다.

References
[C9X] § 7.7.3



Q 14.11
복소수14.2를 C 언어에서 구현하려면 어떤 방법이 제일 좋을까요?
Answer
가장 쉬운 방법은 간단한 구조체를 만들고 이 구조체에 대해 연산할 수 있는 함수를 만드는 것입니다. [C9X]는 `complex'라는 표준 데이터 타입을 지원하려 합니다. 덧붙여 질문 [*]2.7, [*]2.10, [*]14.12도 참고하시기 바랍니다.
References
[C9X] § 6.1.2.5, § 7.8



Q 14.12
I'm looking for some code to do: Fast Fourier Transforms (FFT's), matrix arithmetic (multiplication, inversion, etc.), complex arithmetic?
Answer
Ajay Shah has prepared a nice index of free numerical software which has been archived pretty widely; one URL is
ftp://ftp.math.psu.edu/pub/FAQ/numcomp-free-c.

덧붙여 질문 [*]18.13, [*]18.15c, [*]18.16도 참고하시기 바랍니다.



Q 14.13
Turbo C를 사용하고 있습니다. 그런데 프로그램을 실행하면 “floating point format not linked”라는 메시지를 출력하고 종료해버립니다.
Answer
Borland사의 컴파일러를 포함한 (Ritchie씨의 오리지널 PDP-11 컴파일러도) 규모가 작은 시스템의 어떤 컴파일러들은 실수 처리 부분이 필요없다고 판단되면 실수 처리 루틴을 포함시키지 않습니다. 특히 실수를 처리하지 않는 (즉 %f, %e를 쓰지 않는) printf()scanf()는 많은 공간을 줄일 수 있습니다. 아마도 여러분의 컴파일러에서는 실수 처리 부분이 필요없다고 생각되어 빠진 것 같습니다. 이럴 때에는 간단한 dummy call을 사용하면 됩니다. 즉, sqrt()와 같은 함수를 한 번 불러주면, 실수 처리 루틴이 포함될 것입니다. (자세한 것은 comp.os.msdos.programmer의 FAQ를 읽어 보시기 바랍니다.)
References
[H&S2002] § 6.2.5 p. 40,

Seong-Kook Shin
2018-05-28