Instruction Set

  • 프로세서에서 지원하는 명령어들의 집합
  • 다른 컴퓨터는 다른 명령어셋을 가지고 있지만 기본적으로는 비슷하다.
  • 현대 컴퓨터는 대부분 간단한 명령어셋(MIPS - RISK 방식)을 가지고 있다.

Instruction Set Architecture (ISA)

  • 하드웨어와 낮은 레벨의 소프트웨어를 연결하는 인터페이스로 시스템 소프트웨어(운영체제)라 할 수 있다.
  • ISA가 같으면 같은 소프트웨어를 여러 개의 CPU에서 실행할 수 있다.

ABI

  • ISA와 운영체제 인터페이스를 합친 것
  • ABI만 같으면 같은 프로그램이 어디에서든 실행될 수 있다.
  • 윈도우 운영체제를 쓰는 컴퓨터들은 어떤 컴퓨터를 쓰든 같은 프로그램을 실행할 수 있는 것


Design Principle

1. 정규화(규칙화)해서 간단하게 만들기

  • 간단할수록 저비용으로 고성능을 만들기가 쉽다.

2. 메모리 용량이 작은 것이 빠르다

  • 메모리 용량을 작은 레지스터를 최대한 활용하는 것이 성능 향상에 도움이 된다.

3. 공통 케이스는 빠르게 만들기

  • 자주 사용하고 비중이 높은 연산은 빠르게 처리하도록 만드는 것이 성능 향상에 좋다.

4. 좋은 디자인을 위해 최대한 통일하기

  • 명령어의 포맷 가짓수는 최대한 줄이고 통일하는 것이 좋다.
  • 포맷이 다르면 디코딩 하는 데 복잡해지고 이것은 성능 하락으로 이어지기 때문이다.


Operations of the Computer Hardware

Arithmetic Operations (산술연산)

  • register 간 연산으로 RISK 프로세서 방식
  • addsubtract, 더하기와 빼기로 이루어져 있으며 피연산자 3개가 필요하다.
add a, b, c
  • 위와 같이 쓰면 bc를 더한 값을 a에 저장해라는 의미


Operands of the Computer Hardware

Register Operands

  • 자주 사용하는 데이터에 빠르게 접근하기 위해서 레지스터를 사용한다.
  • 32개의 bit로 이루어져 있으며 word라 부른다.
  • MIPS는 32개의 32bit 레지스터 파일을 가지고 있다.

Byte Addresses

  • 대부분의 아키텍처는 byte 단위로 메모리를 관리한다.
  • word4bytes로 이루어져 있으며 이것은 하나의 명령어 단위가 된다.
  • 레지스터에 있는 데이터를 메모리에 저장할 때 자리수가 큰 게 최하위 비트(LSB)에 오면 Big Endian, 자리수가 제일 작은 것이 최하위 비트에 오면 Little Endian이라 한다.

Memory Operands

  • 메인 메모리는 자료의 집합을 이용한다.
  • 산술 연산을 하기 위해서 메모리에 접근할 수 있는 명령어가 지정되어 있다.(Load/Store)
  • 메모리는 8bit 크기의 주소로 이루어져 있다.
  • 모든 명령어의 크기는 4bytes이기 때문에 메모리 주소 또한 4bytes 간격으로 나열되어 있다.
  • MIPS는 빅 엔디안 방식을 사용한다.

Registers vs. Memory

  • 레지스터가 메모리에 접근하는 것 보다 훨씬 빠르다.
  • 그래서 메모리 접근을 최대한 줄이고 레지스터에서 연산하는 것이 성능 향상에 좋은데 그렇다고 너무 레지스터만 써도 성능이 떨어지니까 자주 쓰지 않는 데이터는 메모리로 내려주는 것이 좋다.
  • 한 주소는 32bit로 이루어져 있는데 레지스터 하나는 5bit 크기이기 때문에 메모리에 접근하는 것 보다는 레지스터를 최대한 사용하는 것이 한 번에 처리할 수 있는 코드가 많아진다.

Immediate Operands

  • 피연산자 중에 하나가 상수일 때 사용하는 명령어
addi $s3 , $s3, 4 // 동작은 add와 같음
addi $s2, $s1, -1 // 뺄셈은 -1 사용
  • 상수를 레지스터에서 만들어 쓰지 않으면 메모리에 접근해서 가져와야 하는데 이러면 느리다.
  • 그래서 0 같이 자주 사용되는 상수는 메모리에 접근할 필요 없이 레지스터에서 바로 연산하면 훨씬 빠르다.

Constant Zero

  • $zero라고 표시하며 read only로만 사용할 수 있다.
  • 주로 레지스터간 move연산을 할 때 사용하는데
add $t1, $s1, $zero
  • move와 같은 명령어를 따로 만들어서 사용하는 것 보다 add연산을 하는데 나머지 연산자를 0으로 만들어서 사용하면 move/copy와 같은 효과를 낼 수 있어서 명령어를 따로 만들지 않고 이렇게 쓴다.
  • 왜냐면 명령어 셋은 최대한 간단한 것이 성능 향상에 좋기 때문이다.
  • 어떤 특정 연산 처리만을 위한 명령어를 많이 만들어 쓰다 보면 구현 자체도 쉽지 않지만 구현해도 그 명령어를 처리하는 시간이 다른 쉬운 명령어보다 늘어나는데, 그 늘어난 시간은 다른 간단한 명령어의 실행 시간에도 영향을 미쳐서 다 같이 느려진다.


Logical Operations

시프트 연산자

  • 왼쪽 시프트
    • 시프트하면서 생기는 빈 비트는 0으로 채운다.
    • 원래 값에 2^i 을 곱한 효과(음수, 양수 둘 다 적용)
  • 오른쪽 시프트
    • 시프트하면서 생기는 빈 비트는 0으로 채운다.
    • 원래 값을 2^i 만큼 나눈 효과(양수에만 적용됨)

비트 연산자

  • and, or, nor 연산자를 사용한다.
  • MIPS에는 NOT 연산자가 없기 때문에 대신 nor 연산자(a도 아니고 b도 아니다)를 사용해서 NOT 연산자와 같은 효과를 낸다.


Instructinos for Making Decisions

Conditional Operations

  • 어떤 조건이 true인 경우에 이름지어져 있는 명령어로 분기를 나누고 false라면 다음 명령어를 계속 실행하는 것 (if ~ else문)
beq rs, rt, L1 // rs와 rt가 같으면 L1에 있는 명령어 실행
bne rs, rt, L2 // rs와 rt가 같지 않으면 L1에 있는 명령어 실행
j L1 // 무조건 L1으로 Jump하는 것
  • 어떤 수들의 대소관계 비교도 전용 명령어를 따로 만들지 않고 beq를 비롯한 여러 명령어들을 조합해서 쓰는 것이 성능 면에서 더 좋기 때문에 조합해서 쓴다.


명령 실행 단계

1) callercallee에게 파라미터를 넘긴다. 2) callercallee에게 제어권을 넘긴다.(실행) 3) callee가 스택에 메모리를 할당한다. 4) callee가 태스크를 수행한다. 5) calleecaller가 접근할 수 있는 곳에 결과를 둔다. 6) calleecaller에게 제어권을 넘긴다.

  • Procedure 실행에는 LeafNon-Leaf 방식이 있는데 Leaf는 자기 자신을 포함한 어떤 함수도 호출하지 않는 것이고 Non-Leaf는 자기 자신을 포함한 함수를 호출하는 것이다.
  • 프로그래밍 할 때 흔히 작성하는 값 하나를 리턴하고 끝나는 함수는 Leaf 방식이고 재귀 함수와 같은 형태는 Non-Leaf로 이루어진다.

메모리 영역

  • Text : 프로그램 코드(명령어)가 있는 영역
  • Static data : 전역 변수가 있는 영역
  • Dynamic data : heap 영역이라고도 하며 동적으로 할당된 메모리가 있는 영역. 메모리 주소를 가리키는 포인터는 아래에서 위로 이동한다.
  • Stack : 함수가 호출되면 생기는 지역 변수가 있는 영역. 메모리 주소를 가리키는 포인터는 위에서 아래로 이동한다.


코드가 실행되는 과정

1) 프로그래밍 언어로 프로그램을 작성한다. 2) 컴파일러가 어셈블리어로 번역한다. 3) 어셈블러가 기계어로 번역한다. 4) 기계어로 번역하면서 내가 쓴 코드를 이용해서 만든 기계어 오브젝트와 라이브러리에서 가져오는 코드로 만든 오브젝트가 생기는데 링커가 두 오브젝트 코드들을 합쳐서 실행파일로 만든다. 5) 로더가 실행파일을 메모리에 올려서 실행상태로 만든다.


알고리즘과 수행속도

  • 명령어 수와 CPI가 낮은 것이 무조건 성능이 좋은 것은 아니다. 성능에 영향을 미치는 것은 여러가지 요인이 있다.
  • 컴파일러 최적화는 알고리즘에 영향을 많이 받는다.
  • 그렇기 때문에 뭐니뭐니해도 알고리즘이 효율적이어야 성능이 좋아진다.


배열과 포인터

  • 둘 다 배열을 다룰 때 사용할 수 있지만 배열 인덱스에 접근하려면 내부적으로 인덱스의 주소값을 계산하는 과정이 필요하다. (시작 주소에서부터 몇 칸 떨어져 있는지…)
  • 하지만 포인터는 그런 연산이 필요없이 그냥 4씩 더해주면서 다음 메모리 주소로 이동하면 된다.
  • 그렇지만… 최신 컴파일러는 내가 직접 포인터 연산하는 코드를 쓰는 것과 배열 인덱스로 접근하는 코드가 같은 성능을 낼 수 있도록 최적화를 다 해 준다.
  • 그래서 같은 동작을 수행하는 코드라면 배열 인덱스를 사용하는 코드를 써도 무방하며 포인터를 사용한 코드보다 이해하기도 더 쉽다. 포인터에 비해 버그를 일으키는 코드를 작성할 확률도 줄어드니까 그냥 배열을 쓰자.


x86 Instructions

  • MIPS와는 명령어 셋이 다르며 설계 면에서도 차이가 있다.
  • MIPS에 비해 다소 복잡하게 설계되어 있어 고성능으로 만들기 어려운 CISK 아키텍처다.
  • 그래서 고성능을 내기 위해 하드웨어가 내부적으로 복잡한 원래 명령어를 간단한 명령어들로 쪼갠 다음에 실행하는 방식을 쓴다.(결국 RISC와 같은 매커니즘으로 실행되게 된다고 볼 수 있다)
  • 이렇게 보면 인텔은 진작에 망했어야 할 거 같지만 시장 점유율을 성공적으로 높이면서 자리잡았기 때문에 인텔 프로세서에서 실행되는 프로그램들이 많다보니 여전히 높은 점유율을 차지하며 지금까지 오고 있는 것이다.


결론

  • 명령어 여러 개를 한꺼번에 처리하면 효율적이겠지만 그만큼 하드웨어 게이트(로직) 수가 많아지면서 실행 속도가 느려진다. 그리고 다른 명령어도 같이 느려진다.
  • 그래서 간단한 명령어를 여러개 조합해서 쓰는 것이 효율적이다. (RISK Processor 철학)
  • 어셈블리 코드를 쓰면 기계어와 가까워서 좋은 성능을 낼 수 있지만 최신 컴파일러는 C 언어와 같은 고급 언어로 쓴 코드도 어셈블리어와 비슷한 성능을 낼 수 있도록 최적화를 다 해 주기 때문에 그냥 고급 언어를 쓰는 것이 생산성이 더 높고 좋다. (어셈블리어는 생산성이 낮다)


출처