반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 3.4의 내용을 읽고 요약한 글입니다. 

 

3.4.1 힙 영역이 필요한 이유

  • 프로그래머가 수명 주기를 포함하여 완전히 직접 제어할 수 있는 매우 큰 메모리 영역
  • 어떻게 힙 영역에 메모리를 할당하고 해제하는지 구현

3.4.2 malloc 메모리 할당자 직접 구현하기

  • 메모리 할당자는 적절한 크기의 메모리 영역을 제공하기만 하고 무엇을 저장할지 신경쓰지 않음
    • integer, floating number 등 신경쓰지 않고 단순한 바이트의 연속에 지나지 않음
    • 커다란 배열 형태

힙 영역 위에서 두가지 문제 해결

  1. malloc 함수 구현
    1. 메모리 영역을 요쳥하면 힙 영역에 가능한 메모리 영역 찾아 요청자에게 반환
  2. free 함수 구현
    1. 메모리 영역의 사용이 완료되었을 때 메모리 영역을 반환하는 방법 구현

 

3.4.3 주차장에서 메모리 관리까지

주차장에 비유하여 표현하면 2가지 목표를 만족시켜야 함

  1. 주차할 위치를 빠르게 찾음. 요청된 크기를 만족하는 여유메모리를 빨리 찾음
  2. 주차장 사용률 극대화. 메모리를 요청할 때 가능한 많은 메모리 할당 요청을 만족시켜야 함

4가지 문제점

  1. 메모리 조각의 할당 상태 추적
  2. 단일 메모리 할당 요청의 요구사항을 만족하는 사용 가능한 메모리 조작이 매우 많을 때 할당 기준
    1. 6바이트 요청했는데 여유 메모리 공간이 16, 32, 8 바이트 일때
  3. 16바이트 메모리를 요청해야 하는데 여유 메모리 조각의 크기가 32바이트라서 16바이트가 남는 경우
  4. 사용 반환된 메모리를 어떻게 처리해야 하나?

3.4.4 여유 메모리 조각 관리하기

  • 위의 4가지 문제점들 중 1번 해결을 위한 방법으로 linked list로 메모리 사용정보를 기록하는 방식

2가지 정보를 기록

  • 메모리 조각의 크기를 기록한 숫자 - header라고 하며 32비트
    • 메모리 조각이 비어있는지 알려주는 설정값(flag) - 1비트
  • 할당 가능한 메모리 조각을 payload라고 함 (메모리 주소)

3.4.5 메모리 할당 상태 추적하기

한조각은 4바이트

  • 16/1: 할당된 메모리 조각 크기가 16바이트를 의미
  • 32/0 : 여유 메모리 조각이 32 바이트
  • 0/1: 끝을 표시하는 특수한 표시로 4바이트

이 방식으로 메모리 조각의 여유/할당 상태 파악 및 추적할 수 있음

3.4.6 어떻게 여유 메모리 조각을 선택할 것인가: 할당 전략

  • 4바이트 메모리를 요청받을 때 요구사항을 충족하는 여유 메모리가 두조각이 있음
    • 8/0, 32/0 중 어떤것을 반환할 것인가? 다양한 할당 전략 방식이 있음

메모리 할당 전략 방식

1. 최초 적합 방식

  • 제일 앞부터 탐색하다가 가장 먼저 발견된 요구사항을 만족하는 항목을 반환
  • 단순하지만, 처음부터 사용가능한 메모리 조각을 찾으므로 앞부분에 작은 메모리 조각이 많이 남을 가능성이 높음

2. 다음 적합 방식

  • 최초 적합 방식과 유사하지만 메모리를 요청할 때 처음부터 검색하는 대신 적합한 여유 메모리 조각이 마지막으로 발견된 위치에서 시작한다는 점이 다름
  • 탐색 시작 위치가 달라져, 더 빠르게 여유 메모리 조각을 탐색할 수 있음
  • 단점: 메모리 사용률은 최초 적합 방식에 미치지 못한다는 것이 연구로 밝혀짐

3. 최적 적합 방식

  • 사용 가능한 메모리 조각을 모두 찾은 후 요구 사항을 모두 만족하면서 크기가 가장 작은 조각 반환
  • 메모리를 더 잘 활용한다는 장점이 있음
  • 사용가능한 모든 메모리 조각을 탐색하기 때문에 빠르지 않음

실제로는 더 복잡한 원리로 적합한 방식을 찾아 할당

3.4.7 메모리 할당하기

  • 12바이트 메모리를 요청했을 때 적절한 여유 메모리 조각이 12 바이트라고 가정
    • 헤더인 4바이트를 제외한 나머지 크기

  • 할당된 것으로 표시하고 머리 정보 뒤에 따라오는 메모리 조각의 주소를 요청자에게 반환하면 됨
  • 위는 이상적인 경우로 12바이트 메모리를 요청했을 때 찾아낸 여유 메모리 조각의 크기가 더 큰 경우가 대부분
  • 아래와 같이 찾아낸 메모리 조각이 32바이트라면? 메모리가 낭비되고 내부 단편화(fragmentation) 발생함

내부 단편화의 예

  • 이 문제를 해결하기 위해 여유 메모리 조각을 두개로 분할하여 앞부분은 할당한 후 반환하고 뒷부분은 좀 더 작은 크기의 새로운 여유 메모리 조각으로 만듦

3.4.8 메모리 해제하기

  • 사용자가 메모리를 요청할 때 얻은 주소를 ADDR이라고 가정
  • free(ADDR)을 호출하면 매개변수인 ADDR에서 머리 정보 크기인 4바이트를 빼는 것으로 메모리 조각의 머리 정보 얻을 수 있음
  • 머리 정보에서 얻은 할당 설정값을 여유 메모리로 바꾸면 해제가 완료됨

메모리 해제 시 중요한 점 하나

  • 해제되는 메모리 조각과 인접한 메모리 조각이 여유 메모리 조각일 때 단순히 해제 여부만 기록하면 아래 상황 발생

  • 해제된 메모리 조각에 인접한 아래쪽 메모리 조각도 비어있음
  • 메모리를 해제 할 때 둘을 합치는게 나을까? 그대로 두는게 나을까?

그대로 두는 경우

  • 20 바이트 요청이 오는 경우 두 조각 중 어느 것도 요구사항을 만족시키지 못함

메모리를 해제 할 때 메모리 조각을 병합할 때

  1. 즉시: 비교적 간단하지만 부담이 발생. 하지만 간단해서 여전히 이 전략을 많이 선택
  2. 요구사항을 충족하는 여유 블록을 찾을 수 없을 때: 실제 메모리 할당자가 선택하는 전략

3.4.9 여유 메모리 조각을 효율적으로 병합하기

  • 해제하는 메모리 조각의 위 아래가 모두 비어있다면? 아래는 헤더 정보로 알 수 있지만 위의 정보는 어떻게 알 수 있나?

  • 메모리 조각 끝에 footer 정보를 추가

  • 조각의 꼬리 정보는 그 다음에 위치한 조각의 머리 정보와 인접해 있어 현재 조각의 머리 정보에서 4바이트를 빼면 이전 조각의 꼬리 정보를 획득할 수 있음

  • header와 footer 정보는 메모리 조각을 doubly linked list로 만듦
반응형
반응형

정식 이름은 Assignment expression operator인데 walrus operator라고도 불린다.

walrus는 “바다코끼리”라는 뜻으로 operator가 바다 코끼리의 눈과 이빨을 닮아서 이렇게 부른다.
때론 colon(:) equals(=) operator라고도 한다.

Python 3.8버전부터 새로 등장했다.

 

https://dev.to/davidarmendariz/python-walrus-operator-j13

 

Statement vs Expression in Python

바다코끼리 연산자의 정식 이름을 보면 Assignment expression operator로, expression이라는 단어가 나온다. 

Python에서 statement와 expression이라는 표현이 비슷해 혼동스러운데 간단히 정리하면, 아래와 같다.

  • statement: 코드를 구성할 수 있는 단위 혹은 모든 것
  • expression: 값을 평가하는 statement로 연산자와 피연산자의 조합으로 구성됨

예시

x = 25          # a statement
x = x + 10      # an expression
  • statement는 변수를 생성하는데 사용된다.
  • expression은 x값에 10을 더하는 연산이 수행된 후 결과가 x에 할당되었다.
>>> walrus = False # (1)
>>> walrus
False

>>> (walrus := True) # (2)
True
>>> walrus
True
  1. walrus = False는 값 False가 walrus에 할당된다. (traditional statement)
  2. (walrus := True) 는 assignment expression으로 walrus에 값 True를 할당한다.

둘의 미묘한 차이중 하나는 walrus = False는 값을 반환하지 않지만 (walrus := True)는 값을 반환한다는 것이다!

>>> walrus = False
>>> (walrus := True)
True

 

등장한 이유

PEP 572에 Abstract에 아래와 expression 내에서 변수에 할당하는 방법을 제안하고 있다. 

creating a way to assign to variables within an expression using the notation NAME := expr.

C언어에서는 변수에 값을 할당하는 statement도 expression인데 강력하지만 찾기 힘든 버그를 생산하기도 한다.

int main(){
	int x = 3, y = 8;
	if (x = y) {
	    printf("x and y are equal (x = %d, y = %d)", x, y);
	}
	return 0;
}

x와 y값을 비교후 값이 같으면 두 값을 출력하는 코드지만 x와 y값이 다르기 때문에 아무것도 출력 안되길 기대되지만 실제 코드 실행 결과는 아래와 같이 print 문이 출력된다. 왜일까?

x and y are equal (x = 8, y = 8)

문제는 위 코드 세번째 줄 if (x = y) 에서 equality comparison operator(==) 대신 assignment operator(=) 를 사용하고 있기 때문이다. if 문의 조건에는 expression이 와야하는데 C언어에서는 x = y를 expression으로 x값이 8로 할당되고 1이상의 값으로 True로 판단되서 print문이 출력된다.

그럼 Python에서는? 

x, y = 3, 8
if x = y:
    print(f"x and y are equal ({x = }, {y = })")
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
  • Syntax Error를 내뱉는데 expression이 아닌 statement이기 때문이다. 파이썬은 이를 분명히 구분하고 walrus operator에도 이러한 설계 원칙이 반영되었다. 그래서 walrus operator를 이용해서 일반적인 assignment를 할 수 없다.
>>> walrus := True
  File "<stdin>", line 1
    walrus := True
           ^
SyntaxError: invalid syntax

이를 해결하기 위해 많은 경우에 assignment expression 에 괄호를 추가해 python에서 syntax error를 피할 수 있다.

>>> (walrus := True)  # Valid, but regular assignments are preferred
True

 

사용 예시

walrus operator는 반복적으로 사용되는 코드를 간단히 하는데 유용하게 사용될 수 있다.

(1) 수식 검증

예로 복잡한 수식을 코드로 작성하고 이름 검증하고 debugging할 때 walrus operator가 유용할 수 있다.

아래와 같은 수식이 있다고 하자 (참고: haversine formula, 지구 표면의 2점 사이의 거리를 구하는 식)

$$
2 \cdot \text{r} \cdot \arcsin\left(
    \sqrt{
        \sin^2\left(\frac{\phi_2 - \phi_1}{2}\right)
        + \cos(\phi_1) \cdot \cos(\phi_2) \cdot \sin^2\left(\frac{\lambda_2 - \lambda_1}{2}\right)
    }
\right)
$$ 

  • ϕ: 위도(latitude), λ: 경도(longitude)

위 수식을 이용해  오슬로(59.9°N 10.8°E) 와 밴쿠버(49.3°N 123.1°W)  사이의 거리를 구하면,

from math import asin, cos, radians, sin, sqrt
# Approximate radius of Earth in kilometers
rad = 6371
# Locations of Oslo and Vancouver
ϕ1, λ1 = radians(59.9), radians(10.8)
ϕ2, λ2 = radians(49.3), radians(-123.1)
# Distance between Oslo and Vancouver
print(2 * rad * asin(
    sqrt(
        sin((ϕ2 - ϕ1) / 2) ** 2
        + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
    )
))

# 7181.7841229421165 (km)
  • 위 수식을 검증하기 위해서 수식의 일부 값을 확인해야할 수 있는데 수식의 일부를 복&붙으로 확인할 수 있다.
  • 이때 walrus operator를 이용하면,
2 * rad * asin(
    sqrt(
        **(ϕ_hav := sin((ϕ2 - ϕ1) / 2) ** 2)**
        + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
    )
)

# 7181.7841229421165

ϕ_hav
# 0.008532325425222883
  • 전체 expression의 값을 계산하면서 동시에 ϕ_hav값을 계속 확인할 수 있어서 copy & paste로 인해 발생할 수 있는 오류의 가능성을 줄일 수 있다.

 

(2) Lists 에서 활용될 수 있는 walrus operator

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

위 list에서 길이, 합계, 평균 값을 dictionary에 저장한다고 가정해보자

description = {
    "length": len(numbers),
    "sum": sum(numbers),
    "mean": sum(numbers) / len(numbers),
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
  • description에서 numbers의 len과 sum이 각각 두번씩 호출된다
  • 짧은 list에서는 큰 문제가 되지 않지만 길이가 더 긴 list나 연산이 복잡할 경우에는 최적화할 필요가 있다

 

물론 아래처럼 len_numbers, sum_numbers 변수를 dictionary 밖에서 선언 후 사용할 수도 있다

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

len_numbers = len(numbers)
sum_numbers = sum(numbers)

description = {
    "length": len_numbers,
    "sum": sum_numbers,
    "mean": sum_numbers / len_numbers,
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}

 

 

하지만 walrus operator를 이용해 len_numbers, sum_numbers 변수를 dictionary 내부에서만 사용하여 code를 최적화할 수 있다

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

description = {
    "length": (len_numbers := len(numbers)),
    "sum": (sum_numbers := sum(numbers)),
    "mean": sum_numbers / len_numbers,
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
  • 이 경우 코드를 읽는 사람들에게 len_numbers와 sum_numbers 변수는 계산을 최적화하기 위해 dictionary 내부에서만 사용했고 다시 사용되지 않음을 명확히 전달 할 수 있다

 

(3) Text 파일에서 lines, words, character 수 세는 예시

# wc.py
import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    counts = (
        path.read_text().count("\\n"),  # Number of lines
        len(path.read_text().split()),  # Number of words
        len(path.read_text()),  # Number of characters
    )
    print(*counts, path) # 11 32 307 wc.py
  • wc.py 파일은 11줄, 32단어, 307 character로 구성되어있다
  • 위 코드를 보면 path.read_text() 가 반복적으로 호출되는걸 알 수 있다 ⇒ walrus operator를 이용해 개선해보면,
import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    counts = (
        **(text := path.read_text()).count("\\n"),  # Number of lines**
        len(text.split()),  # Number of words
        len(text),  # Number of characters
    )
    print(*counts, path)

 

물론 아래처럼 text 변수를 이용하면 코드는 한줄 늘어나지만 readability를 훨신 높일 수 있다.

import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    text = path.read_text()
    counts = (
        text.count("\\n"),  # Number of lines
        len(text.split()),  # Number of words
        len(text),  # Number of characters
    )
    print(*counts, path)

그러므로 walrus operator가 코드를 간결하게 해주더라도 readability를 고려해야 한다.

 

(4) List Comprehensions

  • List comprehension과 함께 연산이 많은 함수를 사용하게 될 때, walrus operator의 사용은 효과적일 수 있다.
import time

t_start = time.time()

def slow(num):
    time.sleep(5)
    return num

numbers = [4, 3, 1, 2, 5]

results = [slow(num) for num in numbers if slow(num) > 4]

t_end = time.time()

print("elapsed time: ", t_end - t_start)

elapsed time: 30.01522707939148

  • numbers 리스트의 각 element에 slow 함수를 적용 후 3보다 큰 경우에만 results에 slow 호출 결과를 저장하는 코드
  • 문제는 slow 함수가 2번 호출됨
    • slow 호출 후 반환 결과가 3보다 큰지 확인할 때
    • results 리스트에 저장하기 위해 slow 호출할 때

가장 일반적인 해결책은 list comprehension 대신 for loop을 사용하는 것이다.

import time

t_start = time.time()

def slow(num):
    time.sleep(5)
    return num

numbers = [4, 3, 1, 2, 5]

results = []
for num in numbers:
    slow_num = slow(num)
    if slow_num > 4:
        results.append(slow_num)

t_end = time.time()

print("elapsed time: ", t_end - t_start)

elapsed time: 25.021725063323975

  • slow 함수가 모든 경우에 한번씩만 호출됨
  • 하지만 코드 양이 늘어나고 가독성이 떨어짐

walrus operator를 사용하면 list comprehension을 유지하면서 가독성을 높일 수 있음

import time

t_start = time.time()

def slow(num):
    time.sleep(5)
    return num

numbers = [4, 3, 1, 2, 5]

results = [slow_num for num in numbers if (slow_num := slow(num)) > 4]
print(results)

t_end = time.time()

print("elapsed time: ", t_end - t_start)

elapsed time: 25.018176908493042

 

(5) While Loop

question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}

user_answer = input(f"\n{question} ")
while user_answer not in valid_answers:
    print(f"Please answer one of {', '.join(valid_answers)}")
    user_answer = input(f"\n{question} ")
  • 위 코드는 사용자의 입력을 받는 input 함수가 두번 반복됨
  • 이를 개선하기 위해 While True 와 break를 사용하여 코드를 다시 작성하는 것이 일반적임
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}

while True:
    user_answer = input(f"\n{question} ")
    if user_answer in valid_answers:
        break
    print(f"Please answer one of {', '.join(valid_answers)}")

 

walrus operator를 이용해서 while loop을 간결하게 할 수 있음

question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}

while (user_answer := input(f"\n{question} ")) not in valid_answers:
    print(f"Please answer one of {', '.join(valid_answers)}")
  • 사용자로부터 받은 input 입력을 user_answer 변수에 저장하고 동시에 valid_answers 내에 포함되어있는지를 체크하여 가독성을 높일 수 있음

 

Reference


https://realpython.com/python-walrus-operator/

 
반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 3.2-3.3 의 내용을 읽고 요약한 글입니다. 

3.2 프로세스는 메모리 안에서 어떤 모습을 하고 있을까?

3.2.1 가상 메모리: 눈에 보이는 것이 항상 실제와 같지는 않다

  • 모든 프로세스의 코드 영역은 0x400000에서 시작
    • 서로 다른 두개의 프로세스가 메모리를 할당하기 위해 malloc을 호출하면 동일한 시작 주소를 반환할 가능성이 매우 높음

Ubuntu 에서 테스트

// malloc_test.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    void *ptr = malloc(10);  // Allocate 10 bytes
    printf("Address returned by malloc: %p\\n", ptr);
    free(ptr);  // Don't forget to free the memory
    return 0;
}

$ ./malloc_test Address returned by malloc: 0x56342fa8d260
$ ./malloc_test Address returned by malloc: 0x55a3e5a9d010

다른 주소가 반환됨. 이유는? → ASLR 이라는 기술이 최근 OS에는 적용되어있음

ASLR (Address Space Layout Randomization)

  • 스택, 힙, 동적 라이브러리 영역의 주소를 랜덤으로 배치해서 공격에 필요한 target address를 예측하기 어렵게 만드는 기술

ASLR 기술을 끄고 테스트하면 같은 주소가 반환되는 걸 볼 수 있다

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ ./malloc_test
$ Address returned by malloc: 0x40068c
$ ./malloc_test
$ Address returned by malloc: 0x40068c
  • 두 프로세스가 모두 같은 주소에 데이터를 쓸 수 있다는 의미인데 괜찮은가?

⇒ 주소값은 가짜 주소(가상 메모리 주소)이고 메모리에 조작이 일어나기 전 실제 물리 메모리 주소로 변경되기 때문에 문제 되지 않음

실제 물리 메모리 구조 예시

주목할 2가지 사항

  1. 프로세스는 동일한 크기의 chunk로 나뉘어 물리 메모리에 저장
    - 위 그림에서 힙 영역은 3개의 chunk로 나뉨
  2. 모든 조각은 물리 메모리 전체에 무작위로 흩어져 있음

보기에 아름답지 않지만 운영 체제가 프로세스에 균일한 가상의 주소 공간을 제공하는 것을 방해하지는 않음 → 가상 메모리와 물리 메모리 사이의 mapping을 나타내는 page table로 관리

3.2.2 페이지와 페이지 테이블: 가상에서 현실로

  • 각각의 프로세스에는 단 하나의 페이지 테이블만 있어야 함
  • mapping은 ‘page’ 라는 단위로 이루어짐
    • 프로세스의 주소 공간을 동일한 크기의 ‘조각’으로 나누고, 이 ‘조각’을 페이지(page)라고 부름
  • 그러므로 두 프로세스가 동일한 메모리 주소에 기록하더라도 페이지 테이블 통해 실제는 다른 물리 메모리 주소에 저장됨

 

3.3 스택 영역: 함수 호출은 어떻게 구현될까?

아래 코드의 문제점은?

void func(int a)
{
	if(a>100000000)
	{
		return;
	}
	
	int arr[100] = { 0 };
	
	func(a + 1);
}
  • 함수 실행 시간 스택(runtime stack)과 함수 호출 스택(call stack) 이해 필요

3.3.2 함수 호출 활동 추적하기: 스택

  • A, B, C, D 4가지 단계가 존재하고 아래그림 처럼 의존성을 가짐

단계별로 진행 과정

  • A→B→D→B→A→C→A
  • 선입 선출(Last In First Out, LIFO) 순서로 stack 과 같은 데이터 구초가 처리하기 적합

 

3.3.3 스택 프레임 및 스택 영역: 거시적 관점

  • 함수 실행시의 자신만의 ‘작은 상자’가 필요한데 이를 call stack 혹은 stack frame 이라고 함
  • 스택은 낮은 주소 방향으로 커지므로 아래와 같이 표현됨

  • stack frame에는 어떤 정보들이 포함되는지?

3.3.4 함수 점프와 반환은 어떻게 구현될까?

  • 함수 A가 함수 B를 호출하면, 제어권이 A에서 B로 옮겨짐
    • 제어권: CPU가 어떤 함수에 속하는 기계 명령어를 실행하는지 의미

제어권이 넘어갈 때는 2가지 정보가 필요함

  1. 반환(return): 어디에서 왔는지에 대한 정보
    • 함수 A의 명령어가 어디까지 실행되었는지에 대한 정보
  2. 점프(jump): 어디로 가는지에 대한 정보
    • 함수 B의 첫 번째 기계 명령어가 위치한 주소

위의 정보는 어디서 가져오는지? → 스택 프레임!

함수 A가 함수 B를 호출할 때 CPU는 함수 A의 기계 명령어(주소: 0x400564)를 실행중이라 가정

  • CPU는 다음 기계 명령어를 실행하는데 call 뒤에 명령어 주소가 함수 B의 첫번째 기계 명령어
    • 이 명령어를 실행한 직후 CPU는 함수 B로 점프하게 됨
  • 함수 B실행이 완료되면 어떻게 함수 A로 돌아오나?
    • call 명령어 실행 후 지정한 함수로 점프 + call 명령어 다음 위치 주소(0x40056a)를 함수 A의 스택 프레임에 넣음

call 명령어를 실행하면 반환주소가 스택 프레임에 저장됨

반환 주소가 추가되면서 스택 프레임이 아래 방향으로 조금 커짐

  • 함수 B에 대응하는 기계 명령어를 실행하면서 B에 대한 스택 프레임도 추가됨
  • 함수 B의 마지막 기계 명령어인 ret은 CPU에 함수 A의 스택 프레임에 저장된 반환주소로 점프하도록 전달하는 역할

 

3.3.5 매개변수 전달과 반환값은 어떻게 구현될까?

대부분의 경우 레지스터를 통해 매개변수와 반환값을 전달

  • 함수 A가 B를 호출하면, A는 매개변수를 상응하는 레지스터에 저장
  • CPU가 함수 B를 실행할 때 이 레지스터에서 매개변수 정보를 얻을 수 있음
  • CPU 내부의 레지스터 수가 제한되는 경우 나머지는 스택 프레임에 넣음

스택 프레임에 함수 호출에 필요한 매개변수 보관

3.3.6 지역 변수는 어디에 있을까?

  • 레지스터에 저장할 수 있지만 로컬 변수가 레지스터 수보다 많으면 스택프레임에 저장

 

3.3.7 레지스터의 저장과 복원

  • 함수 A와 B가 지역변수 저장을 위해 모두 레지스터를 사용하면 값이 덮어써 질 수 있음
  • 그렇기 때문에 초기에 저장된 값을 레지스터에서 사용하고 나면 다시 그 초기값을 함수의 스택 프레임에 저장해야 함

 

3.3.8 큰 그림을 그려보자, 우리는 지금 어디에 있을까?

void func(int a)
{
	if(a>100000000)
	{
		return;
	}
	
	int arr[100] = { 0 };
	
	func(a + 1);
}
  • 앞에서 본 코드를 다시 보면, 자기 자신 함수를 100000000번 호출
  • 호출 할 때마다 스택 프레임이 함수 실행시 정보를 저장하기 위해 생성되며, 함수 호출 단계가 증가하며 스택 영역이 점점 더 많은 메모리 차지 → stack overflow 발생

위 과정 시각화 링크

 

Python Tutor code visualizer: Visualize code in Python, JavaScript, C, C++, and Java

Please wait ... your code is running (up to 10 seconds) Write code in Python 3.11 [newest version, latest features not tested yet] Python 3.6 [reliable stable version, select 3.11 for newest] Java C (C17 + GNU extensions) C++ (C++20 + GNU extensions) JavaS

pythontutor.com

  • 코드가 실행되면서 stack frame이 계속 쌓이는 모습을 볼 수 있음 

 

반응형
반응형

이 글은 책 컴퓨터 밑바닥의 비밀 chapter 3.1 의 내용을 읽고 요약한 글입니다. 

3.1 메모리의 본질, 포인터와 참조

3.1.1 메모리의 본질은 무엇인가? 사물함, 비트, 바이트, 객체

  • 메모리는 메모리 셀(memory cell)로 구성되며 셀에는 0과 1 (1bit) 가 보관될 수 있음
  • 1bit로는 표현할 수 있는 정보가 너무 적기 때문에 8개를 묶어 byte로 표현
  • byte는 메모리 내 자신의 주소를 가지며 memory address 라고 함
  • 1byte도 $2^{8}$=256 개의 조합만 만들 수 있으므로 더 많은 정보를 포함하기에는 부족
  • 12바이트를 묶어서 사용하며 프로그래밍 언어에서는 구조체(structure)또는 객체(object)라고 표현

 

3.1.2 메모리에서 변수로: 변수의 의미

  • java script/python 등의 고급 프로그래밍 언어는 사용할 수 없고 8byte만 사용가능한 메모리가 주어진다면? 메모리 읽기와 쓰기를 직접 수행해야 함
 

  • 8칸의 메모리 공간이 있고 옆에 붙은 번호가 각각 메모리의 주소

1 + 2 값을 계산하고 싶다고 가정하면,

  • 숫자 1과 2를 메모리에 저장해야 함 (메모리 값을 읽어 레지스터에 저장해야 CPU가 연산 수행 가능)
  • 숫자 1을 메모리 주소 6에 넣기 위해 store라는 명령어를 사용한다면,
store 1 6
  • 숫자 2개중 하나는 저장할 숫자 값이고 하나는 메모리 주소

쓰기를 위해 load 라는 명령어를 사용한다면,

load r1 6
  • 명령어 뜻 모호
    • 숫자 6을 r1 레지스터에서 읽는거지,
    • 메모리 주소 6에 저장된 숫자를 r1레지스터에 읽는건지,

즉, 메모리 주소와 숫자 값을 구분하기 위한 방법 필요

  • 예를 들어 $ 기호가 붙어 있으면 값이고, $ 기호가 없다면 메모리 주소를 의미
store $1 6
load r1 6

메모리 주소 6에 1의 값을 저장

  • 이제 주소 6은 숫자 1을 나타냄 (주소 6 → 숫자 1)
  • 하지만 ‘주소 6’이라는 표현은 익숙하지 않기 때문에 쉬운 별칭 필요 (a 같은) ⇒ 변수
a = 1

a 변수라는 의미

  • 값 1을 나타냄
  • 값은 메모리 주소 6에 저장됨
b = a

위 표현은? b변수를 위해 메모리 주소를 하나 할당할 수 있음

하지만 아래그림처럼 a 변수가 여러 바이트(5bytes)를 차지하는 데이터를 나타내고 b=a 를 표현해야 한다면, 메모리 공간이 부족한데 방법은?

3.1.3 변수에서 포인터로: 포인터 이해하기

  • b 변수가 a변수를 가리키고 있다면 데이터의 복사본을 만들 필요가 없음

  • a 변수는 메모리 주소 3에 위치하므로 b 변수에 메모리 주소 숫자 3을 저장 → 포인터

포인터를 메모리 주소로만 이해한다면 간접 주소 지정을 알아야 함 (indirect addressing)

load r1 1 
  • 메모리 주소 1에 있는 값(숫자 3)을 r1 레지스터에 적재하는데, 메모리 주소 1에는 주소값이 있기 때문에 이를 나타내 줘야 함
load r1 @1
  • 메모리 주소1 → 메모리 주소 3→ 데이터 값(1)

어셈블리어에는 변수라는 개념이 없기 때문에 위와 같이 간접주소 지정 계층을 알아야 하지만 고급 언어에서는 변수를 사용하면 됨

메모리 주소 1 -> 메모리 주소 3 -> 데이터 # 어셈블리어 수준
b -> 데이터 # 고급언어 수준

3.1.4 포인터의 힘과 파괴력: 능력과 책임

#include <stdio.h>

void main()
{
	int a = 1;
	printf("variable a is in %p\\n", &a);
}

variable a is in 0x7fff328

  • 포인터를 통해 메모리를 직접 조작할 수 있는 능력은 강력하지만, 모든 상황에 필요한 것은 아니며, 포인터 없는 프로그래밍 언어에도 대신 사용할 수 있고 포인터를 한번 더 추상화한 참조가 있음

3.1.5 포인터에서 참조로: 메모리 주소 감추기

  • 포인터는 메모리를 추상화하 것이고 참조는 포인터를 한번 더 추상화 한 것
반응형
반응형

 

 

파이썬에서 URL을 다루기 위한 패키지로 크게 3가지 종류가 있음; urllib, urllib3, requests

  • urllib은 built-in package이고 나머지 2개는 third party

 

사용방법

 

1. 기본 사용방법

from urllib.request import urlopen                     # (1)

with urlopen("<https://www.example.com>") as response: # (2)
    body = response.read()                             # (3)
    print(type(body))                                  # (4)

(1) urllib.request는 built-in package로 따로 설치하지 않아도 됨. HTTP request를 위해 urlopen을 사용

(2) context manager with 문을 통해 request 후 response를 받을 수 있음

(3) response 는 <http.client.HTTPResponse> 객체

  • read 함수를 통해 bytes로 변환할 수 있음

(4) 실제 body의 type을 print해서 bytes 타입임을 확인

 

2. GET request for json format response

  • API 작업시 response가 json format인 경우가 많음
from urllib.request import urlopen
import json                                            # (1)

url = "<https://jsonplaceholder.typicode.com/todos/1>" # (2)
with urlopen(url) as response:
    body = response.read()

print("body: ", body)                                  # (3)
# body:  b'{\\n  "userId": 1,\\n  "id": 1,\\n  "title": "delectus aut autem",\\n  "completed": false\\n}'

# json bytes to dictionary
todo_item = json.loads(body)                           # (4)
print(todo_item)
# {'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}

(1) urllib 패키지와 함께 json 포맷을 다루기 위해 json package 추가

(2) JSON 형태의 데이터를 얻기 위한 샘플 API 주소

(3) 응답을 print 해보면 json 형태의 bytes format. 이를 dictionary 형태로 변경해주기 위해 json 패키지 필요

(4) json bytes를 파이썬 객체인 dictionary로 변경하기 위해 json.loads 함수 사용

 

3. Response의 header 정보 얻는 방법

from urllib.request import urlopen
from pprint import pprint

with urlopen("<https://www.example.com>") as response:
    pprint(response.headers.items())                       # (1)
    pprint(response.getheader("Connection")) # 'close'     # (2)
  • response의 headers.items()를 통해 header 정보를 얻을 수 있음

(1) pretty print(pprint)를 이용해 header 정보를 보기 좋게 출력하면 아래와 같음

[('Accept-Ranges', 'bytes'), ('Age', '78180'), ('Cache-Control', 'max-age=604800'), ('Content-Type', 'text/html; charset=UTF-8'), ('Date', 'Sat, 24 Aug 2024 18:10:20 GMT'), ('Etag', '"3147526947"'), ('Expires', 'Sat, 31 Aug 2024 18:10:20 GMT'), ('Last-Modified', 'Thu, 17 Oct 2019 07:18:26 GMT'), ('Server', 'ECAcc (lac/5598)'), ('Vary', 'Accept-Encoding'), ('X-Cache', 'HIT'), ('Content-Length', '1256'), ('Connection', 'close')]

(2) header의 개별 정보는 getheader 메소드를 이용해 얻을 수 있음

 

4. bytes를 string으로 변환

from urllib.request import urlopen

with urlopen("<https://www.example.com>") as response:
    body = response.read()                            
    print(type(body)) # <class 'bytes'>                       # (1)

decoded_body = body.decode("utf-8")                           # (2)
print(type(decoded_body)) # <class 'str'>                     # (3)
print(decoded_body[:30])

(1) body의 type을 확인해보면 bytes 이고 아래와 같은 형태이다

b'<!doctype html>\n<html>\n<head>\n

(2) bytes를 string으로 변환하기 위해 decode method를 이용 (”utf-8”을 파라미터로 전달)

(3) decoded_body의 type을 확인해보면 string인걸 확인할 수 있고 decoded_body의 일부를 표시하면 아래와 같은 형태

<!doctype html>
<html>
<head>

 

5. Bytes를 file로 변환

크게 2가지 방법이 있음

encoding & decoding 없이 바로 file로 작성

from urllib.request import urlopen

with urlopen("<https://www.example.com>") as response:
    body = response.read()

with open("example.html", mode="wb") as html_file:
    html_file.write(body)
  • write binary(wb) mode로 파일을 열어 bytes를 바로 example.html 파일에 작성
  • 코드를 실행하면 example.html 파일이 생성됨

 

contents를 file로 encoding해야하는 경우

from urllib.request import urlopen

with urlopen("<https://www.google.com>") as response:
    body = response.read()

character_set = response.headers.get_content_charset()        # (1) 
content = body.decode(character_set)                          # (2)

with open("google.html", encoding="utf-8", mode="w") as file: # (3)
    file.write(content)

(1)&(2): 구글같은 홈페이지는 location에 따라 다른 encoding 방식을 사용하기도 한다. 그래서 get_content_charset () 메소드를 이용해서 encoding 방식을 확인 후 bytes를 string으로 decoding 함

(3) decoded string을 다시 html에 utf-8 모드로 encoding해서 google.html 파일에 저장함

References


https://realpython.com/lessons/python-urllib-request-overview/

 
반응형

+ Recent posts