ACS-1

스트레치 컴퓨터는 결과적으로는 실패했다. 100배의 성능 향상을 목표로 했으나 최종 결과물은 50배에도 못 미쳤다. 그 정도만 해도 대단하다고 생각할 수 있으나, 가격이 너무 비쌌고 5년의 시간이 흐르면서 경쟁 제품의 성능도 향상되었으니 좋은 반응을 이끌어내지 못했다.

스트레치 컴퓨터가 기대치에 못 미치는 성능을 보인 이유 중 하나는 분기 명령이다. 스트레치 컴퓨터 성능 향상의 핵심은 미리보기와 파이프라이닝에 있었다. 두 가지는 메모리에 저장된 기계어 코드들을 순차적으로 접근할 때 최고의 성능을 발휘했다. 하지만 분기 명령이 나타나면 전혀 힘을 발휘하지 못했다.

또 하나의 문제는 컴파일러가 제대로 준비되지 않았다는 점이다. 스트레치의 기계어 코드 종류는 총 735가지에 달했다. 여러 다양한 경우를 기계어 코드로 효과적으로 지원해 주려다 보니 그렇게 종류가 많아졌다. 만약 프로그램을 어셈블리어로 작성한다면 프로그래머는 상황에 맞게 최적의 기계어 코드를 선택할 수 있을 것이다. (말이 그렇지 현실적으로는 그러기 어려울 듯싶다.) 그런데 컴파일러는 735가지의 기계어 코드를 적재적소에 사용할 정도로 똑똑하지 못했다. 컴파일러를 만드는 프로그래머는 몇 개의 선호하는 기계어 코드로 모든 것을 처리하려는 경향이 있었다.

그래서 ACS-1 컴퓨터에서는 분기를 효과적으로 처리하는 방법에 초점이 맞추어졌고 컴파일러 최적화에도 많은 노력이 기울여졌다. ACS-1 컴퓨터가 실제로 만들어지지는 못했지만 이때의 경험은 IBM의 다른 컴퓨터 설계에 큰 도움을 주었다.

분기 처리

단위 시간당 처리할 수 있는 명령어의 개수를 늘리기 위해서는 파이프라인의 깊이를 키우는 것이 좋다. 파이프라인의 깊이가 5라는 것은 한순간에 5개의 기계어 코드가 각각 다른 단계에서 실행되고 있음을 의미한다. 깊이가 10이면 5일 때보다 두 배나 많은 기계어 코드가 처리되는 셈이다.

그런데 파이프라인을 괴롭히는 악당이 있으니 다름 아닌 분기 명령이다. 파이프라인에서는 미리보기 기능이 필수이다. 하나의 기계어 코드가 완료되기 전에 여러 개의 기계어 코드가 처리되기 시작하려면 미리보기를 할 수밖에 없다. 자. 그렇다면 어떤 기계어 코드를 미리 가져올 것인가? 일반적일 때는 당연히 다음 번지에 있는 기계어 코드를 가져온다. 그렇게 열심히 미리보기를 해서 파이프라인을 꽉 채웠는데, 아뿔싸. 방금 최종 단계에 있던 기계어 코드가 엉뚱한 번지로 이동하는 분기 명령이었다면 기껏 미리 진행했던 기계어 코드들을 파이프라인에서 날려버리고 새로운 번지에서 다시 기계어 코드를 읽어와야 한다.

따라서 분기 명령으로 인한 피해를 최소화하려면 파이프라인의 마지막 단계에서 분기 명령이 효력을 발휘하는 일을 피해야 한다. 파이프라인에서 이미 앞서 가 있던 기계어 코드들은 영향을 받지 않으므로 만약 분기 명령의 효력이 파이프라인의 첫 번째 단계에서 발생한다면 가장 좋을 것이다.

그래서 존 코크는 분기 준비prepare-to-branch라는 개념을 도입했다. 그는 분기 명령이 세 가지 단계로 구성된다고 보았다.​7​

  1. 분기 여부 결정
  2. 분기할 주소 결정
  3. 분기 실행

일반적으로는 1번과 2번 단계를 위해서 산술논리연산 회로를 사용하게 되고 3번 단계는 파이프라인의 최종 단계에서 벌어진다. 만약 1번과 2번 단계를 3번 단계와 완전히 다른 기계어 코드로 분리한다면 어떻게 될까? 3번 단계에 해당하는 기계어 코드는 파이프라인의 앞 단계에서 바로 처리될 수 있으므로 그만큼 파이프라인에 주는 악영향이 적을 것이다.

존 코크는 1번과 2번 단계를 하는 명령어는 BRANCH-AT-EXIT라고 정했고 3번 단계를 하는 명령어는 EXIT라고 정했다. 그리고 컴파일러가 지능적으로 BRANCH-AT-EXIT와 EXIT 사이에 다른 기계어 명령을 배치할 수 있게 하여 EXIT의 악영향을 최소화했다.

수퍼스칼라

ACS-1은 CDC-6600에 대항하기 위해 최고의 성능을 목표로 했다. 컴퓨터의 성능은 단위 시간 내에 얼마나 많은 기계어 코드를 수행할 수 있느냐로 따지므로 한 번에 최대한 많은 기계어 코드가 수행되는 것이 바람직했다. 그래서 한 개의 기계어 코드 내에 많은 동작이 일어날 수 있도록 설계했다. 앞에서 예로 사용했던 어셈블리어 코드를 다시 보자.

ADD @100 200 R1

이 어셈블리어 코드에는 덧셈 동작 한 개만 들어 있다. 만약 다음과 같은 코드를 사용할 수 있다면 어떨까?

ADD @100 200 R1, MUL R2 1.2 R4, XOR R5 R6 R7

100번지에 있는 데이터에 200을 더해서 R1 레지스터에 넣고, R2 레지스터에 있는 값에 1.2를 곱해서 R4 레지스터에 넣고, R5 레지스터와 R6 레지스터를 XOR해서 그 결과를 R7 레지스터에 넣는 일을 하나의 어셈블리어 코드(즉, 기계어 코드)로 처리해 버리는 것이다. 생산성을 세 배 증가시킬 수 있다.

이렇게 하나의 기계어 코드 내에 복수 개의 동작을 포함하는 방식을 수퍼스칼라Superscalar 구조라고 부른다. ACS-1에서는 세 개의 고정소수점 동작, 세 개의 부동소수점 동작, 1개의 분기 동작이 한 개의 기계어 코드에 들어갈 수 있도록 허용했다. ACS-1은 캐시 메모리를 채택했는데 메모리 접근이 여러 개 발생할 수 있으므로 캐시에는 두 개의 접근 경로가 지원되었다.

수퍼스칼라 구조에서는 한 개의 기계어 코드 내에 포함되는 동작들이 서로 종속성을 가지지 않아야 하므로 컴파일러의 지원이 중요하다.

동적 명령어 스케줄링

명령어 스케줄링이란 프로그램을 구성하는 기계어 코드들의 순서를 바꾸는 것이다. 당연히 프로그램의 결과값에 영향을 주지 않도록 바꿔야 한다. 명령어 스케줄링이 성능에 도움을 주는 경우는 여러 가지가 있는데 수퍼스칼라 구조에 도움이 되는 경우는 다음과 같다.

R1 + R2 -> R3
R6 + R2 -> R7
R1 x R4 -> R5
R3 x R6 -> R8

위의 순서를 다음과 같이 바꿀 수 있다.

R1 + R2 -> R3
R1 x R4 -> R5
R6 + R2 -> R7
R3 x R6 -> R8

언뜻 보면 이게 왜 더 좋은지 알기 어렵다. 그런데 만약 덧셈과 곱셈 회로가 별개로 있는, 수퍼스칼라 구조라면 덧셈 동작과 곱셈 동작을 하나의 기계어 코드에 포함시킬 수 있다. 이런 수퍼스칼라 구조에서 위의 첫 번째 예는 4개의 기계어 코드가 필요한 반면에 두 번째 예는 2개의 기계어 코드로 가능하다.

명령어 스케줄링은 컴파일러에서 할 수 있다. 하지만 프로그램 중간에 비동기적인 입력을 받아서 처리하는 부분이 있다면 컴파일 과정에서 이런 상황을 모두 대비하기는 어렵다. 실제로 프로그램이 수행되는 과정에서 명령어 스케줄링이 처리될 필요가 생긴다. 이를 동적 명령어 스케줄링이라고 하며 이 기능은 브라이언 랜델과 린 콘웨이가 주도했다.​7​

1 2 3 4 5 6 7