컴퓨터 구조
CPU가 기계어 한 줄을 어떻게 실행하는지에서 출발해 파이프라인, 분기 예측, 비순차 실행, 캐시, 캐시 일관성, 메모리 순서, 전력 벽까지 — 빠른 프로세서를 만드는 장치들을 작은 예를 직접 계산한 그림으로 따라간다.
목차 (15)
폰 노이만 구조와 저장 프로그램
오늘날 거의 모든 컴퓨터는 저장 프로그램 방식이다. 프로그램도 데이터와 똑같이 메모리에 든 숫자이고, CPU는 메모리에서 명령을 하나 가져와(fetch) 해석하고(decode) 실행한 뒤(execute) 다음 명령으로 넘어가는 일을 끝없이 되풀이한다. 다음에 가져올 명령의 주소는 프로그램 카운터(PC) 레지스터가 기억한다. 분기 명령은 PC 값을 바꾸는 명령일 뿐이다. 프로그램을 바꾸려고 배선을 다시 할 필요가 없어졌다는 것이 이 방식의 혁명이었고, 컴파일러·운영체제·JIT처럼 프로그램이 프로그램을 만드는 일도 여기서 가능해졌다.
이 구조를 정리한 사람의 이름을 따 폰 노이만 구조라 한다. CPU(제어 장치·ALU·레지스터), 메모리, 입출력이 버스로 이어진다. 명령과 데이터가 같은 길로 오가므로, CPU가 아무리 빨라져도 메모리와 주고받는 속도가 전체를 묶는다. 이를 폰 노이만 병목이라 하고, 아래 캐시 절까지 이 노트의 상당 부분이 이 병목을 피하는 이야기다. 명령 메모리와 데이터 메모리를 따로 두는 하버드 구조도 있는데, 요즘 CPU는 메모리는 하나로 두되 코어 가장 가까운 L1 캐시를 명령용·데이터용으로 나누는 절충을 쓴다.
명령어 집합: CISC와 RISC
명령어 집합 구조(ISA)는 하드웨어와 소프트웨어 사이의 계약이다. 레지스터가 몇 개이고 이름이 무엇인지, 어떤 명령이 있고 비트로 어떻게 적는지, 메모리는 어떻게 가리키는지, 예외가 나면 무슨 일이 생기는지를 정한다. 같은 ISA를 지키면 회로 설계(마이크로아키텍처)가 완전히 달라도 같은 프로그램이 돈다. 인텔과 AMD의 칩이 같은 윈도우 프로그램을 돌리는 이유다. 어떤 ISA를 고르느냐가 사는 사람에게 무슨 뜻인지는 CPU 노트의 아키텍처 절에 있고, 여기서는 설계 쪽 차이를 본다.
- CISC(x86): 메모리에서 읽어 더하고 다시 쓰는 일을 명령 하나로 하는 식의 강력한 명령이 많고, 명령 길이가 제각각이다. 코드가 촘촘한 대신 해석이 어렵다. 요즘 x86 칩은 앞단에서 명령을 단순한 마이크로 연산(μop)으로 쪼개고, 안쪽은 RISC처럼 돈다.
- RISC(Arm, RISC-V): 메모리는 적재(load)·저장(store) 명령만 만지고 계산은 레지스터끼리만 하는 load/store 구조다. 명령 길이가 고정이라 여러 개를 한꺼번에 해석하기 쉽다. 대신 같은 일에 명령이 더 많이 든다.
- RISC-V: UC 버클리에서 시작한 공개 ISA로, 누구나 사용료 없이 구현할 수 있다. 작은 기본 정수 명령어 집합에 곱셈(M)·원자 연산(A)·부동소수점(F·D)·압축 명령(C)·벡터(V) 같은 확장을 골라 붙인다. 마이크로컨트롤러부터 서버 칩까지 쓰임이 넓어지고 있고, 단순해서 교재가 즐겨 쓴다.
RISC-V에서 add x3, x1, x2는 "x1 + x2를 x3에"라는 뜻이다. 32비트 안에 opcode(무슨 종류의 명령인지), 두 원본 레지스터 번호, 결과 레지스터 번호, 연산을 고르는 funct 필드가 정해진 자리에 들어간다. 즉시값을 쓰는 addi는 rs2·funct7 자리를 12비트 상수로 쓴다. 레지스터 번호 필드가 두 형식에서 같은 비트에 있는 것은 우연이 아니다. 해석 회로가 명령 종류를 알아내기 전에 레지스터 파일부터 미리 읽을 수 있게 한 설계다.
ISA가 정하는 것 하나가 엔디언(바이트 순서)이다. 4바이트 수 0x12345678을 메모리에 놓을 때 큰 자리 바이트부터 낮은 주소에 두면(12 34 56 78) 빅 엔디언, 작은 자리 바이트부터 두면(78 56 34 12) 리틀 엔디언이다. x86은 리틀 엔디언이고, Arm과 RISC-V는 둘 다 지원하지만 실제로는 리틀 엔디언으로 쓴다. 반대로 인터넷 프로토콜의 머리글은 빅 엔디언으로 적기로 해서 이를 네트워크 바이트 순서라 부른다. 그래서 소켓 코드는 포트 번호 같은 값을 보낼 때 htons()처럼 순서를 바꾸는 함수를 거치고, 파일 형식은 어느 순서인지 명세에 적어 둔다. 한 CPU 안에서만 쓰는 값은 신경 쓸 일이 없고, 바이트 줄로 바깥에 나갈 때만 문제가 된다.
데이터 경로: 명령 한 줄이 지나가는 길
CPU 안에서 값이 흐르는 회로를 데이터 경로라 한다. 가장 단순한 설계는 명령 하나를 한 박자에 끝낸다. PC가 가리키는 주소로 명령 메모리를 읽고, 명령의 레지스터 번호로 레지스터 파일에서 두 값을 꺼내고, ALU가 더하기·비교·주소 계산을 하고, 적재·저장 명령이면 데이터 메모리에 접근하고, 결과를 레지스터 파일에 다시 쓴다. 동시에 PC는 다음 명령 주소(PC + 4, 분기면 목적지)로 바뀐다.
어느 길을 쓸지는 제어 장치가 정한다. opcode를 보고 "ALU는 더하기", "메모리는 읽기", "결과는 레지스터에 쓰기" 같은 선택 신호를 낸다. 한 박자 설계의 문제는 박자 길이가 가장 느린 명령(보통 메모리를 읽는 적재)에 맞춰진다는 것이다. 더하기처럼 금방 끝나는 명령도 그만큼 기다린다. 그래서 이 다섯 덩어리를 단계로 나눠 서로 겹치게 돌리는 파이프라인이 나왔다. 아래 그림의 단계 이름(IF·ID·EX·MEM·WB)이 다음 절의 열 이름이다.
성능 식: 명령 수 × CPI × 클럭 주기
프로그램 하나를 돌리는 데 걸리는 CPU 시간은 세 수의 곱이다.
CPU 시간 = 명령 수 × CPI × 클럭 주기
명령 수는 알고리즘·컴파일러·ISA가 정하고, CPI(명령당 평균 박자 수)는 마이크로아키텍처와 프로그램의 성질(캐시 실패, 분기 예측 실패)이 정하고, 클럭 주기(1 / 주파수)는 회로와 공정이 정한다. 세 수는 서로 엮여 있다. 강력한 명령은 명령 수를 줄이지만 CPI를 올리고, 파이프라인을 깊게 하면 클럭은 올라가지만 예측 실패 때 버리는 박자가 늘어 CPI가 오른다. 그래서 하나만 보고 성능을 말할 수 없다. 클럭이 무엇인지와 "클럭 × IPC"라는 구매자용 어림은 CPU 노트의 클럭 절에 있다(IPC는 CPI의 역수다).
예를 들어 같은 프로그램을 두 컴파일러로 만들었다고 하자. A는 명령 10억 개에 CPI 1.5, B는 명령을 8억 개로 줄였지만 무거운 명령을 써서 CPI가 2다. 3 GHz에서 A는 0.500초, B는 0.533초로 명령이 적은 B가 더 느리다. 벤치마크가 "명령 수"나 "MIPS"가 아니라 실제 걸린 시간을 재는 이유다.
파이프라인: 단계를 겹쳐 돌리기
빨래를 세탁기·건조기·개기 세 단계로 나누면, 첫 빨래를 건조기에 넣는 동안 둘째 빨래를 세탁기에 넣을 수 있다. 한 벌이 끝나는 시간은 같아도 단위 시간당 끝나는 벌 수는 늘어난다. 파이프라인도 같다. 데이터 경로를 IF(가져오기)·ID(해석·레지스터 읽기)·EX(계산)·MEM(메모리)·WB(쓰기) 다섯 단계로 나누고 단계 사이에 래치를 두면, 박자마다 새 명령이 하나씩 들어가 이상적으로는 CPI가 1이 되고 박자 길이는 가장 느린 단계 하나로 짧아진다.
문제는 명령끼리 서로 기대는 경우다. 이를 해저드라 한다.
- 데이터 해저드: 뒤 명령이 앞 명령의 결과를 읽어야 하는데 아직 레지스터에 안 써졌다. 기다리면(스톨, 정지) 박자가 버려진다. 포워딩(바이패스)은 ALU 출력을 레지스터 파일을 거치지 않고 바로 다음 명령의 EX 입력에 꽂아 대부분의 정지를 없앤다. 다만 적재 명령의 값은 MEM이 끝나야 나오므로 바로 뒤 명령이 쓰면 포워딩이 있어도 한 박자 멈춘다(load-use 해저드). 컴파일러가 명령 순서를 바꿔 그 틈을 다른 일로 메우기도 한다.
- 제어 해저드: 분기 결과를 알기 전에 다음 명령을 가져와야 한다. 다음 절의 분기 예측이 이를 다룬다.
- 구조 해저드: 두 명령이 같은 회로(예: 메모리 포트 하나)를 동시에 원한다. L1을 명령용·데이터용으로 나누는 이유 하나가 이것이다.
명령 네 개(lw → add → sub → sw)를 박자마다 한 열씩 채운다. 위는 포워딩 없음, 아래는 포워딩 있음이다.
위 예에서 해저드가 없었다면 8박자면 끝났을 명령 네 개가, 포워딩이 없으면 14박자, 있으면 9박자가 걸린다. 실제 고성능 코어는 단계를 이보다 훨씬 잘게 나눠 클럭을 높이는데, 그만큼 해저드가 비싸져서 아래 절의 장치들이 필요해진다.
분기 예측
프로그램에는 대략 몇 명령마다 분기가 있다. 분기 방향은 EX 단계쯤에야 알 수 있는데, 그때까지 파이프라인을 비워 두면 성능이 크게 떨어진다. 그래서 CPU는 방향과 목적지를 예측하고 그쪽 명령을 미리 가져와 실행한다(투기적 실행). 맞으면 공짜이고, 틀리면 그 뒤에 들어온 명령을 모두 버리고 맞는 길에서 다시 시작한다. 이 버리는 박자가 예측 실패 벌칙으로, 요즘 파이프라인에서는 대략 10~20박자다.
가장 단순한 동적 예측기는 분기마다 "지난번에 어땠나" 한 비트를 기억한다. 그런데 루프 끝 분기는 "갔다, 갔다, 갔다, 안 갔다"를 되풀이해서, 1비트는 루프를 빠져나올 때 한 번, 다시 들어갈 때 한 번, 루프마다 두 번 틀린다. 2비트 포화 카운터는 상태를 넷으로 두고 두 번 연속 반대로 가야 예측을 뒤집는다. 한 번의 예외에 흔들리지 않으니 루프마다 한 번만 틀린다. 실제 CPU는 여기에 최근 분기들의 결과(전역 이력)를 섞어 표를 고르는 방식(gshare, TAGE 계열), 분기 목적지 주소를 기억하는 BTB, 함수 복귀 주소를 맞히는 리턴 스택을 더해, 일반적인 코드에서 대부분의 분기를 맞힌다.
예측 실패가 CPI에 더하는 값은 분기 비율 × 실패율 × 벌칙이다. 명령 다섯 개에 하나가 분기(20%)이고 실패율이 5%, 벌칙이 15박자라면 CPI 1이던 코어가 1.15가 되고, 실패율이 두 배가 되면 1.30가 된다. 정렬된 배열을 처리하는 루프가 무작위 배열보다 훨씬 빠른 유명한 현상도 여기서 나온다. 데이터에 따라 방향이 마구 바뀌는 분기를 없애려고 컴파일러는 조건부 이동(cmov) 같은 분기 없는 코드를 쓰기도 한다. 투기적 실행이 남긴 캐시 흔적을 읽어 내는 Spectre 계열 공격은 보안 › 시스템 보안으로 이어진다.
슈퍼스칼라, 비순차 실행, 레지스터 이름 바꾸기
파이프라인은 이상적이어도 CPI 1이 한계다. 더 빨라지려면 박자마다 명령을 여러 개 내보내야 한다. 해석기·실행 유닛을 여러 벌 두고 한 박자에 여러 명령을 처리하는 설계가 슈퍼스칼라다. 그런데 명령을 순서대로만 내보내면, 앞 명령이 캐시 실패로 수백 박자를 기다리는 동안 상관없는 뒤 명령들까지 줄 서서 기다린다.
비순차 실행(out-of-order)은 명령을 해석한 뒤 대기소(예약 스테이션)에 넣어 두고, 입력이 준비된 것부터 실행한다. 결과는 재정렬 버퍼(ROB)에 모아 두었다가 원래 순서대로 확정(retire)해서, 예외나 예측 실패가 나도 프로그래머 눈에는 순서대로 실행한 것처럼 보이게 한다. 순서를 바꿀 때 지켜야 할 것은 진짜 데이터 흐름뿐이다.
- RAW(쓰고 나서 읽기): 앞 명령의 결과를 정말로 쓰는 진짜 의존. 바꿀 수 없다.
- WAR(읽고 나서 쓰기)·WAW(쓰고 나서 또 쓰기): 레지스터 이름이 모자라 같은 이름을 다시 쓰는 바람에 생긴 가짜 의존이다.
레지스터 이름 바꾸기는 가짜 의존을 없앤다. ISA에 보이는 레지스터(아키텍처 레지스터)보다 훨씬 많은 물리 레지스터를 두고, 명령이 레지스터를 쓸 때마다 빈 물리 레지스터를 새로 준다. 같은 r1이라도 다른 시점의 r1은 다른 칸이 되니, 서로 상관없는 계산 사슬이 나란히 돈다. 요즘 큰 코어는 아주 많은 명령을 이렇게 동시에 띄워 놓고 고른다. 세대별 구조와 크기는 Agner Fog의 마이크로아키텍처 안내서가 정리해 두었다. 코어 하나가 스레드 둘의 명령을 섞어 이 빈 실행 유닛을 채우는 것이 CPU 노트에 나오는 SMT(하이퍼스레딩)다.
캐시: 직접 사상과 집합 연관
코어가 박자 몇 개 만에 계산할 동안 DRAM에서 값 하나를 가져오는 데는 그보다 훨씬 많은 박자가 든다. 캐시는 최근에 쓴 데이터를 코어 가까운 작은 SRAM에 두어 이 간격을 메운다. 기대는 성질은 두 가지 지역성이다. 방금 쓴 것을 곧 다시 쓰는 시간 지역성(루프 변수), 방금 쓴 것의 이웃을 곧 쓰는 공간 지역성(배열 순회). 그래서 캐시는 바이트 하나가 아니라 이웃까지 묶은 캐시 줄(예: 64바이트) 단위로 가져온다. L1·L2·L3로 이어지는 계층의 크기와 DRAM 쪽 이야기는 RAM 노트에 있다.
캐시는 주소를 세 조각으로 나눠 찾는다. 맨 아래 오프셋은 줄 안의 바이트 위치, 그 위 인덱스는 몇 번째 묶음(set)에 들어갈지, 나머지 태그는 그 묶음 안에서 진짜 같은 줄인지 확인하는 이름표다. 묶음 하나에 칸(way)이 몇 개 있느냐가 연관도다.
- 직접 사상(1-way): 주소마다 들어갈 칸이 딱 하나다. 회로가 단순하고 빠르지만, 같은 칸에 걸리는 두 주소를 번갈아 쓰면 서로 쫓아낸다(충돌 실패).
- N-way 집합 연관: 묶음 하나에 N칸을 두고 태그 N개를 동시에 비교한다. 충돌 실패가 크게 준다. 대신 비교기가 늘고 어느 칸을 내보낼지(LRU 근사) 정해야 한다. 요즘 CPU의 L1·L2·L3는 모두 집합 연관이다.
- 완전 연관: 어느 칸에나 들어간다. 비교기가 칸 수만큼 필요해 TLB처럼 작은 구조에만 쓴다.
16바이트 줄 4개짜리 캐시 두 가지에 주소 0x00 0x40 0x08 0x48 0x10 0x00 0x14 0x40을 차례로 넣는다.
실패에는 세 종류가 있다(3C). 처음 쓰는 줄이라 어쩔 수 없는 최초 실패(compulsory), 캐시가 작아서 생기는 용량 실패(capacity), 위 그림처럼 칸 배치 때문에 생기는 충돌 실패(conflict)다. 연관도를 높이면 충돌이, 캐시를 키우면 용량이, 줄을 키우거나 미리 가져오기(prefetch)를 하면 최초 실패가 준다.
계층 전체의 성능은 평균 메모리 접근 시간(AMAT) = 적중 시간 + 실패율 × 실패 벌칙으로 본다. 실패 벌칙은 다음 단의 AMAT이다. 예를 들어 L1 4박자·실패율 5%, L2 14박자·30%, L3 40박자·20%, 메모리 200박자라는 교재식 어림을 넣으면 AMAT는 5.9박자다. L2·L3 없이 L1 실패가 바로 메모리로 가면 14.0박자로 두 배 넘게 느려진다. 같은 계산으로, L1 실패율 몇 %p 차이가 큰 속도 차이가 된다는 것도 알 수 있다. 행렬을 행 순서로 읽느냐 열 순서로 읽느냐가 몇 배 차이를 내는 이유다.
쓰기는 따로 정할 것이 있다. write-through는 쓸 때마다 캐시와 아래 단에 함께 쓴다. 아래 단이 늘 최신이라 단순하지만 쓰기마다 느린 길을 간다. write-back은 캐시에만 쓰고 그 줄에 수정됨(dirty) 비트를 켜 두었다가, 줄이 쫓겨날 때 한 번에 아래로 내려 쓴다. 같은 줄을 여러 번 고치면 쓰기가 한 번으로 줄지만, 쫓겨나는 줄이 더러우면 실패 한 번에 메모리 접근이 둘(내려 쓰기와 새로 읽기)이 된다. 쓰려는 줄이 캐시에 없을 때 먼저 가져와 캐시에서 고치는 write-allocate는 보통 write-back과, 바로 아래 단에 쓰고 캐시에는 안 올리는 no-write-allocate는 write-through와 짝을 이룬다. 아래 MESI 절의 M(수정됨) 상태가 바로 write-back 캐시의 "더러운 줄"이다. 같은 고민을 서비스 앞의 캐시 서버에서 하는 모습은 시스템 설계 › 설계의 부품에 있다.
가상 메모리와 TLB
프로그램이 쓰는 주소는 가상 주소이고, MMU가 페이지 테이블로 물리 주소로 바꾼다. 페이지·페이지 테이블·페이지 폴트·교체 정책은 운영체제가 관리하는 쪽이라 운영체제 노트의 가상 메모리 절에서 다룬다. 하드웨어 쪽에서 볼 것은 변환이 메모리 접근마다 끼어든다는 점이다. 다단계 페이지 테이블을 매번 읽으면 메모리 접근 한 번이 여러 번이 되므로, 최근 변환을 기억하는 작은 완전 연관 캐시 TLB가 필수다. TLB 실패 때 표를 걷는 일은 x86·Arm에서는 하드웨어(page walker)가 하고, 일부 RISC에서는 OS의 예외 처리기가 한다.
그런데 L1 캐시를 찾으려면 물리 주소가 필요하고, 물리 주소를 알려면 TLB를 거쳐야 하니 두 단계가 차례로 이어지면 느리다. 대부분의 CPU는 VIPT(가상 주소로 묶음을 고르고 물리 태그로 확인) 방식으로 둘을 겹친다. 인덱스와 오프셋이 페이지 오프셋 비트 안에 들어가면 그 비트는 변환해도 안 바뀌므로, TLB를 찾는 동안 L1 묶음을 미리 읽어 둘 수 있다. 4 KiB 페이지(오프셋 12비트)에서 64바이트 줄(6비트)이면 인덱스로 6비트, 즉 묶음 64개까지만 쓸 수 있다. 그래서 L1을 키우고 싶으면 묶음 수가 아니라 연관도를 올리게 되는데, 위 절에서 본 32 KiB·8-way·64개 묶음 모양이 바로 그 경계다.
멀티코어와 캐시 일관성: MESI
코어가 여럿이면 코어마다 자기 L1·L2가 있다. 코어 0이 캐시에서 x를 바꿨는데 코어 1의 캐시에는 옛 x가 남아 있다면 두 코어가 다른 세상을 본다. 캐시 일관성 프로토콜은 "한 주소에 대해 모든 코어가 쓰기들의 순서에 동의하고, 읽으면 가장 최근 쓰기를 본다"를 보장한다. 핵심 규칙은 하나다. 한순간 한 줄을 쓸 수 있는 캐시는 하나이거나, 아무도 안 쓰고 여럿이 읽기만 한다.
교과서 표준이 MESI다. 캐시 줄마다 상태가 넷이다. M(수정됨: 나만 가졌고 메모리보다 새것), E(독점: 나만 가졌고 메모리와 같음), S(공유: 여럿이 가졌고 읽기만), I(무효). 캐시는 다른 코어가 버스에 내는 요청을 엿듣고(snooping) 자기 상태를 바꾼다. 쓰려면 먼저 남의 사본을 모두 무효로 만들어야 하고, 남이 수정한 줄을 읽으려면 그 캐시가 데이터를 내줘야 한다. E 상태 덕분에 혼자 읽다가 쓰는 흔한 경우에는 버스 메시지가 필요 없다. 실제 칩은 변형인 MOESI·MESIF를 쓰고, 코어가 많으면 버스 엿듣기 대신 누가 어느 줄을 가졌는지 적은 디렉터리로 필요한 코어에만 메시지를 보낸다.
처음에는 두 캐시 모두 이 줄이 없다(I). 메모리 값이 최신이다.
이 규칙이 소프트웨어 성능에 바로 드러난다. 여러 코어가 같은 줄을 번갈아 쓰면 줄이 코어 사이를 오가며 매번 무효화가 일어난다. 서로 다른 변수인데 우연히 같은 줄에 놓여 생기는 경우가 거짓 공유이고, 병렬 프로그래밍 노트에서 측정 예와 함께 다룬다.
메모리 일관성 모델
캐시 일관성은 주소 하나의 이야기다. 서로 다른 주소에 대한 읽기·쓰기가 다른 코어에 어떤 순서로 보이느냐는 별개의 문제이고, 그 규칙이 메모리 일관성 모델(memory consistency model)이다. 가장 직관적인 순차 일관성(SC)은 모든 코어의 연산이 각자의 프로그램 순서를 지킨 채 하나의 줄로 끼워 넣어진 것처럼 보인다는 약속이다.
실제 칩은 SC를 지키지 않는다. 쓰기는 캐시에 반영되기 전 코어 안의 저장 버퍼에 잠시 머무는데, 그동안 뒤의 읽기가 먼저 나간다. 그래서 아래 시험에서 SC로는 불가능한 (0, 0)이 나온다. x86은 이 "저장 뒤 읽기"만 바꿀 수 있는 비교적 강한 모델(TSO)이고, Arm·RISC-V는 거의 모든 순서를 바꿀 수 있는 약한 모델이다. 순서가 중요한 곳에는 메모리 장벽(fence)이나 획득(acquire)·해제(release) 의미를 가진 명령을 넣는다. C++·Rust·Java의 원자 타입과 memory_order가 이 하드웨어 차이를 언어 수준에서 하나로 감싼 것이다. 락을 쓰면 락 구현 안에 장벽이 들어 있어 보통은 신경 쓸 일이 없지만, 락 없는 자료 구조를 짤 때는 이 모델을 정확히 알아야 한다.
전력 벽과 다크 실리콘
칩의 동적 전력은 대략 P = α·C·V²·f다(α 전환 비율, C 정전 용량, V 전압, f 클럭). 1974년 데나드가 정리한 데나드 스케일링에 따르면 트랜지스터를 줄이면 전압도 함께 줄어 면적당 전력이 일정했고, 그래서 세대마다 트랜지스터를 늘리면서 클럭도 올릴 수 있었다. 2005~2007년 무렵 누설 전류와 문턱 전압 때문에 전압이 더는 내려가지 않자 이 공식이 깨졌다. 클럭을 올리면 전력과 열이 감당할 수 없게 늘어, 데스크톱 CPU 클럭은 그 무렵부터 몇 GHz 언저리에 머물렀다. 이것이 전력 벽이다.
업계의 답은 코어를 여럿 두는 것이었다. 전압이 클럭에 비례해 내려간다고 이상화하면, 클럭을 1.2배 올린 코어 하나는 전력이 1.73배 드는데 처리량은 1.2배뿐이다. 반대로 전압·클럭을 0.8배로 낮춘 코어 둘은 전력 1.02배로 처리량 1.6배를 낸다. 단, 일을 둘로 나눌 수 있을 때만이다. 그 조건이 병렬 프로그래밍 노트의 출발점이다.
코어 수도 무한히 늘릴 수 없다. 트랜지스터는 계속 늘어도 전력 예산은 그대로라, 칩의 모든 회로를 동시에 최고 속도로 켤 수 없게 됐다. 켜지 못하고 쉬는 부분을 다크 실리콘이라 한다. 쉬는 면적을 버리지 않는 방법은 특정 일만 아주 효율적으로 하는 회로로 채우는 것이다. 동영상 코덱, 암호화, 이미지 처리, 행렬 곱 전용 회로가 칩에 늘어나는 이유이고, 크고 빠른 P코어와 작고 효율적인 E코어를 섞는 것도 같은 계산이다.
가속기: GPU와 NPU
CPU 코어는 한 갈래의 일을 최대한 빨리 끝내도록(지연 시간 우선) 면적 대부분을 분기 예측·비순차 실행·큰 캐시에 쓴다. 같은 연산을 수많은 데이터에 똑같이 하는 일이라면 그 장치들이 거의 쓸모없다. GPU는 그 면적을 단순한 실행 유닛 수천 개로 채우고, 메모리 지연은 캐시 대신 다른 스레드 묶음으로 갈아타며 숨긴다(처리량 우선). 이 실행 모델과 프로그래밍 방법은 병렬 프로그래밍 노트, 그래픽카드로서의 GPU는 GPU 노트에 있다.
NPU(신경망 가속기, Google TPU 같은 것)는 한 걸음 더 나아가 신경망의 대부분인 행렬 곱만을 위해 만든다. 곱셈-누산기(MAC)를 격자로 깔고 값이 이웃 칸으로 흘러가며 계산되게 하는 시스톨릭 배열이 대표적이다. 메모리에서 한 번 읽은 값을 여러 번 재사용해 폰 노이만 병목을 우회하는 셈이다. 정밀도를 FP16·INT8처럼 낮춰 같은 면적에 더 많은 MAC을 넣는다. 휴대폰·노트북 칩에는 CPU·GPU·NPU·미디어 엔진이 한 다이에 함께 들어가고, 이들을 어떻게 만들고 어떻게 한 패키지로 묶는지는 반도체 노트의 칩 종류·첨단 패키징 절로 이어진다. AI 학습·추론 클러스터 쪽은 AI 인프라 노트에 있다.
이런 기계들을 한 줄로 나누는 오래된 틀이 1966년 Michael Flynn이 내놓은 플린 분류다. 명령 흐름과 데이터 흐름이 몇 갈래인지로 넷을 가른다. 명령 하나가 데이터 하나를 다루는 SISD(옛 단일 코어), 명령 하나를 여러 데이터에 한꺼번에 하는 SIMD(AVX·NEON 같은 벡터 명령, GPU), 여러 명령이 한 데이터를 다루는 MISD(우주왕복선 비행 제어처럼 같은 계산을 겹쳐 고장을 견디는 드문 경우), 여러 명령이 여러 데이터를 각자 다루는 MIMD(멀티코어, 분산 시스템)다. 요즘 칩은 MIMD인 코어마다 SIMD 유닛을 단 혼합이라 분류 자체보다는 "어느 축으로 병렬을 얻는가"를 말할 때 쓰인다.
흔한 오해
- "클럭이 높은 CPU가 더 빠르다" — 시간은 명령 수 × CPI × 클럭 주기의 곱이다. 세대나 설계가 다르면 클럭이 낮아도 박자마다 더 많은 명령을 끝내 더 빠를 수 있다(위 성능 식 절).
- "코어를 두 배로 늘리면 두 배 빠르다" — 나눌 수 없는 부분이 상한을 정한다(병렬 프로그래밍 노트의 암달의 법칙). 코어끼리 같은 줄을 주고받으면 MESI 무효화로 오히려 느려지기도 한다.
- "캐시는 클수록 좋다" — 캐시가 커지면 찾는 시간(적중 시간)도 늘어 AMAT의 첫 항이 커진다. 그래서 작고 빠른 L1과 크고 느린 L3를 층으로 쌓는다.
- "x86은 CISC라 RISC보다 느리다" — 요즘 x86은 앞단에서 μop으로 쪼개 안쪽은 RISC처럼 돌린다. 성능은 ISA 이름보다 마이크로아키텍처와 공정이 더 크게 정한다.
핵심 자료
운영체제가 하드웨어를 어떻게 쓰는지는 운영체제 노트, 컴파일러가 명령 순서·레지스터를 어떻게 고르는지는 컴파일러 노트, 측정과 병목 찾기는 성능 노트에 있다.
| 자료 | 종류 | 요지 | 링크 |
|---|---|---|---|
| The Microarchitecture of Intel, AMD and VIA CPUs | 해설서 | 파이프라인·분기 예측·비순차 실행·레지스터 이름 바꾸기를 세대별 실측으로 정리한 Agner Fog의 무료 안내서. 명령 지연 표도 함께 있다. | agner.org |
| Intel 64 and IA-32 Architectures Software Developer Manuals | 공식 매뉴얼 | x86-64 명령어 집합·메모리 순서 규칙·페이지 테이블의 원전. 막히면 여기서 확인한다. | Intel |
| RISC-V Ratified Specifications | 명세 | 이 노트의 인코딩 예에 쓴 RISC-V 명령어 집합의 비특권·특권 명세. 짧고 읽기 쉬워 ISA 공부의 첫 원전으로 좋다. | RISC-V International |
| What Every Programmer Should Know About Memory | 논문형 글 | DRAM과 캐시 계층이 프로그램 속도에 어떻게 드러나는지 프로그래머 쪽에서 풀어 쓴 Ulrich Drepper의 긴 글. | |
| MIT 6.191 Computation Structures | 강의 | MIT의 컴퓨터 구조 학부 강의 Computation Structures(옛 번호 6.004). 회로에서 프로세서까지 이어지는 교과 과정의 기준점이다. | MIT |
| Linux kernel memory barriers | Linux 문서 | CPU와 컴파일러가 메모리 접근 순서를 바꾸는 방식과 이를 막는 장벽의 종류를 커널 개발자 관점에서 설명한다. | kernel.org |
- The Microarchitecture of Intel, AMD and VIA CPUs — agner.org
- Intel 64 and IA-32 Architectures Software Developer Manuals — Intel
- RISC-V Ratified Specifications — RISC-V International
- What Every Programmer Should Know About Memory — PDF
- MIT 6.191 Computation Structures — MIT
- Linux kernel memory barriers — kernel.org
- Branch predictor — Wikipedia
- MESI protocol — Wikipedia
- Dennard scaling — Wikipedia
- Endianness — Wikipedia
- Cache (computing): Writing policies — Wikipedia
- Flynn's taxonomy — Wikipedia