[컴퓨터구조 4주차] MIPS, 암달의 법칙, RISC-V
잠시만, 3주차는 어디갔지? 3주차는 추석 연휴라 통으로 쉬었다. 나중에 Make Up 하신다니 일단 쉬어보자(물론 난 추석에 못 쉬었다. 매일 깃헙 정리를 하니 대학을 2개 다니는 느낌)
다음주 월요일(9/30)에는 대면 TA 세션으로 진행된다. Verilog에 대한 기초 강의가 있을 예정
Sep 23(Mon)
MIPS(Million Instructions per Second)
이전 시간의 CPI(Clock per Instruction) 말고, OS에서 성능 비교 시 쓰이는 기준(metrics)으로 MIPS라는 것이 있다. MIPS는 초당 몇백 만개의 명령어가 처리되는 지를 나타내는 지표로
\[\text{MIPS}=\frac{\text{Instruction Count}}{\text{Execution Time}\times 10^6}\]다음과 같이 나타낼 수 있다. MIPS 말고도 초당 부동소숫점 연산 횟수를 나타내는 FLOPS가 있다. 흔히 GFLOPS(giga)나 TFLOPS(tera)로 쓰인다.
그런데 MIPS는 한계가 있다(뭐 소개한지 얼마나 됐다고 벌써?) MIPS는 명령어마다 CPI가 다르다는 것을 고려하지 않는다. 기존에 배운 CPI는 각 케이스별로 CPI값을 달리 저장하거나, 혹은 평균(기댓값 방식)을 사용한 것과 대조적이다.
다음 예를 보자. 클럭 진동수는 500MHz(초당 5억 회 진동)이고 클래스 A, B, C별로 CPI가 1, 2, 3이다. 이 클래스들은 컴파일러 1, 2 두 개로 컴파일될 수 있는데, 각각의 경우에 A, B, C가 사용하는 명령어의 개수가 다음과 같다. 누가 더 성능이 좋을까? 먼저 실행 시간(execution time)을 비교해보자.
컴파일러들에서 클럭이 몇 번씩 돌아가는 지를 계산하자. 컴파일러 1의 경우 각각 A에서 500만 개의 명령어가 1개씩의 사이클(CPI)을, B에서 100만 개의 명령어가 2개씩의 사이클을, C에서 100만 개의 명령어가 3개씩의 사이클을 사용하므로 총 1천 만개의 사이클이 들어간다. 같은 방식으로 컴파일러 2에선 1500만 개의 사이클이 필요하다.
해당 machine의 클럭은 초당 5억 회이므로, 컴파일러 1의 처리 시간은 (천만/5억)으로 20ms가, 컴파일러 2의 실행시간은 (천오백만/5억)으로 30ms가 걸린다. 따라서 컴파일러 2보다 컴파일러 1이 1.5배 빠르다!
그런데 이를 MIPS로 퉁쳐서 계산하면 문제가 발생한다. 컴파일러 1에서 CPI를 고려하지 않고 클래스 A, B, C를 처리하는데 걸리는 MIPS를 계산하면 각각 5백만, 백만, 백만이므로 (7백만/20ms)=350가 나온다. 즉 컴파일러 1은 초당 3.5억 개의 명령어를 처리한다고 계산된다.
컴파일러 2의 경우 각각 천만, 백만, 백만이므로 (천이백만/30ms)=400이 나온다(초당 4억 개의 명령어를 처리). 따라서 MIPS는 컴파일러 2가 더 크므로, 컴파일러 2가 더 좋…아니 뭐가 잘못됐다! 분명 Execution Time은 컴파일러 1이 더 좋은데, 왜 MIPS는 컴파일러 2가 더 좋을까?
상술했듯 MIPS가 명령어의 종류를 구분하지 않기 때문에 발생한 문제이다. 각 명령어마다 구현 전략이 다르기에, 내부에서 처리되는 명령어의 수가 다르고, 이에 따라 CPI도 다 다르다. 그런데 이걸 고려하지 않으니, 더 안 좋은 성능의 컴파일러가 MIPS의 수만 뻥튀기되는 상황이 발생하는 것이다.
Benchmarks
따라서 우리는 CPI든 MIPS든 컴퓨터 성능을 비교하는 데 사용되는 표준 성능 기준을 정립하고 싶다. 그게 Benchmark이다! 벤치마크는 타겟 도메인의 중요한 요인들을 지나치게 단순화하지 않기에(not oversimplify) 많은 기업들에서 채택하는 성능 기준이다. 일례로 SPEC이 있다. 자세한 건 패스(안 중요함)
암달의 법칙(저주)
지금까지 성능을 평가한 이유는 무엇일까? 단순히 양자택일을 위한 성능 비교일 수도 있겠지만, 성능을 평가함으로써 어느 부분을 더 개선해야하는지를 판단할 수 있기 때문이다. 그런데 문제는 성능을 개선한다고 모든 프로세스가 개선되지는 않는다는 것이다.
이를 Amdahl’s Law(암달의 법칙)이라고 한다. 때에 따라 암달의 저주라고 번역하기도 한다. 원문 설명은 다음과 같다.
“Execution speedup is proportional to the size of the improvement and the amount affected.”
즉 성능이 개선되는 것은 프로그램 중 일부(proportional)이고, 나머지의 성능은 그대로 유지되므로, 개선의 영향력은 제한된다는 것이다. 개선된 성능은 실행시간으로 평가할 수 있는데
\[\begin{align}\text{ExTime}_{new}&=\frac{\text{Execution time affected by improvement}}{\text{Amount of improvement}}+\text{Execution time unaffected}\\&=\frac{\text{(개선된 부분의 기존 시간)}}{\text{(개선된 정도)}}+\text{(개선되지 않은 부분의 기존 시간)}\end{align}\]혹은 더 자주 쓰이는 식으로는
\[\text{ExTime}_{new}=\text{ExTime}_{old}\times[1-\text{Fraction}_{enhanced}+\frac{\text{Fraction}_{enhanced}}{\text{Speedup}_{enhanced}}]\]암달의 법칙의 요점은 그저 저주가 발생한다는 것을 알라는 것이 아니라, 따라서 가장 영향력이 큰 Instruction Class부터 개선을 해야한다는 것이다. 예제를 보면 부동소숫점 계산이 2배나 빨라졌음에도, 해당 계산은 전체의 10%만 차지하기에 전체 개선 정도는 5%에 불과함을 확인할 수 있다.
Sep 25(Wed)
ISA(Instruction Set Architecture)
프로그래밍 언어는 고수준의 언어와 저수준의 언어로 나뉜다. 고수준 언어는 하드웨어와 무관하게 어느 machine에서나 돌릴 수 있고, 더 표현력이 풍부하다. 하지만 저수준 언어는 특정 machine에 특정 언어를 사용해야 하고, 0과 1을 사용하는 기계어와 1:1로 표현이 대응된다.
저수준 언어의 대표적인 종류인 어셈블리어(Assembly Language)는 기계어를 텍스트로 표현해둔 것으로(따라서 기계어와 일대일 대응이다) 기계어와 고수준 언어의 중간다리 역할을 해준다. 그보다 낮은 기계어(Machine Language)는 컴퓨터의 모국어(native language)로, 0과 1로 구성되어있다. 그런데 이 어셈블리어와 해당 명령어들을 해석하려면 무엇을 봐야할까?
이때 사용되는 것이 ISA(Instruction Set Architecture)이다. ISA는 하드웨어와 소프트웨어를 연결해주는 일종의 인터페이스로 ADT(추상적 데이터 형태)를 사용한다. ISA를 사용하는 목적은 적은 비용의 하드웨어 구현을 통해 높은 수준의 성능을 이끌기 위해서이다.
구체적으로 ISA는 연산, 즉 명령어 집합을 정의하고 명령어의 동작을 결정한다. 물론 ISA는 어떤 언어를 쓰냐에 따라 다르지만, 우리의 컴파일러가 이를 모두 올바른 어셈블리어로 변환해주기에, ISA를 고려하지않고도 편히 고수준 언어를 사용할 수 있는 것이다.
이 그림은 교수님께서 (마우스로) 그리신 빅픽쳐이다. 위엣것부터 보자. 고수준언어로 작성된 코드는 컴파일러를 통해 이진수(binary)로 이루어진 실행파일(exe)로 컴파일된다. 이 실행파일 안에는 각각의 명령어가 32bit(4바이트)씩의 0과 1로 구성되어있다. 그렇다면 이 exe 파일을 실행시키면 하드웨어는 어떻게 반응할까?
일단 로더가 exe 파일들의 내용을 Instruction Memory로 옮겨 온다. 이때 명령어 당 4바이트라 했으므로 주소도 0, 4, 8, 이런식으로 4의 배수씩 지정된다. 옮겨진 명령어들은 다시 Program Counter라는 register로 순차적으로 이동해 실행되는데, 원래는 PC도 0, 4, 8 이런식으로 주소가 지정되지만, 필요에 의해 (반복/조건문 등을 써서) 순서를 바꿀 수도 있다.
또한 프로세서의 레지스터로 데이터가 fetch되어, 명령어와 함께 실행된다. 이 모든 과정에서 명령어는 ISA에 영향을 받기 때문에, 어떤 target HW에서 프로그램을 돌리려면, 해당 target에 알맞는 ISA를 사용하여야 한다.
RISC-V
본격적으로 ISA를 살펴보자. 가장 대표적인 ISA의 종류로 RISC-V(리스크 파이브)가 있다. 여타 ISA들도 다 RISC 기반이지만, 특히 RISC-V는 open된 free 소스라서 쉽게 그 구조를 파악할 수 있다.
RISC-V를 구성하는 첫번째 요소는 연산(Operations)이다. 연산은 (1) 산술/논리연산과 (2) 데이터 전송, (3) 제어 전송 등의 종류가 있다. 또 두번째 요소로 레지스터(Register)가 있는데, 그 중에서 프로그램 카운터(PC: Program Counter)가 가장 중요하다. 현재 실행 중인 명령어를 지정하는 특별한 레지스터로 기억하면 된다.
이때 레지스터는 메모리보다 더 빠르고, 크기도 작고, 구성 회로도 더 성능이 좋다. 또한 주메모리에서 데이터가 직접 접근이 불가한 반면, 레지스터에서는 쉬이 접근 및 계산이 가능하다. 따라서 많은 계산들을 레지스터 내에서 처리하도록 최적화하는 것이 좋다.
별개로, 데이터 전송에는 레지스터와 주메모리 간의 속도 차이로 인해 성능 악화가 발생한다. 전송이 많을 수록 관여하는 명령어 수도 늘기에, 전송 비중을 줄여 성능을 향상시키기 위해서라도 Register Optimization이 중요하다.
RISC-V를 구성하는 마지막 요소는 Addressing(주소 할당)이 있다. 주로 Immediate value 방식과 value in a register 방식이 쓰이는데, 이것과 관련해서는 나중에 다룰 것이기에 그때 다시 돌아와서 확인해보도록 하자.
전체적인 구조는 1바이트짜리 문자(ASCII로 저장됨)와 4바이트짜리 정수(2의 보수로 저장됨)를 마찬가지로 32bit인 word로 지정하여 각 레지스터의 5비트짜리 주소에 이를 기록해두는 방식이다(내가 이해한 게 맞나? 일단 졸리니 오늘은 여기까지)
출처: 컴퓨터구조(COSE222) ㄱㅇㄱ 교수님