이 글은 Crash Course의 Computer Science를 보고 정리한 글입니다.
컴퓨터는 1초에 1개 정도 계산할 수 있는 기계적인 장치에서부터 킬로 헤르츠나 메가헤르츠의 속도로 동작하는 CPU에 이르기까지 오랜 시간 동안 발전해왔다. 현대의 CPU장치는 거의 다 기가헤르츠의 속도로 동작하고 있다. 매 초 수십억 개의 명령어가 실행된다는 것이다.
전자식 컴퓨터의 초기에는 칩 내부의 트랜지스터의 스위칭 시간을 개선함으로써 프로세서의 속도를 빠르게 했다. 이전에 배운 논리 게이트와 ALU를 포함한 모든 부품들이 트랜지스터들로 구성되어 있다. 그런데 트랜지스터를 빠르고 효율적으로 만드는 것만이 다가 아니다. 프로세서 개발자들은 단순히 명령어를 빠르게 처리하는 것뿐만 아니라, 훨씬 더 복잡한 동작을 통해서 성능을 늘릴 수 있는 다양한 기술을 개발해왔다.
이전 에피소드에서는 프로그램을 통해서 ALU에 없는 기능인 나눗셈을 할 수 있었다. 그런데 이 방법은 많은 클락 사이클이 소비되고, 특별하게 효율적이지는 않다. 그래서 요즘 대부분 컴퓨터 프로세서는 ALU에서 하드웨어로 처리하는 하나의 명령어로 나누기를 갖고 있다.
물론 이런 부가 회로들이 ALU의 설계를 크고 복잡하게 만든다. 하지만 CPU를 더 능력있게 만든다. 컴퓨터 역사에서 복잡도와 속도 사이의 협정(trade off)은 여러 번 있어왔다.
예를 들어, 현대 컴퓨터 프로세서는 특수 회로들을 갖고 있다. 그래픽 처리, 압축 비디오 복원, 파일 암호화 같은 일을 처리하는 것들이다. 이런 모든 기능들은 표준적인 동작들로 수행하려면 어마어마한 클럭 사이클이 필요하다.
MMX, 3DNow나 SSE 기능이 탑재된 프로세서를 들어봤을 수 있다. 이런 프로세서들은 게임이나 암호화 같은 일들을 위해서 추가된 명령어들을 수행할 수 있는 회로들이 같이 들어있는 것이다. 이렇게 명령어 집합의 확장이 계속 이어지고 있다. 확장된 명령어들을 사용해서 프로그램하는데 이득을 한 번 보게 되면, 이것을 없애기는 힘들어진다. 그래서 명령어 집합은 점점 커지는 경향이 있다. 오래된 opcode들과 호환성(Backwards Compatibility)을 유지하면서 말이다.
최초의 진정한 집적 CPU인 Intel 4004는 46개의 명령어를 갖고 있었다. 하지만 현대 컴퓨터 프로세서는 수천 개의 명령어를 갖고 있다. 빠르고 복잡한 내부 회로들을 모두 활용하고 있다. 빠른 클럭 속도와 많은 명령어 집합이 이제 또 다른 문제를 유발한다. CPU로 데이터를 가져오거나 내보내는 것이 충분히 빠를 수 있냐는 것이다. 마치 힘센 증기기관차에 석탄을 빠르게 삽질해서 넣지 못하는 것과 같다. 컴퓨터의 경우, RAM이 병목 지점이다.
RAM은 CPU 밖에 있는 메모리 모듈이다. 즉, 데이터는 버스라고 하는 데이터 선들을 통해서 RAM으로 전달되거나 RAM으로부터 가져와야 한다. 버스의 길이는 몇 cm일수도 있지만, 전기 신호는 빛의 속도로 움직이고, CPU는 기가헤르츠의 속도로 (초당 수십억 개) 작동하기 때문에, 이런 자그마한 지연도 문제를 발생시키게 된다. 게다가 RAM은 주소를 보고 데이터를 찾아와서 출력시키는데 시간이 필요하다. 그래서 "RAM에서 LOAD"하는 명령어는 완료되는데 수십 클락 사이클이 필요할 수도 있고, 이 시간 동안 프로세서는 데이터를 기다리면서 쉬고 있게 된다. 이를 해결하는 방법으로 캐시(cache)라고 하는 작은 RAM조각을 CPU안에 넣는 것이다. 프로세서 칩의 공간이 그리 크지 않기 때문에, 대부분의 캐시는 킬로바이트에서 메가바이트의 크기를 갖는다. 반면에 RAM은 기가바이트를 보통 사용한다.
캐시를 갖게 되면, 스피드를 올릴 수가 있다. CPU가 메모리에 한 위치를 접근할 때, RAM은 전체 데이터 블록 중에 달랑 값 한 개를 전달할 수 있다. 값 1개만 가져오는 것보다 시간이 조금 더 걸릴 수 있지만, 캐시에 데이터 블록 전체를 저장하도록 해보자. 이것은 굉장히 도움이 되는데, 보통 컴퓨터 데이터를 잘 정리돼 있고 순차적으로 처리되기 때문이다. 100번의 데이터를 처리하면 101번에 데이터를 처리하려 할 것이다. 이럴 때, 캐시에 이미 데이터가 있기 때문에 RAM까지 가야 할 필요가 없어진다. 캐시는 프로세서에 딱 붙어 있기 때문에, 데이터를 한 클럭 사이클에 보내줄 수가 있다. 기다릴 필요가 없다. 이 방법이 매번 RAM을 왔다 갔다 할 때보다 무시무시하게 속도를 향상할 수 있다.
RAM에 요청할 데이터가 이미 캐시에 저장되어 있는 경우를 캐시 히트(cache hit)라고 한다. 반대로 캐시에 요청한 데이터가 없는 경우는 RAM까지 가야만 한다. 이것을 캐시 미스(cache miss)라고 한다.
캐시는 길고 복잡한 계산을 할 때 중간 값들을 저장하는 임시 저장소로도 사용할 수가 있다. 그게 저장이 더 빠르고, 나중에 계산이 더 필요하면 더 빨리 접근할 수 있게 된다.
그런데, 이 상황이 문제를 일으킨다. 캐시에 복사된 데이터와 실제 RAM에 있는 데이터가 달라진 것이다. 이렇게 발생한 불일치는 반드시 기록해 두어야지, 특정 시간에 동기를 맞출 수가 있다.
이 목적으로, 캐시는 저장된 각 메모리 블록에 대한 특별한 플래그가 있다. dirty bit라고 한다.
캐시가 꽉 차있고, 프로세서가 새로운 메모리 블록을 요청할 때 동기화가 제일 빈번하게 일어난다. 캐시가 오래된 블록을 지워서 공간을 확보하기 전에 dirty bit를 체크하고, dirty 하면, 새 블록을 옮겨오기 전에 우선 오래된 블록을 RAM에 옮겨 적는다.
CPU 성능을 올릴 수 있는 다른 트릭은 명령어 파이프라이닝(instruction pipelining)이라는 것이다. 작업을 병렬(parallelize)로 처리하여 속도를 올리는 것이다. 이전 에피소드에서 CPU는 fetch-decode-execute 사이클을 순차적으로 수행했다. fetch-decode-execute, fetch-decode-execute, fetch-decode-execute 루프를 계속 돌았다. 즉, 우리 설계는 명령어 하나 수행하는데 3개의 클럭 사이클이 필요하다는 것이다. 그런데 각 단계는 CPU의 서로 다른 부분을 사용한다. 즉 병렬 처리할 수 있는 기회가 있다는 것이다.
명령어 한 개가 execute 되고 있는 동안 다음 명령어를 decode 하고, 그다음 명령어를 메모리에서 fetch 할 수 있다는 것이다.
이런 파이프라인 형태에서, 명령어는 매 클럭 사이클마다 실행되어 이론적으로 처리 속도가 3배가 된다. 그러나 캐시에서 처럼, 여기서도 약간의 문제들이 있다.
첫 번째 큰 위험은 명령어 간의 의존성이다. 예를 들면, 현재 실행 중인 명령어가 막 수정한 무언가를 fetch 할 수도 있다. 그렇다는 건 파이프라인 안에는 이전의 값이 있다는 것이 된다.
이 문제를 보완하기 위해서, 파이프라인 형태의 프로세서는 데이터 의존성을 미리 내다볼 수 있어야 하고, 필요하다면 문제가 발생하지 않도록 파이프라인을 지연시킬 수 있어야 한다. 현대 고사양 프로세서들은 한 단계 더 나아가서, 파이프라인이 지연되는 것을 최소화하고 이동을 유지하기 위해 상호 연관성이 있는 명령어들의 순서를 동적으로 바꿀 수 있다. 이것을 비순차적 (out-of-order) 실행이라고 한다. 이런 모든 일을 수행하는 회로는 엄청나게 복잡하다. 그럼에도 불구하고, 파이프라인은 엄청나게 효과적이어서 오늘날 거의 모든 프로세서는 파이프라인으로 구현된다.
또 다른 큰 위험은 조건부 점프 명령어(Conditional jump instructions)이다. 이전 에피소드에서 얘기했던 JUMP_NEGATIVE 같은 것들이다. 이런 명령어는 상태에 따라서 프로그램의 실행 흐름을 바꿀 수 있다. 단순한 파이프라인 프로세서는 점프 명령어를 만나면 오래 지연될 수 있다. 상태 값의 결정이 완료될 때까지 기다려야 하기 때문이다. 점프의 결과를 알아야지 프로세서는 파이프라인을 다시 채우기 시작한다. 그러나 이런 상황은 긴 딜레이를 만들기 때문에, 고사양 프로세서들은 이 문제들도 해결할 수 있는 몇 가지 트릭을 갖고 있다.
점프 명령어를 갈림길이라고 생각하자. 고급 CPU는 예측 실행(speculative execution)이라는 기술로 어느 길로 갈지를 추측하고, 이 추측을 바탕으로 파이프라인에 명령어들을 채우기 시작한다. 점프 명령어 실행이 완료됐을 때, CPU가 맞게 추측했다면, 지연 없이 진행할 수가 있다. 올바른 명령어들이 파이프라인에 이미 채워져 있을 테니 말이다. 하지만, CPU가 추측한 것이 틀렸다면, 예상했던 결과들을 전부 버리고 파이프라인 플러쉬(pipeline flush)라는 파이프라인을 싹 비우는 일을 한다. 마치 교차로에서 길을 놓쳐서 왔던 길로 유턴해서 되돌아가야 되는 상황인 것이다. 이런 플러쉬에 의한 영향을 최소화하기 위해서, CPU 제조사들은 분기 예측(Branch prediction)이라는 어느 갈림길로 갈지 정교하게 추측할 수 있는 방법을 개발해왔다. 현대 프로세서들은 90% 이상의 정확도로 예측할 수 있다.
이상적인 케이스에서 파이프라인은 매 클럭 사이클마다 하나의 명령어 실행을 완료할 수 있다. 그러나 매 클럭 사이클마다 한 개 이상의 명령어를 실행할 수 있는 슈퍼스칼라(superscalar) 프로세서가 등장한다.
파이프라인 디자인에서도 execute 단계에서 프로세서 전체가 아무 일도 하지 않을 수 있다. 예를 들어 메모리에서 값을 fetch 해야 하는 명령어를 실행하는 동안, ALU는 아무것도 하는 일 없이 그대로 쉬게 된다. 명령어 여러 개를 한 번에 fetch 하고 decode 해 놓고, 가능한 시점에 CPU의 서로 다른 부분을 필요로 하는 명령어들을 동시에 실행하는 것이다. 많이 사용하는 명령어들을 위래서 같은 회로를 추가해서 한 단계 더 나아갈 수 있다. 예를 들어, 많은 프로세서들은 4개, 8개 또는 더 많은 동일한 ALU를 사용해서 많은 수학 연산을 병렬로 수행할 수 있다.
지금까지 알아봤던 기술들은 하나의 명령어 흐름(stream)을 처리하는 속도를 최적화하는 것들이다. 하지만 성능을 높이는 또 하나의 방법은 멀티 코어 프로세서(Multi-Core Processors)를 가지고 여러 개의 명령어 흐름들을 동시에 처리하는 것이다. 듀얼 코어나 쿼드 코어란 말은 하나의 CPU 칩 안에 여러 개의 독립적인 프로세싱 유닛이 들어가 있는 것이다. 많은 측면에서 다수의 독립된 CPU를 갖고 있는 것과 비슷해 보이지만, 멀티 코어는 한 칩에 집적되어 있기 때문에 캐시와 같은 자원을 공유할 수 있고, 코어들이 공통의 계산을 같이 처리하는 것도 가능하다.
코어를 더 사용하는 것만으로 충분하지 않다면, 컴퓨터에 여러 개의 독립된 CPU를 갖고 조립할 수도 있다. 유튜브 데이터 센터에서 비디오를 스트리밍 하는 서버 같은 고사양 컴퓨터들은 수백 명의 사람들이 동시에 시청하더라도 비디오가 부드럽고 매끄럽게 유지되도록 특별한 능력이 필요하다.
요즘은 2개, 4개 프로세서 구성이 가장 일반적이지만, 특별한 목적을 위해서 슈퍼 컴퓨터(Super Computer)를 만들게 된다. 우주의 형성에 관한 시뮬레이션 같은 진짜 괴물 같은 계산이 필요하다면, 엄청나게 심각한 양의 컴퓨팅 파워가 필요할 것이다. 데스크톱의 몇 개의 프로세서를 추가하는 것으로 해결할 수 없다. 2017년 세계에서 가장 빠른 컴퓨터는 중국 우시에 있는 National Supercomputing Center에 있다. The Sunway Taihulight는 40,960개의 CPU를 갖고 있고, 각 CPU는 256개 Core를 포함한다. 즉, 총 천만 개 이상의 코어가 있고, 각 코어는 1.45 GHz로 동작한다. 이 머신은 93 Quadrillion FLOPS 부동소수점 연산을 할 수가 있다. (Quadrillion=천조, 십억 개가 백만 개 | FLOPS=초당 처리 가능한 부동소수점 연산 개수)
'컴퓨터공학 > 기초' 카테고리의 다른 글
최초의 프로그래밍 언어 (0) | 2022.08.03 |
---|---|
초기의 프로그래밍 (0) | 2022.08.03 |
명령어와 프로그램 (0) | 2022.07.31 |
중앙 처리 장치 (CPU) (0) | 2022.07.30 |
레지스터와 RAM (0) | 2022.07.30 |
댓글