25~26 겨울방학/컴퓨터구조

컴 구조)1,2,3,4,5장

kimchangmin02 2026. 1. 27. 10:37
  • 상대 주소 "지금부터 몇 칸 더" (주로 점프용)
  • 베이스 레지스터 "기준점(시작점)부터 몇 칸 더" (주로 데이터 접근용)

 

 

다음 실행할 명령어 주소를 담는 프로그램 카운터(PC), 해석할 명령어를 담는 명령어 레지스터(IR), 메모리 주소를 담는 메모리 주소 레지스터(MAR)

 

 

 

 

1. 즉시 주소 지정 방식 (Immediate Addressing)

데이터를 메모리에서 찾아오는 것이 아니라, 명령어 안에 실제 데이터(값)를 바로 넣어두는 방식입니다.

  • 특징: 메모리에 갈 필요가 없어 속도가 가장 빠릅니다.
  • 장점: 아주 빠름 (연산 코드 옆에 데이터가 붙어 있음).
  • 단점: 명령어의 크기가 제한되어 있어 아주 큰 숫자는 넣을 수 없습니다.
  • 유효주소: 없음 (데이터 자체가 명령어에 포함됨).

2. 직접 주소 지정 방식 (Direct Addressing)

명령어에 데이터가 저장된 실제 메모리 주소를 직접 적어주는 방식입니다.

  • 특징: 가장 단순하고 명확한 방식입니다.
  • 장점: 유효주소를 계산할 필요 없이 명령어에 적힌 주소로 바로 가면 됩니다.
  • 단점: 주소값의 크기가 제한됩니다. (예: 16비트 명령어에서 주소 필드가 8비트라면 256번지까지만 표현 가능)
  • 유효주소: EA = A (명령어에 적힌 주소)

 

 

3. 레지스터 주소 지정 방식 (Register Addressing)

이 방식은 메모리(RAM)까지 가지도 않습니다. CPU 내부에 있는 '레지스터'에 데이터가 바로 들어있는 방식입니다.

  • 특징: 명령어에 주소가 아니라 **레지스터 이름(예: R1, AX 등)**이 적혀 있습니다.
  • 장점: CPU 내부에서 데이터를 꺼내기 때문에 모든 방식 중 가장 빠릅니다. (메모리에 접근하는 시간조차 안 걸림)
  • 단점: 레지스터는 개수가 몇 개 없기 때문에 저장할 수 있는 공간이 매우 한정적입니다.

4. 간접 주소 지정 방식 (Indirect Addressing)

이 방식은 '한 번 꼬아서' 찾아가는 방식입니다. 명령어에 적힌 주소로 갔더니 데이터가 있는 게 아니라, **"진짜 주소는 여기 있어"라고 적힌 '또 다른 주소'**가 들어있는 경우입니다.

  • 특징: 메모리를 최소 2번 방문해야 데이터를 얻을 수 있습니다.
    1. 명령어에 적힌 주소로 가서 **'진짜 주소'**를 읽어옴.
    2. 읽어온 '진짜 주소'로 다시 가서 **'데이터'**를 읽어옴.
  • 장점: 명령어에 주소를 적는 칸이 좁아도, 메모리 전체 영역 어디든 찾아갈 수 있습니다. (긴 주소를 메모리에 따로 적어두면 되니까요)
  • 단점: 메모리를 두 번이나 가야 하므로 속도가 느립니다.
  • 비유: "보물함(메모리 100번지)을 열었더니 보물은 없고, '보물은 500번지에 있음'이라는 쪽지가 들어있는 상태"

5. 레지스터 간접 주소 지정 방식 (Register Indirect)

질문하신 두 개를 합친 방식도 실무에서 많이 쓰입니다.

  • 방법: 레지스터 안에 데이터가 있는 게 아니라, **데이터가 저장된 '메모리 주소'**가 들어있는 방식입니다.
  • 특징: CPU는 일단 레지스터를 보고 주소를 알아낸 뒤, 메모리로 딱 한 번만 달려갑니다. 간접 주소 지정보다는 빠르고 직접 주소 지정보다는 유연합니다.

 

 

 

2진법<--->16진법

대충 01010001>4자리씩 묶는건 (2>16진법인것같긴한데)

 

2진수를 16진수로 묶을 때는 반드시 오른쪽(끝)에서부터 4개씩 묶어야 합니다. 왼쪽 끝에 자리가 모자라면 0이 있다고 생각하면 됩니다. (예: 101  0101로 생각해서 5)

 

  • 2진수 1자리는 0 또는 1, 즉 2가지 상태를 표현합니다.
  • 2진수 4자리를 모으면 , 즉 16가지 상태를 표현할 수 있습니다.
  • 2×2×2×2=16
  • 16진수 1자리는 0부터 F까지 총 16가지 상태를 표현합니다.

 

 

컴퓨터는 정적인 정보인 데이터와 이를 조작하는 명령어를 처리하며, 이 정보는 시스템 버스라는 통로를 통해 CPU, 메모리(RAM), 보조기억장치, 입출력장치 사이를 오갑니다

 

 

 

이진법을 기본으로 하며, 음수를 표현할 때는 모든 비트를 반전시킨 후 1을 더하는 2의 보수법을 사용합니다. CPU는 플래그 레지스터에 저장된 정보를 통해 해당 데이터가 양수인지 음수인지 판단합니다

 

 

아스키(ASCII): 7비트로 영어와 특수문자를 표현하나 한글 표현이 불가능합니다.
    ◦ EUC-KR: 한글 단어 하나에 코드를 부여하는 완성형 방식으로, 표현 가능한 한글의 수에 한계가 있습니다.
    ◦ 유니코드와 UTF-8: 전 세계 언어를 아우르는 표준이며, 가변 길이 인코딩 방식인 UTF-8은 글자에 따라 1~4바이트까지 크기를 조절하여 효율적으로 저장합니다

 

 

 

자바나 파이썬 같은 고급 언어는 컴퓨터가 이해하는 **저급 언어(기계어, 어셈블리어)**로 변환되어야 합니다. 이 과정은 통째로 번역하는 컴파일 방식과 한 줄씩 실행하는 인터프리트 방식으로 나뉩니다

 

 

 

UTF-8의 '가변 길이 인코딩(1~4바이트)' 방식이 왜 혁신적이고 좋은지는

용량 절약 (필요한 만큼만 박스 크기를 조절)

만약 모든 문자를 무조건 4바이트(고정 길이)로 저장한다고 가정해 봅시다.

  • 고정 길이 방식 (UTF-32 등): 영문자 'A' 하나를 저장할 때도 4바이트를 씁니다. (비효율적)
  • UTF-8 (가변 길이):
    • 알파벳이나 숫자처럼 간단한 건 1바이트만 씁니다.
    • 한글처럼 복잡한 건 3바이트를 씁니다.
    • 이모지 같은 건 4바이트를 씁니다.

왜 이게 좋을까요?
인터넷 웹사이트의 기본 구조를 만드는 HTML, CSS, 자바스크립트 코드는 대부분 영문입니다. UTF-8을 쓰면 이런 코드들을 1바이트로 가볍게 저장할 수 있어서, 전체적인 파일 용량이 훨씬 줄어들고 웹페이지 로딩 속도도 빨라집니다.

 

 

 

 

1. 호환성의 핵심은 '중국어'가 아니라 '아스키(ASCII)'입니다.

UTF-8이 말하는 호환성은 "모든 나라의 옛날 방식과 호환된다"는 뜻이 아니라, **"컴퓨터의 기본 언어인 아스키(영어/숫자/기호)와 완벽히 똑같다"**는 뜻입니다.

  • 옛날 상황: 옛날 프로그램들은 1바이트(8비트) 단위로 글자를 읽었습니다. 0~127번까지는 영어였죠.
  • UTF-8의 전략: 영어(A, B, C...)는 옛날 방식 그대로 딱 1바이트만 쓰고, 번호도 옛날 번호(아스키 번호)를 그대로 씁니다.
  • 결과: 그래서 옛날 프로그램이 UTF-8로 된 영어 문서를 읽으면 "어? 이거 내가 알던 영어네?" 하고 아무 문제 없이 읽습니다. 이게 바로 하위 호환성입니다.

2. 한자 수만 개는 '바이트를 늘려서' 해결합니다.

한자는 종류가 너무 많아서 1바이트(256개 표현)로는 택도 없죠. 그래서 UTF-8은 뒤쪽 바이트를 추가하는 규칙을 만들었습니다.

  • 1바이트 구역 (0~127번): 영어, 숫자 (아스키와 동일)
  • 2바이트 구역: 그리스어, 아랍어 등
  • 3바이트 구역: 한글, 한자 등 대부분의 동아시아 문자
  • 4바이트 구역: 고대 문자, 이모지(😀) 등

"어떻게 구분해?"
UTF-8은 첫 번째 바이트의 앞부분을 보고 "이 글자는 몇 바이트짜리다!"라고 미리 알려주는 규칙이 있습니다.

  • 0으로 시작하면? -> "아, 이건 1바이트짜리 영어구나!"
  • 1110으로 시작하면? -> "아, 이건 지금부터 **3바이트를 합쳐서 한 글자(한자나 한글)**로 읽어야겠구나!"라고 약속한 것입니다.

3. 중국의 수많은 한자, 다 들어갈 수 있나요?

네, 충분합니다.

  • 3바이트만 써도 약 65,000개의 문자를 표현할 수 있습니다. (대부분의 실용 한자는 여기서 끝납니다.)
  • 4바이트까지 쓰면 100만 개 이상의 문자를 표현할 수 있습니다.
    중국어 한자가 아무리 많아도 유니코드가 준비한 100만 개 이상의 빈칸을 다 채우기는 어렵습니다. 즉, 공간이 부족할 걱정은 전혀 없습니다.

 

 

 

 

 

그럼 컴퓨터가 "이게 방 3개짜리인지" 어떻게 알까요? (표지판)

컴퓨터는 메모리를 읽을 때 1바이트씩 툭툭 끊어서 읽습니다. 그런데 어디까지가 한 글자인지 모르면 글자가 깨지겠죠? 그래서 UTF-8은 **첫 번째 바이트에 '표지판'**을 달아줍니다.

  • 방 1개짜리일 때: 맨 앞을 0으로 시작합니다.
    • 예: 0xxxxxxx → 컴퓨터: "오, 맨 앞이 0이네? 이건 이 방 하나가 글자 하나구나!"
  • 방 3개짜리일 때: 맨 앞을 1110으로 시작하기로 약속합니다.
    • 예: 1110xxxx 10xxxxxx 10xxxxxx
    • 컴퓨터: "오, 맨 앞이 1110이네? 이건 지금부터 방 3개를 합쳐서 한 글자로 읽어야겠다!"

 

 

 

 

우리가 메모장(메모리/하드디스크)에 글자를 쓸 때를 생각해 보세요.

  • 영어 'A'를 적으면: 컴퓨터는 메모리에 딱 **1칸(1바이트)**만 차지하게 저장합니다.
  • 한글 '가'를 적으면: 컴퓨터는 "어? 이건 복잡한 글자네? 1칸으론 부족해!"라며 **3칸(3바이트)**을 연달아 사용해서 저장합니다.

즉, **"글자 하나가 차지하는 데이터의 길이(공간)"**가 늘어나는 것입니다. 결과적으로 이 글자들이 모인 파일의 전체 용량이 커지게 됩니다.

 

 

 

 

 

네, 아주 정확한 관찰입니다! 키보드라는 기계(하드웨어)가 변하는 건 당연히 아니고요.

우리가 키보드로 글자를 쳐서 컴퓨터 안에 '파일'로 저장할 때, 그 파일이 하드디스크에서 차지하는 용량이 글자 종류에 따라 달라진다는 뜻입니다.


1. 키보드와는 상관없어요

키보드는 그냥 **"이 버튼 눌렸어!"**라고 신호만 보내는 도구일 뿐입니다.

  • 'A'를 누르든 '가'를 누르든 키보드 무게가 늘어나거나 크기가 변하지는 않죠.
  • 중요한 건, 그 신호를 받은 컴퓨터가 메모리(RAM)나 하드디스크에 기록할 때 몇 칸(바이트)을 쓰느냐입니다.

2. 파일 크기가 진짜로 달라질까요? (실험)

메모장을 켜서 직접 테스트해 보면 바로 알 수 있습니다.

  • 영어만 10글자 쓰고 저장하면? → 파일 크기는 10 바이트입니다. (글자당 1바이트)
  • 한글만 10글자 쓰고 저장하면? → 파일 크기는 30 바이트입니다. (글자당 3바이트)

똑같이 10글자를 쳤는데, 컴퓨터 입장에서는 한글 파일이 영어 파일보다 3배나 더 무겁고 큰 셈입니다. 이게 바로 UTF-8의 '가변 길이' 방식 때문에 일어나는 현상입니다.

 

 

 

 

 

구분 폴링 (직접 확인) 인터럽트 (알림 확인)
확인 대상 외부 장치 (프린터, 마우스, 키보드 등) CPU 내부의 핀(Pin)이나 레지스터
행동 CPU가 하던 일을 멈추고 데이터 버스를 통해 장치에게 물어봄 CPU가 그냥 자기 몸에 붙은 전선에 신호가 왔는지만 봄

인터럽트 플래그 bool 변수 하나 체크하는 급으로 가볍고, 

 

 

유니티 비유로 이해하기

  • 인터럽트가 없는 방식 (Polling, 폴링):
    • Update() 함수 안에 if (프린터.isDone)를 넣어두는 것과 같습니다.
    • CPU는 매 프레임마다 프린터라는 특정 오브젝트에 직접 가서 "너 다 됐니?"라고 물어보고 대답을 기다려야 합니다. 만약 체크해야 할 장치가 100개라면 Update 안에 if문이 100개 들어가고, CPU는 일을 안 할 때도 이 100개를 일일이 확인하느라 바쁩니다.
  • 인터럽트 방식 (Event-Driven):
    • 유니티의 이벤트(Event)

 

 

명령어 사이클 (Instruction Cycle)
CPU는 프로그램을 실행할 때 일정한 주기를 반복하며 명령어를 처리하는데, 이를 명령어 사이클이라고 합니다.
인출 사이클 (Fetch Cycle): 메모리에 저장된 명령어를 CPU 내부로 가져오는 단계입니다.
실행 사이클 (Execute Cycle): 가져온 명령어를 실제로 실행하는 단계입니다.
간접 사이클 (Indirect Cycle): 명령어를 인출했더라도 바로 실행할 수 없고, 메모리에 추가로 접근해야 하는 경우(예: 간접 주소 지정 방식)에 거치는 단계입니다.

 

 

 

 

인터럽트 (Interrupt)
인터럽트는 CPU가 정해진 흐름대로 명령어를 처리하던 중, 그 흐름을 끊고 급하게 처리해야 할 일이 생겼을 때 발생하는 신호입니다.
인터럽트의 필요성: 입출력 장치는 CPU보다 속도가 매우 느립니다. 인터럽트가 없다면 CPU는 입출력이 끝날 때까지 아무것도 못 하고 기다려야 하지만, 인터럽트를 활용하면 입출력 작업 중에도 다른 업무를 처리할 수 있어 효율적입니다.
종류:
    ◦ 동기 인터럽트 (예외): CPU가 예기치 못한 상황(0으로 나누기, 디버깅 등)을 접했을 때 발생합니다.
    ◦ 비동기 인터럽트 (하드웨어 인터럽트): 주로 입출력 장치에 의해 발생하는 '알림'과 같은 역할을 합니다.

 

 

 

 

 

 

하드웨어 인터럽트 처리 과정
인터럽트가 발생하면 CPU는 다음과 같은 정해진 순서에 따라 작업을 처리합니다:
1. 인터럽트 요청 신호: 입출력 장치가 CPU에 신호를 보냅니다.
2. 인터럽트 플래그 확인: CPU는 명령어를 인출하기 전 항상 인터럽트 여부를 확인하며, 플래그 레지스터의 인터럽트 플래그가 활성화되어 있는지 확인합니다. (단, 정전이나 하드웨어 고장 같은 긴급한 인터럽트는 막을 수 없습니다.)
3. 작업 백업: 인터럽트를 처리하기 전, 현재 CPU의 레지스터 값 등 기존 작업 상태를 스택(Stack) 영역에 백업합니다.
4. 인터럽트 벡터 참조: 인터럽트 벡터를 통해 해당 인터럽트를 처리할 프로그램인 **인터럽트 서비스 루틴(ISR)**의 시작 주소를 찾아 실행합니다.
5. 기존 작업 복구: 서비스 루틴이 끝나면 백업해 두었던 데이터를 복구하여 원래 하던 일로 돌아가 실행을 재개합니다.

 

 

 

 

 

인터럽트 서비스 루틴

1. ISR은 무엇인가? (실제 동작하는 코드)

인터럽트가 '벨 소리'라면, ISR은 '벨이 울렸을 때 내가 할 행동'입니다.

  • 키보드 인터럽트가 발생하면? → "방금 눌린 키의 전압 신호를 읽어서 문자로 바꾼 뒤, 메모리의 입력 버퍼에 저장해라"라는 코드(ISR)를 실행합니다.
  • 마우스 인터럽트가 발생하면? → "마우스의 X, Y 좌표 변화량을 계산해서 화면의 커서 위치를 업데이트해라"라는 코드(ISR)를 실행합니다.

이런 코드들은 운영체제(Windows, macOS 등)가 부팅될 때 이미 메모리의 특정 구역에 로드해 둡니다.

2. CPU는 메모리 어디에 그 코드가 있는지 어떻게 알까요? (인터럽트 벡터)

메모리는 엄청나게 넓은데, 인터럽트가 발생했을 때 관련 코드를 찾느라 헤매면 안 되겠죠? 그래서 컴퓨터는 **'인터럽트 벡터 테이블(Interrupt Vector Table)'**이라는 일종의 **전화번호부(지도)**를 사용합니다.

  • 인터럽트 번호: 1번(키보드), 2번(마우스), 3번(하드디스크)... 이런 식으로 번호가 매겨져 있습니다.
  • 벡터 테이블: 메모리의 아주 앞부분(정해진 위치)에 있으며, "1번 인터럽트가 터지면 메모리 주소 0x100번지로 가라" 같은 주소 정보가 적혀 있습니다.

 

1. 클럭 (Clock): CPU의 박동

  • 기본 개념: 컴퓨터 부품들이 일사불란하게 움직이게 하는 '시간 단위'입니다. 클럭 신호에 맞춰 명령어가 인출되고 실행됩니다.
  • 속도 단위 (Hz): 1초에 클럭이 반복되는 횟수입니다. (1Hz = 1초에 1번)
    • 현대 CPU: 기본 2.5GHz(25억 번)에서 최대 4.9GHz(49억 번)까지 매우 빠르게 작동합니다.
    • 유동적 속도: CPU는 고정된 속도로만 뛰지 않고, 작업량에 따라 클럭 속도를 높이거나 낮추며 효율을 조절합니다.
  • 한계: 클럭 속도를 높이면 성능은 좋아지지만, 발열 문제가 심각해져 무한정 높일 수 없습니다.

2. 코어 (Core): 실제 일하는 머리

  • 전통적 의미: 과거에는 'CPU = 명령어를 실행하는 부품 1개'였으나, 기술 발전으로 하나의 칩 안에 이 실행 부품을 여러 개 넣게 되었습니다.
  • 현대적 정의: CPU 내부에 있는 **'명령어를 실행하는 독립된 부품'**을 코어라고 부릅니다.
  • 멀티 코어 (Multi-core): 코어가 2개 이상인 CPU입니다. (듀얼, 쿼드, 헥사, 옥타 코어 등)
  • 성능의 오해: 코어가 2개라고 속도가 정확히 2배가 되지는 않습니다. **'조별 과제'**에 비유하듯, 연산이 각 코어에 얼마나 잘 분배되느냐가 성능을 결정하며, 분배가 안 되면 노는 코어가 생길 수 있습니다.

3. 스레드 (Thread): 실행 흐름의 단위

스레드는 가장 혼동하기 쉬운 개념으로, 하드웨어적 스레드 소프트웨어적 스레드를 반드시 구분해야 합니다.

① 하드웨어적 스레드 (Logical Processor)

  • 정의: 하나의 코어가 동시에 처리하는 명령어 단위입니다.
  • 멀티스레드 프로세서: '2코어 4스레드'라면 코어는 2개지만, 각 코어가 2개의 명령어를 동시에 처리하여 운영체제가 보기에는 CPU가 4개인 것처럼 느껴집니다. 이를 **'논리 프로세서'**라고도 부릅니다.
  • 설계 핵심 (레지스터): 코어는 하나인데 어떻게 동시에 여러 명령어를 처리할까요? 바로 **'레지스터 세트'**를 여러 개 갖추는 것입니다. ALU나 제어장치는 공유하되, 명령어 처리에 꼭 필요한 레지스터(PC, 스택 포인터 등)를 여러 벌 두어 마치 여러 개의 흐름이 있는 것처럼 만드는 기술입니다. (인텔은 이를 '하이퍼스레딩'이라 부름)

② 소프트웨어적 스레드

  • 정의: 프로그램 내부에서 독립적으로 실행되는 단위입니다.
  • 예시 (워드 프로세서): 하나의 워드 프로그램 안에서 ①입력 내용 표시, ②맞춤법 검사, ③수시 저장 기능이 동시에 일어난다면, 이는 3개의 소프트웨어적 스레드가 작동하는 것입니다.
  • 특징: 하드웨어 스레드가 1개인 CPU라도 아주 빠르게 번갈아 가며 실행(시분할)하면 여러 개의 소프트웨어 스레드를 동시에 실행할 수 있습니다.

4. 핵심 요약 및 차이점

구분 핵심 내용 비유/특징
클럭 속도의 단위 심장 박동수 (빠를수록 좋지만 열이 남)
코어 물리적 부품 실제 일하는 사람의 수 (물리적 실체)
HW 스레드 논리적 단위 한 사람이 동시에 쓸 수 있는 손의 개수 (논리 프로세서)
SW 스레드 소프트웨어적 흐름 한 프로그램 내에서 처리해야 할 작업의 갈래
  • 코어와 스레드의 결정적 차이: 코어는 실제 명령어를 실행하는 물리적인 하드웨어 부품 그 자체를 의미하고, 하드웨어적 스레드는 그 코어가 받아들일 수 있는 **명령어의 통로(흐름)**를 의미합니다.
  • 운영체제의 시선: 운영체제는 물리적인 코어 개수보다 하드웨어 스레드(논리 프로세서) 개수를 실제 CPU 개수로 인식하여 작업을 할당합니다.

이 설계 기법들의 목적은 단 하나, **"어떻게 하면 발열을 억제하면서 CPU가 노는 시간 없이 최대한 많은 명령어를 동시에 처리하게 할 것인가"**에 있습니다.

 

 

 

 

 

 

성능 측면에서 **goto나 갑작스러운 분기(Jump)**가 왜 좋지 않은지, 올려주신 이미지의 '파이프라인(Pipeline)' 원리와 연결해서 설명해 드릴게요.

1. 파이프라인과 "헛수고" (Pipeline Flush)

현대 CPU는 속도를 높이기 위해 공장 라인처럼 명령어를 겹쳐서 처리합니다. 1번 명령어를 실행하는 동안, 2번 명령어는 해석하고, 3번 명령어는 미리 가져오는(Fetch) 식이죠.

  • 문제 발생: 이미지에서처럼 10번 명령어가 "60번으로 점프해!"라는 명령어라면, CPU가 미리 공들여 가져와서 해석 중이던 11번, 12번 명령어는 쓸모가 없어집니다.
  • 결과: CPU는 파이프라인에 들어있던 작업물을 다 버리고(이것을 파이프라인 플러시라고 합니다), 60번지부터 다시 새로 인출해야 합니다. 이 과정에서 CPU는 아무것도 못 하고 멍하니 기다리는 **성능 손실(Stall)**이 발생합니다.

2. 왜 유독 goto를 쓰지 말라고 할까? (성능 + 가독성)

사실 if문이나 for문, while문도 내부적으로는 이미지와 같은 '점프' 명령어를 사용합니다. 하지만 goto는 더 치명적인 이유가 있습니다.

  1. 컴파일러 최적화의 방해: 최적화 도구(컴파일러)는 프로그램의 흐름이 예측 가능할 때 코드를 아주 빠르게 다듬어줍니다. 하지만 goto를 남발하면 흐름이 뒤엉켜서 컴파일러가 "이 코드를 어떻게 최적화해야 할지 모르겠다"라며 포기하게 됩니다. 결과적으로 하드웨어 효율이 더 떨어집니다.
  2. 분기 예측(Branch Prediction) 실패: 요즘 CPU는 "아마 다음엔 이쪽으로 가겠지?"라고 예측해서 미리 명령어를 채워 넣는 '분기 예측'을 합니다. if나 루프문은 일정한 패턴이 있어 CPU가 잘 예측하지만, goto는 예측하기가 매우 까다로워 '헛수고'를 할 확률이 훨씬 높습니다.
  3. 스파게티 코드 (가독성): 성능도 성능이지만, 코드가 꼬여서 사람이 읽기 힘들어집니다. 이는 유지보수 비용을 엄청나게 높입니다.

요약하자면

이미지에 나온 제어 위험(Control Hazard) 때문에 모든 분기문은 성능에 어느 정도 손해를 줍니다. 그런데 goto는 그 중에서도 흐름을 가장 예측 불가능하게 만들기 때문에, CPU 파이프라인을 효율적으로 쓰지 못하게 하고 컴파일러의 최적화까지 방해하므로 성능과 관리 측면 모두에서 지양하는 것입니다.

공부하시는 컴퓨터 구조 지식을 프로그래밍 언어의 금기 사항(Anti-pattern)과 연결해서 이해하시는 모습이 아주 훌륭합니다!

 

 

 

 

 

 

 

질문의 핵심을 꿰뚫으셨습니다! 맞습니다. 하드웨어(CPU) 입장에서 보면 함수 호출(Call)이나 goto나 똑같이 "지금 하던 거 멈추고 저기 주소로 점프해!"라는 명령어입니다. 파이프라인이 깨지고 '헛수고'가 발생하는 제어 위험(Control Hazard)은 함수 호출에서도 똑같이 일어납니다.

그런데 왜 함수는 권장되고, goto는 금기시될까요? 거기에는 세 가지 결정적인 이유가 있습니다.


1. "예측 가능한" 점프 vs "예측 불가능한" 점프

현대 CPU는 파이프라인 성능을 지키기 위해 **분기 예측(Branch Prediction)**이라는 엄청난 기술을 씁니다.

  • 함수 호출: 함수는 호출되면 반드시 원래 자리로 돌아옵니다(Return). CPU 내부에는 '함수 호출 전용 저장소(RAS, Return Address Stack)'가 있어서, 함수가 끝날 때 어디로 돌아갈지 거의 100% 예측합니다. 그래서 파이프라인이 깨지는 걸 최소화할 수 있습니다.
  • goto: 얘는 어디로 튈지 모릅니다. 뒤로 갈 수도, 앞으로 갈 수도, 루프 밖으로 나갈 수도 있습니다. 규칙성이 없기 때문에 CPU가 예측하기 매우 힘들고, 예측에 실패하면 파이프라인을 몽땅 비워야 하는 성능 손해가 발생합니다.

2. 컴파일러의 마법: "인라인(Inline) 최적화"

함수가 성능에 손해를 주는 건 사실입니다(주소 이동뿐만 아니라 레지스터 값을 메모리에 저장하는 비용도 듭니다). 그래서 컴파일러는 똑똑한 짓을 합니다.

  • 함수가 너무 짧거나 자주 호출되면, 컴파일러가 알아서 함수 호출 코드를 지우고 그 자리에 함수의 내용물을 복사해서 붙여넣어 버립니다. 이걸 **인라이닝(Inlining)**이라고 합니다.
  • 이렇게 되면 실제 실행될 때는 점프 자체가 사라져서 성능 저하가 0이 됩니다. 반면, goto는 로직이 복잡하게 꼬여있는 경우가 많아 이런 최적화를 하기가 훨씬 어렵습니다.

3. CPU보다 비싼 건 "사람의 시간"

이게 가장 큰 이유일 수도 있습니다.

  • 함수: 코드를 의미 있는 단위로 나눕니다. "이 부분은 로그인을 처리하는 곳이야"라고 이름표(함수명)를 붙이는 거죠. 사람이 읽기 편하고 관리가 쉽습니다.
  • goto: 점프 주소가 여기저기 얽히면 코드가 '스파게티'처럼 꼬입니다. 나중에 버그가 생겼을 때 흐름을 추적하는 게 불가능에 가까워집니다.

요약하자면

  1. 성능 측면: 함수 호출도 비용이 들지만, 현대 CPU는 함수 전용 예측 하드웨어가 따로 있을 정도로 함수에 최적화되어 있습니다. 또한, 컴파일러가 인라이닝으로 점프 자체를 없애버리기도 합니다.
  2. 구조 측면: goto는 흐름을 불규칙하게 만들어 CPU와 컴파일러를 혼란스럽게 하지만, 함수는 규칙적인 흐름을 만들어 최적화를 돕습니다.

즉, **"어차피 점프할 거면, 하드웨어가 예측하기 쉬운 질서 정연한 방식으로 점프하자"**는 것이 현대 프로그래밍의 철학입니다. 그 질서 정연한 방식이 바로 함수(Function)와 제어문(if, for)입니다.

 

 

 

1. 명령어 파이프라이닝 (Instruction Pipelining)
명령어 파이프라이닝은 하나의 명령어가 처리되는 과정을 여러 단계로 나누어, 각 단계를 동시에 실행하는 기법입니다.
처리 단계: 일반적으로 **인출(Fetch), 해석(Decode), 실행(Execute), 저장(Write-back)**의 4단계로 나누지만, 전공서에 따라 2단계나 5단계로 나누기도 합니다.
원리: 각 단계가 겹치지만 않는다면, CPU는 한 명령어를 실행하는 동안 다른 명령어를 해석하고 또 다른 명령어를 인출할 수 있습니다. 이는 마치 컨베이어 벨트와 같은 방식으로 동작하여, 명령어를 하나씩 순차적으로 처리할 때보다 훨씬 효율적입니다.
2. 파이프라인 위험 (Pipeline Hazards)
파이프라이닝이 항상 성능 향상을 보장하는 것은 아니며, 제대로 동작하지 못하는 상황을 파이프라인 위험이라고 부릅니다.
데이터 위험 (Data Hazard): 명령어 간의 의존성 때문에 발생합니다. 예를 들어, 이전 명령어의 결과값이 다음 명령어의 입력값으로 쓰여야 하는 경우, 이전 명령어가 끝날 때까지 다음 명령어를 처리할 수 없습니다,.
제어 위험 (Control Hazard): JUMP, CALL, 인터럽트 등으로 인해 프로그램 카운터(PC) 값이 갑작스럽게 변할 때 발생합니다. 실행 흐름이 바뀌면 이미 파이프라인에서 처리 중이던 다음 명령어들이 헛수고가 되며, 이를 방지하기 위해 어디로 점프할지 미리 예측하는 분기 예측 기술이 사용되기도 합니다,.
구조적 위험 (Structural Hazard): 서로 다른 명령어가 ALU나 레지스터와 같은 동일한 CPU 부품을 동시에 사용하려고 할 때 발생합니다.
3. 슈퍼스칼라 (Superscalar)
슈퍼스칼라는 여러 개의 명령어 파이프라인을 두는 구조를 의미합니다.
현대의 멀티스레드 프로세서처럼 한 번에 여러 명령어를 인출하고 실행할 수 있는 CPU에서 사용됩니다,.
이론적으로는 파이프라인 개수에 비례하여 속도가 빨라지지만, 파이프라인이 늘어날수록 관리해야 할 위험(Hazard) 요소도 증가하기 때문에 실제 처리 속도가 항상 개수에 비례하여 증가하는 것은 아닙니다.
4. 비순차적 명령어 처리 (Out-of-Order Execution, OoOE)
파이프라인의 중단을 막기 위해 명령어들을 저장된 순서대로 실행하지 않고, 실행 가능한 것부터 먼저 처리하는 기법입니다.
의존성이 없는 명령어들의 순서를 바꿈으로써, 데이터 위험 등으로 인해 발생할 수 있는 대기 시간을 줄입니다,.
명령어의 순서를 바꿔도 전체 프로그램의 실행 결과와 흐름에 영향이 없는 경우에만 CPU가 스스로 판단하여 순서를 변경합니다. 이 기술은 현대 CPU 발전에 매우 중요한 기여를 했습니다

 

 

 

 

1. 명령어 집합 구조 (ISA: Instruction Set Architecture)
**명령어 집합 구조(ISA)**는 CPU가 이해할 수 있는 명령어들의 모음을 의미합니다.
하드웨어와 소프트웨어 사이의 약속: ISA는 하드웨어가 소프트웨어를 어떻게 이해해야 할지에 대한 일종의 약속입니다. CPU 제조사나 모델에 따라 이해하는 명령어의 종류, 연산 방식, 주소 지정 방식이 다르기 때문에 CPU마다 고유한 ISA를 가집니다.
컴퓨터 구조의 결정 요소: 명령어가 달라지면 이를 해석하는 방식, 레지스터의 종류와 개수, 파이프라이닝의 난이도 등 CPU와 컴퓨터의 전반적인 구조가 결정됩니다.
호환성 문제: 인텔 CPU(x86)에서 실행되도록 만들어진 실행 파일이 아이폰(ARM)에서 작동하지 않는 이유는 두 기기의 CPU가 이해하는 명령어 집합(ISA)이 다르기 때문입니다,.
--------------------------------------------------------------------------------
2. CISC (Complex Instruction Set Computer)
CISC는 **'복잡한 명령어 집합을 활용하는 컴퓨터'**를 의미하며, 인텔과 AMD의 x86, x86-64 구조가 대표적입니다,.
특징:
    ◦ 가변 길이 명령어: 명령어의 크기와 형태가 다양합니다.
    ◦ 강력하고 다양한 명령어: 명령어 하나가 복잡하고 강력한 기능을 수행하며, 다양한 주소 지정 방식을 지원합니다.
    ◦ 적은 명령어 수: 명령어 하나하나가 강력하기 때문에 프로그램을 실행하는 데 필요한 전체 명령어의 개수가 상대적으로 적습니다.
단점:
    ◦ 파이프라이닝에 불리: 명령어의 크기와 실행 시간이 일정하지 않고, 하나의 명령어를 실행하는 데 여러 클럭 주기가 필요하여 현대 CPU의 핵심 기술인 명령어 파이프라이닝을 적용하기 어렵습니다,.
    ◦ 낮은 효율성: 복잡한 명령어 중 실제 자주 사용되는 것은 전체의 약 20%에 불과하며, 나머지 80%는 사용 빈도가 낮습니다.
--------------------------------------------------------------------------------
3. RISC (Reduced Instruction Set Computer)
RISC는 CISC의 한계를 극복하기 위해 등장한 **'명령어 집합의 수를 줄인 컴퓨터'**입니다. 아이폰 등에 사용되는 ARM 프로세서가 대표적인 RISC 기반 ISA입니다.
특징:
    ◦ 고정 길이 명령어: 명령어가 짧고 규격화되어 있으며, 가급적 1 클럭 내외로 실행됩니다,.
    ◦ 파이프라이닝에 유리: 명령어가 정형화되어 있어 명령어를 겹쳐서 실행하는 파이프라이닝 기술을 효율적으로 적용할 수 있습니다.
    ◦ 레지스터 활용: 메모리 접근을 'Load'와 'Store' 명령어로 최소화하는 대신, 범용 레지스터를 10분 활용하도록 설계되었습니다.
단점:
    ◦ 많은 명령어 수: 명령어 종류가 적고 단순하기 때문에, CISC에서는 한 줄로 끝날 작업을 여러 줄의 명령어로 수행해야 하므로 컴파일 시 결과물인 명령어의 개수가 늘어납니다.
--------------------------------------------------------------------------------
4. 현대 CPU의 발전 방향
이론적으로는 CISC와 RISC가 뚜렷하게 구분되지만, 현대의 CPU 설계는 두 방식의 장점을 결합하는 방향으로 발전했습니다.
CISC의 리스크화: 현대의 인텔/AMD(CISC 기반) CPU는 파이프라이닝 효율을 포기할 수 없기 때문에, 내부적으로 복잡한 명령어를 **'마이크로 명령어(Micro-op)'**라는 작은 단위로 잘게 쪼개서 실행합니다.
즉, 겉으로는 CISC처럼 동작하여 명령어 수를 줄이면서도, CPU 내부적으로는 RISC처럼 명령어를 처리하여 파이프라이닝의 효율성을 극대화하고 있습니다.

 

 

 

 

1. '동기(Synchronous)'가 왜 소프트웨어적인가요?

컴퓨터 공학에서 '동기식'이라는 말은 **"현재 실행 중인 명령어와 타이밍이 일치한다"**는 뜻입니다.[1]

  • 동기 인터럽트 (Synchronous Interrupt): CPU가 특정 명령어를 실행하는 그 순간 발생합니다.[2][3] 예를 들어 '0으로 나누기' 명령어를 실행하면, 그 명령어가 끝날 때 즉시 인터럽트가 발생하죠. 즉, 프로그램(소프트웨어)의 흐름 속에서 예측 가능한 시점에 발생하기 때문에 '동기'라고 부릅니다.[2]
  • 비동기 인터럽트 (Asynchronous Interrupt): CPU가 무슨 일을 하든 상관없이 **외부(하드웨어)**에서 갑자기 신호를 보냅니다. 키보드를 언제 누를지, 마우스를 언제 움직일지 CPU는 알 수 없죠. 프로그램 실행 흐름과 상관없이 '갑툭튀' 하기 때문에 '비동기'라고 부릅니다.

 

 

 

'25~26 겨울방학 > 컴퓨터구조' 카테고리의 다른 글

컴 구조)6,7,8장  (0) 2026.01.28