컴퓨터 하드웨어에게 일을 시키려면 하드웨어가 알아들을 수 있는 언어로 말을 해야합니다. 컴퓨터 언어에서 단어를 명령어(instruction)이라 하고 그 어휘를 명령어 집합(instruction set)이라 합니다. 이번 장에서는 실제 컴퓨터의 명령어 집합을 살펴봅니다. 이 글에서 사용할 명령어 집합은 MIPS를 택했는데, MIPS는 1980년대 이후에 설계된 명령어 집합들을 대표할 만한 것 중 하나입니다. 2장 마지막 쯤에 MIPS 말고도 널리 쓰이는 명령어 집합이 있는데 간단히 살펴볼 예정입니다.
1. ARMv7은 MIPS와 유사합니다. 최근 엄청나게 많은 ARM프로세서가 제조되어 세계에서 가장 많이 쓰이는 명령어 집합이 되었습니다.
2. Intel x86은 PC와 포스트PC시대 클라우드 컴퓨터 모두를 견인하고 있습니다.
3. ARMv8은 32비트 ARMv7의 주소 크기가 64비트로 확장된 프로세서인데, 아이러니하게도 이 2013명령어 집합은 ARMv7보다 오히려 MIPS와 더 비슷합니다.
이장의 목표는 명령어를 가르치면서 동시에 명령어 집합의 하드웨어 표현 방식 및 C언어와 같은 상위 수준 언어와의 관계를 보이고자 합니다. 명령어의 표현 방식을 배우고, 표현방식을 배우면 컴퓨터의 가장 큰 비밀인 내장 프로그램 개념을 이해할 수 있습니다. 그 후에 컴퓨터 언어로 프로그램을 작성하고 이 책에서 제공하는 시뮬레이터로 실행시킴으로써 구사 능력을 연마시킵니다. 그리고 프로그래밍 언어와 컴파일러 최적화가 성능에 미치는 영향을 배운 후, 명령어 집합의 발달 과정과 다른 컴퓨터 언어를 간략히 살펴보는 것으로 마무리 합니다.
하드웨어 연산
MIPS 산술 명령어는 반드시 한 종류의 연산만 지시하며 항상 변수 3개를 갖는 형식을 지킵니다. 즉, b c d e 4변수를 더해 a변수에 넣는 과정을 생각해봅시다. 다음과 같이 작성할 수 있습니다.
add a,b,c
add a,a,d
add a,a,e 따라서 네 변수의 합을 구하려면 명령어 3개가 필요합니다. 한 줄에 명령어 하나만 쓸 수 있습니다. (C와의 차이점)
설계 원칙 1 : 간단하게 하기 위해서는 규칙적인 것이 좋다.
덧셈 같은 연산의 피연산자(operand)는 더해질 숫자 2개와 합을 기억할 장소 하나, 모두 3개인 것이 자연스럽다. 이렇게 모든 명령어가 피연산자를 반드시 3개씩 갖도록 제한하는 것은 하드웨어를 단순하게 하자는 원칙에 부합합니다.
상위 수준 언어로 쓴 프로그램과 어셈블리 명령어 사이의 관계 예)
C code
f = (g+h) - (i+j);
MIPS code
MIPS는 한 번에 하나의 연산만을 처리하기 때문에 여러 개의 어셈블리 명령어로 나누어야 한다. 컴파일러는 t0라는 임시 변수를 생성한다.
add t0, g, h
add t1, i, j
sub f, t0, t1
피연산자
상위 수준 언어 프로그램과 달리 산술 명령어의 피연산자에는 제약이 있습니다. 레지스터(register)라고 하는 하드웨어로 직접 구현된 특수 위치 몇 곳에만 있는 것만을 사용할 수 있습니다. MIPS구조에서 레지스터의 크기는 32비트입니다. MIPS에서는 32비트가 한덩어리로 처리되는 일이 매우 빈번하므로 이것을 워드(word)라고 부릅니다.
프로그래밍 언어에서 사용하는 변수와 하드웨어 레지스터의 큰 차이점 하나는 레지스터는 개수가 한정되어 있다는 것입니다. 현대 컴퓨터에는 MIPS처럼 보통 32개의 레지스터가 있습니다. 그러므로 MIPS언어를 단계적으로 구체화할 때 산술 명령어의 각 피연산자는 32개의 32비트 레지스터 중 하나여야 한다는 제약이 추가됩니다.
설계원칙2 : 작은 것이 더 빠르다.
레지스터가 아주 많아지면 저기 신호가 더 멀리까지 전달되어야 하므로 클럭 사이클 시간이 길어집니다. 컴퓨터 설계자는 더 많은 레지스터를 원하는 프로그램의 갈망과 클럭 사이클을 빠르게 하고 싶은 본인의 바람 사이에서 적절한 타협점을 찾아야 합니다. 32개 이상의 레지스터를 사용하지 않는 또다른 이유 하나는 명령어 형식에서 레지스터가 사용하는 비트 수와 관련이 있습니다. C나 Java의 변수에 해당하는 레지스터들은 $s0, $s1등으로 표기하고, 컴파일 과정에서 필요한 임시 레지스터들은 $t0, $t1등으로 표기합니다.
메모리 피연산자
프로그래밍 언어에는 단순 변수 외에도 배열(array)나 구조체(structure)같은 복잡한 자료구조가 있습니다. 이런 복잡한 자료구조하나에는 레지스터 개수보다 훨씬 많은 데이터 원소가 있을 수 있습니다. 이런 큰 구조는 컴퓨터 내에서 어떻게 표현 되고 사용될까요?
프로세스는 소량의 데이터만을 레지스터에 저장할 수 있지만 컴퓨터 메모리는 수십억 개의 데이터를 저장할 수 있습니다. 그러므로 배열이나 구조체 같은 자료구조는 메모리에 보관합니다. MIPS의 산술 연산은 레지스터에서만 실행되므로 메모리와 레지스터 간에 데이터를 주고받는 명령어가 있어야만 합니다. 이런 명령어를 데이터 전송 명령어(data transfer instruction)라 합니다. 메모리에 기억된 데이터 워드에 접근하려면 명령어가 메모리 주소(memory address)를 지정해야 합니다.
메모리 피연산자를 사용하는 치환문의 번역 예)
A는 100워드 배열이고, 변수 g,h는 레지스터 $1, $s2에 할당되었다고 가정한다. 또 배열 A의 시작주소(base address)가 $s3에 기억되어 있다고 할 때 다음 C 문장을 컴파일하라. g = h + A[8]
이 치환문에 연산은 하나밖에 없지만, 피연산자 중 하나가 메모리에 있으므로 먼저 A[8]을 레지스터로 옮긴 후 연산을 시작해야 합니다. 이 배열 원소의 주소는 $s3에 있는 배열의 시작 주소에 인덱스 8을 더한 값이다.
lw $t0, 8($s3)
이제 필요한 값 A[8]을 레지스터 $t0에 넣었으므로 덧셈을 수행 할 수 있습니다.
add %s1, $s2, $t0
데이터 전송 명령어의 상수부분 8을 변위(offset)라 하고, 주소 계산을 위해 여기에 더해지는 레지스터($s3)를 베이스 레지스터(base register)라 합니다.
프로그램에서 8비트로 구성된 바이트를 많이 사용하므로 대부분의 컴퓨터는 바이트 단위로 주소를 지정한다. 워드 주소는 워드를 구성하는 4바이트 주소 중 하나를 사용한다. 그러므로 연속된 워드의 주소는 4씩 차이가 난다. 다음 그림의 실제 MIPS주소를 보여줍니다.
MIPS에서 워드의 시작 주소는 항상 4의 배수여야하는데 이러한 요구사항을 정렬제약이라하며 많은 컴퓨터에서 이방법을 사용합니다.(정렬을 사용하면 데이터 전송이 빨라집니다.)
컴퓨터는 제일 왼쪽, 즉 최상위 바이트 주소를 워드 주소로 사용하는것과 제일 오른쪽, 즉 최하위 바이트 주소를 워드 주소로 사용하는 것 두종류로 나누어 집니다. MIPS는 최상위 주소를 사용하는 계열에 속합니다.
바이트 주소의 사용은 배열의 인덱스에도 영향을 미칩니다. 앞의 코드에서 바이트 주소를 제대로 구하려면 베이스 레지스터 $s3에 offset 4*8 즉, 32를 더해야 합니다. 그래야 A[8]의 주소가 구해집니다.
load와 반대로 레지스터에서 메모리로 데이터를 보내는 명령을 저장 store이라고 합니다.
컴퓨터가 갖고 있는 레지스터보다 프로그램에 사용하는 변수가 더 많은 경우가 자주 있다. 그러므로 컴파일러는 자주 사용되는 변수를 가능한 한 많이 레지스터에 넣고 나머지 변수는 메모리에 저장했다가 필요할 때 거내서 레지스터에 넣는다. 자주 사용하지 않는 변수를 메로리에 넣는 일을 레지스터 스필링(spilling register)이라 한다.
" 작을 수록 빠르다. "는 원칙에 의하면 레지스터가 더 작으므로 메모리는 레지스터보다 속도가 느려야 한다. 이것은 사실이며 데이터가 레지스터에 있으면 더 빨리 접근할 수 있다.
레지스터에 저장된 데이터는 메모리 데이터보다 사용하기도 편리하다. MIPS의 산술 연산 명령은 레지스터 2개를 읽어서 연산한 다음 결과를 레지스터에 쓴다. 하지만 데이터 전송 명령은 피연산자 하나를 읽거나 쓰는 일만 할 뿐 데이터에 대한 연산은 하지 못한다.
레지스터는 메모리보다 접근시간이 짧고 처리량도 많으므로, 레지스터에 저장된 데이터를 사용하면 시간이 절약되고 사용하기도 간편하다. 그뿐만 아니라 레지스터 접근은 메모리 접근보다 에너지도 적게 든다. 그러므로 좋은 성능을 얻고 에너지를 절약하기 위해서는 컴파일러가 레지스터를 효율적으로 사용하여야 합니다.
상수 또는 수치 피연산자
프로그램의 연산에서 상수를 사용하는 경우가 많습니다. 이제까지 배운 명령어만으로 상수를 사용하려면 메모리에서 상수를 읽어 와야합니다. 예를 들어 레지스터 $s3에 상수 4를 더하는 코드는 아래와 같습니다.
lw $t0, AddrConstant4($s1) // 여기서 $1+AddrConstant4는 상수4가 저장되어 있는 메모리 주소라고 가정.
add $s3, $s3, $t0
로드 명령을 사용하지 않는 방법은 피연산자 중 하나가 상수인 산술 연산 명령어를 제공하는 것입니다. 이 상수를 수치(immediate)피연산자라 합니다. 수치 피연산자를 갖는 덧셈 명령어는 addi인데 레지스터 $s3에 4를 더하려면 다음과 같이 쓰면 됩니다.
addi $s3,$s3,4
상수 피연산자는 자주 사용되므로, 상수 필드를 갖는 산술 명령어를 사용하면 매번 메모리에서 상수를 가져오는 것보다 연산이 훨씬 빨라지고 에너지를 덜 소모하게 됩니다.
상수중에서도 0은 또 다른 역할이 있습니다. 명령어에 여러가지 유용한 변형을 제공함으로써 명령어 집합을 단순하게 하는 것입니다. 예를 들면 복사(move)연산은 피연산자 중 하나가 0인 add 명령어입니다. 따라서 MIPS에서는 레지스터 $zero를 값0으로 묶어 두도록 회로가 구현되어있습니다.
※ 용어 정리
워드 : 컴퓨터에서 자연스러운 접근 단위. 보통 32비트이다. MIPS구조에서 레지스터 크기에 해당한다.
'Computer Architecture > 컴퓨터 구조' 카테고리의 다른 글
[10] CH2 명령어:컴퓨터 언어 < MIPS 버전 3 > (0) | 2022.01.19 |
---|---|
[9] CH2 명령어:컴퓨터 언어 < MIPS 버전 2 > (0) | 2022.01.16 |
[9] CH2 명령어:컴퓨터 언어 < Arm 버전 > (0) | 2022.01.13 |
[7] CH1 컴퓨터 추상화 및 관련 기술 < 결론 > (0) | 2022.01.12 |
[6] CH1 컴퓨터 추상화 및 관련 기술 < 오류 및 함정 > (0) | 2022.01.12 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!