레이블이 Computer System인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Computer System인 게시물을 표시합니다. 모든 게시물 표시

일요일, 11월 05, 2017

[C/C++] 고정 소수점의 모든 것 (All about Fixed Point)

일요일, 11월 05, 2017
1. 개요
이 글에서는 일반적으로 널리 사용되는 부동 소수점(Floating Point)과 달리 다소 생소한 고정 소수점(Fixed Point)에 대해 알아본다. 일부 프로그래밍 언어의 경우 고정 소수점 방식을 기본적으로 제공해주기도 하지만, 많은 경우 실수 표현에 있어서 부동 소수점 방식을 기반으로 하기 때문에 고정 소수점 방식을 사용하고자 한다면 직접 구현하거나, 외부 라이브러리를 통해 사용해야 한다는 불편함이 있다. 이 글은 C++ 언어를 사용하여 고정 소수점을 직접 구현해보면서 깊은 이해를 하는 것을 목표로 한다.
우선 고정 소수점의 정의부터 간단히 정리한 후에 부동 소수점 방식의 수를 고정 소수점 방식으로 변환(Floating Point to Fixed Point Conversion)하는 법, 그리고 고정 소수점 방식에서의 사칙연산(Arithmetic Operations)에 대해서 코드와 함께 살펴보도록 하겠다. 이후 고정 소수점 방식을 사용하는 이유나 소수점의 위치를 설정하는 방법 등 심화적인 내용에 대해 다룰 것이다. 이 글에서 설명에 사용된 모든 코드는 깃허브(Github)에 업로드되어 있으므로, 전체적인 코드를 보고 싶다면 한 번 살펴보길 권한다.

2. 고정 소수점의 정의
고정 소수점은 쉽게 말해서 특정 숫자의 소수점의 위치를 말그대로 고정하는 방식을 뜻한다. 컴퓨터 언어에서의 데이터 타입은 항상 최대 길이가 고정되어 있기 때문에 이러한 아이디어가 생긴 것이라고 생각할 수 있다. 예를 들어, 32-bit 운영체제에서 int형의 크기는 32비트이므로 앞의 10개의 비트는 정수 부분을 표현하도록 하고, 나머지 22개의 비트는 소수점 이하의 부분을 표현하도록 할 수 있다. 소수점 위치를 어디로 설정해야 하는지에 대해서는 정해진 답이 없다. 사용자의 임의대로 설정하면 된다. 다만, 사용하는 수의 범위를 감안하여 오버플로우(Overflow)가 발생하지 않도록 설정해야 한다. 이와 관련된 내용은 뒤에서 더 자세히 다루도록 하겠다.
이 글에서는 연산의 효율성을 위해 이진법을 기반으로 하면서 부호가 있는 고정 소수점을 기준으로 한다. 음수를 표현할 때는 2의 보수(2's Complement)를 사용한다. 따라서 그림에서 볼 수 있듯이 가장 왼쪽의 1비트(MSB)는 부호를 표현하기 위한 비트(Sign)로 사용된다. 그리고 IWL(Integer Word Length)은 정수 부분을 표현하는 비트의 수를 의미하며, FWL(Fractional Word Length)은 소수점 이하 부분을 표현하는 비트의 수를 의미한다. 전체 비트의 수를 WL(Word Length)이라고 할 때, FWL = WL - 1 - IWL이 성립하므로, 고정 소수점을 정의하기 위한 Parameter는 WL과 IWL 두 가지만 있으면 된다. 즉, 전체 길이가 WL비트이면서 정수 부분에 IWL비트를 사용하는 고정 소수점의 경우 (WL, IWL)와 같은 투플(Tuple)로 간단히 정의할 수 있다. 이 글에서는 편의상 F(WL, IWL)로 표기한다. 위 그림의 경우 F(16, 3)에 해당한다.

3. 부동 소수점에서 고정 소수점으로의 변환
고정 소수점을 제대로 구현하려면 우선 부동 소수점의 기본 형식부터 자세히 살펴볼 필요가 있다.  IEEE754의 표준을 따르는 32비트의 float 데이터 타입은 다음과 같은 정보를 담고 있다.
맨 왼쪽의 1비트는 부호(Sign)를 나타내는데 사용되며, 이어지는 8비트는 지수부(Exponent)를, 그리고 나머지 23비트는 실제 숫자들(Mantissa)을 표현하는데 사용된다. 이는 실수를 표현할 때 다음과 같이 정수 부분이 한 자리인 소수(가수)와 정수의 거듭제곱의 곱 형태로 나타낼 수 있다는 아이디어를 바탕으로 하는 것이다.
이 예시는 10진법을 기반으로 한 부동 소수점 방식으로 0.123을 표현한 것이다. 그런데, IEEE754의 float은 2진법을 기반으로 한다. 이는 (밑) 부분이 10이 아니라 2가 됨을 의미한다.
그렇다면, float의 세 가지 영역에 대해 자세히 살펴보도록 하자. 우선 맨 앞의 Sign 비트는 1일 경우 음수를 나타내고, 0일 경우 양수를 나타낸다. Exponent는 말그대로 (지수) 부분을 나타낸다. 단, 주의해야 할 점은 실제 (지수) 값에 127이라는 Bias를 더한 값을 저장하고 있다는 것이다. 예를 들어, 지수가 3인 경우 float의 Exponent 부분을 출력해보면 130이라는 값이 나온다. 따라서 실제 지수 값을 얻으려면 Exponent에서 127을 빼주면 된다. 마지막으로 Mantissa는 (가수) 부분을 나타낸다. 이진수의 경우 이 (가수) 부분에서의 정수 부분은 항상 1이 된다. 즉, 무조건 1.xxx와 같은 형태로 나타난다. 가장 왼쪽에 위치한 1을 기준으로 정수 부분이 한 자리수가 되도록 만들어주기 때문이다. 이러한 특성을 이용해 1비트를 절약하기 위하여 Mantissa에는 소수점 아래의 값들만 저장한다. 예를 들어, 1.11011이라는 수의 Mantissa는 맨 왼쪽의 1을 제외한 11011이 된다. 고정 소수점으로 변환할 때 이 점을 주의해야 한다.
이 정보를 바탕으로 고정 소수점으로 변환해주는 함수를 만들도록 하자. 그런데, 구현에 앞서 한 가지 짚고 넘어가야 할 것이 있다. 단순히 특정 위치에 있는 비트를 읽어서 처리를 하면 간단할 것 같지만, C++에서는 float으로 선언한 변수에 대해 비트 연산(Bitwise Operation)을 사용할 수 없다. 물론, 특정 위치의 비트에 접근조차 불가능하다. 하지만, 이는 기본으로 제공되는 ieee754.h 헤더파일을 통해 해결이 가능하다. ieee754.h에는 IEEE754 표준을 따르는 float의 Sign, Exponent, Mantissa 부분에 접근할 수 있도록 해주는 ieee754_float이라는 union이 정의되어 있다. 이를 통해 원하는 영역의 비트들을 int형으로 받아올 수 있다. (참고: https://stuff.mit.edu/afs/sipb/project/merakidev/include/ieee754.h)
앞서 고정 소수점의 정의 부분에서 언급했듯이, 이 글에서는 이진법을 기반으로 하면서 2의 보수법을 통해 부호를 표현하는 고정 소수점을 구현한다.
#include <stdio.h>
#include <ieee754.h>
#define SIGN 1
#define EXPONENT 8
#define MANTISSA 23
#define EXP_BIAS 127
#define INT_SIZE 32
 
typedef short fix16;
typedef char fix8;
 
int fix(float f, int wl, int iwl) {
    ieee754_float standard;
    standard.f = f;
 
    int ret = standard.ieee.mantissa | (1 << MANTISSA);
    int exp = standard.ieee.exponent - EXP_BIAS;
    int fwl = wl - iwl - 1;
    int fraction = MANTISSA - exp;
    int filter = (1 << wl) - 1;
 
    if(fraction > INT_SIZE) {
        ret = ret >> fraction - INT_SIZE;
        fraction = INT_SIZE;
    }
 
    ret = ret >> fraction - fwl;
 
    if(standard.ieee.negative) {
        ret = ~ret + 1;
    }
 
    return ret & filter;
}
fix 함수는 float형의 실수와 WL, IWL을 인자로 받아서 32bit의 int형 값을 반환한다. 유효한 비트들이 오른쪽으로 쏠려있기 때문에, 원하는 WL에 따라 작은 크기의 자료형(fix8, fix16 등)으로 형변환하여 사용하면 된다.
float에서 fix로 변환을 하면서 중요한 점은, float이 2의 보수법을 사용하지 않는다는 점이다. 따라서 음수의 경우 2의 보수법을 적용해주기 위해 코드의 마지막 부분에 standard.ieee.negative를 확인하여 처리하는 것을 볼 수 있다.
이 함수를 통해 3.4567을 F(16, 3)와 F(8, 3)로 변환하면 약간의 오차가 있지만 유사한 값이 출력되는 것을 볼 수 있다. 정수 부분이 4자리인 이유는 맨 왼쪽의 비트가 부호를 표현하는데 사용되기 때문이다. F(16, 3)보다 F(8, 3)이 오차가 더 큰 것을 확인할 수 있다.
부호만 바꿔서 -3.4567을 고정 소수점 방식으로 변환하면 역시 유사한 값이 나오는 것을 확인할 수 있다. 한 가지 주의할 점은, 2의 보수법으로 음수를 표현할 때 정수 부분과 소수점 이하 부분을 따로 생각하지 않고 한 몸으로 생각한다는 것이다. 즉, 이 두 부분을 합쳐서 하나의 int형 값이라고 생각하고 2의 보수법을 적용하는 것이다. 따라서, 정수 부분의 이진수가 1100인 것을 보고 십진수로 표현하면 -4이기 때문에 틀렸다고 단정지으면 안된다. 결과로 나온 위의 두 이진수들을 더해서 0이 나오는 것을 확인하면 이러한 의문이 해결될 것이라고 생각한다.

4. 고정 소수점의 사칙연산
고정 소수점 방식의 꽃은 바로 사칙연산의 효율성이다. 앞선 예시를 통해 짐작했겠지만, 2의 보수법을 적용한 고정 소수점의 경우 int형의 값들과의 사칙연산 과정이 동일해진다. 복잡한 실수의 사칙연산이 단순한 정수의 사칙연산으로 간소화되는 것이다.
같은 IWL을 가진 고정 소수점끼리의 덧셈과 뺄셈은 정수의 덧셈과 뺄셈과 완전히 동일하다. 따라서 따로 추가적인 구현을 할 필요가 없다. 그러나, 곱셈과 나눗셈의 경우는 그렇지 않다. 곱셈과 나눗셈의 과정에서는 16비트 이상의 공간을 사용해야 값의 손실없이 정확한 결과를 얻을 수 있다. 정확히 말하자면, A * B 또는 A / B의 경우는 A와 B 각각의 전체 길이를 합한 만큼의 공간이 필요하다. 따라서, 이 글에서 구현한 fix16의 경우는 16 + 16 = 32비트 크기의 int형 변수를 여분의 공간(Extra Space)으로 활용하여 연산을 진행한다.
이진수의 곱셈과 나눗셈을 하나하나 뜯어보면 다소 복잡하지만, 결국 가장 중요한 것은 결과값의 소수점의 위치이다. 이것에 집중하면 간단하게 고정 소수점 사칙연산을 구현할 수 있다.
이 글에서는 같은 IWL을 가진 고정 소수점끼리의 사칙연산을 가정한다. 그렇다면 피연산자들의 FWL도 같게 되는데, 이를 L이라고 하자. 두 개의 고정 소수점 수 A와 B에 대한 사칙연산을 수행한다고 할 때, 결과값도 역시 FWL이 L인 고정 소수점 수가 나와야 한다. A와 B에서 소수점을 찍지 않은 상태의 정수 값을 각각 a와 b라고 하면 다음과 같이 고정 소수점 수를 분수로 표현할 수 있다.
구현의 특성상 고정 소수점 방식의 소수점은 실제로 찍는 것이 아니라, 가상의 점이기 때문에 이러한 표현은 유용하다. 예를 들어, 1.011이라는 이진수가 있다면 메모리에는 1011이라는 정수만 저장된다. 즉, 앞서 구현한 fix8 또는 fix16형 변수에 저장되는 실제 값은 정수인 a와 b인 것이다. 정수 부분의 길이가 1이라는 것은 사용자가 임의로 정하는 것(IWL)이기 때문에 별도로 표시되지 않는다. 따라서 1011과 같은 정수의 사칙연산으로 문제가 바뀌게 되는 것이다.
이 분수 표현을 바탕으로 사칙연산에 대한 각각의 식을 세워보자. 연산의 결과도 FWL이 L이어야 하므로, 분모는 항상 2의 L승으로 유지해야 한다는 것에 유념한다.
알다시피 덧셈과 뺄셈은 정수의 경우와 동일하게 진행하면 되지만, 곱셈과 나눗셈의 경우 추가적인 연산이 필요하다. 곱셈의 경우는 a와 b를 곱한 결과에 2^L을 나누어주면 되고, 나눗셈의 경우는 a에 2^L을 곱한 뒤 b로 나누어주면 된다.
2의 지수승을 곱하거나 나누는 행위는 시프트 연산(Bitwise Shift Operation)를 통해 간단히, 그리고 효율적으로 수행할 수 있기 때문에 곱셈과 나눗셈은 다음과 같이 구현하면 된다.
int mul_fix(int a, int b, int wl, int iwl) {
    int c = a * b;
    return c >> wl - 1 - iwl;
}
int div_fix(int a, int b, int wl, int iwl) {
    int c = a << wl - 1 - iwl;
    return c / b;
}

5. 고정 소수점의 장단점
고정 소수점의 장점은 크게 두 가지가 있다. 첫 번째는 연산 효율성이 높다는 것이다. 앞서 살펴봤듯이, 실수 사칙연산 문제가 정수 사칙연산 문제로 바뀌기 때문에 연산에 요구되는 시스템의 자원이 줄어들게 된다.
두 번째는 적은 수의 비트를 사용한다는 것이다. 이는 특히 최근 유행하는 뉴럴 네트워크(Neural Network) 기반의 딥 러닝(Deep Learning)에서 빛을 발한다. 딥 러닝에서는 무수히 많은 수의 float형 실수를 Parameter로 사용하기 때문에, 항상 메모리 문제에 시달리게 된다. 예를 들어, 특정 Layer의 Weight 행렬의 크기가 512 x 512라면, 행렬 연산을 위해 메모리 상에 32 x 512 x 512 = 8388608 = 1GB의 공간을 확보해야 한다. 만약 8비트의 고정 소수점 방식을 사용한다면, 이 크기를 4분의 1로 줄일 수 있다. 이러한 강점은 특히 임베디드 시스템처럼 하드웨어 자원이 한정된 환경에서 유용하다.
단점은 역시 수를 표현함에 있어서 정확도가 떨어진다는 것이다. float형에 비해 적은 범위의 수만 제한적으로 표현이 가능하기 때문에 정확도를 요구하는 문제에서는 적합하지 않다.
하지만, Qiu, Jiantao, et al. "Going Deeper with Embedded FPGA Platform for Convolutional Neural Network"에 의하면, CNN을 활용한 ImageNet 이미지 분류같은 복잡한 딥 러닝 문제에서 적은 비트 수의 고정 소수점을 사용하고도 정확도 손실이 그다지 크지 않다는 점이 인상적이다. 거의 절반 혹은 4분의 1의 Bandwidth만 사용하면서 정확도의 손실을 최소화할 수 있는 F(WL, IWL)을 사용하여 메모리 부담을 덜 수 있다.

6. 고정 소수점 위치 설정
고정 소수점의 위치는 사용자의 임의대로 Static하게 설정하는 것이지만, Overflow의 발생을 최소화하고 정확도를 최대한 높이기 위해서 Dynamic하게 자동으로 설정할 수도 있다. 이를 Dynamic-precision Data Quantization이라고도 한다. 이 방법은 사용할 실수들의 값을 미리 다 알고 있다는 가정 하에 이루어진다. 간단한 두 가지 방법을 살펴보자.
첫 번째 방법은 모든 수를 검토해서 정확도의 손실이 최소화되도록 고정 소수점 수로 변환하는 것이다.
사용하고자 하는 비트 수는 WL로 고정되어 있다고 가정하고, N개의 float형 실수 x가 주어졌을 때 IWL 값을 변경해보면서 정확도의 손실이 최소가 되는 IWL을 찾으면 된다. 단, 이 방법은 부동 소수점과 고정 소수점간의 연산을 별도로 구현해야 한다는 단점이 있다.
이를 보완하기 위한 두 번째 방법을 생각해보자. 고정 소수점의 위치를 설정할 때는 사용할 데이터의 정수 부분의 범위, 즉, IWL에 주목하면 된다. 정수를 Overflow 없이 모두 표현할 수 있는 IWL의 최솟값이 가장 이상적이다. IWL이 작을수록 FWL이 커져서 소수 부분을 더욱 정밀하게 표현할 수 있기 때문이다.
이렇게 각각의 실수 중 가장 정수 부분의 수가 큰 값을 구한 뒤 Log를 취하여 IWL로 설정하면 좀 더 효율적이고 직관적으로 최적의 고정 소수점 위치를 얻을 수 있다. 이를 간단하게 코드로 구현하면 다음과 같다.
#include <math.h>
int optiwl(float floats[], int len) {
    int max, iwl = 0;
   
    for(int i = 0; i < len; i++) {
        int n = floats[i];
   
        if(n < 0) n *= (-1);
        if(max < n) max = n;
    }
   
    if(max) iwl = log2(max) + 1;
   
    return iwl;
}

7. Source Code
https://github.com/arkainoh/fixedpoint

8. References
https://en.wikipedia.org/wiki/Fixed-point_arithmetic
http://bab2min.tistory.com/183
http://www.puntoflotante.net/FLOATING-POINT-FORMAT-IEEE-754.htm
Qiu, Jiantao, et al. "Going deeper with embedded fpga platform for convolutional neural network." Proceedings of the 2016 ACM/SIGDA International Symposium on Field-Programmable Gate Arrays. ACM, 2016.

토요일, 10월 15, 2016

ISA(Instruction Set Architecture와 MIPS Instructions

토요일, 10월 15, 2016

1. ISA란?
ISA(Instruction Set Architecture)라는 것은 하드웨어와 low-level 소프트웨어 사이의 추상화 인터페이스(Interface)이다. 쉽게 말해 OS와 HW 사이의 layer에 위치해 양자 간의 의사소통 방법을 제공해주는 약속이라고 보면 된다.
ISA는 CPU 프로세서가 읽고 이해하여 실행할 수 있는 기계어로된 명령어의 집합인데, 이를 좀 더 사람이 읽을 수 있도록 가독성을 높인 것이 어셈블리어(Assembly language)이다.

2. Instruction이란?
Instruction은 컴퓨터가 수행하는 일종의 일의 단위이다. 32 bit의 MIPS CPU를 기준으로 세 가지의 다른 format이 존재한다. R format, I format, J format이 그에 해당한다.
먼저 R format의 Field들은 opcode(연산의 종류), rs(첫 번째 Source operand의 Register number) rt(두 번째 Source operand의 Register number), rd(결과가 저장될 목적지의 Register number), shamt(Shift amount), funct(Function code)로 이루어져 있다.

각각의 Field는 다음과 같은 수의 bit를 사용한다.
R format
op: 6 bit
rs: 5 bit
rt: 5 bit
rd: 5 bit
shamt: 5 bit
funct: 6 bit
(총 32 bit)
그리고 I format의 경우는 뒤의 절반 16 bit를 Immediate라는 값으로 사용한다. 이 Immediate는 일종의 상수같은 개념인데, Operand를 모두 Register로부터 불러와서 사용하는 R format과는 달리, Instruction 내부에서 값 자체를 제공하는 것이다. 따라서 하나의 Operand를 레지스터로부터 받고, 16 bit의 Immediate 값과 연산을 수행하는 방식으로 동작한다.

각각의 Field는 다음과 같은 수의 bit를 사용한다.
I format
op: 6 bit
rs: 5 bit
rt: 5 bit
imm: 16 bit
(총 32 bit)
rd가 없어서 혼동이 될수도 있는데, 이항연산의 경우 rs와 imm의 값을 Operand로 사용하여 연산을 하고, 그 결과를 rd대신 rt에 저장하는 형태로 편법(?)을 사용한다. 그리고 또한 Immediate filed가 16 bit밖에 없는 점을 보아 4 Byte (32 bit) 의 Word를 사용하는 체계에서 상수의 값의 범위가 제한되어 버리지 않나하는 의문점도 생길 수 있다.
32 bit의 상수를 사용하려면 두 개의 Instruction을 사용하면 된다. lui(Load upper immediate)라는 Instruction과 ori(Or immediate)라는 Instruction을 연달아 사용하면 32 bit의 상수도 문제없이 읽어들일 수 있다.
우선 읽고자 하는 상수를 16 bit씩 절반으로 쪼갠 뒤에, 앞쪽(MSB쪽)의 16 bit에 해당하는 부분을 lui instruction을 통해 Immediate 값으로 제공하여 특정 Register에 넣어두고, 나머지 뒤쪽(LSB쪽)의 16 bit에 해당하는 부분을 Immediate 값으로 제공하여 앞서 사용한 Register의 값과 ori 연산을 수행하면 된다.
예를 들어, 0x1234_5678이라는 32 bit 의 상수를 $t0에 저장하려고 한다면,
lui $t0 0x1234
ori $t0 $t0 0x5678
이렇게 하면 된다. 그러면 처음 lui Instruction을 수행했을 때 $t0 <= 0x1234_0000, 그리고 ori 연산을 할 때 $t0 Register에서는
   0x1234_0000
| 0x0000_5678
---------------------
   0x1234_5678
이런식으로 연산이 진행된다. 두 번째 0x5678을 불러올 때 Sign extension이 아니라 Zero extension이 된다는 점에 유의하자.

그리고 마지막으로 J format은 Jump 관련 Instruction을 위해 사용되며, Field 구성은 매우 간단한 형태로 되어 있다.
J format
op: 6 bit
jump target: 26 bit

3. Appendix
어셈블리어를 통해 앞서 살펴본 것들 외에 MIPS의 여러 Instruction에 대한 예시들을 살펴보자.

add rd rs rt
rs와 rt의 값을 더해서 rd에 저장한다.
0 rs rt rd 0 32 (R format)

add rd rs rt
rs의 값에서 rt의 값을 빼서 rd에 저장한다.
0 rs rt rd 0 34 (R format)

addi rt rs imm
rs의 값과 imm 값을 더해서 rt에 저장한다.
8 rs rt imm (I format)

and/or/nor rd rs rt
rs의 값과 rt의 값으로 비트수준의 논리 연산(and/or/nor)을 수행한 뒤 rt에 저장한다.
0 rs rt rd 0 39 (R format)

andi/ori rt rs imm
rs의 값과 imm 값으로 비트수준의 논리 연산을 수행한 뒤 rt에 저장한다.
13 rs rt imm (I format)

sll/srl/sra rd rt shamt
rt의 값을 shamt 값만큼 shift 시킨 뒤에 rd에 저장한다.
sll은 Logical shift left, srl은 Logical shift right, 그리고 sra는 Arithmetic shift right를 뜻한다.
0 0 rt rd shamt 3 (R format)

sllv/srlv/srav rd rt rs
rt의 값을 rs에 저장된 값만큼 shift 시킨 뒤에 rd에 저장한다.
v가 붙은 것은 variable shift임을 뜻한다. 즉, Shift amount를 상수로 전달하기에는 값의 범위에 제한이 크기 때문에, Register에 저장되어 있는 32 bit의 값만큼 shift를 시키겠다는 뜻이다.
0 rs rt rd 0 7 (R format)

lw rt imm(rs)
rs에 저장된 값을 base address로 하여 imm값만큼의 offset을 더한 뒤, 이를 메모리 주소로 사용하여 메모리의 해당 위치에 저장되어 있는 값을 rt 레지스터에 불러온다. 즉, 메모리에서 레지스터로의 데이터 전송이다.
35 rs rt imm (I format)

sw rt imm(rs)
lw Instruction과 반대로, rt에 저장되어 있는 값을 메모리에 저장한다. 레지스터에서 메모리로의 데이터 전송이다.
43 rs rt imm (I format)

lb rt imm(rs)
Load byte Instruction이다. 메모리로부터 레지스터에 1 byte(8 bit)만큼의 데이터를 불러온다. 이때 signed 방식이기 때문에 불러온 8 bit 값은 레지스터의 LSB 끝쪽에 붙어서 저장되고 앞의 24 bit는 Sign extension이 이루어진다.
32 rs rt 1 (I format)

lbu rt imm(rs)
Load byte의 unsigned 버전이다. Zero extension이 된다는 것 외에는 lb와 똑같다.
36 rs rt 1 (I format)

sb rt imm(rs)
Save byte Instruction이다. sb와 반대로 레지스터(rt)에 있는 값 중 LSB 쪽의 8 bit를 base address + offset (rs + imm)에 위치한 메모리 주소에 저장한다. 해당 메모리 주소의 Word 중 1 byte만 건드린다. 나머지 3 byte는 전혀 건드리지 않기 때문에 값의 변화가 없다.
40 rs rt imm (I format)

lui rt imm
Load upper immediate Instruction이다. imm값을 레지스터의 MSB쪽 16 bit에 저장한다. 나머지 16 bit는 0으로 채워진다. 다른 I format Instruction과 다르게 인자가 두 개만 주어지는데, rs가 생략된 것이다. rs 부분엔 어떤 값이 들어오든 상관이 없다.
15 _ rt imm (I format)

beq/bne rs rt label
Branch on equal, Branch on not equal Instruction이다. rs와 rt가 다른 I format Instruction들과 달리 순서가 바뀌어 있는 것에 유의해야 한다. rs와 rt의 값이 같을 경우 label에 해당하는 곳으로 이동한다. label이란 C언어에서 goto 연산자와 함께 쓰이는 label과 대동소이하다. 예를 들어 bne $t0 $t1 skip 이라는 Instruction이 수행되면, $t0과 $t1의 값을 비교하여 다를 경우 skip이라고 지정된 부분으로 이동한다. 조건문이나 반복문 등에 폭넓게 사용된다.
4 rs rt label (I format)

slt/sltu rd rs rt
Set on less than Instruction이다. slt는 signed, sltu는 unsigned 버전이다. rs의 값이 rt의 값보다 작을 경우 rd의 값을 1로 셋팅한다. 만약 조건이 만족되지 않으면 0으로 셋팅한다.
0 rs rt rd 0 42 (I format)

slti/sltiu rt rs imm
slt Instruction의 I format 버전이다. Register에 저장되어 있는 값이 아닌 imm값과의 비교를 수행한다.
10 rs rt imm (I format)

4. References
http://www.mrc.uidaho.edu/mrc/people/jff/digital/MIPSir.html

컴퓨터의 메모리 계층 구조와 레지스터 파일

토요일, 10월 15, 2016

컴퓨터는 기본적으로 메모리 계층 구조를 가지는데, CPU - 캐시 - 메인 메모리 - 디스크 순서로 처리 속도가 빠르다. 실제 파일 등의 데이터는 물리적인 디스크 안에 저장이 되어 있는데, 컴퓨터를 뜯어보면 알 수 있다시피 하드 디스크는 일종의 레코드판과 같은 구조를 가지고 있다. 어떤 파일에 대해 읽기/쓰기 작업을 수행하려면 디스크 암(Arm)이 찾고자 하는 파일이 위치한 섹터가 위치한 실린더로 헤드를 움직여야 하기 때문에 탐색 시간(Seek time)이 소요되며, 디스크 자체도 회전을 하면서 필요한 섹터를 그 헤드에 위치시켜야 하므로 회전 지연(Rotation delay)이 발생한다. 게다가 디스크로부터 읽어들인 데이터를 운영체제 측에 넘겨주어야 하므로 전송 지연(Transfer delay)도 또한 발생한다.
따라서 요약을 하면 디스크에 대한 접근시간(Access time)은 다음과 같이 나타낼 수 있다.
Access time = Seek time + Rotation delay + Transfer delay
즉, 수많은 연산을 처리하기 위해서 이렇게 매번 디스크에 접근을 한다면 매우 비효율적이다. 따라서 컴퓨터는 내부적으로 더 빠른 장치로 데이터를 옮겨서 처리하려는 경향이 있다. 이러한 과정을 흔히 캐싱(Caching)이라고 한다. 물리 디스크에 있는 데이터를 메인 메모리에 올리고, 그 메인 메모리에 있는 데이터를 캐시로 불러와서 처리하는 식의 과정을 거친다. 하드웨어의 성능에 있어서 위 그림의 higher level로 갈수록 속도가 비약적으로 증가하기 때문에 소프트웨어의 관점에서도 알고리즘이나 프로그램을 짤 때 '캐시 활용률'을 중요하게 생각한다.
CPU core의 내부에는 심지어 메인 메모리와 캐시보다 더 빠른 하드웨어가 내장되어 있는데, 그것이 바로 레지스터 파일(Register file)이다. 제조사마다 다르지만 MIPS CPU의 경우, 이 Register file은 32 bit 워드 체계를 기준으로 32 bit 짜리 레지스터(Register) 32개와 PC(Program Counter)로 이루어져 있다. 각각의 Register는 32 bit 짜리 데이터를 담을 수 있는 일종의 저장소이며, 산술이나 논리 연산 등 아주 간단한 연산을 할 때 활용된다. 처리속도가 매우 빠르기 때문에 컴파일러(Compiler)가 프로그램을 컴파일할 때 이 Register들을 적극적으로 활용한다. 조사한 바에 따르면, 각 저장 장치들에 대한 access time을 CPU의 cycle 수로 측정했을 때 대략적으로 다음과 같은 수치가 나온다고 한다.
1 cycle to read a register
4 cycles to reach to L1 cache
10 cycles to reach L2 cache
75 cycles to reach L3 cache
200 cycles to reach main memory
그리고 instruction들을 수행할 때도 cycle을 매우 적게 잡아먹는다. MIPS의 CPU는 다음과 같이 거의 3~5 cycles 정도가 소모된다고 한다.
Load (5 cycles)
Store (4 cycles)
R-type (4 cycles)
Branch (3 cycles)
Jump (3 cycles)


Register file의 구조는 위의 그림과 같이 생겼다. 주소를 통과시키는 선은 5 bit, 그리고 데이터를 통과시키는 선은 32 bit를 사용하는데, 디코더(Decoder)를 떠올려보면, 5 bit로 32개의 decimal number를 가리킬 수 있기 때문이다. 즉 src1 addr, src2 addr, dst addr은 모두 Register의 number를 가리킨다. 흔히 instruction을 수행할 때 "몇 번 레지스터에 있는 값을 불러와서 연산을 하고 몇 번 레지스터에 저장해라~"라는 식으로 진행이 되기 때문에 이렇게 Register number로 indexing을 하면 매우 편리하다.
이 Register file을 Verilog 코드로 간단하게 구현하면 다음과 같다.
module regfile(input         clk,
                      input         we,
                      input  [4:0]  ra1, ra2, wa,
                      input  [31:0] wd,
                      output [31:0] rd1, rd2);
  reg [31:0] rf[31:0];
  // three ported register file
  // read two ports combinationally
  // write third port on rising edge of clock
  // register 0 hardwired to 0
  always @(posedge clk)
    if (we) rf[wa] <= wd;
  assign rd1 = (ra1 != 0) ? rf[ra1] : 0;
  assign rd2 = (ra2 != 0) ? rf[ra2] : 0;
endmodule

Reference & Source:
Computer Organization and Design by David Patterson and John Hennessy, 5th edition, Morgan Kaufmann, 2013
Lecture 4. MIPS & MIPS Instructions #1, Prof. Taeweon Suh, Computer Science & Engineering, Korea University