Gradle 3.X Migration


UE4 4.20 으로 마이그레이션하면 "arr-imports.gradle" 파일 같은 곳에서 gradle build 에러가 날 수 있습니다. 원래 UE4 가 gradle 2.x 를 사용하고 있었는데, 4.20 에서 3.x 버전으로 변경하면서 "Build" 디렉토리에 있는 템플릿 "build.gradle" 파일과 Android 용 build-tool 코드에 변경이 있었습니다.


이 때문에 매우 다양한 에러가 발생할 수 있는데요, 대표적인 것이 implementation() 과 관련한 컴파일 에러입니다. 2.x 에서는 implementation() 이 없습니다. 3.x 에서 compile() 이 implementation() 으로 대체되었죠. 그래서 implementation() 을 찾을 수 없다고 에러가 납니다. 만약 그 에러가 뜬다면 gradle 이 3.x 로 갱신되지 않은 것입니다. 그러므로 Setup.bat 을 실행해서 새로운 build.gradle 파일을 받아야 합니다.


그 외에 UPL 을 통해 직접 gradle 코드를 삽입한 경우 compile() 을 implementation() 으로 변경하셔야 합니다. 3.x 버전에서는 compile() 이 에러까지는 아니지만 경고를 뱉습니다.


그 외에도 여러 가지 에러가 있을 수 있는데요, [ Gradle용 Android 플러그인 3.0.0으로 마이그레이션 ] 을 참고하시기 바랍니다.


Crashlytics Symbols 폴더 변경


아마 Crashlytics 를 사용하시는 분들은 당황하셨으리라 생각하는데, UE4 가 기존의 "jni" 와 "obj" 폴더를 제거하고 "jniLibs" 라는 폴더로 바이너리 출력 폴더를 통합했습니다. 아마도 4.20 에서 Android Studio 를 통한 디버깅 기능 개선을 하면서 정리를 한 것이 아닌가 싶습니다.


"jniLibs" 폴더는 debug symbols 를 포함하고 있으니, 반드시 폴더를 바꿔줘야 합니다. 그리고 "Libs" 에는 심볼이 없기 때문에 debug 든 release 든 모두 "jniLibs" 를 사용해야 합니다.



Motivation

 

개인적으로 진행하는 프로젝트에서 double 수학이 필요한 상황이 되었습니다. 그래서 UE4 가 가지고 있는 수학 라이브러리를 미믹( mimic )해서 double 수학 라이브러리를 플러그인에서 구현하고 있었죠.

 

그런데 이 작업을 하다가 보니 matrix-matrix 곱에서 막히게 되었습니다. UE4 는 SSE 를 사용한다는 가정하에서 matrix-matrix, matrix-vector 곱을 SSE 로 구현해 놨습니다. 문제는 double 은 single( single-precision floating point )와는 SSE 구현 방식이 다르다는 거죠.

 

간단하게 예를 들면 Intel CPU 는 16 개의 mmx 레지스터를 제공하는데, 이것을 __m128 로 접근하죠. 128 비트를 가지고 있으니 이걸 32 로 나누면 4 가 됩니다. 즉 4 개의 single 을 하나의 레지스터에 저장할 수 있다는 것이죠. 하지만 double 을 사용하기 위해서는 __m128d 를 사용해야 하며, 그것의 크기는 256 비트입니다. 즉 2 개의 mmx 레지스터를 사용해 single 을 위해 했던 작업을 에뮬레이션해야 한다는 이야기입니다.

 

이런 작업이 좋은 경험이 될 것 같기는 하지만 귀찮아서 그냥 non-SSE 코드를 구현하기로 했습니다. 하지만 시작부터 벽에 부딪히게 되었습니다. 일단 UE4 는 matrix 에 다음과 같은 주석을 달았습니다.

 

 

여기에서 몇 가지 고민해 볼 지점이 발생합니다.

    • pre-multiplication 이란 무엇인가?
    • 이 행렬의 major 는 무엇인가?
    • 계산 후에 행렬의 major 는 유지되는가?

 

이 때문에 어떤 식으로 계산해야 하는지 혼란이 왔고, 제가 생각하는 것보다 행렬 구현이 쉽지 않다는 것을 깨닫게 되었습니다. 역시 단순히 사용하는 것과 그것을 이해하고 구현하는 것은 다른 법이더군요.

 

이 문서에서는 그런 구현을 하는 데 있어서 필요한 배경 지식들을 정리하고자 합니다.

 

Basic rules & major

 

일단 기본을 점검해 보기로 했습니다. 수학에서 M X N 행렬과 N X K 행렬을 곱하면 M X K 행렬이 나옵니다. 그리고 앞의 행렬의 "행"과 뒤의 행렬의 "열"의 내적이 새로운 행렬의 행과 열이 되죠. [ 4 ] 에서는 다음과 같이 도식화해서 설명하고 있습니다.

 

그림1. Matrix multiplication rule. 출처 : [ 4 ]

 

그림 2 : Dot product rule. 출처 : [ 4 ].

 

수학적으로 볼 때 이런 규칙들은 반드시 지켜져야 합니다. 물론 프로그래밍 언어에서 구현할 때는 여러 가지 이유때문에 우리가 수학에서 보는 것과는 다른 순서로 데이터가 저장되어 있을 수 있습니다. 여기에서 major 라는 것이 중요해지죠.

 

[ 1 ] 에 의하면 row-major 와 column-major 를 구분하는 것은 메모리 상에서의 연속성이 행렬의 어느 방향을 향하고 있느냐를 의미한다고 합니다.

 

그림3. row-major 와 column-major 의 순서. 출처 : [ 1 ]

 

그림3 에서 볼 수 있듯이 실제 메모리에 행을 저장하느냐 열을 저장하느냐 에 따라 row-major 와 column-major 가 결정됩니다. [ 1 ] 에 의하면 row-major 를 택한 언어들은 C, C++, Objecttive-C, PLI, Pascal, Speakeasy, SAS, Rasdaman 등이고, column-major 를 택한 언어들은 Fortran, MATLAB, GNU Octave, S-Plus, R, Julia, Scilab 등이라고 합니다. OpenGL 과 OpenGL ES 의 경우에는 row 에다가 vector 를 저장함에도 불구하고 그것을 column 으로 취급합니다. 그리고 이도 저도 아닌 Java, C#, CLI, .NET, Scala, Swift, Python, Lua 같은 언어들도 있다고 하네요.

 

어쨌든 column-major 냐 row-major 냐는 것은 해석과 구현의 문제입니다. [ 3 ] 에서는 view matrix 를 다음과 같이 표현합니다.

 

그림4. View Matrix. 출처 : [ 3 ].

 

 

그리고 나서 열을 하나의 vector 로 보고 matrix 배열의 한 행에 넣습니다.

 

 

한 행에 벡터들이 들어 가고 있으므로, 즉 column 들이 연속적인 메모리 공간에서 이어지고 있으므로 이는 column-major 라고 할 수 있습니다. 메모리에서의 저장순서는 아래와 같습니다.

 

 

Pre/Post-Multiplication

 

이 챕터는 [ 2 ] 의 [ The Matrix Chapter ] 의 번역입니다.

 

행렬곱은 두 행렬들에 대한 곱셈입니다. 행렬곱에는 결합법칙( associative law )이 적용되지만 교환법칙( communitative law )은 적용되지 않습니다.

 

결합법칙이 허용되면 다음과 같이 됩니다:

 

 

하지만 교환법칙이 허용되지 않으면 다음과 같이 됩니다:

 

 

행렬곱은 pre-multiplication 이나 post-multiplication 을 사용하여 계산될 수 있습니다. 이것은 벡터가 matrix 의 왼쪽( 혹은 "앞" )에 있어야 하는지 아니면 오른쪽( 혹은 "뒤")에 있어야 하는지를 가리킵니다.

 

 

행렬곱에는 교환법칙이 적용되지 않기 때문에 행렬 곱을 계산할 때 벡터가 행렬의 어느쪽에 있느냐에 따라 완전히 다른 결과가 발생됩니다. 그래픽 API 가 pre 혹은 post multiplication 을 사용하기로 선택하면, 동일한 결과 벡터를 얻으려면 전치된( transoposed ) 행렬을 사용해야합니다.

 

수학 문서에서는 post-multiplicaiton 이 일반적입니다. 이것은 행렬곱이 다음과 같이 계산된다는 것을 의미합니다. 주어진 행 R 과 열 C 에서 결과의 각 구성 요소는 왼쪽 행 행 R 과 오른쪽 열 C 의 내적으로 계산됩니다. 예를 들어 :

 

 

일 때, post-multiplication 을 사용하면, C 행렬의 1 행 2 열의 컴포넌트 b 의 결과를 계산하기 위해, A 의 1 행과 B 의 2 열을 내적합니다( 이것을 row-major 표기법이라 합니다 ). 모든 결과는 다음과 같습니다.

 

 

그러므로 그 결과는 다음과 같습니다:

 

 

이어지는 글에서는 별도의 표기가 없다면 row-major 표기법을 사용합니다.

 

하지만 이것은 정방 행렬을 곱하지 않을 때는 조금 더 까다로워 집니다. 3x3 행렬에 3x1 행렬( 3 행 1 열 벡터 )을 곱하면 3x1 행렬이 됩니다. 4x2 행렬에 2x3 행렬을 곱하면 4x3 행렬이 됩니다. 첫 번째 행렬의 행과 두 번째 행렬의 열에 대한 내적을 취하기 때문에, 첫 번째 행렬에는 두 번째 행렬의 행 수와 동일한 개수의 열이 있어야 합니다. 첫 번째 행렬의 각 행과 두 번째 행렬의 각 열에 대해 이 작업을 수행하므로, 결과 행렬의 행 개수는 첫 번째 행렬의 행의 개수와 같고 열 개수는 두 번째 행렬의 열 개수와 같습니다.

 

일반적으로, 서로 옆에 쓰여진 두 행렬의 차원을 취합니다. 내부의 두 값은 같아야하고 결과 행렬은 외부 값의 크기를가집니다. 4x3 * 3x2 = 4x2. 2x4 * 4x1 = 2x1. 4x3 및 2x4 행렬을 곱할 수는 없습니다. 왜냐하면 3 은 2 와 같지 않기 때문입니다.

 

자, 이제 pre-multiplication 에 대해서 이야기해 보도록 하겠습니다.

 

Pre-multiplication 을 사용하면 이제 행 벡터를 사용해야합니다. 열 벡터는 4x1 행렬이지만, 4x1 행렬에 4x4 행렬을 곱할 수는 없습니다. 대신 1x4 행 벡터를 사용해 1x4 행렬에 4x4 행렬 곱하기( 내부/가장 가까운 두 값의 일치 )의 결과로 1x4 행렬을 만들 수 있습니다( 바깥 쪽 두 값은 1 과 4 입니다 ).

 

그러나 pre-multiplication 을 사용하면 연산이 달라집니다. post-multiplicatin 의 4x4 행렬에 4x1 행 벡터를 곱한 결과 행렬의 각 행에 벡터가 포함됩니다. 이제 pre-multiplication 을 통한 내적은 ( 행렬이 곱셈 연산자의 오른쪽에 있기 때문에 ) 벡터와 행렬의 각 열을 사용해 연산을 합니다.

 

고맙게도, 이것은 쉽게 수정할 수 있습니다. Pre-multiplication 을 위해 행렬을 계산할 때, 단순히 post-multiplicatin 을 위해 사용했던 행렬을 전치하기만 하면 됩니다. 이것은 벡터를 4x1 열 벡터에서 1x4 행 벡터로 변환하기 때문에 의미가 있습니다.

 

2x2 행렬과 2x1 벡터에 대한 post-multiplication 은 다음과 같습니다:

 

 

1x2 벡터와 전치된 2x2 에 대한 pre-multiplication 은 다음과 같습니다:

 

 

결과는 동일하지만 전치된 벡터입니다:

 

 

OpenGL에서 일반적으로 사용하는 것처럼, column-major 표기법으로 post-multiplication 을 사용하면 어떻게 될까요. 수학적 결과는 같지만 행렬 크기의 순서는 바뀝니다. 따라서 column-major 4x3 행렬에 3x2 행렬을 곱할 수는 없지만, 2x4 및 1x2의 곱을 취하면 4x1 행렬(4 행 행 벡터)이 됩니다. 결과와 작동 방식은 모두 동일합니다. 표기법 만 다를 뿐입니다.

 

The Tricky Part

 

중요한 것은 pre-multiplication 이 사용되었는지 post-multiplication 이 사용되었는지의 여부입니다. D3DX 수학 라이브러리는 pre-multiplication 을 사용하고 OpenGL의 표준 행렬 스택은 post-multiplication 을 사용합니다. 그것은 일반적으로 행렬이 전치되어야 한다는 것을 의미합니다.

 

그러나 D3DX 는 row-major 행렬을 사용하고 OpenGL은 column-major 행렬을 사용한다는 점을 기억하십시오. 따라서 그들은 이미 전치되어 있습니다. 다음의 간단한 3x3 변환 행렬을 살펴 보죠. 2 의 uniform scale 과 (10, -5) 의 translation 을 표현합니다.

 

다음의 3x3 행렬은 row-vector 와 pre-multiplication 을 위해 사용되도록 의도되었습니다:

 

 

이것의 row-major 메모리 레이아웃은 다음과 같습니다:

 

 

다음의 3x3 행렬은 column-vector 와 post-multiplication 을 위해 사용되도록 의도되었습니다:

 

 

이것의 column-major 메모리 레이아웃은 다음과 같습니다 :

 

 

행렬은 특정 개념으로 작성되면 전치됩니다. 하지만 메모리에 저장되면 똑같습니다.

 

그것은 까다로우면서 멋진 부분입니다. 여러분의 코드들은 서로 다른 표기법과 서로 다른 행렬곱 순서 때문에 다르게 느껴질 겁니다. 하지만 실제 행렬값은 서로 교환가능합니다.

 

결과적으로 Direct3D 및 OpenGL 스타일을 모두 처리하는 단일 행렬 클래스를 쉽게 작성할 수 있습니다. Column-major 와 post-multiplication 을 사용하거나, row-major 와 pre-multiplication 을 사용하면, 그 데이터는 완전히 호환됩니다.

 

이것을 다시 강조하기 위해서 : 손으로 하는 일은 이것과 아무 상관이 없습니다! 왼손 좌표계 행렬이나 오른손 좌표계 행렬이라는 것은 존재하지 않습니다. 좌표계는 완전히 별도의 이슈입니다.

 

References

 

[ 1 ] Row- and column-major order, Wikipedia.

 

[ 2 ] Matrices, Handedness, Pre and Post Multiplication, Row vs Column Major, and Notations, Game Development by Sean.

 

[ 3 ] Understanding the View Matrix, 3D Game Engine Programming.

 

[ 4 ] Linear Algebra, Artifical Inteligence.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.

주의 : 번역이 개판이므로 이상하면 원문을 참조하세요.

원문 : The Perils of Floating Point.



제가 "The Perils of Floating Point" 를 작성할 때, 이게 어느 정도까지 일이 커질지 알지 못했습니다. 그 문서에 수 백개의 링크가 걸렸고, 수 천번 참조되었습니다. 대학이나 프로그래밍 문서, 그리고 출간된 책들이나 많은 블로그들에서 참조되었죠. 얼마나 많은 사람들이 이 도움을 받게 되었는지 알고 싶네요.


The Perils of Floating Point by Bruce M. Bush © 1996 Lahey Computer Systems, Inc.

원문에 대한 표기와 함께 복제가 허용됩니다.


The Perils of Floating Point by Bruce M.Bush


최근 수십년 동안에 있었던 수 많은 훌륭한 엔지니어링과 과학적 모험들은 디지털 컴퓨터의 floating-point( 부동소수점 ) 기능없이는 불가능했을 것입니다. 하지만 여전히 일부 부동소수점 계산의 결과는 좀 이상하고, 심지어는 수학적 경험을 몇 년 동안 해 온 사람들에게조차도 이상합니다. 저는 이러한 이상한 결과들이 발생하는 원인에 대해서 설명하고 적용가능한 제안들을 제공할 계획입니다.


부동소수점 표현과 연산( arithmetic )은 부정확합니다. 하지만 그게 대부분의 프로그래머들에게 특별히 문제가 된다고는 생각하지 않습니다. 많은 입력값들이 내재적으로 부정확한 양( measurement )입니다. 그래서 출력 값에 대한 질문은 에러가 존재하느냐가 아니라 얼마나 많은 에러가 예상되느냐입니다. 그래서 여러분이 부동소수점을 가지고 컴퓨터보다 더 정확한 결과를 암산할 수 있다면, 의혹을 가지기 시작할 것입니다.


저는 몇 가지 이유로 FORTRAN 을 사용해 예제를 프로그래밍 했습니다 :

  1. 포트란에서는 다른 컴퓨터 언어보다 더 많은 부동소수점 계산이 수행됩니다.
  2. Lahey Computer System 이라는 회사에서 일하고 있는데, FORTRAN 언어 시스템을 개발하고 판매하고 있습니다.


이상한 결과들이 나오는 원인의 핵심은 하나에 기반합니다 : 컴퓨터에서 부동소수점이 보통 2 진수라는 것입니다. 반면에 외부적인 표현은 10 진수입니다. 우리는 1/3 이 정확하게 표현되지 않을 것이라 생각하지만, 그것은 직관적입니다. .01 이 되죠. Not so! IEEE 단일정밀도 형식( single-precision format )에서 .01 은 정확히 10737418/1073741824 이거나 대략 0.009999999776482582 입니다. 여러분은 다음과 같은 코드를 확인하기 전까지는 그 차이를 인지하지도 못할 것입니다:




10 진수 부동소수점 구현은 이런 변칙을 포함하지 않습니다. 하지만 10 진수 부동소수점 구현은 거의 보기가 힘듭니다. 왜냐하면 2 진수( binary ) 연산이 디지털 컴퓨터에서는 훨씬 빠르기 때문입니다.


Inexactness


디지털 컴퓨터에서 부동소수점 연산은 내재적으로 부정확합니다. 32 비트 부동소수점 수의 ( 숨겨진 비트를 포함한 ) 24 비트의 가수( mantissa )는 약 7 개의 유효 십진수( significant decimal digit ) 를 표현합니다. 예를 들어 대략 8,388,607 개의 단일정밀도 숫자가 1.0 과 2.0 사이에 존재합니다. 반면에 1024.0 과 2024.0 사이에는 단지 8191 개만이 존재합니다.


모든 컴퓨터에서, 수학적으로 동등한 표현식이 부동소수점 연산을 사용하면 서로 다른 결과를 산출할 수 있습니다. 다음 예제에서, Z 와 Z1 은 보통 서로 다른 값을 가지게 되는데, (1/Y) 혹은 1/7 이 이진 부동소수점에서 정확하게 표현되지 않기 때문입니다.




Insignificant Digits



다음 코드 예제는 유효 숫자로 보일 수 있는 무의미한 숫자들이 나오는 현상을 설명합니다.




단일 정밀도 (REAL) 요소는 대략 최대 7 개의 십진수 정밀도를 표현할 수 있습니다. 그래서 위의 빼기는 (1000.200 - 1000.000) 을 표현합니다. 그러므로 그 결과는 약 3 개의 십진수만을 표현할 수 있습니다. 하지만 프로그램은 "0.200012" 를 출력할 것입니다. 1000.2 는 이진 부동소수점에서 정확하게 표현되지 않고 1000.0 도 그렇기 때문에, 결과 A 는 0.2 보다 약간 큽니다. 하지만 컴퓨터는 ".200" 뒤쪽에 있는 숫자가 무의미하다는 것을 알지 못합니다.


아마도 언젠가는 컴퓨터가 결과에 있는 여러 개의 비트들에 대한 추척을 하겠죠. 하지만 지금은 여전히 그것은 프로그래머의 책임하에 있습니다. 만약 여러분이 데이터형에 의해 표현되는 십진수의 개수에 주의한다면, 유효 숫자의 개수를 근사계산하는 것은 직관적입니다. 하지만 아마도 시간이 걸리는 작업이 되겠죠. 가장 주의해야 할 점에 대해서 이야기해 보도록 하겠습니다:

  1. 뺄셈은 값이 거의 같습니다.
  2. 덧셈은 가수값이 거의 같습니다. 하지만 부호( sign )가 반대인 숫자에서는
  3. 덧셈이나 뺄셈은 매우 다른 가수값을 반환합니다.



Crazy Conversions


정수로의 변환은 부동소수점 수에서의 부정확함을 드러낼 수 있습니다. 다음 예제에서 설명할 것입니다. 21.33 에 가장 가까운 단일정밀도 부동소수점 수는 21.33 보다는 약간 작습니다. 그래서 100. 을 곱하게 되면, 그 결과 Y 는 2133.0 보다 약간 작습니다. 만약 여러분이 Y 를 일반적인 부동소수점 형식으로 출력한다면, 반올림( rounding )이 발생해서 2133.00 으로 출력될 것입니다. 하지만, Y 를 정수에 할당하게 되면, 반올림이 발생하지 않습니다. 그래서 그 숫자는 2132 가 됩니다.



다음 프로그램은 Lahey 의 LF90 으로 컴파일하면 "1.66661000251770" 을 출력합니다.



여러분은 "왜 단일정밀도 수에 무작위인 것으로 보이는 '000251770' 이 붙은 걸까" 라는 의문이 들 것입니다. 글쎄요, 그 숫자는 무작위 숫자가 아닙니다; 컴퓨터의 부동소수점은 이진 표현에서 0 을 사용해 패딩( padding )함으로써 변환을 수행합니다. 그래서 D 는 X 와 정확히 같죠. 하지만 15 개의 십진수로 출력한다면, 부정확함이 드러나는 것입니다. 이것은 비유효( insignificant ) 숫자의 다른 예이기도 합니다. 단일정밀도 숫자를 배정밀도( double-precision ) 숫자로 변환하는 것은 유효 숫자의 개수를 증가시키지 않는다는 것을 기억하십시오.


Too Many Digits


여러분은 이전의 프로그램 예제를 사용해 D 와 X 를 같은 형식으로 출력함으로써 값을 확인해 보려고 할 것입니다:



일부 FORTRAN 구현에서는 두 숫자가 동일한 값을 출력합니다. 여러분은 만족하고 떠나겠죠. 하지만 실제로는 단일 정밀도 수의 낮은자리 숫자( low-order digits )가 출력되지 않는 것을 잘못 판단한 것입니다. Lahey FORTRAN 에서는 그 숫자들이 다음과 같이 출력됩니다.


1.66661000000000       1.66661000251770


이렇게 되는 이유는 매우 단순합니다: Formatted I/O 변환 루틴들은 절대적인 최대 십진수를 알고 있으며, 단일정밀도 요소를 출력할 때 모든 유효숫자는 9 가 됩니다. 나머지 부분들은 현재 "precision-fill" 문자로 채워집니다. 그것의 기본값은 "0" 입니다. Precision-fill 문자는 다른 ASCII 문자로 대체될 수 있는데요, '*'( asterik ) 나 ' '(blank )같은 것들을 예로 들 수 있습니다. Precision-fill 문자를 "*" 로 변경하는 것은 낮은자리 숫자의 비유효성을 강조합니다.


1.66661000******       1.66661000251770


Too Much Precision


IEEE 단일정밀도 형식은 23 비트의 가수, 8 비트의 지수, 1 비트의 부호로 구성되어 있습니다. 펜티엄( Pentium )과 같은 인텔( Intel ) 마이크로 프로세서에서는 내부 부동소수점 레지스터가 64 비트의 가수, 15 비트의 지수, 1 비트의 부호로 구성됩니다. 이는 다른 구현들보다 해상도 손실이 훨씬 더 적은 중간 계산( intermediate calculation )을 가능하게 합니다. 이것의 단점은 중간 값들이 레지스터에 유지되는 방식에 따라서 똑같아 보이는 계산의 결과가 달라질 수 있다는 것입니다.


다음 예제에서, 컴파일러는 A/B 를 계산해서 중간 결과를 단일 정밀도 임시 변수에 저장하고, X/Y 를 계산해서 임시 변수에서 빼고, 결과를 Z 에 저장하는 코드를 생성합니다. Z 는 0 이 아닐 것입니다. 왜냐하면 단일정밀도 임시변수에 저장하면서 해상도가 손실될 것이기 때문입니다. 만약 생성된 코드가 중간 값을 레지스터에 유지한다면, 해상도는 손실되지 않을 것이고, Z 는 0 이 될 것입니다.



다음 예제는 이전 예제의 변형들에 대해서 설명합니다. 컴파일러는 여전히 중간 결과 C 를 레지스터에 유지하는 코드를 생성할 수 있습니다. 이건 Z 값이 0 임을 의미합니다. 만약 A/B 를 C 에 넣음으로써 해상도가 손실된다면, Z 는 0 이 아닐 것입니다.



C 가 레지스터에서 유지되는 것을 막기 위해서 레이블( label ) 100 을 추가했습니다. 그래서 Z 는 아마도 거의 대부분의 컴파일러에서 0 이 아닐 것입니다.



Safe Comparison


다양한 컴퓨터들은 다양한 비트를 사용해 부동소수점 숫자를 저장합니다. 심지어는 같은 IEEE 형식을 사용해 숫자를 저장하고 있더라도, 계산하는 동안에는 다른 일이 벌어질 수 있습니다. 왜냐하면 중간 레지스터의 크기가 다르기 때문입니다. 이식을 증대하고 일정한( consistent ) 결과를 보장하기 위해서, 저는 FORTRAN 에서 실수의 정확한 동일성( equality )을 비교하는 것은 좋지 않다고 생각합니다. 더 좋은 기법은 두 숫자의 차이의 절대값을 비교하는 것인데, 이 때 적절한 엡실론( epsilon )값을 사용해서 거의 같은 관계, 명확하게 더 큰 관계 등을 얻는 것입니다. 예를 들어 :



비교 대상 중 하나를 사용하여 엡실론에 곱셈을하면 그 숫자의 범위가 조정됩니다. 그래서 하나의 엡실론을 여러 가지 비교를 위해 사용할 수 있습니다. 가장 예측 가능한 결과를 얻으려면 엡실론의 절반 정도로 큰 값을 사용하고 다음 예제에서와 같이 비교 대상의 합계를 곱합니다:



부동 소수점 계산이 수학적으로 가능하지 않은 값을 생성할 수 있기 때문에, 큼 이나 작거나 같음 등의 비교도 예기치 않은 결과를 생성 할 수 있습니다. 다음 예제에서 X 는 항상 수학적으로 J 보다 큽니다. 그래서 X/J 는 1.0 보다는 항상 커야 합니다. 하지만 큰 숫자의 J 를 사용했을 때 델타( delta )의 합은 X 로 표현되지 않습니다. 왜냐하면 가수 크기를 넘어서기 때문이죠.



Programming with the Perils


쉬운 답은 존재하지 않습니다. 앞에서 설명했던 방식으로 동작하는 것은 이진 부동소수점의 본성입니다. 컴퓨터 부동 소수점의 힘을 이용하려면 그 한계를 알아야하며 그 한계 내에서 작업해야합니다. 부동 소수점 연산을 프로그래밍 할 때 다음 사항을 염두에 두면 많은 도움이됩니다 :

  1. 단일 정밀도 IEEE 형식에서는 약 7 자릿수, 배 정밀도 IEEE 형식에서는 약 16 자릿수만 표현할 수 있습니다.
  2. 숫자가 외부 십진수에서 내부 이진수로 혹은 그 반대로 바뀔 때마다 정밀도가 손실 될 수 있습니다.
  3. 항상 안전한 비교를 사용하십시오.
  4. 덧셈과 뺄셈은 결과에서 실제 유효 숫자를 빠르게 침식할 수 있다는 것에 주의하십시오. 컴퓨터는 실제로 어떤 비트가 중요한지 알지 못합니다.
  5. 데이터 유형 간의 변환은 까다로울 수 있습니다. 배 정밀도로의 변환이 실제 유효 비트수를 늘리지는 않습니다. 부동소수점 숫자가 더 큰 정수로 출력되는 경우에도 정수로의 변환은 항상 0 으로 잘립니다.
  6. 두 가지 다른 부동소수점 구현에서 동일한 결과를 기대하지 마십시오.


제가 여러분에게 부동소수점 연산의 내부에서 어떤 일들이 발생하고 있는지 좀 더 알려줬기를 바랍니다. 그리고 그 이상한 결과들에 대해서 이해할 수 있게 되었기를 바랍니다.


"위험"중 일부는 피할 수 있지만 대부분은 이해하고 받아들일 필요가 있습니다.


IEEE Standard Floating-Point Formats


IEEE (Institute of Electrical and Electronics Engineers, Inc.)는 부동 소수점 표현 및 계산 결과에 대한 표준을 정의했습니다 (IEEE Std 754-1985). 이 절에서는 부동 소수점 수를 나타내는 IEEE 표준에 대한 개요를 제공합니다. 여기에 포함 된 데이터는이 기사의 나머지 부분에서 일부 내용을 설명하는 데 도움이되지만 기본 개념을 이해하는 데 반드시 필요한 것은 아닙니다.


대부분의 이진 부동 소수점 수는 1.ffffff x 2^n 으로 나타낼 수 있습니다. 여기서 1 은 정수 비트이고, f 는 소수 비트이며 n 은 지수입니다. 정수 비트와 소수 비트의 조합을 가수 ( 또는 significand )라고 합니다. 대부분의 숫자는 정수 비트에 1이 있도록 지수를 조정할 수 있기 때문에( 정규화라고 부르는 프로세스 ), 1을 저장할 필요가 없으므로 효율적으로 추가적인 정밀도를 허용하게 됩니다. 이 비트를 숨겨진 비트라고합니다. 숫자는 부호있는 가수로 표현되므로, 음수는 동일한 크기의 양수와 동일한 가수를 갖지만 부호 비트가 1 입니다. 바이어스( bias )라고 하는 상수가 지수에 더해져 모든 지수는 양수가 됩니다.


지수가 0이고 가수가 0 인 값 0.0 은 음수 기호를 가질 수 있습니다. 음수 0 은 대부분의 프로그램에서 분명하지 않은 미묘한 속성을 가지고 있습니다. 0 이 아닌 가수를 가진진 지수 0 은 "비정규 수( denormal )"입니다. 비정규 수는 크기가 너무 작아 정수 비트 1 로 표현 될 수가 없고 하나의 유효 비트보다 작은 값을 가질 수 있습니다.


지수의 비트값이 모두 1 인( 가장 큰 지수 ) 경우에는 특수한 숫자를 표현합니다. 가수가 0 이면 무한대( 양수 또는 음수 )를 표현합니다. 가수가 0 이 아니면 NAN( not-a-number )을 표현합니다. 잘못된 수치 연산의 결과로 발생하는 NAN 에 대해서는 이 기사에서 더 설명하지 않습니다.


IEEE 표준은 32 비트 및 64 비트 부동 소수점 표현을 정의합니다. 32 비트 ( 단일 정밀도 ) 형식은 상위에서 하위로, 부호 비트, 127 의 바이어스가 있는 8 비트 지수, 23 비트의 가수를 가집니다. 64 비트 ( 배 정밀도 ) 형식은 부호 비트, 1023 의 바이어스가 있는 11 비트 지수, 52 비트의 가수를 가집니다. 숨겨진 비트를 사용하면 정규화된 숫자의 유효 정밀도는 각각 24 비트와 53 비트입니다.


Single-precision format

31, 30-23, 22-0

S, Exponent, Significand


Double-precision format

63, 62-52, 51-0

S, Exponent, Significand


Bibliography


American National Standards Institute (1978), "American National Standard, Programming Language FORTRAN", ANSI X3.9-1978, ISO 1539-1980 (E).


IEEE Computer Society (1985), "IEEE Standard for Binary Floating-Point Arithmetic", IEEE Std 754-1985.

'물리_수학_기하학' 카테고리의 다른 글

중력과 수직항력  (10) 2019.07.07
장력( Tension )  (13) 2019.06.26
Matrix major & multiplication  (2) 2018.08.12
[ 번역 ] Depth Precision Visualized  (11) 2017.06.04
[ 번역 ] Moment of inertia 일부 번역  (0) 2016.11.07
[ 번역 ] 캐릭터 애니메이션 : 스켈레톤과 역운동학  (0) 2016.10.01
모멘트( moment )  (22) 2016.01.06
Curve  (0) 2015.10.04
[ 일부 번역 ] CLIPPING USING HOMOGENEOUS COORDINATES  (0) 2013.04.29
Arc length of curve  (0) 2012.09.21

+ Recent posts