Devin.KR

CS · 기본

비전공자를 위한 컴퓨터공학 지도 - 전공 4년을 개발자 눈으로 따라잡기

컴퓨터 구조 - 비트·CPU·메모리 계층

2진수와 정수 범위, 부동소수 오차, 캐시와 지역성, 완성 코드는 0.1 덧셈 오차와 정수 비트 표현 출력

개발자KR · 원고 갱신

이 장에서 배우는 것

앞 장에서 같은 문제라도 알고리즘에 따라 필요한 연산 횟수가 달라진다는 점을 살펴봤다. 이번에는 그 연산이 어떤 장치에서 수행되고, 숫자와 데이터가 어떤 형태로 보관되는지 알아본다. 컴퓨터 구조는 프로그램을 실행하는 장치의 구성과 동작을 다루는 과목이다. 전공 수업에서는 숫자의 표현, 명령어 실행, 저장 장치 사이의 속도 차이를 함께 배운다.

다른 전공에서 개발로 옮겨 왔다면 계산 결과가 예상과 다르거나, 같은 횟수의 반복문이 서로 다른 속도로 실행될 때 이 과목의 필요성을 느끼기 쉽다. 이 장에서는 회로를 설계하는 대신 Python 코드의 관찰 결과를 장치의 관점으로 설명하는 데 집중한다. Python이 감춰 주는 부분과 직접 확인할 수 있는 부분도 구분한다.

  • 비트와 2진수로 정수를 표현하고, 정해진 비트 수에서 표현할 수 있는 범위를 계산한다.
  • 0.1을 더한 결과를 통해 부동소수점 수의 근삿값과 비교 방법을 확인한다.
  • CPU가 명령어를 처리할 때 레지스터와 메모리가 맡는 역할을 설명한다.
  • 캐시와 지역성을 작은 모형으로 실험하고, 실제 성능 측정과 구별한다.

문제 상황

다른 전공을 공부하다 개발자가 된 사람이 과목별 학습 시간을 집계하는 도구를 만들고 있다. 학습 한 번에 0.1시간을 기록했는데 세 기록의 합을 출력하니 0.3 뒤에 숫자가 더 붙는다. 외부 장치에서 받은 한 바이트의 값은 251인데, 장치 설명에는 같은 자리가 음수를 나타낸다고 적혀 있다. 학습 기록을 순서대로 훑는 코드와 일정 간격으로 건너뛰는 코드는 처리하는 항목 수가 같은데도 실행 시간이 다르다.

세 현상은 각각 숫자의 표현 방식, 정수 해석 규칙, 데이터 접근 순서와 연결된다. 화면에 보이는 숫자와 컴퓨터 내부의 표현은 같은 것이 아니다. 반복문의 횟수만 세어서는 데이터가 어디에서 오는지 알 수 없다. 원인을 찾으려면 프로그램이 사용하는 추상화 아래를 한 단계 내려다봐야 한다. 여기서 추상화란 내부의 세부 동작을 감추고 필요한 기능만 드러내는 방식이다.

이번 실험은 학습 계획 도구에서 만날 법한 작은 숫자를 사용한다. 실제 장치에 연결하거나 실행 시간을 재지 않는다. 정수의 비트 표현과 실수의 오차를 직접 출력하고, 명령어 실행 및 캐시 접근은 규칙이 공개된 모형으로 확인한다. 따라서 실행할 때마다 같은 결과를 얻을 수 있다.

숫자는 비트로 저장되고 규칙에 따라 해석된다

2진수와 정수의 범위

비트(bit)는 0 또는 1을 나타내는 정보의 단위다. 바이트(byte)는 8비트다. 우리가 보통 쓰는 10진수에서는 자릿수가 왼쪽으로 이동할 때 자리의 값이 10배가 된다. 2진수에서는 2배가 된다. 네 자리 2진수 1101의 자리 값은 왼쪽부터 8, 4, 2, 1이다. 따라서 1101은 8 + 4 + 0 + 1, 즉 10진수 13이다.

n비트에는 서로 다른 비트 배열이 2의 n제곱 개 있다. 모든 배열을 0 이상의 정수에 배정하면 범위는 0부터 2의 n제곱 - 1까지다. 전공 수업에서는 이를 부호 없는 정수라고 부른다. 음수도 담아야 한다면 같은 배열을 다른 규칙으로 해석해야 한다.

같은 비트 수라도 부호 해석에 따라 정수 범위가 달라진다
비트 수서로 다른 배열 수부호 없는 범위2의 보수 범위
4160부터 15-8부터 7
82560부터 255-128부터 127
1665,5360부터 65,535-32,768부터 32,767

음수를 표현하는 대표적인 규칙이 2의 보수다. n비트에서 최상위 비트, 즉 가장 왼쪽 비트가 0이면 부호 없는 값 그대로 읽는다. 1이면 부호 없는 값에서 2의 n제곱을 뺀다. 따라서 8비트의 11111011은 부호 없이 읽으면 251이고, 2의 보수로 읽으면 251 - 256 = -5다. 최상위 비트만 떼어 음수 표시로 붙이고 나머지를 크기로 읽는 규칙과는 다르다.

같은 8비트 배열 11111011은 해석 규칙에 따라 251 또는 -5가 된다

n비트 2의 보수 정수의 범위는 -(2의 (n - 1)제곱)부터 2의 (n - 1)제곱 - 1까지다. 양수 쪽 끝보다 음수 쪽 끝의 크기가 하나 더 크다. 0이 음수가 아닌 쪽에 포함되기 때문이다. 고정된 비트 수의 범위를 넘어서는 현상은 전공 수업에서 오버플로라고 부른다. 이때의 처리 방식은 언어나 연산 규칙에 따라 다르므로, 모든 환경에서 저절로 순환한다고 가정하면 안 된다.

Python의 int는 보통의 고정 폭 정수와 다르다. 사용할 수 있는 메모리 안에서 필요한 만큼 자릿수를 늘린다. 그래서 255 + 1은 256이며, 8비트로 자동 제한되지 않는다. 완성 코드에서는 나머지 연산으로 0부터 255 사이의 값을 만들어 8비트 표현을 모형화한다. 실제 파일이나 장치와 데이터를 주고받을 때는 비트 수와 부호 규칙을 따로 확인해야 한다.

코드의 1 << width는 1을 왼쪽으로 width자리 이동하는 비트 이동 연산이다. 여기서는 2의 width제곱을 계산하는 용도로 쓴다. format(value, "08b")의 b는 2진수 형식이고, 08은 적어도 여덟 자리를 쓰되 빈자리를 0으로 채우라는 뜻이다. 이 형식 지정 자체가 숫자를 8비트 범위로 제한하지는 않는다.

0.1이 정확한 값으로 들어가지 않는 이유

부동소수점(floating point)은 유효한 숫자 부분과 크기를 조절하는 지수를 조합해 수를 표현하는 방식이다. 전공 수업에서는 각각 가수와 지수라는 말을 사용한다. 이 장의 환경에서 Python의 float는 보통 64비트 이진 부동소수점 형식으로 동작한다. 표현할 수 있는 값의 개수가 한정되어 있으므로 모든 실수를 정확히 담을 수는 없다.

10진수에서 1을 3으로 나누면 소수점 아래 숫자가 끝없이 이어진다. 비슷하게 10진수 0.1은 2진수에서 유한한 소수로 끝나지 않는다. 저장할 때는 가까운 표현 가능 값으로 반올림한다. 덧셈 결과에도 반올림이 일어날 수 있다. 따라서 0.1 + 0.1 + 0.1과 0.3은 서로 다른 저장 값이 될 수 있다. 컴퓨터가 덧셈 규칙을 잊은 것이 아니라, 계산에 들어간 값이 이미 근삿값인 것이다.

완성 코드에서는 repr로 값의 표현을 확인한다. float의 repr는 모든 내부 자릿수를 늘어놓는 기능이 아니라, 같은 float로 다시 읽을 수 있는 짧은 표현을 만든다. 0.1 자체가 짧게 출력된다고 해서 내부에 정확한 10분의 1이 들어 있다는 뜻은 아니다. 코드의 as_integer_ratio는 저장된 값을 정확한 정수 비율로 보여 준다.

근삿값끼리 비교할 때는 차이가 허용 범위 안인지 판단한다. 표준 라이브러리의 math.isclose는 값의 크기에 비례한 허용 오차와 고정된 허용 오차를 사용할 수 있다. 이 장에서는 동작을 분명히 하려고 상대 허용 오차를 0으로 두고, 고정 허용 오차만 0.000000000001로 지정한다. 실제 기준은 측정 단위와 요구사항에 맞춰 정해야 한다.

반대로 소수 자릿수 자체가 업무 규칙이라면 decimal 모듈의 Decimal을 고려한다. Decimal("0.1")처럼 문자열로 만들면 이 사례의 10진 소수를 정확히 표현할 수 있다. Decimal도 정밀도 설정과 반올림 규칙을 갖는다. 모든 종류의 계산을 오차 없이 처리하는 도구라는 뜻은 아니다.

CPU는 명령어를 실행하고 중간값을 가까이 둔다

중앙 처리 장치(CPU)는 명령어를 읽고 그에 맞는 연산을 수행하는 장치다. 명령어는 장치가 수행할 동작을 나타내는 단위다. Python 소스의 한 줄이 CPU 명령어 하나에 대응하지는 않는다. Python 실행 환경이 코드를 처리하는 과정에서 여러 기계 명령어가 실행된다. 여기서는 그 변환 과정 대신, 명령어가 값과 저장 위치를 바꾼다는 핵심만 모형으로 확인한다.

레지스터(register)는 CPU 안에서 연산에 필요한 값이나 주소 등을 담는 작은 저장 공간이다. 주기억장치인 메모리에는 실행에 필요한 코드와 데이터가 놓인다. 주소는 저장 위치를 구별하기 위한 번호다. CPU는 메모리에서 가져온 값을 레지스터에 두고 계산한 다음, 결과를 다시 메모리에 저장할 수 있다.

전공 수업에서는 명령어를 가져오는 인출, 의미를 파악하는 해독, 동작을 수행하는 실행으로 기본 흐름을 설명한다. 실제 CPU는 여러 명령어의 처리를 겹치는 등 더 복잡하게 동작할 수 있다. 이 장의 모형은 명령어를 하나씩 끝내고 다음으로 넘어간다.

완성 코드의 cpu_demo는 LOAD 5, ADD 3, STORE result라는 세 명령을 순서대로 처리한다. LOAD는 레지스터에 값을 넣고, ADD는 그 값에 피연산자를 더하며, STORE는 결과를 저장한다. 피연산자는 명령이 처리할 대상 값이다. 여기서 명령 이름은 설명을 위해 정한 것이며 특정 CPU의 실제 명령어 형식을 재현하지 않는다.

모형의 register 변수는 레지스터의 역할을, memory 사전은 메모리의 역할을 맡는다. Python 변수를 선언했다고 그 값이 특정 하드웨어 레지스터에 배치되는 것은 아니다. 역할을 표현하는 프로그램과 실제 장치 배치를 구별해야 모형에서 배운 내용을 과도하게 일반화하지 않을 수 있다.

캐시는 최근 데이터와 가까운 데이터를 활용한다

캐시(cache)는 자주 필요할 것으로 예상되는 데이터의 복사본을 가까운 저장 공간에 두는 장치다. CPU가 쓰는 저장 공간은 대체로 레지스터, 여러 단계의 캐시, 주기억장치로 이어진다. 전공 수업에서는 이런 구성을 메모리 계층이라고 부른다. 가까운 계층은 대체로 빠르고 작으며, 먼 계층은 대체로 느리고 크다. 실제 접근 시간은 장치와 접근 상황에 따라 달라진다.

캐시는 보통 필요한 데이터 하나만 가져오지 않고 주변의 연속된 데이터도 묶어서 가져온다. 이 묶음을 캐시 라인(cache line)이라고 부른다. 요청한 데이터가 캐시에 있으면 적중, 없어서 다음 계층에서 가져와야 하면 실패라고 한다. 실패가 났을 때 가져온 데이터가 나중의 접근에 쓰이면 추가 접근 비용을 줄일 수 있다.

한 번 사용한 데이터를 가까운 시간에 다시 사용하는 성질을 시간 지역성이라고 부른다. 접근한 위치 주변을 이어서 사용하는 성질은 공간 지역성이다. 둘 다 프로그램의 접근 패턴에 관한 설명이다. 캐시는 이런 성질을 활용하지만, 모든 프로그램이 같은 정도로 이득을 얻지는 않는다.

한 번 가져온 네 칸을 연속해서 읽으면 이후 세 번은 모형 캐시에 적중한다

완성 코드에서는 데이터 16개를 두고, 캐시 한 묶음에 데이터 네 개가 들어간다고 가정한다. 캐시에는 두 묶음만 들어간다. 데이터 위치를 4로 나눈 몫이 묶음 번호이며, 묶음 번호를 2로 나눈 나머지가 저장 칸이다. 각 묶음이 들어갈 칸이 정해진 이런 방식을 전공 수업에서는 직접 사상이라고 부른다.

0, 1, 2, 3을 읽으면 첫 접근에서 묶음을 가져오고 나머지 세 접근은 적중한다. 같은 자리를 네 번 읽으면 처음 한 번을 제외한 세 번이 적중하므로 시간 지역성도 확인할 수 있다. 반면 0, 4, 8, 12 순으로 읽으면 매번 다른 묶음이 필요하다. 저장 칸이 겹치는 묶음들은 서로를 밀어낸다.

실험에서는 0부터 15까지 순서대로 읽는 경우와, 네 칸 간격으로 읽은 뒤 시작 위치를 옮기는 경우를 비교한다. 두 순서는 같은 데이터를 한 번씩 읽으므로 합계가 같다. 하지만 이 모형에서는 적중 횟수가 다르다. 이것은 실제 CPU 캐시를 측정한 결과가 아니다. Python 리스트는 객체를 가리키는 참조를 담으며, 참조가 가리키는 객체의 배치는 리스트 순서만으로 결정되지 않는다. 실제 성능은 실행 시간 측정과 장치 분석이 추가로 필요하다.

완성 코드

다음 내용을 main.py에 저장한다. 외부 패키지나 입력 파일이 필요하지 않다. 고정된 값과 접근 순서만 사용하므로 난수를 사용하지 않으며, 실행 후 바로 종료한다. 캐시 함수는 호출할 때마다 빈 상태에서 시작한다.

import math
from decimal import Decimal


def integer_demo():
    width = 8
    modulus = 1 << width
    value = -5
    unsigned = value % modulus
    bits = format(unsigned, f"0{width}b")
    sign_boundary = modulus // 2
    signed = unsigned if unsigned < sign_boundary else unsigned - modulus
    print(f"8비트 표현: {bits}")
    print(f"부호 없음: {unsigned}, 2의 보수: {signed}")
    print(f"8비트 범위: 0..{modulus - 1}, {-sign_boundary}..{sign_boundary - 1}")
    print(f"Python 정수: {255 + 1}, 8비트 모형: {(255 + 1) % modulus}")


def float_demo():
    total = 0.1 + 0.1 + 0.1
    numerator, denominator = (0.1).as_integer_ratio()
    close = math.isclose(total, 0.3, rel_tol=0.0, abs_tol=1e-12)
    exact = Decimal("0.1") + Decimal("0.1") + Decimal("0.1")
    print(f"0.1의 저장 비율: {numerator}/{denominator}")
    print(f"float 합계: {total!r}")
    print(f"0.3과 같은가: {total == 0.3}")
    print(f"허용 오차 안인가: {close}")
    print(f"Decimal 합계: {exact}")


def cpu_demo():
    program = [("LOAD", 5), ("ADD", 3), ("STORE", "result")]
    register = 0
    memory = {}
    for operation, operand in program:
        if operation == "LOAD":
            register = operand
        elif operation == "ADD":
            register += operand
        elif operation == "STORE":
            memory[operand] = register
    print(f"명령 실행: {len(program)}개, 저장 결과: {memory['result']}")


def cache_demo(order):
    data = list(range(16))
    cache = [None, None]
    hits = 0
    misses = 0
    total = 0
    for address in order:
        block = address // 4
        slot = block % len(cache)
        if cache[slot] == block:
            hits += 1
        else:
            misses += 1
            cache[slot] = block
        total += data[address]
    return total, hits, misses


def main():
    integer_demo()
    float_demo()
    cpu_demo()
    orders = [
        ("같은 자리 반복", [0, 0, 0, 0]),
        ("연속 접근", list(range(16))),
        ("간격 접근", [row * 4 + col for col in range(4) for row in range(4)]),
    ]
    for name, order in orders:
        total, hits, misses = cache_demo(order)
        print(f"{name}: 합계={total}, 적중={hits}, 실패={misses}")


if __name__ == "__main__":
    main()

줄별 해설

import math와 from decimal import Decimal은 각각 근삿값 비교 함수와 10진 소수 계산 도구를 불러온다. 둘 다 Python 표준 라이브러리에 포함된다. 추가 설치는 필요하지 않다.

width = 8부터 bits = ...까지는 표현할 폭을 정하고 -5에 대응하는 비트 배열을 만든다. 양수인 256으로 나눈 나머지는 0부터 255 사이에 있다. 따라서 -5의 나머지 251을 2진수로 출력하면 여덟 자리 표현을 얻는다. 나머지를 구하기 전에 부호 있는 범위를 검사하지 않았으므로, 이 함수는 입력값의 범위 검증 함수가 아니다.

sign_boundary는 128이다. signed = ... if ... else ...는 조건에 따라 두 식 중 하나를 고르는 조건 표현식이다. 251은 128 이상이므로 256을 빼서 -5로 해석한다. 이어지는 출력문은 범위와 덧셈 결과를 나란히 보여 준다. Python 정수의 덧셈과 폭을 제한한 모형의 덧셈이 다르다는 점을 확인하는 부분이다.

total = 0.1 + 0.1 + 0.1은 이진 근삿값들을 더한다. as_integer_ratio()가 돌려주는 두 정수는 저장된 0.1을 정확하게 나타내는 분자와 분모다. 출력문의 !r은 repr 표현을 사용한다. total == 0.3은 두 저장 값이 같은지 확인하고, math.isclose는 정해 둔 허용 오차 안에 있는지 확인한다. 두 질문은 서로 다르므로 결과도 다를 수 있다.

Decimal("0.1")은 문자열의 10진 숫자를 읽는다. Decimal(0.1)로 바꾸면 먼저 만들어진 float의 근삿값을 전달하게 되므로 같은 실험이 아니다. 여기서는 세 번의 덧셈에 필요한 정밀도가 기본 설정 안에 들어가 0.3이 출력된다.

program에는 동작 이름과 피연산자를 짝지은 튜플 세 개가 들어 있다. 반복문은 이 쌍을 하나씩 꺼낸다. LOAD 분기는 값을 넣고, ADD 분기는 기존 값에 더하며, STORE 분기는 사전의 result 항목에 저장한다. 명령 개수는 세 개지만, 이 Python 함수가 실제 CPU 명령어 세 개만 실행한다는 뜻은 아니다.

cache = [None, None]은 비어 있는 저장 칸 두 개를 만든다. 모형은 데이터 복사본 전체 대신 현재 들어 있는 묶음 번호만 기록한다. address // 4는 위치 0부터 3을 묶음 0으로, 4부터 7을 묶음 1로 분류한다. block % len(cache)는 그 묶음이 들어갈 칸을 고른다. 해당 칸의 묶음 번호가 일치하면 적중 횟수를 늘리고, 다르면 실패 횟수를 늘린 뒤 번호를 교체한다.

total += data[address]는 접근 순서가 달라도 같은 데이터를 읽었다는 사실을 검증하기 위한 합계다. 실제 데이터 읽기는 Python이 수행하며, 앞의 캐시 판정과 하드웨어 수준에서 연결되어 있지 않다. 이 함수가 반환하는 값은 합계, 적중 횟수, 실패 횟수의 세 묶음이다.

orders의 마지막 항목은 반복문 두 개를 담은 리스트 내포다. col을 0으로 둔 채 row를 바꾸어 0, 4, 8, 12를 만들고, 다음 col 값에서 1, 5, 9, 13을 만든다. 마지막 조건문은 이 파일을 직접 실행할 때 main을 호출하는 관례다.

실행 결과

macOS나 Linux의 터미널에서 파일이 있는 디렉터리로 이동한 뒤 다음 명령을 실행한다.

python3 main.py

예상 출력은 다음과 같다.

8비트 표현: 11111011
부호 없음: 251, 2의 보수: -5
8비트 범위: 0..255, -128..127
Python 정수: 256, 8비트 모형: 0
0.1의 저장 비율: 3602879701896397/36028797018963968
float 합계: 0.30000000000000004
0.3과 같은가: False
허용 오차 안인가: True
Decimal 합계: 0.3
명령 실행: 3개, 저장 결과: 8
같은 자리 반복: 합계=0, 적중=3, 실패=1
연속 접근: 합계=120, 적중=12, 실패=4
간격 접근: 합계=120, 적중=0, 실패=16

연속 접근은 네 묶음마다 한 번씩 실패하므로 실패가 네 번이다. 간격 접근에서는 묶음 0과 2가 같은 칸을, 묶음 1과 3이 다른 한 칸을 번갈아 차지한다. 다음 열을 읽을 때 필요한 묶음이 이미 밀려나 있어 열여섯 번 모두 실패한다. 두 경우 모두 합계는 120이다. 수행한 덧셈 수만 같다고 데이터 접근 비용까지 같아지는 것은 아니라는 점을 보여 준다.

실무에서 자주 틀리는 것

출력 자릿수를 정수 범위 제한으로 생각한다

다음 코드는 여덟 자리 형식을 지정했지만 아홉 자리를 출력한다. 형식 지정의 폭은 최소 출력 폭이기 때문이다.

value = 256
print(format(value, "08b"))  # 100000000

0부터 255까지 받는 입력이라면 범위를 먼저 검사한다. 범위를 벗어난 입력을 나머지 연산으로 바꾸는 것은 검증이 아니라 값의 변경이다.

value = 256
if 0 <= value < 256:
    print(format(value, "08b"))
else:
    print("8비트 부호 없는 범위를 벗어났다")

근삿값을 정확한 소수처럼 비교한다

아래 조건은 False다. 표시할 소수 자릿수와 계산에 사용한 저장 값은 구별해야 한다.

total = 0.1 + 0.1 + 0.1
print(total == 0.3)  # False

측정값처럼 작은 차이를 허용하는 요구사항이라면 그 범위를 명시한다. 다음 코드는 True를 출력한다. 0에 가까운 값의 비교에서는 고정 허용 오차를 어떻게 정했는지도 살펴봐야 한다.

import math

total = 0.1 + 0.1 + 0.1
print(math.isclose(total, 0.3, rel_tol=0.0, abs_tol=1e-12))  # True

Decimal에 float를 넣으면 의도한 10진 소수가 복원된다고 생각한다

다음 코드는 이미 근사된 값을 Decimal로 옮긴다. 결과를 0.1과 비교하면 False다.

from decimal import Decimal

value = Decimal(0.1)
print(value == Decimal("0.1"))  # False

사용자가 입력한 10진 소수를 보존하려면 문자열 단계에서 Decimal을 만든다. 입력 단계의 자료형 선택이 뒤의 계산 의미를 바꾼다.

from decimal import Decimal

value = Decimal("0.1")
print(value + value + value)  # 0.3

캐시 적중 횟수를 실행 시간으로 읽는다

모형의 적중 한 번에 걸리는 시간을 정하지 않았는데도 단위를 붙이면 근거 없는 결과가 된다. 아래 코드는 완성 코드의 cache_demo가 있는 위치에서 비교하는 예다.

_, hits, misses = cache_demo(list(range(16)))
print(f"실행 시간: {misses}밀리초")  # 실패 횟수는 시간이 아니다

모형에서는 관찰한 횟수를 그대로 보고한다. 실제 성능을 판단하려면 별도로 시간을 측정하고 입력 크기, 반복 횟수, 실행 환경을 함께 기록해야 한다.

_, hits, misses = cache_demo(list(range(16)))
accesses = hits + misses
print(f"적중률: {hits / accesses:.2f}")  # 적중률: 0.75

한눈에 보기

컴퓨터 구조의 핵심을 실무와 면접 질문에 연결한다
핵심실무에서 쓰이는 곳코드에서 확인한 것면접에서 묻는 방식
정수 표현과 범위파일 형식, 외부 장치 데이터11111011을 두 규칙으로 해석8비트로 -5를 어떻게 나타내는가
부동소수점측정값, 합계, 비교 조건세 번의 0.1 덧셈과 허용 오차0.1을 더하면 왜 예상과 달라지는가
명령어와 레지스터실행 과정과 성능 원인 이해적재, 덧셈, 저장의 상태 변화CPU는 메모리의 값을 어떻게 계산하는가
캐시와 지역성대량 데이터 순회와 배치같은 합계와 다른 적중 횟수연속 접근이 유리할 수 있는 이유는 무엇인가

면접에서는 정의를 외운 뒤 끝내기보다 조건을 붙여 설명하는 편이 좋다. 정수 범위에는 비트 수와 부호 규칙을, 소수 비교에는 허용 오차의 기준을, 캐시 설명에는 데이터 배치와 접근 순서를 함께 말한다. Python의 동작과 하드웨어 모형의 경계까지 밝히면 설명의 적용 범위가 분명해진다.

더 깊게 볼 서재의 책

서재에서 책을 고를 때는 전공 전체를 한 번에 메우려 하기보다 지금 생긴 질문을 기준으로 목차를 살펴본다. 이 장에서 숫자 표현이 낯설었다면 정수와 실수 표현을 먼저 읽고, 데이터 처리 속도가 관심사라면 메모리 계층 부분으로 이어 간다. 번역 제목은 판본에 따라 다를 수 있어 원제를 함께 확인할 수 있다.

  • Computer Organization and Design: 명령어와 데이터 경로, 메모리 계층을 장치 관점에서 더 살펴볼 때 맞는다. 데이터 경로란 값이 저장 공간과 연산 장치 사이를 이동하는 구성이다.
  • Computer Systems: A Programmer's Perspective: 숫자 표현과 기계 수준의 실행을 프로그램 관점에서 연결할 때 맞는다. 이번 장 뒤에는 정수 표현과 캐시 관련 부분을 우선 찾아볼 수 있다.

책을 읽을 때는 먼저 완성 코드의 한 값을 바꿔 예상하고, 실행 결과와 대조한 뒤 해당 개념을 찾아보는 순서를 권한다. 다음 장에서는 프로그램이 실행될 때 운영체제가 관리하는 단위를 살펴본다. 이번 장에서 구별한 CPU와 메모리의 역할이 그 출발점이 된다.

연습 문제

  1. 완성 코드의 integer_demo에서 value를 -128로 바꾼다. 비트 표현과 부호 없는 해석 값을 실행 전에 예상한다. 이어서 -129를 넣었을 때 원래 값이 복원되는지도 확인하고 이유를 설명한다.
  2. float_demo에 0.5 + 0.5 + 0.5를 추가하고 1.5와 같은지 출력한다. 0.1의 사례와 결과가 다른 이유를 2진수 표현으로 설명한다.
  3. cache_demo에 [0, 1, 0, 1]과 [0, 8, 0, 8]을 각각 전달한다. 합계, 적중, 실패를 예상한 뒤 실행한다. 같은 위치를 재사용해도 적중하지 않을 수 있는 이유를 설명한다.
  4. cpu_demo의 명령 목록을 LOAD 7, ADD -2, ADD 4, STORE result로 바꾼다. 명령 개수와 저장 결과를 예상한다. 이 개수가 실제 CPU 명령어 개수와 같은지 설명한다.

정답과 해설

  1. -128의 표현은 10000000이고 부호 없는 해석 값은 128이다. -129의 나머지는 127이므로 표현은 01111111이고 2의 보수 해석도 127이다. -129는 8비트 2의 보수 범위 밖이므로 원래 값을 보존하지 못한다. 다음 코드를 완성 코드에 추가해 확인할 수 있다.

    for value in [-128, -129]:
        unsigned = value % 256
        signed = unsigned if unsigned < 128 else unsigned - 256
        print(value, format(unsigned, "08b"), unsigned, signed)
    # -128 10000000 128 -128
    # -129 01111111 127 127
  2. 0.5는 2진수로 0.1이며 유한하게 표현된다. 이 사례의 1.5도 정확히 표현할 수 있으므로 비교 결과는 True다. 모든 소수 덧셈에서 같은 오차가 생기는 것은 아니다.

    total = 0.5 + 0.5 + 0.5
    print(total, total == 1.5)
    # 1.5 True
  3. 첫 순서는 같은 묶음 안에서 접근하므로 처음 한 번만 실패한다. 둘째 순서의 위치 0과 8은 각각 묶음 0과 2에 속하며 둘 다 저장 칸 0을 사용한다. 서로를 밀어내므로 모든 접근이 실패한다. 시간 지역성이 있는 접근도 캐시 배치와 용량의 영향을 받는다.

    print(cache_demo([0, 1, 0, 1]))
    print(cache_demo([0, 8, 0, 8]))
    # (2, 3, 1)
    # (16, 0, 4)
  4. 명령은 네 개이고 저장 결과는 9다. 모형 레지스터의 값은 7, 5, 9 순으로 바뀐 뒤 저장된다. Python의 반복문과 조건문 자체도 실제 실행 작업을 필요로 하므로 모형 명령 수를 실제 CPU 명령 수로 해석하면 안 된다.

    program = [("LOAD", 7), ("ADD", -2), ("ADD", 4), ("STORE", "result")]
    # cpu_demo의 기존 program을 위 목록으로 바꾸면 다음 줄이 출력된다.
    # 명령 실행: 4개, 저장 결과: 9
오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.