Computer Architecture 3 - Arithmetic of computer

컴퓨터는 사람과 다르다. 사람은 개념을 상상할 수 있지만 컴퓨터는 그럴 수 없다. 사람은 무한의 개념이나 엡실론의 개념을 생각할 수 있지만 컴퓨터는 그럴 수 없다. 이런 차이 때문에 근사치가 존재하고, 올바르게 코딩하였음에도 이상한 결과값이 발생하는 것이다. 

이런 컴퓨터의 한계와 Arithmetic calculation을 올바르게 이해하고 사용하여야 원하는 값을 얻을 수 있다. 따라서 오버플로나 언더플로, IEEE floating point expression, Multiplication, Division 등등에 대해 간단히 정리해보자.

 

 

Arithmetic for computer : 먼저 int/ float 표현과 precision에 대해 알아보자. 어떤 비트건 우리는 fixed length를 가지고 정보를 표현해야한다. 우리는 허수, 무한 등 상상할 수 있는 모든 것을 생각할 수 있지만 컴퓨터는 오로지 가지고 있는 비트만큼 밖에 생각 못한다는 것을 주의해야 한다.


Integer addition & subtraction : 2진법 더하기 빼기. 단, 이 때 우리는 항상 한정된 bit를 가지고 계산해야 하므로 carry(올림)되는 값들이 내가 가지고 있는 bit를 넘어서면, 그 값을 버린 값이 결과가 될 수 밖에 없다. 그리고 이것은 틀린 것이 아니다. 예를 들어 7과 6을 더하는데, bit가 3개 밖에 없다면, 2^3bit는 버려지고 오직 101(2)만 남아 5가 되버린다. 그러나 이는 비트가 3개라서 이런 결과가 나온 것이지 계산자체가 틀린 것은 아니라는 말이다. (7(2)+6(2)=5(2) 는 correct & expected result for 3 bit computer) 왜냐하면 앞서 우리가 보수를 사용하여 음수와의 계산을 할 때 올림을 이용해 가장 오른쪽이 버려지게 되므로써(MSB) 계산이 올바르게 되는 것을 이용하기 때문이다. 이렇게 carry되는 bit가 날아가는 것을 overflow라고 한다.


Subtraction : 앞서 음수는 해당 수의 보수에 +1을 한 것이라고 배웠으므로 그 값인 negation을 원하는 값에다가 더하면 빼기가 된다. (7-6 = 7 + (-6) )


Overflow : if result is out of range, it became overflow. 내 레지스터에 적을 수 있는 범위를 넘어서면 그것이 오버플로. (16bit, 32bit, 64bit…) 덧셈과 뺄셈에서 오버플로가 나는 상황을 알아보자.


먼저 양수 + 음수는 오버플로가 날 수 없다. (가장 작은 양수에다가 가장 큰 음수를 더해도 표현가능 & 가장 큰 양수에다가 가장 작은 음수를 더해도 표현가능) 그리고 양수 + 양수일 때 MSB가 1일 경우, 오버플로가 난 것이다. MSB는 음수일 때만 1이 되므로(앞서 양수/음수 표기 확인) 양수+양수가 음수가 될 수 없기 때문에 오버플로를 확인할 수 있다. 마찬가지로 음수 + 음수일 때도 MSB가 0이라면 오버플로가 난 것이다. 


뺏셈에서도 동일하게 양수 – 양수나 음수 – 음수는 절대 오버플로가 날 수 없다. (앞선 경우의 같은 것의 덧셈과 동일) 그리고 음수 – 양수나 양수 – 음수는 MSB가 각각 0, 1일 때 앞서와 동일하게 오버플로가 난다. 


C는 이런 오버플로를 무시한다. 즉, 어떤 에러도 내뱉지 않고 그냥 결과 그대로 출력한다. 앞서서 expected & correct result라고 했던 이유가 바로 이것. 따라서 프로그래머가 이런 것들을 알고 대처해야 한다. 포트란 등 다른 언어에서는 이를 받지 않는 경우도 있다. (error, exception, interrupt.등)

 

이제, 실제 MIPS CPU에서 Multiplication과 Division을 하는 회로를 알아보자.


 

Multiplication : multiplicand * multiplier = product. (32*32 -> 64bit가 될 수 있으므로 레지스터, ALU, 저장장치는 64bit를 사용한다.)


먼저 product를 0으로 초기화하고, multiplier에서 한자리씩 계산한다. 이진법 곱셈은 0과 1밖에 없으므로 1을 곱하는 것은 결국 해당 자릿수만큼의 수를 더하는것과 같다. 따라서 control test는 multiplier의 값이 0인지 1인지 판별하고, 1이면 multiplicand의 값을 product에 더한다. (당연하게도 0이면 안 더하고 넘어감) 

그리고 multiplicand와 multiplier의 값을 각각 shift left, shift right 하여 다음 자릿수를 계산한다. (bit만큼 step이 필요함) 이를 반복하면 곱셈 결과를 얻을 수 있게 된다. 즉 shifting과 adding의 반복으로 곱셈을 수행할 수 있다. 


Optimized multiplication : 낭비되는 공간을 막기 위해 32bit 연산에서도 32bit ALU, multiplicand를 사용하고 multiplier를 없애버린 회로. 대신 product의 크기는 여전히 64bit이기 때문에 mutliplier를 product 안에 넣는다. 전체 중 왼쪽 절반은 product(0으로 초기화), 오른쪽 절반은 multiplier가 된다. 

이 때, ALU가 32bit이므로 연산 결과는 항상 왼쪽 절반에만 전달되고 사용된다. 그리고 동일하게 product의 오른쪽 절반에 저장된 multiplier의 마지막 비트를 빼서 확인하고, multiplicand를 product의 왼쪽 절반에 더하고, product의 전체를 shift right 한다. 이 때, 가장 낮은 자릿수를 맞춰가는 과정이 되므로 multiplicand는 shift left 연산을 할 필요가 없어진다. (multiplier가 모두 shift right 되고 나면 마지막 자릿수는 product의 가장 맨처음 연산결과, 즉 가장 낮은 자릿수가 자동으로 되기 때문) 

따라서 product의 마지막 자릿수만 확인하고 add & shift를 할 것인지, shift만 할 것인지 결정하고, 그 결과에 따라 multiplicand를 더해주면 곱셈을 할 수 있다. (더 적은 용량으로도)


이렇게 곱셈 과정을 살펴보면, multiplication은 상당히 많은 step이 필요하다는 것을 알 수 있다. (bit만큼 필요하므로) 따라서 덧셈이나 뺄셈보다 훨씬 많은 cycle이 필요하고, 오랜 시간이 걸리는 연산이다. 따라서 곱셈을 다른 방식으로 표현할 수 있으면 사용하지 않는 것이 좋다.

예를 들어 y=4 *x 를 한다면, y = x <<2 로 표현할 수 있으므로 무조건 바꿔서 표현해야 한다. (performance에 영향을 미치므로 / Shift는 1 ALU operation, multiplication은 32 ALU operation) shift가 아닌 몇 줄의 덧셈으로 표현할 수 있는 경우에도 동일하다(2*x = x+x). 물론, 앞서와 동일하게 좋은 컴파일러는 이런 것들을 알아서 바꿔준다. 

또한, 그냥 무식하게 ALU를 bit 수만큼 때려박아서 곱셈을 빠르게 하는 방법도 있다. 즉, cost와 performance 사이에서 취사 선택이 여전히 필요하다.


MIPS에서는 32-bit register 두 개가 곱셈을 위해 할당되어 있다. HI for MSB, LO for LSB. 즉, HI가 앞서 말한 Product의 왼쪽 부분이 되고, LO가 product의 오른쪽 부분(=multiplier)이 된다. Instruction은 mult rs, rt로 되고 이 결과값은 두 개의 레지스터에 나누어져 있으므로 mfhi rd / mflo rd로 읽어야 한다. 물론 전체를 사용하지 않는 계산을 할 경우에는 mul rd, rs, rt로 계산해도 된다. 이 경우에는 upper part of product의 32bit가 날아간다는 것을 알고 있어야 한다.



Division : 앞서 곱셈과 마찬가지로 나누기도 이진수, 십진수 상관없이 배운대로 동일하게 가능하다. 앞서 곱셈에서 multiplier의 0이냐 1이냐에 따라 곱하는가 아닌가가 결정되는 것 처럼, 여기서도 해당 자릿수에서 divisor를 dividend에서 뺄 수 있느냐 없느냐에 따라 1 또는 0을 quotient(몫)로 가지게 된다. (dividend = quotient * divisor + remainder) 앞서와 동일하게 nbit operands(divisor / dividend)가 있으면 nbit quotient와 nbit remainder를 결과로 내뱉게 된다. 


컴퓨터의 나눗셈에서 0으로 나누는 케이스는 exception으로 분리하여 따로 핸들해야 한다.
앞서 일반적인 나눗셈방법(long division approach)를 사용할 때 사람은 해당 값에서 뺄 수 있는지 없는지를 직관적으로(심지어 이진법이므로) 바로 알 수 있다. 

그러나 컴퓨터는 해당 계산을 진행하고, 결과가 0보다 큰지 작은지를 판별해야 해당 뺄셈이 가능한 지 아닌지 알 수 있다. 따라서 컴퓨터는 일단 빼보고, 크기에 따라서 restoring division의 과정을 진행해야 한다. Restoring division은 그래서 일단 뺄셈을 진행하고, remainder가 0보다 작아지면, divisor를 다시 더하는 방식으로 진행한다.


그리고 부호가 있는 수의 나눗셈을 진행할 경우(음수를 양수로 나누는 등) dividend와 remainder는 반드시 부호가 같아야 한다. 수학적으로는 -5를 2로 나눈다고 가정했을 때, 몫이 -3이고 나머지가 1인 경우와 몫이 -2이고 나머지가 -1일 경우 모두 올바르다. 그러나 컴퓨터의 계산에서는 무조건 후자가 되어야 한다(나눠지는 수와 나머지의 부호가 항상 동일해야 함). 그래서 컴퓨터가 계산을 할 때는 사인을 무시하고 계산을 진행한 뒤에 몫과 나머지의 부호를 알맞게 맞춰준다.
회로의 형태는 앞서 곱셈과 거의 비슷하다. 다만, dividend에 divisor를 빼고, 다시 더하고 하는 과정은 모두 remainder register에서 일어난다는 것을 반드시 기억해야 한다.


그리고 매 스텝마다 divisor register는 shift right를 하고(뺄셈이 성공이던 아니던) quotient register는 shift left를 한다.(마찬가지로 성공이던 아니던. 단, 이 때 성공하면 1, 실패하면 0을 놓은 다음 shift를 해야 한다.) 그리고 remainder register의 음수 여부는 MSB의 값이 1인 것으로 바로 확인할 수 있다. (따라서 MSB가 1이면 바로 add back)


앞서 optimized multiplier와 동일하게 optimized divider도 있다. Optimized version을 아는 이상, 처음 회로를 볼 때 메모리 낭비가 존재한다는 것을 바로 느낄 수 있다. 따라서 곱셈 방식과 거의 동일하게 optimized divider도 만들어진다. ALU와 divisor를 32bit로 줄이고, quotient는 remainder의 right part에 넣는다. Remainder는 왼쪽 부분에 있으므로 빼고 더하고 하는 과정은 여전히 remainder register의 왼쪽에서 일어난다.
그리고 회로 구성 자체는 거의 동일하다. 따라서 같은 하드웨어(회로 구성)으로 곱셈과 나눗셈을 모두 수행할 수 있다.


MIPS에서 저장하는 레지스터는 앞서와 동일하게 HI, LO로 되어 있고, instruction은 div rs, rt로 나타낸다.mfhi, mflo instruction으로 결과에 접근한다.
예전에 설명한 것 처럼, shift right 연산은 오직 unsigned version에서만 사용가능하다. Signed 일 경우 logical right shift나 arithmetic right shift 모두 연산 결과가 맞을 때도, 틀릴 때도 있어서 일관적이지 않기 때문에 unsigned integer일 때만 사용하는 것이 올바르다. 그리고 곱셈과 동일하게 shift가 훨씬 빠르기 때문에 가능하면 사용하는 것이 좋다.

 

Floating point (representing real number) : 소수점을 나타내는 point가 움직인다(floating). Non-integer number를 나타내는 방법(including very small & large numbers) 

십진법을 사용할 때 우리는 십의 제곱으로 큰 수 혹은 작은 수를 표현할 수 있다. 마찬가지로 이진법도 2의 제곱을 이용해 한 자리 수와 2의 제곱의 곱으로 수를 표현할 수 있다. 또한 원리가 같으므로 십진법 표현을 이진법 표현으로 바꿀 수 있다. (scientific notation : 0.000*10^n, -> normalized : only one non zero value on the left of floating point)


그러나 2의 음수 제곱은 0.5, 0.25, 0.125 …. 로 되므로 정확한 수를 나타낼 수 없다. 오직 근사치만 표현가능하다. 따라서 for문에 제어변수로 floating을 넣고 +0.1 씩 하면 무한히 반복된다. 비슷한 원리로 sqrt(1+10^-16 – 1) 을 하면 0이 나오고, sqrt(1+10^-16 -1 -10^-16)은 NAN(=error)이 된다.
따라서 컴퓨터는 어떤 수도 (even 0.1) 정확하게 계산하지 못한다. 즉, 모든 수는 approximate이므로 결국 사람이 수학을 계산하여 알고리즘 등 수학적 모델을 개선해야 한다. (우리가 수학을 공부해야 한다)


컴퓨터는 수를 저장하는 bit가 한정되어 있으므로 무한소수를 정확하게 표현할 수 없다. 즉, 근사치에 한계가 존재한다. 사람은 무한소수와 무한의 개념을 이해할 수 있지만 컴퓨터는 사용할 수 있는 메모리의 물리적 한계로 인해 이런 근사치 문제점이 발생한다.


앞서 사용한 과학적 표현법을 이진법으로 나타내면(normalized scientific notation of binary) 1.xxxxx(2) * 2^yyyy가 된다. 이 때 소수점 앞에 오는 숫자는 반드시 1이다. (왜냐하면 2진법에서 올 수 있는 것은 0 또는 1 뿐이므로.)
Floating point standard : 컴퓨터 초기에는 서로 다른 방식으로 수를 표현하여 portability issue가 있었다. 따라서 이를 해결하기 위해 IEEE Std 754-1985 라는 포맷을 사용하게 되었다. 32-bit에서 single precision의 float, 64-bit의 double precision의 double 두가지의 표현법이 있다. 


Floating point format에서 맨 첫 비트는 1이 음수, 0이 양수가 된다. (2의 보수 표현과 floating 표현은 무관) 앞서 xxxxxxx(1 뒤의 소수점 자리들)은 fraction이 된다. Float(fraction 23bit) 표기라면 2^-1부터 2^-23까지 된다. 또한 소수점 앞에 1은 반드시 존재하므로 표현하지 않는다. (floating point 수를 표현할 때 아예 고려하지 않음. 그냥 있다고 치고 계산함) Fraction의 최댓값은 1과 거의 같지만 조금 작은 수이고, 0보다 크거나 같다. 따라서 (1 + .fraction) = significand은 항상 1보다 크거나 같고, 2보다 작게 된다.


Exponent (float 8bit) 자리에는 actual exponent + Bias 값을 저장한다. (excess representation) single에서느 Bias는 127이고 double Bias는 1023이 된다. 이렇게 하는 이유는 exponent part를 unsigned로 남겨두려고 하기 위함이다. 따라서 우리가 실제 저장하고 싶은 값(actual exponent)는 actual exponent = Exponent – Bias가 되고 float에서는 (0~255) actual exponent가 -127~128이 된다.( 0~ 255 – 127 이므로) double에서는 (0~2047) 이므로 actual exponent는 -1023~1024)가 된다. 즉, Exponent 자리에 넣는 비트를 양수로 유지하기 위해서 나타낼 수 있는 범위가 절반으로 줄어드는 것이다.
그리고 exponent의 모든 bit가 1 / 0인 경우는 따로 빼서 처리한다. (special number로 빼놓음)


만약 integer 값을 float/double에 저장하면(5 ->5.0) int와 다르게 저장된다. (앞서 배운 것과 동일하게 저장된다. 왜냐하면 그렇게 해도 소수점 이하를 0을 만들어서 동일한 값을 나타낼 수 있기 때문) 따라서 지속적으로 언급했듯이 컴퓨터에 저장되는 것은 그냥 비트 들이고 의미를 부여하는 것은 프로그래머이다. 즉, 어떤 비트가 있을 때 비트만 보고는 뜻을 알 수 없고, 어떻게 읽느냐에 따라 달라지는 것이다. 동일한 비트더라도 int와 float으로 읽을 때 값이 다르고, 같은 값이더라도 타입에 따라 비트로 나타내면 달라진다. (크기가 4바이트더라도 내부적으로 int 5 와 float 5의 비트는 다르다! Bits have no inherent meaing)


그래서 typecasting을 할 때 conversion이 일어나는 이유이다. (메모리값을 바로 집어넣는게 아니라해당 타입의 변수가 나타내는 값을 넣고자 하는 타입의 변수 값에 맞도록 비트를 바꿔줌)그리고 이 과정은 그냥 메모리의 주소를 복사하여 서로 다른 타입의 변수에 집어넣을 수 있는 memcpy(&a, &b, 4)와 다르다. 

예를 들어 int a와 float b가 있을 때 대입문으로 a=b; 혹은 b=a;를 하면 typecasting이 발생하여 값이 보존되는 동시에 타입이 달라지지만, memcpy를 하면 그 메모리 주소 안에 있는 비트값이 그대로 들어가므로 전혀 다른 값이 된다. (위에서 봤듯이 int와 float는 같은 값이더라도 나타내는 비트가 다르므로) 


어떤 소수를 이진법으로 바꾸는 법 : 소수가 정수가 될 때까지 2를 곱한다. 그리고 나온 정수를 2진법으로 바꾼 뒤, 곱해진 2만큼 나눠주면(2의 음수 제곱을 곱하면)된다.
예를 들어 0.625 *2 =1.25, 1.25 *2 =2.5, 2.5 * 2=5 로 총 3번 곱하여 정수 5가 나왔으므로 0.625=5의 2진법 표시 * 2^(-3)을 하면 된다. 따라서 0.625 = 0.101(2)이 된다는 것을 쉽게 알 수 있다.


이를 통해 앞서 컴퓨터는 0.1도 정확하게 표현할 수 없다는 것의 진의를 알 수 있다. 0.1을 아무리 2와 곱해봐도 이는 절대 정수가 될 수 없다. 따라서 근사치가 된다는 것을 알 수 있다. (무한히 반복되므로 정해진 비트 내에서 나타낼 수 없음. 따라서 discard 되는 부분들이 존재) 그래서 앞서 한 예시들은 2로 나누어떨어져서 정확히 나타낼 수 있는 매우 적은 경우들인데 반해, 대부분의 소수는 2로 나누어지지 않으므로 근사치로 밖에 나타낼 수 없다.

Range of floating point representation.
앞서 언급한 것 처럼, Exponent 값이 모두 0인것과 모두 1인 것은 특별한 값으로 이미 reserve(예약어처럼) 되어 있다.
이런 special case는 4개가 있다. (single / double precision 는 exp 값만 다르고 모두 동일)


1. exponent=0, fraction=0 : 0. Floating point를 나타내는 notation을 생각해보면 1의 자릿수는 항상 1이라고 하였기 때문에 0을 표현할 수 없다. 표현할 수 있는 가장 작은 수는 exponent 자리 값이 1인 1.0*2^(-126)이다. 그렇지만 0을 표현할 방법은 있어야 하므로 특별한 경우로 분리해놓음.
번외로, 가장 큰 수는 exp가 모두 1111….0(마지막 자리만 0인 경우)이므로 약 2*2^127이 된다. (fraction이 모두 1이 되므로 significand가 거의 2가 된다고 생각)


2. exp =255(모두 1인 경우), frac =0 : infinity


3. exp=255, frac !=0 : NaN(not a number = illegal or undefined)


4. exp=0, frac !=0 : denormal number(normalized 되지 않은 숫자) exponent가 모두 0이고 나타나지 않는 hidden bit까지 모두 0이라고 생각하는 것. 그렇지만 fraction이 존재하므로 어떤 값은 있음. 그래서 0에 매우 근접한 아주 작은 숫자의 값 표현할 때 사용. 



Fixed number of bit -> fixed number of numbers. 특정 비트 용량으로 나타낼 수 있는 수의 개수는 반드시 고정되어 있다.(2^비트 개 만큼 표현가능) 그렇기 때문에 오버, 언더플로가 날 수 있고 또한 floating point로 나타낼 수 있는 수의 범위를 생각해보면 중간 중간 비는 부분이 엄청나게 많을 수 밖에 없다는 것을 알 수 있다.(비트 수는 32bit인데 수의 범위가 2^+-127까지 변화하므로) 따라서 floating point format은 0과 가까운 부분은 촘촘하게 표현 가능하지만, +-infinity로 갈수록 나타낼 수 있는 수가 듬성하게  된다. 

이는 notation을 생각해볼 때, 반드시 2^n승만큼 곱해지는 수밖에 표현하지 못하는 것으로 예상할 수 있다. 소수점이하를 나타내는 fraction이 23bit밖에 되지 않으므로 2의 지수가 한번 더 늘어날 때 2^23개보다 많은 숫자가 그 사이에 있다면(x * 2 > 2^32라면) 필연적으로 표현하지 못하는 수가 생긴다. 이런 표현 못하는 수들 중 하나가 바로 0.1인 것이다.


그래서 int가 나타낼 수 있는 범위보다 넓은 범위를 float와 double이 커버하지만 그 사이 비는 값들이 많이 있다. 그리고 double은 비트가 더 많은 만큼 더 넓은 범위이고 더 촘촘하게 저장 가능하다.


Relative precision : fraction이 유효숫자(나타낼 수 있는 가장 작은 unit)가 되므로 모든 bit는 significant하다. single이라면 23bit이므로 2^-23, 즉 약 6개의 decimal digit(0.000001) 정도의 정확도를 가진다. Double은 2^-52까지이므로 약 16개가 된다.

Floating point addition & multiplication
먼저 10진법의 예시를 통해 곱셈을 하는 과정을 알아보자. Scientific notation으로 normalized 된 서로 다른 숫자들은 10^n이 서로 다르므로 앞에 계수를 바로 더할 수 없다. 따라서 자릿수가 가가 큰 것을 기준으로 작은 것의 n을 맞춰주고, 앞의 계수를 더한 다음 언더플로나 오버플로가 나는지 확인하고 다시 normalize / round & renormalize 를 하면 된다. 

중요한 것은 지수표기법이므로 지수의 값을 동일하게 맞춰줘야 한다는 것이다. 이진법도 이와 동일하다.
이런 과정이 필요하므로 float adder 논리 회로는 훨씬 복잡하다. 우리는 integer adder와 분리된 floating point(FP) adder가 필요하다. 그래서 이런 소수점 계산이 필요없는 임베디드 컴퓨터(자판기나 엘베 등)에는 굳이 이것을 추가하지 않는 것이 효율적이다. (복잡한 추가 회로를 안넣는 것이 칩을 소형화하고 싸게 만드는데 이득) 그리고 integer 계산만 되는 회로라도 소프트웨어적으로 복잡하게 구현하면 소수점 계산이 가능하게 할 수 있다. 다만 이럴 경우 프로그램이 복잡해지고 느려진다. 


곱셈의 경우 exponent를 더하고 계수(sigificand)끼리 곱하면 된다. 그리고 normalize/ round /renormalize를 앞과 동일하게 하면 된다. 부호의 경우 operand의 부호를 보고 결정하면 된다. 


이 때 주의할 점은 Actual exponent가 아닌 Exponent를 가지고 계산할 경우, 두 개를 더해버리면 bias가 두번 더해지므로 bias를 한번 빼줘야 원하는 exponent 값을 구할 수 있다는 것이다.

이진법에서 예를 들어보면 unbiased값(actual exponent)이 -1과 -2라면 bias 를 더한 exponent 값은 126과 125가 된다. 따라서 이 값을 이용하여 더할 경우, 127을 한번 빼줘야 actual exponent에다가 bias를 한번만 더한 biased exponent를 구할 수 있다.


곱셈도 덧셈과 동일하게 논리회로가 복잡하고 여러 step을 거쳐야 해서 느리다. 그렇지만 fp arithmetic hardware(floating point 계산 회로)는 사칙연산과 루트, 그리고 float to integer conversion까지 가능하다. 


MIPS는 이 회로를 가지고 있으며 floating point 계산을 위해 분리된 FP register를 $f0~$f31로 가지고 있다. (single precision) 이 register들은 앞서 우리가 봤던 32개의 register set과는 분리된 또다른 register들이다. 그리고 double precision을 하고 싶으면 fp register들을 pair로 묶어 64비트를 나타낼 수 있다. 


Fp instruction은 주로 fp register에서만 돌아간다. 그리고 당연하게도, integer operation은 여기서 돌아가지 않는다. (역도 마찬가지) 그리고 이렇게 추가 레지스터를 구현하는 이유는, 기존의 32개에다가 float 계산을 넣으면 너무 복잡해지고 길어지기 때문에 그냥 따로 빼는 것이 더 효율적이기 때문이다. 


Fp instcution들 뒤에 .s, .d가 붙은 것은 각각 single/double precision. 그리고 lw, sw 등등 뒤에 붙은 c1은 그냥 fp processor를 co-processor 1 이라고 불러서 붙은 것. (거기서 실행하는 명령이므로)

Accurate Arithmetic
Floating point 기준인 IEEE Std 754는 rounding control에 특화되어 있다. 우리가 가지고 있는 비트보다 큰 용량이 필요한 계산을 할 때 혹은 결과 값을 얻기 전에 중간과정에서 더 큰 용량이 필요한 경우가 생긴다. 이를 해결하기 위해 제공하는 추가 공간(extra bit)를 guard, round, sticky라 한다. 내가 가지고 있는 비트 바로 다음 값이 guard, 그다음값은 round, 그다음 값이 sticky이다. 


서로 다른 지수의 값을 더한 후 결과값의 비트가 늘어날 수 있으므로 이런 방법이 필요하다.
이런 rounding option이 중요한 이유는 어떤 데이터 셋을 가지고 있을 때 이런 데이터 셋에 대해 변경을 적용하는 것이기 때문이다. 

Up을 적용하면 전체 데이터셋이 약간 올라갈 것이고, trunc는 0에 가깝게 모이고, away from 0은 0에서 커지는 방향으로 퍼질 것이다 (매우 약간씩) 이렇게 되면 당연하게도 통계적 측정치(var,avg 등)이 바뀐다. 단, Nearest even은 절반은 내려가고 절반은 올라갈 것이므로 통계적 측정치가 거의 비슷하게 유지된다. 


이런 값들이 작아보이지만 대출/예금 이자를 계산하거나 인공지능의 학습 데이터 셋을 선택하는 등에는 큰 영향을 미칠 수 있으므로 중요하다. 따라서 적절한 (혹은 이득이 되는) 방법을 선택하는 것이 중요하다.

Associativity : 결합법칙이 수학적으로 적용되는 operator일지라도 컴퓨터에서는 계산 순서에 따라 결과값이 달라질 수 있다. (x+y)+z != x+(y+z)인 경우. 왜냐하면 앞서 했던 것처럼 매우 크고 작은 수를 계산할 때 중간 결과값이 어떻게 rounding되고 버려지는지에 따라 달렸기 때문이다. 


따라서 계산할 때는 반드시 큰수와 작은 수를 섞어서 계산하면 안된다. 끼리끼리 분리하여 계산해야 한다. 그리고 반드시 해야 할 경우 가장 마지막에 해야 한다. (작은 숫자가 무시될 수 있기 때문)

댓글

이 블로그의 인기 게시물

IIKH Class from Timothy Budd's introduction to OOP

Compiler 9 - Efficient Code generation

Software Engineering 10 - V&V, SOLID principle