[컴퓨터구조] 스터디 시작하기
#CS#Computer Architecture

[컴퓨터구조] 스터디 시작하기

스터디 시작하기

컴퓨터의 구조 스터디에 오신 것을 환영합니다! 이 스터디에서는 다음 강의 자료를 주요 교재로 사용합니다.

15-213: Introduction to Computer Systems / Schedule Fall 2015

또한 다음의 교재 세 권을 참고 자료로 사용합니다:

  • Randal E. Bryant and David R. O'Hallaron, Computer Systems: A Programmer's Perspective, Third Edition, Pearson, 2016
    • 이 모듈에서는 줄임말인 CSAPP으로 부를 것입니다.
  • Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language, Second Edition, Prentice Hall, 1988
  • John L. Hennessy and David A. Patterson, Computer Architecture: A Quantitative Approach, Sixth Edition, Morgan Kaufmann, 2019

무엇을 다루나요?

요약하자면, 컴퓨터 시스템의 현실을 배웁니다.

컴퓨터 과학(CS)과 컴퓨터 공학(CE) 과정은 추상화에 주로 초점을 맞춥니다. 예를 들어, 추상 데이터 타입과 점근적 분석을 다룹니다. 그러나 특히 버그가 있을 때 이런 추상화는 한계를 가지고 있습니다. 따라서 기본 구현 세부 사항을 이해하는 것이 중요합니다. 이 모듈의 목표는 프로그래머가 시스템 구성 요소의 작동 방식을 이해하고, 이것이 프로그램의 정확성과 성능에 어떻게 영향을 미치는지 파악하여 자신의 기술을 향상시킬 수 있게 돕는 것입니다.

구체적으로, 컴퓨터가 숫자를 표현하는 방식에 따른 수치 오류를 방지하는 방법, 현대 프로세서와 메모리 시스템의 설계를 활용한 C 코드 최적화 방법 등을 배우게 됩니다. 또한, 컴파일러가 프로시저 호출을 구현하는 방법, 버퍼 오버플로우 취약점으로부터의 보안 방법, 링크 오류를 인식하고 회피하는 방법 등도 배우게 됩니다. Unix 셸, 동적 메모리 할당 패키지, 웹 서버 작성 방법과 동시성에 대한 이해도 학습합니다.

컴퓨터 시스템의 현실 요약

컴퓨터 연산

컴퓨터 시스템에서의 정수와 실수 연산은 수학적 개념의 정수와 실수와 차이가 있을 수 있습니다. 이는 컴퓨터가 숫자를 유한한 비트로 표현하기 때문에 발생하는 문제입니다. 컴퓨터에서는 32비트 정수형(int)과 부동소수점(float)을 각각 고정된 비트 수로 표현합니다. 이 때문에 다음과 같은 문제가 발생할 수 있습니다:

  • 정수 오버플로우: 32비트 정수형에서는, 예를 들어 50000×5000050000 \times 50000와 같은 큰 수의 곱셈이 오버플로우를 일으켜 예상치 못한 값을 반환할 수 있습니다.
  • 부동소수점 정밀도 손실: 부동소수점 연산에서는 매우 큰 수와 작은 수의 덧셈 또는 뺄셈에서 정밀도 손실이 발생할 수 있습니다. 예를 들면, 1e20+(1e20+3.14)1e20 + (-1e20 + 3.14)의 결과는 예상과 다를 수 있습니다.

컴퓨터의 산술 연산은 임의의 값을 생성하지 않으며 중요한 수학적 특성을 가지고 있지만, 모든 일반적인 수학적 특성을 만족시키지는 않습니다. 이는 표현의 제한성 때문입니다.

  • 정수 연산: 정수 연산은 '환(ring)'의 속성을 만족시킵니다. 즉, 교환 법칙, 결합 법칙, 분배 법칙을 따릅니다.
  • 부동소수점 연산: 부동소수점 연산은 '순서(ordering)'의 속성을 만족시킵니다. 이는 단조성과 부호의 값을 의미합니다.

기계어

현대 프로그래머가 어셈블리 언어로 프로그래밍하는 일은 드물지만, 어셈블리 언어를 이해하는 것은 매우 중요합니다. 이는 기계 수준의 실행 모델을 이해하는데 필수적이며, 어셈블리를 이해함으로써 다음과 같은 이점을 얻을 수 있습니다:

  • 버그가 있는 프로그램의 동작 이해
  • 프로그램 성능 조정: 컴파일러의 최적화 이해와 프로그램의 비효율성 파악
  • 시스템 소프트웨어 구현: 컴파일러가 기계 코드를 생성하는 방법과 운영 체제가 프로세스 상태를 관리하는 방식 이해
  • 악성 소프트웨어 생성 및 대응

메모리

메모리는 제한적이기에 반드시 할당하고 관리해야 합니다. 많은 애플리케이션들이 메모리를 많이 요구하므로, 효율적인 메모리 관리와 최적화는 필수입니다.

메모리 참조와 관련된 버그는 매우 치명적일 수 있습니다. 이런 버그들은 시간과 공간의 다양한 부분에 영향을 줄 수 있습니다. 예를 들어, 프로그램의 한 부분에서 발생한 메모리 참조 버그로 인해 다른 부분에서 예상치 못한 오류가 발생할 수 있습니다.

메모리 성능은 일정하지 않습니다. 캐시와 가상 메모리의 효과는 프로그램 성능에 큰 영향을 미칠 수 있으며, 프로그램을 메모리 시스템의 특성에 맞게 조정하면 속도가 크게 향상될 수 있습니다.

다음은 메모리 참조와 관련된 버그에 대한 예입니다.

typedef struct {
    int a[2];
    double d;
} struct_t;
 
double fun(int i) {
    volatile struct_t s;
    s.d = 3.14;
    s.a[i] = 1073741824; /* 범위를 벗어날 수 있음 */
    return s.d;
}

위 코드에서, fun 함수는 struct_t 구조체를 선언하고, i 인덱스에서 배열 a에 값을 할당합니다. 만약 i 값이 배열의 범위를 벗어나면 메모리 참조 오류가 발생합니다. 예를 들어:

  • fun(0)은 3.14를 반환합니다.
  • fun(1)도 3.14를 반환합니다.
  • fun(2)는 배열 범위를 벗어나므로 이상한 값을 반환할 수 있습니다.
  • fun(6)는 세그멘테이션 폴트(Segmentation Fault)를 발생시킬 수 있습니다.

C와 C++는 메모리 보호를 제공하지 않습니다. 이로 인해 배열 범위 초과 참조, 잘못된 포인터 값, malloc/free 함수의 오용 등 다양한 오류가 발생할 수 있습니다. 이러한 오류는 시스템과 컴파일러에 따라 다양한 형태로 나타날 수 있습니다.

메모리 참조 오류는 버그로 이어질 수 있습니다. 버그가 실제로 어떤 영향을 미칠지는 시스템과 컴파일러에 따라 다릅니다. 이러한 버그는 다른 객체를 손상시키거나, 발생한 지 오래된 후에야 발견될 수도 있습니다. 버그를 해결하려면 Java, Ruby, Python, ML 등의 언어로 프로그래밍하거나, 다양한 상호작용을 이해하고, Valgrind와 같은 도구를 사용하여 참조 오류를 탐지해야 합니다.

성능

컴퓨터 시스템의 성능은 단순히 점근적 복잡성(asymptotic complexity)만으로 결정되지 않습니다. 실제로 다음과 같은 요소들이 중요한 역할을 합니다:

  • 상수 요소: 알고리즘의 상수 요소는 성능에 큰 영향을 미칠 수 있습니다. 예를 들어, 동일한 작업을 수행하는 두 코드가 있을 때, 상수 요소에 따라 성능이 크게 달라질 수 있습니다.
  • 성능 예측은 단순히 연산의 수를 세는 것으로 충분하지 않습니다. 코드 작성 방식에 따라 성능이 10배 차이나기도 합니다. 따라서 알고리즘, 데이터 표현, 절차, 루프 등 여러 수준에서 최적화가 필요합니다.

성능 최적화를 위해서는 시스템에 대한 이해가 필요합니다. 프로그램이 어떻게 컴파일되고 실행되는지, 성능을 측정하고 병목 현상을 식별하는 방법, 코드의 모듈성과 일반성을 해치지 않으면서 성능을 개선하는 방법 등을 이해해야 합니다.

메모리 시스템의 성능은 메모리 접근 패턴에 따라 크게 달라집니다. 다음 예제는 동일한 작업을 수행하는 두 가지 코드의 성능 차이를 보여줍니다:

void copyij(int src[2048][2048], int dst[2048][2048]) {
    int i, j;
    for (i = 0; i < 2048; i++)
        for (j = 0; j < 2048; j++)
            dst[i][j] = src[i][j];
}
 
void copyji(int src[2048][2048], int dst[2048][2048]) {
    int i, j;
    for (j = 0; j < 2048; j++)
        for (i = 0; i < 2048; i++)
            dst[i][j] = src[i][j];
}
 

위 코드에서 copyij 함수는 4.3ms, copyji 함수는 81.8ms가 걸립니다. 이 차이는 메모리 접근 패턴 때문입니다. copyij는 행 우선 접근을 사용하고, copyji는 열 우선 접근을 사용하여 캐시 효율성이 크게 달라집니다.

캐시는 프로세서와 메모리 사이의 속도 차이를 줄이고, 자주 사용되는 데이터를 캐시에 저장하여 접근 시간을 단축합니다. 캐시 메모리는 여러 계층으로 구성되어 있으며, 각 계층은 다른 속도와 크기를 가지고 있습니다:

  • L1 캐시: 프로세서에 가장 가까운 캐시로, 가장 빠르지만 용량이 작습니다.
  • L2 캐시: L1 캐시보다 크고 느리지만, 여전히 빠른 속도를 제공합니다.
  • L3 캐시: L2 캐시보다 크고 느리지만, 메인 메모리보다는 빠릅니다.

프로그램 이외의 요소

컴퓨터 시스템은 단순히 프로그램을 실행하는 것뿐만 아니라 데이터의 입출력(I/O)도 처리해야 합니다. I/O 시스템은 프로그램의 신뢰성과 성능에 큰 영향을 미칩니다.

또한, 컴퓨터 시스템은 다양한 스토리지 장치를 계층적으로 구성하여 사용합니다. 이 계층 구조는 다음과 같은 다양한 속도와 비용을 가진 스토리지 장치를 포함합니다:

  • 레지스터 (L0): CPU 레지스터가 위치한 가장 빠르고 작은 저장 장치입니다.
  • 캐시 메모리 (L1, L2, L3): CPU와 메인 메모리 사이에 위치하여 데이터 접근 속도를 높입니다.
  • 주 메모리 (L4): DRAM으로 구성된 메인 메모리입니다.
  • 로컬 2차 저장 장치 (L5): 하드 디스크 등의 로컬 저장 장치입니다.
  • 원격 2차 저장 장치 (L6): 분산 파일 시스템이나 웹 서버 등의 원격 저장 장치입니다.

한편, 운영 체제는 하드웨어와 소프트웨어 사이의 인터페이스를 담당하며, 다음과 같은 주요 기능을 수행합니다:

  • 프로세스 관리: 여러 프로세스가 동시에 실행되도록 관리하며, 프로세스 간의 전환은 컨텍스트 스위칭을 통해 이루어집니다.
  • 가상 메모리: 각 프로세스가 메인 메모리를 독점적으로 사용하는 것처럼 보이도록 하여, 메모리 공간을 효율적으로 사용하게 합니다.
  • 파일 시스템: 모든 I/O 장치를 파일로 추상화하여 일관된 인터페이스를 제공합니다. 이를 통해 프로그램은 디스크 파일을 쉽게 조작할 수 있습니다.

그리고 컴퓨터 시스템은 네트워크를 통해 다른 시스템과 데이터를 주고받습니다. 이때 네트워크 통신은 다음과 같은 주요 문제를 다룹니다:

  • 동시성 (Concurrency): 여러 자율적 프로세스가 네트워크 상에서 동시에 작업을 수행합니다.
  • 신뢰할 수 없는 매체: 네트워크는 신뢰할 수 없는 매체를 통해 데이터를 전송할 때 발생할 수 있는 문제를 처리해야 합니다.
  • 크로스 플랫폼 호환성: 서로 다른 플랫폼 간의 호환성을 유지해야 합니다.
  • 복잡한 성능 문제: 네트워크 통신은 복잡한 성능 문제를 발생시킬 수 있으며, 이를 최적화하기 위해 다양한 기법이 필요합니다.

따라서 컴퓨터 시스템은 프로그램 실행 뿐만 아니라 데이터의 입출력, 네트워크 통신, 스토리지 계층 구조, 운영 체제의 관리 등 다양한 요소를 처리합니다. 이러한 요소들은 모두 시스템의 성능과 신뢰성에 큰 영향을 미치며, 이를 이해하고 최적화하는 것이 중요합니다.

hello.c를 통해 본 컴퓨터 시스템

CSAPP 교재에 등장하는 hello.c 프로그램은 다음의 간단한 C 코드로 이루어져 있습니다:

#include <stdio.h>
 
int main() {
    printf("hello, world\n");
    return 0;
}

이 프로그램은 "hello, world"라는 메시지를 출력합니다. 이것은 단순해 보이지만, 이 프로그램을 통해 컴퓨터 시스템의 여러 중요한 개념을 이해할 수 있습니다.

컴파일 과정

"hello.c" 프로그램이 기계어로 번역되는 과정은 다음의 단계로 구성됩니다:

  • 전처리 단계: 전처리기(cpp)가 원본 C 프로그램을 수정합니다. 예시로, #include <stdio.h> 명령은 stdio.h 헤더 파일의 내용을 프로그램 텍스트에 삽입합니다. 결과물은 hello.i라는 다른 C 프로그램입니다.
  • 컴파일 단계: 컴파일러(cc1)가 수정된 C 프로그램을 어셈블리어 프로그램(hello.s)으로 변환합니다. 이 프로그램은 텍스트 형식의 저수준 기계어 명령어를 포함합니다.
  • 어셈블리 단계: 어셈블러(as)가 어셈블리어 프로그램을 기계어 명령어로 번역하고, 이를 재배치 가능한 객체 프로그램(hello.o)으로 패키징합니다.
  • 링킹 단계: 링커(ld)가 여러 객체 파일을 결합하여 실행 가능한 객체 파일(hello)을 생성합니다.

실행 과정

프로그램이 실행되는 과정은 다음과 같습니다:

  • 로딩 (Loading): 운영 체제는 실행 파일을 메모리에 로드합니다.
  • 실행 (Execution): CPU는 프로그램의 명령어를 순차적으로 실행합니다.
  • 출력 (Output): printf 함수는 "hello, world" 메시지를 화면에 출력합니다.

네트워크 통신

hello.c 프로그램은 네트워크를 통해 원격으로 실행될 수 있습니다. 원격 실행의 예시는 다음과 같습니다:

  1. 로컬 텔넷 클라이언트에서 "hello"를 입력합니다.
  2. 클라이언트는 문자열을 원격 텔넷 서버로 전송합니다.
  3. 서버는 문자열을 쉘로 전달하고, 쉘은 hello 프로그램을 실행합니다.
  4. 프로그램의 출력은 다시 클라이언트로 전달됩니다.
  5. 클라이언트는 출력을 로컬 터미널에 표시합니다.

hello.c 프로그램은 단순히 "hello, world" 메시지를 출력하지만, 이를 통해 컴파일 과정, 메모리 관리, 성능 최적화, 네트워크 통신, 운영 체제의 역할 등 다양한 컴퓨터 시스템의 개념을 이해할 수 있습니다. 이러한 이해는 복잡한 프로그램을 작성하고 최적화하는데 필수적입니다.

컴퓨터 구조론의 중요한 주제들

CSAPP 교재의 1.9절에서는 다음과 같은 중요한 주제들을 다룹니다.

Amdahl의 법칙 (Amdahl's Law)

Amdahl의 법칙은 시스템 성능 향상의 한계를 설명합니다. 시스템의 일부분을 개선했을 때, 그것이 전체 시스템 성능에 미치는 영향을 평가하는 데 사용됩니다. Amdahl의 법칙은 다음의 식으로 표현됩니다:

S=1(1α)+αkS = \frac{1}{(1 - \alpha) + \frac{\alpha}{k}}

여기서 α\alpha는 시스템에서 개선될 부분의 비율을, kk는 그 부분의 성능 개선 배수를 나타냅니다. 이 법칙의 핵심은 '전체 시스템에서 개선 가능한 부분이 제한되어 있다면, 그 부분을 아무리 개선해도 전체 시스템 성능의 향상은 제한적'이라는 것입니다. 즉, 시스템의 대부분을 차지하는 부분을 개선해야 전체적인 성능 향상이 가능합니다.

동시성과 병렬성

동시성(Concurrency)과 병렬성(Parallelism)은 시스템 성능 향상을 위한 두 가지 중요한 개념입니다. 동시성은 여러 작업이 동시에 실행되는 것처럼 보이게 스케줄링하여 시스템 자원을 효율적으로 사용하는 개념입니다. 반면에 병렬성은 여러 작업을 실제로 동시에 실행하여, 작업을 분할하고 동시에 처리함으로써 성능을 향상시키는 개념입니다. 이와 관련하여 교재에서는 다음 세 단계를 제시합니다:

  • 스레드 레벨 동시성: 하나의 프로그램 내에서 여러 스레드를 동시에 동작시키는 것을 말합니다.
  • 명령어 레벨 병렬성: 하나의 프로세서 내에서 여러 명령어를 동시에 실행하는 것을 말합니다.
  • SIMD(single-instruction, multiple-data) 병렬성: 단일 명령어로 여러 데이터 요소를 동시에 처리하는 것을 말합니다.

추상화의 중요성

추상화는 컴퓨터 시스템에서 복잡성을 관리하고, 시스템의 다른 계층을 이해하는 데 중요한 역할을 합니다. 컴퓨터 시스템에서 사용되는 추상화의 예시는 다음과 같습니다:

  • 명령어 집합 구조 (Instruction Set Architecture): 기계어 프로그램이 한 번에 한 명령어씩 수행하도록 하는 프로세서의 동작을 추상화합니다.
  • 파일 시스템: 모든 I/O 장치를 파일로 추상화하여, 프로그래머가 다양한 장치를 일관된 방식으로 처리할 수 있게 합니다.
  • 가상 메모리: 각 프로세스가 독립적인 메모리 공간을 가지는 것처럼 동작하게 하여 메모리 보호와 효율성을 높입니다.
  • 프로세스: 운영체제가 각 프로그램이 독립적으로 실행되는 것처럼 보이게 하여, 멀티태스킹을 가능하게 합니다.
  • 가상 머신: 하드웨어 자원을 소프트웨어적으로 에뮬레이션하여, 여러 운영체제가 동시에 실행될 수 있게 합니다.