컴퓨터 구조론 Lecture 2 - 4

D0Lim·2021년 1월 7일
0

컴퓨터 구조론

목록 보기
7/13
post-thumbnail

시작하기 전

이 글은 필자가 수업시간에 들은 내용과 강의록을 토대로 정리한 글입니다.
수업 필기이다 보니, 오류가 있거나 설명이 부족한 부분이 있을 수 있습니다.
궁금하신 점이나 지적하실 점이 있다면 댓글로 달아주세요! 확인 후 내용을 추가하거나 답변해드리도록 하겠습니다 :)

시프트 연산(Shift Operations)

mips 1

  • 위의 이미지에서 shamt에 해당하는 부분에 따라 연산한다.
  • shamt는 shift amount로써, 몇 칸이나 시프트 연산을 수행할지에 대한 정보를 담고 있다.
  • MIPS 명령어에서 시프트를 수행하는 명령어는 아래 두 가지가 있다.
    • 논리적 왼쪽 시프트(Shift left logical) : sll
      • 0을 채우면서 왼쪽으로 시프트 한다.
      • ii 비트만큼 sll 하면, 2i2^i 만큼 곱하는 것과 같다.
      • sll $t2, $s0, 4 와 같이 사용한다.
    • 논리적 오른쪽 시프트(Shift right logical) : srl
      • 0을 채우면서 오른쪽으로 시프트 한다.
      • 부호가 없을 경우, ii 비트만큼 srl 하면, 2i2^i 만큼 나누는 것과 같다.
      • srl $t2, $s0, 4 와 같이 사용한다.
  • 위의 시프트 명령마다 붙은 논리적의 의미는 음수를 나눌 때 중요해진다.
    • 예를 들어, -16은 11110000211110000_2 이다. 이것에 2칸만큼 오른쪽 시프트를 수행한다고 가정해보자. 이 때, 0을 채우면서 시프트 할 것인가 아니면 1을 채우면서 시프트 할 것인가?
      • 0을 채우면서 시프트하는 것이 기본 논리대로는 옳지만, 이 경우 결과값은 00111100200111100_2 로, 60이 되어버린다. 이와 같이 0을 채우면서 시프트 하는 것을 논리적 오른쪽 시프트(Logical right shift)라고 한다.
      • 1을 채우면서 시프트하는 것은 왼쪽 시프트와는 논리적인 거리가 있지만, 이 경우 결과값은 11111100211111100_2 로, -4가 된다. 이와 같이 1을 채우면서 부호를 유지하며, 일반적인 계산 값을 얻어내는 시프트를 산술적 오른쪽 시프트(Arithmetic right shift)라고 한다.
    • 프로그래머들은 필요에 따라서 오른쪽 시프트 명령을 잘 사용하면 된다.

AND / OR 연산(AND / OR Operations)

  • AND : word에서 비트를 마스크(mask) 할 때 유용하다. (마스크는 가리는 것을 의미한다고 한다.)
    • 비교하는 비트가 둘 다 1일 때만 결과값이 1, 아니면 0이다.
and $t0, $t1, $t2

and

  • OR : word에 비트를 포함(include)시킬 때 유용하다.
    • 비교하는 비트 둘 중 하나만 1이어도 결과값이 1이다.
or $t0, $t1, $t2

or


NOT 연산(NOT Operations)

  • word에서 비트를 뒤집을 때 유용하다.
    • 0은 1로, 1은 0으로 …
  • MIPS에는 NOT 명령(Instruction)이 없다.
  • 대신 3개의 피연산자가 필요한 NOR을 사용한다.
    • ‘a NOR b’ 는 ‘NOT (a OR b)’ 와 같다.
nor $t0, $t1, $zero # not $t1

(# 은 주석이다)
not


프로그램 내장형 컴퓨터(Stored Program Computers)

현대의 컴퓨터들은 기본적으로 프로그램 내장형 컴퓨터이다. 프로그램 내장형 컴퓨터가 나타나기 전 까지는, 모든 컴퓨터들은 논리 회로들을 이용해서 직접 특정 문제를 해결하는 컴퓨터를 만들어야 했고, 다른 문제를 해결하기 위해서는 하드웨어(논리 회로) 자체를 다르게 연결했어야 했다. 그러나 현대 컴퓨터들은 프로그램이 내장되어 있는 컴퓨터이기 때문에, 프로그램(소프트웨어)만 다시 작성하면 프로세서(하드웨어)가 그를 순차적으로 실행하기 때문에, 하드웨어를 다시 설계할 필요가 없어졌다.


결정을 위한 명령(Instructions for Making Decisions)

  • 컴퓨터는 입력 데이터(input data)나 컴퓨팅 과정 중에 만들어지는 값(value)에 기반하여 결정을 내릴 수 있다.
  • 조건 분기 명령(Conditional branch instructions)
    • 조건을 만족할 경우 라벨된(labeled) 명령으로 분기한다.(if … goto … 와 비슷하다.)
      • 만약 그렇지 않을 경우, 순차적으로 계속 명령을 수행한다.
    • beq rs, rt, L1
      • branch-if-equal : 만약 rs == rt 이면, L1 이라고 라벨된 명령으로 분기한다.
    • bne rs, rt, L1
      • branch-if-not-equal : 만약 rs != rt 이면, L1 이라고 라벨된 명령으로 분기한다.
  • 무조건 분기 명령(Unconditional branch instructions)
    • j L1
      • 무조건적으로 L1 이라고 라벨된 명령으로 점프(jump)한다.

If 구문의 컴파일(Compiling If Statements)

  • C 코드
if (i == j)
    f = g + h;
else
    f = g - h;
  • f, g, h, i, j는 $s0 , $s1 , $s2 , $s3 , $s4 에 저장된다.

  • 컴파일된 MIPS 코드

       bne $s3, $s4, Else
       add $s0, $s1, $s2
       j   Exit
Else : sub $s0, $s1, $s2
Exit : ...
  • 참고로, beq를 사용해서도 같은 작업을 할 수 있지만 C 코드의 if 문 조건을 만족할 경우의 코드가 먼저 오는 것이 직관적이기 때문에 bne를 많이 사용한다고 한다.

반복문의 컴파일(Compiling Loop Statements)

  • C 코드
while (save[i] == k)
    i += 1;
  • i는 $s3 , k는 $s5 , save(배열 포인터)는 $s6 에 저장된다.

  • 컴파일된 MIPS 코드

Loop : sll  $t1, $s3, 2       # i << 2 , $t1 = i * 4
       add  $t1, $t1, $s6     # $t1 = save + i * 4 = &save[i]
       lw   $t0, 0($t1)       # $t0 = *(&save[i]) = save[i]
       bne  $t0, $s5, Exit
       addi $s3, $s3, 1       # i = i + 1;
       j    Loop
Exit : ...

위의 코드를 간단하게 설명해보면,
1. sll 을 이용하여 $t1 에 배열의 인덱스(index)를 저장한다. (4를 곱하기 위해 2번 시프트 한다)
2. 그 $t1$s6 을 더하여 $t1 에 save의 i 번째 인덱스(index)의 주소를 저장한다.
3. 그리고 lw 를 이용하여 $t0save[i] 를 불러온다.
4. 그 후, 조건 분기를 통해서 while 문의 조건을 확인하고, j 를 통해 반복문을 구현한다.


기본 블럭(Basic Blocks)

  • 기본 블럭은 다음을 만족하는 일련의 연속적인 명령(Instruction)들이다.
    • 블럭의 끝을 제외하고는 분기 명령이 없어야 한다.
    • 블럭의 시작을 제외하고는 분기 명령이 들어오는 곳(라벨)이 없어야 한다.
  • 컴파일러는 최적화를 위해 기본 블럭들을 식별한다.
  • 고수준 프로세서는 기본 블럭의 실행을 가속할 수 있다.

다른 조건 연산들(More Conditional Operations)

  • 값들을 비교하고, 만약 조건이 참이라면 결과는 1이 되게 한다.

    • 그렇지 않으면, 0이 되게 한다.
    • slt rd, rs , rt
      • set-on-less-than : 만약, rs < rt 이면, rd = 1 이고, 아니면 rd = 0 이다.
    • slti rd, rs , constant
      • set-on-less-than-immediate : 만약, rs < constant 이면, rd = 1 이고, 아니면 rd = 0 이다.
  • 위의 명령들을 beq , bne 와 함께 사용한다.

    • 그러면 모든 조건들을 만들어 낼 수 있게 된다. (==, !=, <, <=, >, >=)
    • 예를 들어, if ($s1 < $s2) 를 구현하려면 다음과 같이 쓰면 된다.
slt $t0 $s1, $s2  # if ($s1 < s2), $t0 = 1
bne $t0, $zero, L # then, branch to L

부호의 유무(Signed vs Unsigned)

  • 부호가 있는 경우의 비교 : slt , slti

  • 부호가 없는 경우의 비교 : sltu , sltui

  • 참고로, 비교에 사용되는 값들의 비트는 어차피 음수나 양수나 같은 비트이다. 따라서 어떤 명령을 사용할 지에 대한 것은 온전히 프로그래머의 선택에 달려있다.


분기 명령 디자인(Branch Instruction Design)

  • '<' 혹은 '>=' 를 한번에 연산할 수 있는 명령들은 없는걸까?
  1. '>' 혹은 '>=' 등을 위한 하드웨어는 =나 !=를 위한 하드웨어보다 느리다.
  • 분기와 결합하게 되면 명령 당 작업 수를 증가시키게 되고, 결국 더 느린 클럭을 요구하게 된다.
  • 그렇게 되면 빠른 클럭으로 처리할 수 있는 명령들 또한 패널티를 입게 된다. 즉, 느려진다.
  1. beq , bne 는 매우 기본적인 경우에 사용되는 명령으로, 자주 쓰인다.
  2. 이것은 좋은 디자인을 위한 타협이다.

프로시저 호출(Procedure Calling)

  • 프로시저 호출 절차

    1. 매개 변수(Parameter)들을 레지스터에 넣는다.
    2. 제어권을 프로시저로 넘긴다.
    3. 프로시저를 수행하기 위한 저장 공간을 할당받는다.
    4. 프로시저의 연산을 수행한다.
    5. 결과값을 호출자를 위한 레지스터에 넣는다.
    6. 호출을 한 시점(Instruction)으로 돌아간다.
  • PC : 프로그램 카운터(Program Counter)

    • 특별한 레지스터로, 현재 실행되고 있는 명령의 주소를 저장하고 있는 레지스터이다.
  • $ra : 반환 주소(return address)

    • 프로시저의 반환 주소(어디로 돌아갈지)를 가지고 있는 레지스터이다.
  • 프로시저 호출 명령

    • jal ProcedureLabel
      • jump-and-link
      • link : 다음에 올 명령의 주소(PC + 4)를 $ra 에 저장한다.
      • jump : 목적지 주소(target address)로 점프한다.
  • 프로시저 반환(return) 명령

    • jr $ra
      • jump-register
      • PC로 $ra 를 복사한다.
      • case나 switch와 같이 계산된 점프에도 사용될 수 있다.
  • 예시
    다음과 같은 코드가 있다고 하자.

int func(int arg1)
{
    ...
    ...
    foo(b); // call
    ...
    ...
} 

int foo(int arg2)
{
    ...
    ...
    bar(c); // call
    ...
    ...
} // return

int bar(int arg3)
{
    ...
    ...
    ...
} // return

그러면, 다음 그림과 같은 과정을 거치게 된다.
memory

위 그림에서 순서는 아래와 같다.
1. foo 호출
2. bar 호출
3. bar 반환
4. foo 반환

0개의 댓글