비순차 실행: 나중 명령을 먼저 계산해도 프로그램 순서를 지키는 법
CPU가 느린 메모리 읽기 결과 하나를 기다리는 동안 독립적인 계산까지 멈추면 실행 자원을 낭비한다. 비순차 실행은 필요한 입력과 자원이 준비된 명령을 먼저 수행해 이런 대기를 숨기는 기법이다. 소스나 기계어의 순서를 무시하고 아무 값이나 계산하는 것이 아니다. 실제 값 의존을 지키면서 계산 시점과 최종 결과 확정 시점을 구분한다.
실행 가능한 독립 명령을 먼저 처리할 수 있어도, 결과를 확정하는 커밋은 프로그램 순서를 지킨다.
실행 순서와 완료 확정 순서
첫 명령이 메모리에서 a를 읽고 두 번째는 a를 사용한다고 하자. 두 번째는 기다려야 한다. 그러나 세 번째가 이미 준비된 x와 y를 더하는 독립 연산이라면 먼저 계산할 수 있다. 프로그램에 필요한 관계만 유지하면 이런 실행 순서 변경으로 CPU 장치를 활용할 수 있다. 준비된 명령을 고르는 범위는 하드웨어가 보관할 수 있는 작업 창에 제한된다.
현대적인 설계에서는 실행 결과를 임시 상태에 두고 원래 순서대로 커밋해 정해진 프로그램 상태에 반영한다. 재정렬 버퍼 같은 구조가 아직 확정하지 않은 명령을 추적한다. CMU의 슈퍼스칼라 처리 자료는 동적 실행과 상태 관리 구조를 설명한다. 모든 구현이 동일한 버퍼 형식을 쓰는 것은 아니다.
이름을 다시 사용한 제약은 풀 수 있다
레지스터 이름 수가 제한되면 서로 독립적인 계산도 같은 이름을 재사용할 수 있다. 이를 실제 값 의존과 혼동하면 불필요하게 기다린다. 레지스터 재명명은 프로그램이 보는 논리 레지스터를 내부의 다른 물리 저장 위치에 연결해 이름 때문에 생기는 제약을 줄인다. 앞 계산의 값을 뒤 명령이 실제로 필요로 하는 읽기 의존까지 없애지는 못한다.
예를 들어 앞 명령이 r1의 이전 값을 읽고 뒤 명령이 새 결과를 r1에 쓴다면, 내부 위치를 나눠 앞 읽기와 뒤 쓰기를 다룰 수 있다. 순서가 바뀌어도 앞 명령은 원래 읽어야 할 값을 받는다. 같은 이름이 가리키는 논리적인 버전을 구분하는 것이지, 프로그램에 보이는 레지스터 수를 무작정 늘리는 기능은 아니다.
오류가 나도 일관된 위치에서 보인다
먼저 계산한 나중 명령의 결과가 있는데 앞 명령에서 예외가 확인될 수 있다. 결과를 너무 일찍 확정했다면 프로그램이 앞 오류 이후의 일을 이미 수행한 상태가 된다. 원래 순서대로 확정하는 구조는 앞에서 예외가 발생했을 때 이후 미확정 결과를 정리하고, 정해진 위치의 상태로 처리 경로에 들어가는 데 도움을 준다.
메모리 연산은 더 까다롭다. 두 접근이 같은 주소인지 늦게 확인될 수 있고, 쓰기를 읽기보다 먼저 보여도 되는지는 아키텍처의 메모리 모델에 제약된다. CPU는 의존을 추정하거나 검사하고 필요한 경우 다시 실행한다. 비순차 실행이 모든 코어에 임의의 메모리 순서를 허용한다는 뜻은 아니다. 언어의 동기화 규칙도 여전히 필요하다.
성능 이득에는 한계가 있다. 길게 이어진 참된 의존 사슬은 기다려야 하고, 작업 창 밖의 명령은 선택할 수 없다. 메모리 대기보다 창이 작거나 분기 예측이 자주 틀리면 독립 일을 찾기 어렵다. 구조가 복잡해질수록 면적과 전력, 의존성 검사 비용도 증가한다.
이 개념은 같은 명령 수와 주파수에서도 CPU 성능이 달라지는 이유를 설명한다. 코드에 독립적인 작업이 충분하고 데이터를 적절히 공급하면 유리하다. 계산은 가능한 순서로 진행하되 프로그램이 볼 수 있는 결과는 규정된 의미를 유지한다는 분리가 비순차 실행의 중심이다.