<대학원생 때 알았더라면 좋았을 것들>은 전반적인 대학원 생활, 논문을 읽는 방법, 논문 리뷰 발표하는 방법 등에 대해 다루고 있고

<그렇다면 실험실 죽순이가 될 수밖에>는 실험 실패, 대학원 생활 시 느끼는 감정과 다짐, 그리고 극복 방법 등을 위주로 서술되어 있다.

 

사실 이 책들은 어쩌다보니 대학원 입학 후 1년간 방황하고 난 뒤 읽게 되었는데, 마인드 셋에 큰 도움이 됐다.

요즘 회사를 퇴사하고 대학원 가시는 분들도 많아졌고, 대학원에 갈지 말지 고민하는 분들이 주변에 많이 생겼다. 회사 경험, 대학원 경험 모두 삶을 살아가는데 중요하다고 할 수 있어서 대학원을 갈지, 회사를 갈지 둘 중에 하나를 선택하는 양자택일의 관점으로 생각하기보다는 둘 중에 무엇을 먼저 할지 선후의 문제로 바라보면 고민이 간단해지는 것 같다.

 

대학원 생활에서 배울 수 있는 점은 크게 4가지라고 할 수 있다.

  • 논문을 통해 지식을 얻는 습관
  • 문제 정의 및 과학적 방법론으로 문제 해결 경험
  • 시간 관리부터 연구에 대한 실행까지 스스로 규율을 세우고, 스스로 탐색해나가는 경험
  • 실패했다면 왜 실패했는지 분석

 

우리 연구실은 비교적 자유롭기 때문에 시간 관리는 스스로 해야했고, 이로 인해 나태해지기 쉬운 환경이었다. 처음 대학원에 왔을 때는 학부 때와는 달리 동기나 선배가 없어서 적응하기가 조금 힘들었다. 그리고 내 벼락치기 성격으로 인해 결과를 내야 할 때는 며칠간 밤을 새서 수면 사이클이 엉망이 됐었다. 그래도 요즘에는 새로 들어오신 박사 과정 언니랑 연구 얘기도 하고, 플래너를 다시 쓰기 시작하면서 많이 안정된 것 같다.

 

아무튼 대학원에 와서 배운 가장 큰 것은 지식 습득뿐만 아니라, 더 크게 와닿는 점은 자율적인 시간 컨트롤 방법을 깨닫게 된 것이다. 논문 작성을 목표로 한정된 내 시간을 분배하고 관리하면서 목표를 이뤄나가기 위해 일의 우선순위대로 처리하는 과정에 대해 배웠다.

 

그렇지만 목표를 세우더라도 실험이 항상 성공하는 것은 아니었다. 주제를 잡는데 결과가 잘 안나와서 몇 번이나 갈아엎었고, 지금도 사실 연구가 잘 되고 있진 않는 것 같다. 그~래~도 이전에는 뭐가 잘 안되면 바로 구글링해서 문제를 해결하거나 다른 방법을 시도했지만, 지금은 적어도 실험자의 책임감을 가지고 실패했다면 왜 실패했는지에 대한 원인을 분석하게 됐다. 만약 명확한 이유를 모르겠더라도 가장 가능성이 높은 이유에 대해 생각해보고 동기 언니와 의견을 공유할 수 있게 된 것 같다.

 

대학원에 오니까 다시 느끼는거지만, 커뮤니케이션 능력의 중요성을 매번 느끼고 있다. 수업 때, 내가 하고 있는 연구를 다른 사람이 이해하기 쉽게 전달하는 것이 너무 어렵고, 논문 읽은 것을 다른 사람들과 공유하는 것도 너무 어렵다.

 

학기 중에는 매 달 소프트 런치를 진행하면서 다른 사람들의 연구를 듣곤 하는데, 혼자 연구만 하는 것 보다 확실히 생각 환기도 되는 것 같고 좋더라. 자주 만나진 못하지만, 코스웍에서 만난 다른 분야의 분들이나 종종 연구실 외 사람들, 그리고 이미 취업한 친구들에게 말을 걸다 보면 지금의 기술 트렌드와 내 연구 분야에 대해 새로운 의견을 들으면서 생각이 확장되는 것 같다. 

 

 


 

대학원에 처음 들어왔을땐 (지금도 그렇지만) 논문 읽는게 너무너무 어려웠다. 영어인데 집중도 안되고, 10페이지도 넘으면서 배경지식도 없어서 논문 하나를 일주일 걸려 읽었었다. 논문을 읽기 전에 논문을 선택하는것부터도 너무 어려웠다.

 

이 책에도 나오듯이 논문은 google scholar에서 주제와 키워드를 넣어서 검색하도록 하고, 출판연도/논문 인용수/저자 확인을 하는 것이 좋다.

키워드: review(검토), overview(훑어보기), survey(조사), tutorial(쉽게 설명)

 

그리고 논문을 읽을 땐 왜 읽는지 생각하면서 읽는 것이 좋다. 

💡 논문을 읽으며 생각할 것들

1. 이 논문이 왜 중요한가?
논문을 읽을 때 다 이해할 필요는 없으나 해당 논문의 중요한 내용(컨트리뷰션)이 무엇인지, 왜 accept 되었는지 이해하며 읽자

2. 결론이 무엇인가?
결국 이 논문을 어떻게 써먹을 수 있을까? 여기서 얻어갈 것은 무엇이 있을까? 에 대한 대답은 결론에서 나온다.
⇒ 왜 읽었는지, 이 논문에서 알고 넘어가야 할 것은 무엇인지 읽으면서 항상 생각

3. 세미나에서 어떻게 설명할 것인가?
데이터 셋, 풀려고 하는 문제, 목적, 결과 (큰 그림을 보자)
논문에 있는 그림과 글을 매칭시키며 읽고, 그림에 있는 것들의 물리적인 의미를 잘 생각할 것
⇒ 그림의 순서 (일반적으로 본격적인 설계에 앞서 기본 이론을 설명하기 위한 개념도, 기본 설계 구조, 설계 시 발생하는 문제점 소개, 어떻게 해결했는지, 성능 지표와 비교표 등의 순서)

4. 논문 리뷰 시 대략적인 틀
1) ~을 제안한 논문
2) ~을 사용
3) 풀려고 하는 문제는 ~을 목적으로 ~ 데이터 셋을 사용하여 ~ 라는 결과를 얻었다
4) 기존연구에는 ~라는 한계가 존재했다
5) 그래서 이 연구는 ~라는 컨트리뷰션해서 accept 되었다
6) 우리는 이걸 읽고 ~에 필요한 ~를 얻으면 된다

 

마지막으로 논문 읽은 것을 정리해 둘 땐, 만약 이 논문을 인용한다면 어떻게 할지 각 단락을 한 문장으로 요약해보도록 하자.

  • abstract: 나는 이런 문제를 풀거야
    • 무슨 문제를 풀고있고, 어떤 기여를 담았는지 관한 내용
    • ex) 딥 러닝을 웨어러블에 적용할 때, 데이터가 부족한 경우, 이를 증강하는 방법 연구
  • introduction: 이 문제는 이런 동기에서 연구가 시작됐는데,
    • 관련 연구 요약 (1. 풀려는 문제가 무엇인지 2. 왜 이 연구가 중요한지 3. 다른 연구와의 차별점)
    • ex) 파킨슨병 환자의 증상을 생활속에서 진단하려면 딥 러닝이 필요한데, 수집할 수 있는 데이터 수가 적다.
  • related works: 관련해서 이런 접근들이 있었다
    • ex) 데이터가 적은 경우 비전, 음성과 같은 다른 도메인에서는 어떻게 다루었는지 조사 & 파킨슨 병 환자의 모니터링 연구에 대한 조사
  • method: 나는 이런 방식으로 접근해보려 하는데
    • ex) data augmentation 방법 제시
  • experiment: 정말 이게 효과적인지 실험을 해 보니
    • 10% 가량의 성능 향상
  • discussion: 실험 결과는 이렇게 해석할 수 있다.
    • 적절한 data augmentation 방법은 타깃 태스크마다 다를 수 있어 직접 실험, 검증하는 것이 더 좋다는 한계점
  • conclusion: 요약
    • abstract와 비슷하나, 과정, 결과, 의의가 좀 더 자세하다

 

 

리뷰논문은 조금 다르다. 나는 이 블로그 글이 많이 도움 됐다.

리뷰논문은 구성이 어떻게 되는지 파악하고(abstract, introduction 이 중요하다), 주제의 발전 과정과 각 연구의 차별점/한계점을 중점으로 알아둔다. 

 

 


 

랩 미팅을 할 땐 무작정 가지 말고 준비를 해서 가도록 하자.

💡 미팅 체크리스트
1. 주제
2. 연구 목적/배경
    - 논문의 연구목적 연구 배경에 들어갈만한 내용
    - 연구를 해야 하는 이유를 문장으로 작성
3. 연구 문제 및 가설
    - 가설이나 연구 문제는 자질구레한 설명 없이 문장으로 모두 완성해야 피드백을 받을 수 있다.
4. 연구 방법
    - 가설을 이렇게 세웠기 때문에 이런 분석 방법을 통해 검증하고자 합니다

실험은 성공하기도, 실패하기도 한다. 주의할 점은 실험결과가 긍정적일수록 몇번을 더 확인하고, 보수적인 자세로 결과를 해석해야 한다는 것이다. 좋은 결과든 나쁜 결과든 결과 보고를 할때는 신중하게 팩트만 보고하고, 주관적인 해석보다는 데이터를 객관화 시켜 결론을 도출하는 것이 좋다. 그리고 다음에는 이번 실험 결과를 바탕으로 어떻게 해 보겠다는 내용으로 마무리 지어야 한다. 실험 결과가 좋고 나쁘고는 의미가 없으며, 그 결과를 바탕으로 다음 방향을 의미있게 설정하는 것이 중요하다.

 

  • 실패했으면 죄송해하지 말고 원인을 찾아보자
    • 실험을 기획한 사람 입장에서 잘 모르겠거나 애매모호한 부분은 최대한 가능성 있는 대안을 첨부하자
    • 다음에 추가로 꼭 확인해볼 것을 분명히 해서 책임감있는 태도를 전하자
    • 마지막으로 랩미팅 자료에 무엇이 들어있어야 하는지, 교수님과 무엇을 논의해야 하는지 생각하자

 


 

첫 논문을 작성할 때는 아무리 논문 수정을 해도 비슷한 내용 반복에 어떻게 써야하는지 감이 전혀 잡히지 않았었다. 이 책에서도 나오는데, 논문 작성은 일단은 써야 수정과정을 거칠 수 있다.

 

논문은 연구를 다 하고 쓰는 순차적인 과정이 아닌, 연구 <-> 논문쓰기 를 반복 하면서 연구만 할 때 몰랐던 논문조사/추가실험 필요성 보완하는 것이 중요하다. 따라서 일단 목차를 나누고, 각 섹션에 들어갈 내용을 정리한 뒤, 어떤 그래프가 들어가야 할지 일단 생각해둔다.

  • abstract 과 introduction: 서론에서는 넓은 범위에서의 문제 제기부터 시작하여 본인의 연구 영역까지 점진적으로 범위를 좁히며 초점을 맞춘다. 그 과정에서 다른 사람들이 문제를 해결해온 방식도 간략히 소개한다. 그리고 기존 연구의 한계점을 설명한 뒤 본 연구의 필요성을 역설하고 마지막으로 본 연구의 기여를 요약해준다.
    • 초록과 서론을 읽고 난 뒤 독자가 어떤 느낌을 받으면 좋겠는지
      • 이 논문이 다루는 문제는 정말 꼭 해결해야 하는 문제같아
      • 이전의 솔루션들은 아직 많은 한계점을 가지고 있군
      • 여러 시도와 비교해도 이 논문의 기여는 의미가 있겠어
      • 이 논문의 기여 중 핵심은 ~~이군
      • 앞으로 논문의 나머지 부분에선 이런 내용이 나올 것 같아
  • related works: 다른 사람의 연구를 평가할 땐 객관적인 수치와 함께 조심스럽게 그 한계점을 드러낸다.
    • ex) B연구는 어떤 뿐에 어떤 아이디어를 적용하여 A연구를 정확도 ~에서 ~로 개선하였다.
  • methods 와 experiments
  • conclusion
    • abstract 내용을 반복하지 않는다. abstract는 논문을 읽지 않은 사람들이 처음 읽는 글이고, 결론은 논문을 모두 읽은 뒤 전체 내용을 조망하는 글이다.
    • abstract는 문제 제기와 연구의 중요성에 조금 더 큰 방점을 두고 결론은 실험을 통해 얻은 지식과 의의에 대해 조금 더 큰 방점을 두고 풀어낸다.
    • 결론에서 갑자기 새로운 내용을 꺼내는 것은 부적절하다

 


 

논문을 쓰고 나면 컨퍼런스 등에 발표를 하게 되는데,

우리 지도 교수님께서 가장 중요하게 생각하시는 점은 다른 사람들이 이해하기 쉽게 발표를 하는 것이다. 

  • 그래프의 x축과 y축이 무엇인지 짚어주자
  • 그래프를 보고 그 경향성이 의미하는 바가 무엇인지 짚어주자
  • 발표 내용을 이해하는데 중심이 되는 이론을 설명하는데 인색하지 말자
  • 서론에서 자신의 연구 분야의 필요성이 잘 전달되어야 한다. 문제 제기를 명확히 하고, 자신의 연구가 어떤 솔루션을 제시하는지 밝힌다.
  • 매 슬라이드가 넘어갈 때 앞장과 뒷장의 인과관계가 있도록 만들자. 슬라이드가 넘어갈 때 적절한 연결어를 적극적으로 활용하자

 

 

 

 

후기

이제 벌써 석박 통합 5학기 차인데, 그동안 배운점도 많고 아직 부족한 점도 많은 것 같다.

이 책을 읽고 계속 되새겨볼만한 점과 대학원 생활을 하면서 깨달은 점을 정리해봤다. 

이 책은 목차만 보고 읽기로 결정했는데, 책의 전반부는 코드를 잘 읽는 방법, 후반부는 코드를 잘 작성하는 방법을 다루고 있다. 흥미로웠던 점은 기초적인 뇌과학 원리를 설명하면서, 동시에 프로그래밍을 할 때 어떠한 방식으로 행동해야하는지 여러 연구를 통해 보여준다. 요즘 선배들이 연구했던 코드를 볼 일이 생겨서 틈틈이 보고 있는데.. 시간만 가고 잘 이해가 되지 않았는데.. 흑흑 😭 이 책에서 언급하는 방법들을 통해 처음 보는 코드도 잘 이해할 수 있도록 해봐야겠다 

 

 

1. 코드 더 잘 읽기

프로그래밍을 하다보면, 마냥 "이해가 안된다" 또는 "어렵다" 는 단순한 혼란이 아닌, 알고보면 서로 다른 3가지 유형의 혼란이 발생한다. 그리고 각 혼란은 뇌의 서로 다른 부분에서 발생한다.

코드 분석 시 3가지 인지 과정

 

(1) 지식의 부족: 우리는 APL 언어에 대해 알지 못하고, 특히 T 가 의미하는 바를 모르기 때문에 혼란스럽다. = LTM

// APL에서의 이진수 표현
2 2 2 2 2 T n

(2) 정보의 부족: Integer.toBinaryString 이라는 메서드 이름으로 어떤 일을 하는지 유추할 수 있지만, 내부적으로 어떻게 동작하는지에 대한 정보를 얻으려면 메서드 내부 코드를 살펴봐야 한다. = STM

// Java 에서 이진수 표현
public class BinaryCalculator {
	public static void main(Integer n){
    	System.out.println(Integer.toBinaryString(n));
    }
}

(3) 처리능력의 부족: 변수에 임시로 저장되는 값 등을 따라가기 위해 머릿속으로만 처리하기 어려워서 종이에 따로 표시 등을 해둬야 한다. = 작업기억공간

// 베이직에서 이진수 표현
LET N2 = ABS(INT(N))
LET B$ = ""
FOR N1 = N2 TO 0 STEP 0
	LET N2 = INT(N1/2)
    LET B$ = STR$(N1-N2*2) + B$
    LET N1 = N2
NEXT N1
PRINT B$

 

 

그래서 코드를 잘 읽기 위해서는 뇌의 다양한 부분이 사용되고 있다는 점을 인지하고, 뇌의 각 부분에 대한 특징을 이해해야한다.

우리가 코드를 기억할 때는 짧은 기간동안 정보를 저장하는 STM 과 장기적으로 정보를 저장하는 LTM 이 함께 작용하는데, STM 의 용량이 적고 정보를 저장하는 시간이 짧기 때문에 이해하거나 기억한 정보를 처리할 때는 LTM 과 상호작용한다. 이때 기억 공간에는 `청크` 단위로 저장된다. 청크는 배경 지식에 따라 큰 단위가 될 수도, 작은 단위가 될 수도 있다. 숙련된 프로그래머들은 LTM 에 저장된 익숙한 키워드, 구조, 도메인 개념 등을 활용하여 코드를 이해한다. 실험 결과, 특히 디자인 패턴에 대한 지식을 가지면 청킹 능력이 향상되고, 코드를 빠르게 이해 및 수정 할 수 있었다. 주석도 도움이 될 수 있다.

 

이 책에서는 쉽게 정보를 찾을 수 있는 환경에서는 문법을 기억하지 않아도 된다는 인식이 발생하여 인출 강도가 약해지고(알긴 아는데 계속 입 안에서만 맴도는 상태), 악순환이 이어진다고 주장한다. 이를 해결하기 위해서는 검색하기 전에 먼저 의도적으로 기억하려고 노력해야 한다.


 

복잡한 코드를 읽는 일은 작업 기억 공간에 과부하를 일으키기도 한다. 작업 기억 공간은 STM과는 다르게, 정보를 처리하는 프로세서 역할을 한다는 차이점이 있다. 작업 기억 공간 또한 STM 과 마찬가지로 용량(인지 부하)이 정해져있으며, 청크 단위로 이루어진다. 만약 청크 단위로 나뉘지 않는 너무 많은 요소를 가진 문제를 풀려고 하면 과부하가 걸린다.

 

여기서 말하는 인지부하는 크게 3가지로 나뉘는데,

  • 내재적 부하: 문제 자체가 얼마나 복잡한지
  • 외재적 부하: 외부적 요인에 의해 문제에 추가된 것
"""
10보다 큰 항목을 선택하는 파이썬 프로그램 = 동일한 내재적 인지 부하 (문제 그 자체)
"""

# List Comprehension 에 대한 배경지식에 따라 외재적 인지 부하가 달라질 수 있다
above_ten = [a for a in items if a > 10]

# 일반적인 for 문
above_ten = []
for a in items:
	if a > 10:
    	above_ten.append(a)
  • 본유적 부하: 생각을 LTM 에 저장하는 과정에서 일어나는 인지적 부하

 

 

인지 부하를 줄이기 위해서는 리팩터링을 하면 된다. 리팩터링은 코드의 기능은 유지하되, 구조를 개선하는 것이다. 일반적으로는 코드의 유지 보수를 쉽게 하기 위한 목적으로 이루어지지만, 저자는 장기적으로 가독성이 높은 코드를 작성하도록 리팩터링 하는, 인지적 리팩터링도 중요하다고 한다.

 

의존 그래프(왼쪽)와 상태표(오른쪽)

그러나 리팩터링을 해도 코드의 구조가 복잡하다면, 예를 들어 정확히 코드의 어디를 파악해야 하는지 모르거나 코드가 서로 밀접하게 연결되어 많은 코드를 읽어야 하는 경우 작업 기억 공간 용량을 초과할 수 있다. 이런 상황에서는 코드 기반으로 의존 그래프 또는 상태표를 만들어 작업 기억 공간을 보조하면 도움이 된다. 

 

 

 

 

변수 역할의 11가지 분류 - 사야니에미의 프레임워크

 

프로그램을 깊이 이해하기 위해서는 변수가 중심적인 역할을 하는데, 변수를 파악할 때에는 변수 이름뿐만 아니라 타입과 역할을 살펴보는 것이 도움된다.

 

앞서 코드를 이해하는데 도움이 되는 의존 그래프  상태표를 소개했는데, 이 방식은 다소 국지적이라는 단점이 있다. 코드를 보다 깊게 이해하기 위해서는 텍스트 구조 지식 계획 지식에 대해 살펴봐야 한다.

  • 텍스트 구조 지식: 키워드가 하는 일이나 변수의 역할 등과 같은 프로그램의 표면적인 이해와 관련됨
  • 계획 지식: 프로그래머가 프로그램을 작성할 때, 계획한 것이 무엇인지 혹은 무엇을 달성하려고 했는지

 

뿐만 아니라, 코드가 어떤 구조로 되어 있고 어떻게 연결되어 있는지 살펴봐야 하는데

다음 4가지 단계를 거쳐 표면적 지식으로부터 좀 더 깊은 이해로 진행할 수 있다.

  1. 프로그램의 시작점(초점, focal point)을 찾는다. ex) main()
  2. 초점으로부터 지식을 확장 ex) 초점에서 시작해서 변수, 메서드, 클래스 등의 연결 파악
  3. 관련된 개체로부터 개념 이해 ex) 입력값에 대한 함수의 결과 이해, 클래스가 가지는 필드의 지식 이해 등
  4. 여러 개체에 걸쳐있는 개념 이해 ex) 제약조건이나 오류 등

코드를 읽을 때에는 모든 라인을 순차적으로 읽기보다는, 전체적으로 스캔 후 함수의 콜 스택을 따라 읽으면 좋다.

 

 

 

2. 코드에 대해 생각하기

지금까지 코드를 읽고 이해하는 데 도움이 되는 방법들에 대해 알아봤으니, 문제를 해결을 위한 통찰력을 얻는 방법에 대해 알아보자. '모델링'은 소프트웨어 설계 결정을 내리는 데 도움이 된다. 상태표, 의존 그래프, 개체 관계도 등 실제 모델이

아니라, 우리가 머릿속에서 생각하는 정신적 모델을 사용하는 것도 문제 해결에 도움이 된다.

  • 정신모델: 풀어야 할 문제에 대해 추론하기 위해 사용할 수 있는 작업 기억 공간 내의 추상화
    • 방향/무방향 그래프 또는 다양한 형태의 리스트와 같은 자료구조
    • 디자인 패턴
    • 모델 뷰 컨트롤러와 같은 아키텍쳐 패턴
    • 개체 관계도 또는 시퀀스 다이어그램과 같은 도표
    • 상태도 또는 페트리 넷과 같은 모델링 도구 등

 

또한, 이미 배운 지식은 다른 영역에서도 유용하다. 이를 학습 전이학습 도중 전이라고 한다. 

그림과 같이 새로운 정보를 습득하면, 먼저 감각 기억 공간과 STM에 의해 처리되며 작업 기억 공간으로 전송된다. 작업 기억 공간은 이에 대해 생각하는 동시에 LTM에서 관련 정보를 검색하여 가져온다. 그래서 자바를 알고 있는 상태에서 파이썬 메서드에 대해 배우면 조금 다르게 동작하더라도 파이썬 메서드에 대해 더 빨리 학습할 수 있는 것이다.

  • 저도 전이: 자동화된 기술을 이전하는 것 ex) Ctrl+C, Ctrl+V
  • 고도 전이: 복잡한 작업이 전이되는 것 ex) 프로그래밍 시 변수를 사용하려면 먼저 선언을 해야한다고 생각하는 것
  • 근거리 전이: 서로 가까운 영역 사이에서 지식이 전이되는 것 ex) 미적분학과 대수학
  • 원거리 전이: 서로 먼 영역 간에 일어나는 전이 ex) 라틴어와 논어학

 

그렇다고 해서 전이가 항상 도움되는 것은 아닐 수 있다. 

  • 긍정적 전이: 무언가를 알고 있어 새로운 작업을 할 때 도움이 되는 전이
  • 부정적 전이: 기존 지식이 새로운 것을 배우는 데 방해가 되는 경우

긍정적 전이가 일어나면, 새로운 상황에 대한 정신 모델을 처음부터 만들 필요 없이 LTM에 저장된 정신 모델을 부분적으로 활용하여 형성할 수 있다. 그러나 이미 알고 있던 지식이 새로운 지식을 배울 때 유사하지 않음에도 불구하고 비슷하다고 인식될 경우 오개념을 발생시킬 수 있는데, 이러한 상황을 부정적 전이라고 한다. 예를 들어 자바에서는 변수를 초기화하지 않으면 사용할 수 없지만, 파이썬에서는 이러한 가정이 혼란을 야기할 수 있다. 오개념을 방지하는 데 코드베이스 내의 테스트문서화가 도움이 된다.

 

 

 

3. 좋은 코드 작성하기

좋은 코드를 작성하기 위해서는 변수, 클래스 및 메서드 등 코드 내 구성 요소들에 이름을 잘 명명하는 것이 중요하다. 변수명은 그자체로 코드를 설명하는 주석문처럼 작용하기도 하며, 초기에 부여한 이름이 대부분 유지되기 때문이기도 하다.

 

좋은 변수명을 인지적 관점으로 바라보면, 의미가 담긴 구성요소가 명확한 이름이라고 할 수 있다. 1부에서 배운대로 변수명을 읽을 때는 각 부분을 분할하고, 작업 기억 공간으로 전송될 때 각 부분과 관련된 정보를 LTM에서 검색하여 작업 기억 공간으로 전송한다. 

nmcntrlst 보다 name_counter_list 가 이해하기 쉽다

 

이 책에서는 더 나은 변수명을 위해 페이텔슨은 3단계 모델을 제시하고 있다.

  1. 이름에 포함할 개념을 선택: 의도를 고려하여 개체가 어떤 정보를 보유하고 있으며 무엇을 위해 사용되는지 나타낸다.
  2. 각 개념을 나타낼 단어 선택: 일관성 있는 단어를 사용한다.
  3. 단어들을 사용하여 이름 구성: 자연어에 맞추어 이름 틀을 사용 ex) points_max (x), max_points (o)

주의할 점은 클린 코드를 위반하는 구조적 안티패턴뿐만 아니라 언어적 안티패턴도 고려하여 이름을 지어야 한다는 점이다. 이러한 안티패턴은 우리 뇌에 혼란을 초래하여 인지 부하를 일으킬 수 있다. 

 

 


 

좋은 코드를 작성하면서 어려운 문제를 해결하는 것은 LTM을 강화하는 것이 가장 도움 된다. 문제 해결은 문제 이해, 계획 수립, 계획 실행 단계로 구성되며, 각 단계에서 LTM이 중요한 역할을 한다. 뿐만 아니라 LTM에 저장된 다양한 유형의 기억이 문제 해결을 돕는데 기여한다.

기억의 종류

  • 절차적(암시적) 기억: 운동 능력이나 의식하지 않고 발휘하는 기술로, 반복 연습에 의해 형성된다. ex) 단축키, 괄호를 열면 자동으로 괄호를 닫는 코드 작성 등
  • 선언적(명시적) 기억: 기억할 수 있는 사실이 있고, 그 사실을 자신이 알고있다는 것도 안다. 명시적 기억은 명시적 주의가 필요하다.  
    • 일화적 기억(경험): 과거에 문제를 어떻게 해결했는지 기억할 때 사용된다. 전문가들은 주로 익숙한 문제를 '해결' 하기보다는 '재현'한다. 즉 새로운 해결책을 찾는 대신 이전에 유사한 문제에 효과가 있었던 해결책에 의존하는 경향이 있다. ex) 계층 구조와 관련된 문제를 해결할 땐 트리를 사용했었지~
    • 의미적 기억(사실): 
    • 일화적 기억은 추가적인 노력을 하지 않아도 생성되지만, 의미적 기억과 마찬가지로 기억을 많이 생각해야만 인출 강도가 높아진다.

위 기억 분류에 따르면 암시적 기억은 작업을 신속하게 실행하는데 도움이 됨을 알 수 있으며, 암시적 기억이 형성되기 위해서는 인지 단계, 연상 단계, 자율 단계가 필요하다.

  • 인지 단계: 새로운 것을 배우는 때로, 새로운 정보를 더 작은 부분으로 나누고 당면한 작업에 대해 명시적으로 생각한다. ex) 배열의 인덱스는 0부터 시작하니까, 인덱스 3인 원소는 4번째 원소구나!
  • 연상 단계: 응답 패턴이 나타날 때까지 새 정보를 적극적으로 반복한다. ex) 인덱스를 계산할 때는 항상 1을 빼면 되네!
  • 자율 단계: 기술을 자동화 한 상태로, 인지 부하가 증가하지 않는다. ex) 리스트와 리스트 연산을 마주하면 숫자를 세거나 명시적으로 생각하지 않고도 인덱스를 즉시 알 수 있는 상태

 

문제 해결 능력을 향상시키는 또 다른 방법은 본유적 부하이다. 프로그래밍 능력을 향상시키기 위해 모든 프로그램을 처음부터 작성하는 것이 중요하다고 생각하는 사람들도 있지만, 연구 결과에 따르면 다른 사람이 작성한 코드를 읽고 해당 코드를 분석하는 것이 프로그래밍 능력 향상에 도움이 될 수 있다고 한다. 이는 작업 기억 공간의 인지 부하(본유적 부하)와 관련 있다.

세번째 유형의 인지 부하, 본유적 부하 - 두뇌가 정보를 LTM에 다시 저장하기 위해 수행하는 노력

작업 기억 공간의 내용이 LTM 에 다시 저장되기 위해서는 본유적 부하가 뒤따른다. 그러나 작업 기억 공간의 부하가 너무 많으면 결과가 LTM으로 옮겨지는 것이 어려울 수 있다. 따라서 본인만의 힘으로 프로그래밍 구현을 연습하는 것은 물론 실력이 향상되지만, 내재적 부하와 외재적 부하로 인해 본질적 부하가 부족할 수 있다. 다른 사람의 코드를 읽을 때는 인지 부하가 그렇게 높지 않으므로 코드 구조를 자세히 검토하고 이를 기억할 수 있게 된다. 따라서 프로그래밍 실력을 향상시키기 위해서 GitHub 탐색, 오픈 소스 코드 분석 등을 통해 코드를 연구하는 것이 유용하다.

 

 

 

4. 코딩에서의 협업

실제로 소프트웨어는 팀에 의해 개발되므로 동료와 함께 몰입하며서 코드를 작성하는 것이 중요하다. 특히 다른 사람들과 함께 작업할 때는 업무 중단이 흔하고 다시 돌아와 원래 하던 업무를 계속하기가 어려운데, 어떻게 하면 중단에 더 잘 대비할 수 있을까?

  • 정신 모델 저장(brain dump): 정신 모델은 중단으로부터 복구하는데 유용할 수 있으므로 정신 모델에 대한 메모를 주석문으로 남기는 것이 도움된다. 코드는 프로그래머의 사고 과정을 모두 설명하진 못하므로 코드에 특정 접근 방식을 선택한 이유, 코드의 목표 또는 구현을 위해 고려한 다른 대안과 같은 내용은 주석으로 남겨두면 좋다. 
  • 미래 기억 향상: 미래 기억은 미래에 무언가를 할 것에 대한 기억으로, to-do 주석문 등을 활용할 수 있다. 
  • 하위 목표 라벨링: 문제를 어떤 작은 단계로 나눌 수 있는지 명시적으로 기록함으로써 업무가 중단된 후 하던 업무를 계속할 때 자신의 생각을 정리하는데 유용하다.

 

 

 

후기

뇌가 코드를 처리하는 방식에 대해 새롭게 알 수  있었다.

저자가 LTM의 중요성에 대해 정말 많이 강조하고 있어서 그동안 모르면 구글링 하면 되지~ㅋ 하고 대수롭지 않게 생각했던 나를 반성함 🥲

새로운 코드를 읽을 때는 왠지 집중도 안되고 코드 분석도 어려워서 그냥 ADHD인줄 알았는데,,,

이 책을 읽고나서부터는 여기 나오는 방법대로 새로운 코드를 마주칠 때 잘 대처할 수 있을 것 같다.

 

4부도 딱히 정리는 안했지만 흥미로웠던 부분이, 사람은 멀티태스킹이 안된다고 한다. 그런데 멀티태스킹을 하는 사람들은 자신이 매우 생산적이라고 느낀다고 한다...!! 그들 자신의 수행능력이 만족스럽다고 느끼지만, 실상은 한가지 일만 하는 사람들에 비해 결과도 좋지 않고 시간도 오래쓴다는 것이다

 

아무튼 뇌의 행동 기반으로 인간이 왜 이렇게 되는지 설명해줘서 좋았고, 사회 과학 분야에서 이루어진 실험과 함께 예시를 들어줘서 이해가 쉬웠다. 앞으론 학교에서 수업을 듣거나.. 시험공부하거나.. 연구할땐 이 책 내용을 떠올리면서 의식적으로 노력해야겠다.

요즘 K-mooc 에서 요런 행사를 하고 있다.

유데미와 코세라 협약을 통해 보고싶은 강의를 무료로 + 무제한 볼 수 있는 이벤트다.

사실 유데미에 보고싶은 강의가 생겨서 신청하게 되었는데, 아쉽게도 구독권이 모두 소진되어 코세라를 신청해보라는 메일을 받았다. 

그리고 어제 코세라 학습에 초대되었다는 메일을 받고 구독권을 활성화 해봤다.

글로벌 강좌 구독권 이용기간은 4개월이고, 학습 부진자는 구독권이 조기 회수될 수 있다고 한다.

 

구글 스터디잼 할 때 종종 코세라를 써 보긴 했는데, 강의를 들으려고 둘러보니 생각보다 괜찮은 강의들이 있는 것 같다.

특히 시스템 관련은 강의가 별로 없어서 혼자 공부하기 어려움을 느꼈는데, 해외 대학교 강의나 회사에서 해주는 강의가 많이 도움 될 것 같다.

연구 잘 안될 때 봐야지.. 일단 담 학기에 스토리지 관련 수업을 들으니 리눅스 파일 시스템에 관해 먼저 공부를 좀 해 둬야겠다.

https://www.honeybadger.io/blog/memory-management-in-python/

 

Memory Management in Python

Understanding memory management is a superpower that will help you design memory-efficient applications and make it easier to debug memory issues. Join Rupesh Mishra for a deep dive into the internals of CPython.

www.honeybadger.io

이 글을 번역한 글입니다.

목차

1. Memory Management in Python (1) - Python 메모리 할당
2. Memory Management in Python (2) - Python 가비지 컬렉터

 


가비지 컬렉션은 프로그램에서 더 이상 필요하지 않을 때 할당된 메모리를 회수하거나 해제하는 프로세스이다.

가비지 컬렉션(GC)은 메모리를 자동으로 관리하는 방법 중 하나이다.
가비지 컬렉션은 프로그램에 의해 할당되었지만 더 이상 참조되지 않는 메모리(가비지)를 회수하려고 시도한다.
- Wikipedia

C 와 같은 언어에서는 프로그래머가 사용하지 않는 객체(사용되지 않는 객체의 가비지 컬렉션)에 대한 메모리를 수동으로 해제해야 하는 반면, Python 에서는 언어 자체가 자동으로 관리한다.

 

Python 은 자동 가비지 컬렉션에 두 가지 방법을 사용한다:

1. 참조 횟수를 기반으로 한 가비지 컬렉션

2. 세대별 가비지 컬렉션

먼저 참조 횟수가 무엇인지 설명하고, 참조 카운팅을 기반으로 한 가비지 컬렉션에 대해 자세히 알아보자!

 

Reference Count

이전 글에서 본 것처럼, CPython 은 내부적으로 각 객체에 대한 속성 유형과 참조 횟수를 생성한다.

ref count 속성을 더 잘 이해하기 위한 예를 살펴보자

a = "memory"

위 코드가 실행되면, CPython 은 문자열 유형의 객체 메모리를 생성한다.

 

필드 ref count 는 객체에 대한 참조 횟수를 나타낸다. 이전 글에서 우리는 Python 변수가 객체에 대한 참조일 뿐이라는 것을 알고 있다. 위 예에서 변수 a 는 문자열 객체 메모리에 대한 유일한 참조이다. 따라서 문자열 객체 메모리의 참조 카운트 값은 1이다.

 

 

 

 

 

 

 

 

 

getrefcount 메서드를 사용하면 Python 에서 모든 객체의 참조 횟수를 얻을 수 있다. 

아래와 같이 문자열 객체 메모리의 참조 카운트를 구해보면:

import sys

ref_count = sys.getrefcount(a)

print(ref_count)   # Output: 2

결과값은 2가 나온다. 즉, 문자열 객체 메모리가 두개의 변수에 의해 참조되고 있음을 나타낸다.

앞에서 우리는 메모리 객체가 변수 a에 의해서만 참조된다는 것을 보았는데, getrefcount 메서드를 사용할 때의 문자열 객체 메모리의 참조 횟수가 2인 이유는 무엇일까?

 

이를 이해하기 위해서 먼저 getrefcount 메서드의 정의를 살펴보자:

 

def getrefcount(var):
    ...

 

변수 a를 getrefcount 메서드에 전달하면, 메서드의 매개 변수 var도 메모리 객체를 참조하게 되므로 메모리 객체의 참조 횟수는 2가 되는 것이다.

 

그래서 객체의 참조 횟수를 얻기 위해 getrefcount 메서드를 사용할 때마다, 참조 횟수는 항상 객체의 실제 참조 횟수보다 1이 더 많아진다.

 

 

 

 

 

 

 

 

 

 

 

 

 

이 상황에서 동일한 문자열 객체 메모리를 가리키는 또 다른 변수 b 를 생성해 보자:

import sys

ref_count = getrefcount(a)

print(ref_count) # Output: 3

 

변수 a 와 b 는 모두 문자열 객체 메모리를 가리킨다. 그 결과 문자열 객체 메모리의 참조 횟수는 2가 되고, 앞선 예시와 마찬가지로 getrefcount 메서드의 출력은 3이 된다.

 

 

 

 

 

 

 

 

 

 

Decreasing the Reference Count

위 상태에서 참조 횟수를 줄이려면 변수에 대한 참조를 제거해야 한다.

예를 들면 아래의 방법을 사용할 수 있다:

b = None

이 경우 변수 b는 더 이상 문자열 객체 메모리를 가리키지 않기 때문에 문자열 객체 메모리의 참조 횟수가 줄어든다.

import sys

ref_count = getrefcount(a)

print(ref_count) # Output: 2

 

 

Decreasing the Reference Count Using the del Keyword

우리는 del 키워드를 사용하여 개체의 참조 횟수를 줄일 수도 있다.  

변수 b 에 None을 할당하면 (b = None), b는 더 이상 문자열 객체 메모리를 가리키지 않게 된다.

del 키워드는 같은 방식으로 작동하며, 객체의 참조를 삭제하여 참조 횟수를 줄이는 데 사용된다. (단, del 키워드는 개체를 삭제하지는 않는다.) 

 

del b

다음 예시와 같이 del b 는 b 의 참조만 삭제하며, 문자열 객체 메모리를 삭제하는 것이 아니다.

 

이제 문자열 객체 메모리의 참조 카운트를 구해보면, b의 참조 횟수는 2가 된다.

import sys

ref_count = getrefcount(b)

print(ref_count) # Output: 2

 

 

 

 

 

 

참조 횟수에 대해 배운 내용을 요약해 보자!

동일한 객체를 새로운 변수에 할당하면, 객체의 참조 횟수가 증가한다. 만약 del 키워드를 사용하거나 객체가 None 을 가리키도록 하여 객체를 역참조하면 해당 객체의 참조 횟수가 감소한다.

Python 의 참조 횟수 개념에 대해 더 잘 이해했으니까, 이제 참조 횟수를 기반으로 가비지 컬렉션이 작동하는 방식에 대해 알아보자

 

Garbage Collection on the Basis of Reference Count

참조 횟수를 기반으로 하는 가비지 컬렉션은 객체의 참조 횟수를 사용하여 메모리를 회수한다.

객체의 참조 횟수가 0 일때, Python 의 가비지 컬렉션이 시작되어 메모리에서 객체를 제거한다.

그리고 객체가 메모리에서 삭제되면, 다른 객체들도 함께 삭제될 수 있다.

 

다음 예를 살펴보자:

import sys

x = "garbage"
b = [x, 20]

ref_count_x = getrefcount(x)
print(ref_count_x) # Output: 3

ref_count_b = getrefcount(b)
print(ref_count_b) # Output: 1

위 예시에서 getrefcount 메소드로 인한 참조 횟수 증가를 무시하면 문자열 객체 "garbage" 의 참조 횟수는 2이고, 배열 객체의 참조 횟수는 1이다. (garbage: 변수 x 에서 참조되고, 리스트 b 에서도 참조됨)

 

 

 

 

 

 

이 때, 변수 x 를 삭제하면

del x

b[0] # Output: garbage

문자열 객체 "garbage" 의 참조 횟수는 아래와 같이 배열 객체 [x, 20]로 인해 1이 된다.

우리는 변수 b 에 의해 문자열 객체 "garbage" 에 여전히 접근할 수 있다.

 

 

 

 

 

 

 

 

 

여기서 변수 b 를 삭제 해보자:

del b

위 코드가 실행되면, 배열 객체의 참조 횟수는 0 이 되고, 가비지 컬렉션은 배열 객체를 삭제한다.

배열 객체 [x, 20] 를 삭제하면 배열 객체에서 문자열 객체 "garbage" 의 참조 x 도 삭제된다.

이렇게 하면 "garbage" 객체의 참조 횟수가 0이 되고, 결과적으로 "garbage" 개체도 수집된다.

따라서 객체의 가비지 컬렉션은 객체가 참조하는 다른 객체의 가비지 수집을 유발할 수도 있다.

 

참조 횟수에 따른 가비지 컬렉션은 실시간으로 이루어진다!

가비지 컬렉션은 객체의 참조 횟수가 0 이 되는 즉시 시작된다. 이는 Python 의 주요 가비지 컬렉션 알고리즘 방식이며 비활성화할 수 없다.

순환 참조가 있는 경우에는 참조 횟수를 기반으로 하는 가비지 컬렉션이 작동하지 않는다. 그래서 순환 참조가 있는 객체의 메모리를 해제하기 위해 Python 은 세대별 가비지 컬렉션 알고리즘을 사용한다.

 

다음으로 순환 참조에 대해 논의한 다음 세대별 가비지 컬렉션 알고리즘에 대해 더 깊이 이해해보자!

 

Cyclic References in Python

순환 참조는 객체가 자기 자신을 참조하거나 두개의 다른 객체가 서로를 참조하는 상태를 의미한다. 이러한 순환 참조는 리스트, 딕셔너리 및 사용자 정의 객체 등의 컨테이너 객체에서만 가능하다. 그리고 정수, 실수 또는 문자열과 같은 불변 데이터 타입에서는 불가능하다.

 

다음 예시를 살펴보자:

왼편에서 볼 수 있듯이 배열 객체 b 는 자신을 참조하고 있다.

오른쪽처럼 참조 b 를 삭제하면 Python 코드에서 배열 객체에는 접근할 수 없지만, 오른쪽 이미지와 같이 메모리에는 계속 존재하게 된다. 배열 객체가 자기 자신을 참조하므로 배열 객체의 참조 횟수는 영원히 0이 되지 않으며, 참조 횟수 기반 가비지 컬렉션에 의해서는 수집될 수 없다.

 

다른 예시를 살펴보면:

각각 ClassA 및 ClassB 클래스의 두 객체 object_a 및 object_b를 정의한다고 하자. object_a 는 변수 A 에서 참조하고 object_b 는 변수 B에서 참조 할 때, object_a 에는 object_b 를 가리키는 속성 ref_b 가 있다. 마찬가지로 객체 object_b 에는 object_a 를 가리키는 속성 ref_a 가 있다. 즉, object_a 와 object_b 는 각각 변수 A 와 B 를 이용하여 Python 코드에서 접근 할 수 있다.

 

만약 변수 A 와 B 를 삭제할 경우,

del A
del B

 

위 코드와 같이 변수 A 와 B 를 삭제하면, object_a 와 object_b 는 Python 에서 접근할 수 없지만 메모리에는 계속 남아있게 된다.

 

이렇게 객체는 서로 가리키고 있는 상태이므로(ref_a 및 ref_b 속성에 의해) 참조 횟수는 결코 0 이 될 수 없다. 따라서 이러한 객체는 참조 카운트 기반 가비지 컬렉터에 의해 수집되지 않는다. 따라서 이러한 경우 Python 은 세대별 가비지 컬렉션이라는 또 다른 가비지 컬렉션 알고리즘을 제공한다.

 

 

 

 

 

 

 

 

 

 

 

 

 

Generational Garbage Collection in Python

세대별 가비지 컬렉션 알고리즘은 순환 참조를 가지는 객체를 가비지 컬렉션하는 문제를 해결할 수 있다.

순환 참조는 컨테이너 객체에서만 가능하므로, 이 알고리즘은 모든 컨테이너 객체를 스캔하여 순환 참조를 가지는 객체를 검출한 다음, 가비지 컬렉션이 가능한 경우 제거한다.

 

스캔되는 객체 수를 줄이기 위해, 세대별 가비지 컬렉션은 불변 타입(예: int 및 문자열)만 포함하는 튜플을 무시한다.

 

객체를 탐색하고 순환 참조를 감지하는 것은 시간이 많이 걸리는 작업이므로 세대별 가비지 컬렉션 알고리즘은 실시간으로 작동하지 않으며 주기적으로 실행된다. 세대별 가비지 컬렉션 알고리즘이 실행되면 다른 모든 것이 중지된다. 따라서 세대별 가비지 컬렉션 알고리즘이 트리거되는 횟수를 줄이기 위해 CPython 은 컨테이너 객체를 여러 세대로 분류하고, 각 세대에 속할 수 있는 컨테이너 객체 수에 대한 임계값을 정의함으로써 지정된 세대의 객체 수가 정의된 임계값을 초과하면 세대별 가비지 컬렉션이 실행된다.

 

CPython 은 객체를 3세대(0, 1, 2세대) 로 분류한다. 새 객체가 생성되면 먼저 0 세대에 속하며, CPython 이 세대별 가비지 컬렉션 알고리즘을 실행할 때 이 객체가 수집되지 않으면 1 세대로 이동한다. CPython이 세대별 가비지 컬렉션을 다시 실행할 때 개체가 수집되지 않으면 2 세대로 이동한다. 이것은 마지막 세대이며 객체는 그대로 유지된다. 일반적으로 대부분의 객체는 1 세대에서 수집된다.

 

주어진 세대에 대해 세대별 가비지 컬렉션이 실행되면, 그동안 더 어린 세대들도 함께 가비지 컬렉션이 수행된다. 예를 들어 1 세대에 대해 가비지 컬렉션이 트리거되면, 0세대에 있는 객체들도 함께 수집된다. 모든 세 세대(0, 1, 2)가 가비지 컬렉션이 수행되는 상황을 풀 컬렉션(full collection) 이라고 한다. 풀 컬렉션은 대량의 객체를 스캔하고 순환 참조를 감지하는 것이 포함되므로 CPython 은 가능한 한 풀 컬렉션을 피하려고 한다.

Python 에서 제공하는 gc 모듈을 사용하면 세대별 가비지 컬렉션의 동작을 변경할 수 있다. 세대별 가비지 컬렉션은 gc 모듈을 사용하여 비활성화할 수 있다. 이를 선택적 가비지 컬렉션이라고 한다.

 

다음으로 gc 모듈의 중요한 메소드에 대해 알아보자.

 

 

gc Module in Python

1. 각 세대에 대한 임계값 얻기:

import gc
gc.get_threshold()

# Output: (700, 10, 10)

위의 출력값은 0 세대의 임계값이 700, 2 세대 및 3 세대의 임계값이 10임을 나타낸다.

0 세대에 700개 이상의 개체가 있는 경우 0 세대의 모든 개체에 대해 세대별 가비지 컬렉션이 트리거된다.

그리고 1 세대에 10개 이상의 개체가 있는 경우 1 세대와 0 세대 모두에 대해 세대별 가비지 컬렉션이 트리거된다.

 

2. 각 세대에 속한 객체의 수를 확인하기:

import gc
gc.get_count()

# Output: (679, 8, 0)   # Note: Output will be different on different machines

여기서 0 세대는 679개, 1 세대는 8개, 2 세대는 0개이다.  

 

3. 세대별 가비지 컬렉션 알고리즘을 수동으로 실행하기:

import gc
gc.collect()

gc.collect() 는 세대별 가비지 컬렉션을 트리거하며, 초기 설정 상 풀 컬렉션(full collection)을 실행한다.

 

만약 특정한 세대에 대한 세대별 가비지 수집을 실행하려면 아래와 같이 파라미터를 설정한다.

import gc
gc.collect(generation=1)

 

가비지 컬렉션 과정이 끝난 후에도 남아 있는 객체의 수를 확인해보면 아래와 같이 나타날 수 있다.

import gc

gc.get_count()

# Output: (4, 0, 0)   # Note: Output will be different on different machines

 

4. 각 세대의 세대 임계값을 업데이트하기:

import gc
gc.get_threshold()

gc.set_threshold(800, 12, 12)

gc.get_threshold()

# Output: (800, 12, 12)   # Note: Output will be different on different machines

 

5. 세대별 가비지 컬렉션을 비활성화/활성화 하기

gc.disable() # disable generational garbage collection
gc.enable() # enable it again

 

 

Conclusion

이 글에서는 Python 이 규칙과 스펙들의 집합이며, CPython 은 C 로 구현된 Python 의 참조 구현체임을 배웠다.

또한 Python 이 메모리를 효율적으로 관리하기 위해 사용하는 여러 메모리 할당자, 예를 들어 Object Allocator, Object-specific Allocator, Raw memory allocator, General purpose Allocator 등을 배웠다. 그리고 Python 메모리 관리자가 소규모 객체(512 byte 이하)에 대한 메모리 할당/해제를 최적화하는 데 사용하는 Arena, Pool, Block 에 대해 배웠다. 마지막으로 참조 횟수 및 세대 기반의 가비지 컬렉션 알고리즘에 대해서도 배웠다.

https://www.honeybadger.io/blog/memory-management-in-python/

 

Memory Management in Python

Understanding memory management is a superpower that will help you design memory-efficient applications and make it easier to debug memory issues. Join Rupesh Mishra for a deep dive into the internals of CPython.

www.honeybadger.io

이 글을 번역한 글입니다.

목차

1. Memory Management in Python (1) - Python 메모리 할당
2. Memory Management in Python (2) - Python 가비지 컬렉터

 


메모리 관리는 컴퓨터 메모리(RAM)를 효율적으로 관리하기 위한 과정으로, 프로그램 요청 시 런타임에 메모리 조각을 프로그램을 할당하고 프로그램이 더 이상 메모리를 필요로 하지 않을 때 할당했던 메모리를 재사용하기 위해 할당된 메모리를 해제하는 작업을 포함한다.

 

C 또는 Rust 의 경우 프로그램에서 메모리를 사용하기 전에 수동으로 메모리를 할당하고 프로그램이 더 이상 필요하지 않을 때 메모리를 해제해야 한다. 즉, 메모리 관리는 프로그래머의 책임이다. Python 에서는 자동으로 메모리 할당 및 할당 해제를 처리한다.

 

이 글에서는 Python 의 메모리 관리 내부적인 동작에 관해 설명한다. 또한, 객체와 같은 기본 단위가 메모리에 저장되는 방법, Python 의 다양한 메모리 할당자 유형, Python 의 메모리 관리자가 메모리를 효율적으로 관리하는 방법에 대해 설명한다.

 

Python 메모리 관리의 내부 동작을 이해하면, 메모리 효율적인 애플리케이션을 설게하는 데 도움이 될것이다. 또한 애플리케이션의 메모리 문제를 더 쉽게 디버깅할 수 있다.

 

Pythond 의 언어 사양에 대해 이해하는 것부터 시작하여 CPython 에 대해 자세히 살펴보자! 

 

Python as a Language Spectification

Python 은 프로그래밍 언어로, 이 문서는 Python 언어에 대한 규칙 및 사양을 명시한 Python 참조 문서이다.

 

예를 들어, Python 언어 사양에 따르면 함수를 정의하기 위해서는 def 키워드를 사용해야 한다고 명시되어 있다.

이는 사양일 뿐이며 컴퓨터가 def function_name 을 사용하면 => function_name 이라는 이름의 함수를 정의하려고 한다는 것을 컴퓨터가 이해할 수 있도록 규칙과 사양에 따라 프로그램을 작성해야 한다.

 

Python 의 언어 규칙 및 사양은 C, Java, 그리고 C# 과 같은 다양한 프로그래밍 언어로 구현된다.

C로 Python 언어를 구현한 것을 CPython 이라고 하고, Java 와 C# 으로 Python 언어를 구현한 것을 각각 Jython 과 IronPython 이라고 한다.

 

What is CPython?

CPython 은 Python 언어의 기본이자 가장 널리 사용되는 구현이다. Python 이라고 하면 대부분 CPython을 의미한다.

python.org 에서 Python 을 다운로드 하면 기본적으로 CPython 코드가 다운로드 된다. 따라서 CPython 은 C 언어로 작성된 프로그램으로, Python 언어에 정의된 모든 규칙과 사양을 구현한다.

CPython 은 Python 프로그래밍 언어의 참조구현이다.
CPython 은 Python 코드를 바이트 코드로 컴파일 후 한 줄씩 실행하기 때문에,
인터프리터와 컴파일러 둘 다로 정의될 수 있다.
- Wikipedia

CPython 은 참조 구현이므로 Python 언어의 모든 새로운 규칙과 사양은 먼저 CPython 을 통해 구현된다.이 글에서는 CPython 의 메모리 관리 내부에 대해 설명한다.참고로 JPython 및 IronPython 과 같은 다른 구현에서는 메모리 관리는 다른 방식으로 할 수도 있다. CPython 은 C 프로그래밍 언어로 구현되었으므로 먼저 C 의 메모리 관리와 관련된 두가지 중요한 기능: malloc 과 free 에 대해 알아보자.

 

What are malloc and free Functions in C?

malloc 은 런타임에 운영 체제에서 메모리 블록을 요청하기 위해 C 프로그래밍 언어에서 사용되는 방법니다. 프로그램이 런타임에 메모리가 필요할 때, 필요한 메모리를 얻기 위해 malloc 메서드를 호출한다.

 

free 는 프로그램이 더 이상 메모리를 필요로 하지 않을 때, 프로그램에 할당된 메모리를 다시 운영체제로 해제하기 위해 C 프로그래밍 언어에서 사용되는 방법이다.

 

Python 프로그램에 메모리가 필요한 경우는 CPython 이 내부적으로 malloc 메서드를 호출하여 메모리를 할당한다. 만약 프로그램에 더 이상 메모리가 필요하지 않으면 CPython 은 free 메서드를 호출하여 메모리를 해제한다.

 

이제 파이썬에서 서로 다른 객체에 메모리가 어떻게 할당되는지 살펴보자!

 

Objects in Python

Python 에서는 모든것이 객체이다. 클래스, 함수, 그리고 integer, floats, string 과 같은 간단한 데이터 유형도 객체이다.

예를 들어, Python 에서 정수를 정의하면 CPython은 내부적으로 정수 유형의 객체를 생성한다. 이러한 개체는 힙 메모리에 저장된다.

 

각 객체는 value, type, reference count 세 필드를 가진다. 

그림1

a = 100

위 코드가 실행되면, CPython 은 정수 유형의 객체를 생성하고 이 객체에 대한 메모리를 힙 메모리에 할당한다.

type 은 CPython 에서 객체의 타입을 나타내며, value 필드는 이름에서 알 수 있듯이 객체의 값(100) 을 저장한다.

 

이 글 뒷부분에서 ref count 필드에 관해 더 자세히 설명할 것이다.

 

 

 

Variables in Python

Python 의 변수는 메모리의 실제 객체에 대한 참조일 뿐이다. 변수들은 메모리에 있는 실제 객체를 가리키는 이름이나 레이블이며, 어떤 값도 저장하지는 않는다. 위 그림과 같이 변수 a 를 사용하여 Python 프로그램에서 정수 객체에 접근할 수 있다.

 

a = 100
b = a

그렇다면 이 정수 객체를 다른 변수 b 에 할당해보자

 

 

 

 

 

 

 

 

 

 

 

그리고 정수 객체의 값을 1 증가해보면,

 

# Increment a by 1
a = a + 1

위 코드가 실행되면 CPython은 값이 101 인 새 정수 객체를 만들고 변수를 이 새 정수 객체를 가리키도록 한다. 변수 b는 100인 정수 개체를 계속 가리킨다.

여기서 우리는 100 의 값을 101 로 덮어쓰지 않고 CPython 이 값 101 의 새 객체를 생성하는 것을 확인할 수 있다.

즉, 일단 생성되면 수정할 수 없으며, 부동 소수점 및 문자열 데이터 유형 또한 Python 에서 변경할 수 없다.

 

 

 

 

 

 

 

 

 

 

이 개념을 더 자세히 설명하기 위해 간단한 Python 프로그램을 살펴보자

# 변수 i 값이 100 미만이 될 때까지 증가시키는 while 루프

i = 0

while i < 100:
"""
# 변수 i 가 증가할 때마다 CPython은 증가된 값으로 새로운 정수 객체를 생성하고
# 이전 정수 객체는 메모리에서 삭제 대상이 된다.
"""
    i = i + 1

CPython 은 이와 같이 각각의 새 객체에 대해 malloc 메서드를 호출하여 해당 객체에 대한 메모리를 할당하고, free 메서드를 호ㅗ출하여 메모리에서 이전 객체를 삭제한다.

 

위의 코드를 malloc 과 free 를 사용한 코드로 변환해보면 아래와 같다.

i = 0  # malloc(i)

while i < 100:
    # malloc(i + 1)
    # free(i)
    i = i + 1

우리는 CPython 이 이렇게 간단한 프로그램에도 많은 수의 객체를 생성하고 삭제하는 것을 확인할 수 있다.

각 객체 생성 및 삭제에 대해 매번 malloc 과 free 메서드를 호출하면 프로그램의 실행 성능이 저하되고 프로그램이 느려진다.

 

따라서 CPython 은 각각의 작은 객체 생성 및 삭제에 대해 malloc 및 free 호출 횟수를 줄이기 위해 다양한 기술을 도입한다.

이제 CPython 이 메모리를 관리하는 방법에 대해 이해해보자

 

Memory Management in CPython

Python 의 메모리 관리에는 프라이빗 힙 관리가 포함된다. 프라이빗 힙은 Python 프로세스의 전용 메모리 영역이다.

모든 Python 객체 및 데이터 구조는 프라이빗 힙에 저장된다.

 

운영체제는 이 메모리 영역을 다른 프로세스에 할당할 수 없으며, 프라이빗 힙의크기는 Python 프로세스의 메모리 요구 사항에 따라 늘어나고 줄어들 수 있다. 프라이빗 힙은 CPython 코드 내부에 정의된 Python 메모리 관리자에 의해 관리된다.

 

CPython 의 프라이빗 힙은 아래 그림과 같이 여러 부분으로 나뉜다.

이러한 각 부분의 경계는 고정되어 있지 않으며, 요구 사항에 따라 확장되거나 축소될 수 있다.

  1. Python Core Non-object memory: Python Core Non-Object 데이터에 할당된 메모리 부분이다.
  2. Internal Buffers: 내부 버퍼에 할당된 메모리 부분이다.
  3. Object-specific memory: 객체별 메모리 할당자가 있는 객체에 할당된 메모리 부분이다.
  4. Object memory: 객체에 할당된 메모리 부분이다.

 

 

 

 

 

 

프로그램이 메모리를 요청하면, CPython 은 malloc 메서드를 사용하여 운영체제에서 해당 메모리를 요청하고, 프라이빗 힙의 크기가 커진다. 그리고 CPython에서는 각각의 작은 객체 생성 및 삭제를 위한 malloc과 free를 호출하지 않도록 다양한 할당자와 해제자를 정의한다. 다음 섹션에서 각각 상세히 알아보자!

 

Memory Allocators

malloc 및 free 메서드를 자주 호출하지 않기 위해 CPython은 아래와 같이 할당자의 계층 구조를 정의한다.

그림의 메모리 계층 구조의 아래에서부터,

  • General Purpose Allocator (CPython 의 malloc 메서드): 메모리 계층 구조의 가장 아래에는 General Purpose Allocator 가 있다. 범용 할당자는 CPython 용 `C 언어의 malloc` 메서드이다. 이는 운영 체제의 가상 메모리 관리자와 상호작용하여 Python 프로세스에 필요한 메모리를 할당한다. 또한, 운영체제의 가상 메모리 관리자와 통신하는 유일한 할당자이다. 
  • Raw memory Allocator (512 byte 보다 큰 객체용): General Purpose Allocator 위에는 Python 의  Raw memory Allocator 가 있다. Raw memory Allocator 는 General Purpose Allocator(=malloc) 에 대한 추상화를 제공한다. Python 프로세스에 메모리가 필요한 경우, Raw memory Allocator 는 General Purpose Allocator 와 상호작용하여 필요한 메모리를 제공한다. 그리고 Python 프로세스의 모든 데이터를 저장할 충분한 메모리 공간이 있는지 확인한다. 
  • Object Allocator (512 byte 보다 작거나 같은 객체용): Raw memory Allocator 위에는 Object Allocator 가 있는데, 이 할당자는 512 byte 이하의 작은 객체에 대한 메모리 할당에 사용된다. 만약 객체에 512 byte 이상의 메모리가 필요한 경우 Python 의 메모리 관리자는 Raw memory Allocator 를 직접 호출한다.
  • Object-specific Allocators (특정 데이터 유형에 대한 특정 메모리 할당자): Object Allocator 위에는 Object-specific Allocators 가 있다. 정수, 실수, 문자열 및 리스트와 같은 단순 데이터 유형에는 각각의 객체별 할당자가 있다. 이러한 객체별 할당자는 객체의 요구 사항에 따라 메모리 관리 정책을 구현한다. 즉, 정수에 대한 객체별 할당자는 실수에 대한 객체별 할당자와 구현 방식이 다르다.
    • Object Allocator 와 Object-specific Allocators 모두 Raw memory Allocator 에 의해 Python 프로세스에 이미 할당된 메모리에서 작동한다. 이러한 할당자는 운영 체제에서 메모리를 직접 요청하지 않고, 프라이빗 힙에서 작동한다. Object Allocator 와 Object-specific Allocators 에 더 많은 메모리가 필요한 경우 Python 의 Raw memory Allocator 가 General Purpose Allocator 와 상호작용하여 메모리를 제공한다.

 

Hierarchy of Memory Allocators in Python

객체가 메모리를 요청하고 객체에 object-specific allocator 가 정의되어 있으면, object-specific allocator 가 메모리를 할당하는데 사용된다.

 

객체에 object-specific allocator 가 없고 512 byte 이상의 메모리가 요청된 경우 Python 메모리 관리자는 Raw memory Allocator 를 직접 호출하여 메모리를 할당한다.

 

요청된 메모리 크기가 512 byte 미만일 때, Object Allocator 를 사용하여 할당한다.

 

 

 

 

 

 

 

 

 

 

 

 

Object Allocator

object allocator 는 pymalloc이라고도 하며, 크기가 512 byte 미만인 작은 개체에 메모리를 할당하는 데 사용된다.

CPython 코드베이스는 object allocator 를 다음과 같이 설명한다.

general-purpose malloc 위에 사용되는 작은 블록을 위한 빠른 특수 목적 메모리 할당자.
object-specific allocator 가 독점 할당 체계를 구현하지 않는 한 모든 객체 할당/할당 해제에 대해 호출된다.
(예: int 는 간단한 free 리스트를 사용함)

이는 순환 가비지 컬렉터가 컨테이너 객체에서 선택적으로 작동하는 곳이기도 하다.

작은 객체가 메모리를 요청하면 해당 객체에 대한 메모리를 할당하는 대신, 객체 할당자가 운영체제에서 큰 메모리 블록을 요청한다. 이 큰 메모리 블록은 나중에 다른 작은 객체에 메모리를 할당하는 데 사용된다.

CPython 코드베이스에서 발췌한 내용에 따르면

이런식으로 객체 할당자는 각각의 작은 객체에 대해 malloc 을 직접 호출하는 것을 방지한다. 

 

객체 할당자가 할당하는 큰 메모리 블록을 Arena 라고 한다. Arena 의 크기는 256KB 이다.

 

Arena 를 효율적으로 사용하기 위해서 CPython 은 Arena 를 Pool(4KB) 로 나눈다. 따라서 Arena 는 64개의 Pool 로 구성될 수 있다.

 

Pool 은 다시 블록으로 나뉜다.

다음 장에서는 이러한 각 구성 요소에 대해 설명한다.

 

 

 

 

 

 

 

 

 

Blocks

블록은 object allocator 가 객체에 할당할 수 있는 가장 작은 메모리 단위이다. 블록은 하나의 객체에만 할당될 수 있고, 객체 또한 하나의 블록에만 할당될 수 있다. 즉, 객체의 일부를 두 개 이상의 개별 블록에 배치하는 것은 불가능하다.

 

 

블록은 다양한 크기로 제공된다. 블록의 최소 크기는 8 byte 이고, 최대 크기는 512 byte 이다. 블록 크기는 8의 배수로 8, 16, 24, 32, ..., 504, 512 바이트가 될 수 있다. 각 블록 크기를 Size Class 라고 한다. Size Class 는 아래와 같이 64 개의 등급으로 나뉜다.

 

이 표에서 볼 수 있듯이 size class 0 블록의 크기는 8 byte 이고, size class 1 블록의 크기는 16 byte 이다.

 

프로그램은 항상 블록 하나 전체에 할당되거나, 아예 할당이 되지 않거나 두 가지 상태만 존재한다. 즉, 프로그램이 14 byte 의 메모리를 요청하면 16 byte 의 블록이 할당된다. 마찬가지로 프로그램이 35 byte 의 메모리를 요청하면 40 byte 의 블록이 할당된다.

 

 

 

 

 

 

 

 

 

Pools

풀은 size class 가 하나인 블록들로 구성된다. 예를 들어 size class 0 의 블록이 있는 풀은 다른 size class 의 블록을 가질 수 없다.

풀의 크기는 가상 메모리 페이지의 크기와 같다.

가상 메모리 페이지:
페이지, 메모리 페이지, 또는 가상 페이지는 가상 메모리의 고정 길이 연속 블록이다.
가상 메모리 운영 체제에서 메모리 관리를 위한 가장 작은 데이터 단위이다.
- Wikipedia

대부분의 경우, 풀 크기는 4KB 이다.

풀은 요청된 size class 의 블록이 있는 사용 가능한 다른 풀이 없는 경우에만 아레나에서 잘라낸다.

풀은 다음 세 가지 상태 중 하나일 수 있다.

  • Used: 풀에 할당에 사용된 블록이 있는 경우 사용됨 상태에 있다고 한다.
  • Full: 풀의 모든 블록이 할당된 경우 풀이 가득찬 상태에 있다고 한다.
  • Empty: 풀의 모든 블록을 할당할 수 있는 경우, 풀이 비어있는 상태라고 한다. 빈 풀에는 size class 개념이 없으며, 모든 size class 등급의 블록을 할당하는 데 사용할 수 있다.

 

풀은 CPython 코드에서 아래와 같이 정의된다:

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};
  • szidx: 풀의 size class를 나타낸다. 풀에 대해 szidx 가 0이면 size class 0 의 블록, 즉 8 byte 블록만 포함된다.
  • arenaindex: 풀이 속한 arena 의 인덱스를 나타낸다.
  • 동일한 size class 의 풀은 이중 연결 리스트를 사용하여 서로 연결된다.
  • nextpool: 같은 size class 의 다음 풀을 가리키는 포인터
  • prevpool: 같은 크기 클래스의 이전 풀을 가리키는 포인터

 

동일한 size class 의 풀이 연결되는 방법은 아래 그림과 같다.

 

  • freeblock: 풀 내에서 사용가능한 블록의 단일 연결 리스트의 시작을 가리키는 포인터로, 할당된 블록이 해제되면 freeblock 포인터 앞에 추가된다.

왼쪽 그림에서 볼 수 있듯이, allocated 블록은 객체에 할당된 상태이고, free 블록은 개체에 할당되었지만 이제 새 개체에 할당 가능한 블록이다.

메모리에 대한 요청이 생기면, 요청된 size class 의 블록을 사용할 수 있는 풀이 없는 경우 CPython 은 Arena 에서 새 풀을 만든다고 했다.

그렇다고 해서 새 풀이 만들어지면 전체 풀이 즉시 블록으로 설정되는 것이 아니다. 블록은 필요에 따라 풀에서 만들어질 수 있다.

위 그림에서 풀의 파란색 영역은 해당 부분이 블록으로 아직 만들어지지 않았음을 나타낸다.

 

 

 

 

 

CPython 코드베이스에서 발췌한 내용에 따르면

블록조각:
풀에서 사용 가능한 블록은 풀이 초기화될 때 (블록끼리)함께 연결되지 않는다.
대신 가장 낮은 주소를 가지는, 처음 두개 블록만 설정되어 첫 번째 블록을 반환하고, pool->freeblock 을 두 번째 블록을 포함하는 단일 블록 리스트로 설정한다. 이는 pymalloc 이 실제로 필요할 때까지 메모리 조각을 건드리지 않으려고 아레나, 풀 및 블록 등 모든 수준에서 노력하는 것이다.

 

우리는 아레나에서 새 풀을 만들 때 풀에서 처음 두 블록만 만들어지는 것을 볼 수 있다.

블록 하나는 메모리를 요청한 객체에 할당되고, 다른 블록은 사용 가능하거나 변경되지 않는다. freeblock 포인터가 이 사용되지 않은 블록을 가리킨다.

 

 

 

 

 

 

CPython 은 모든 size class 의 used 상태에서 풀을 추적하기 위해 usedpools 라는 배열을 유지 및 관리한다. 이때 used 상태는 할당에 사용되었지만, 현재는 할당에 사용 가능한 상태를 일컫는다.

usedpools 배열의 인덱스는 풀의 size class 인덱스와 동일하다. usedpools 배열의 각 인덱스 i 에 대해 usedpools[i] 는 size class i 의 풀 헤더를 가리킨다.

예를 들어, usedpools[0] 은 size class 0 의 풀 헤더를 가리키고, usedpools[1] 은 size class 1 의 풀 헤더를 가리킨다.

 

아래 그림을 보면 더 이해하기 쉬울 것 같다:

동일한 size class 의 풀은 이중 연결 리스트를 사용하여 서로 연결되어 있으므로, 각 size class 의 used 상태에 있는 모든 풀은 usedpools 배열을 사용하여 순회할 수 있다.

usedpools[i] == null 이면 used 상태의 size class i 인 풀이 없음을 의미한다. 객체가 size class i 의 블록을 요청하면 CPython 은 size class i 의 새 풀을 만들고 usedpools[i] 가 이 새 풀을 가리키도록 업데이트 한다.

 

 

 

 

 

 

 

 

 

 

 

 

full 상태의 블록이 free 가 되면, 풀 상태는 full 에서 used 상태로 변경된다. CPython 은 이 풀을 size class 풀의 이중 연결 리스트 앞에 추가한다.

그림과 같이, poolX는 size class 0 이며 full 상태이다. 블록이 poolX에서 해제되면, 상태가 full 에서 used 로 변경되는데, 이때 CPython 은 이 풀을 size class 0 의 풀 이중 연결 리스트의 맨 앞에 추가하고, usedpools[0] 이 poolX 를 가리키도록 한다.

 

 

 

 

 

 

 

 

Arenas

아레나는 작은 개체에 메모리를 할당하는 데 사용되는 큰 메모리 블록이다.

이들은 raw memory allocator 에 의해 256KB 크기로 할당된다. 

 

작은 객체가 메모리를 요청하되, 이 요청을 처리할 기존 영역이 없는 경우 raw memory allocator 는 운영 체제에서 큰 메모리 블록 256KB 를 요청한다. 이러한 큰 메모리 블록을 아레나라고 한다. (그리고 풀은 필요할 때 아레나에서 만들어진다.)

 

CPython 코드베이스에 정의된 arena_object 를 살펴보자:

struct arena_object {
    /* The address of the arena, as returned by malloc */ 
    uintptr_t address;

    /* Pool-aligned pointer to the next pool to be carved off. */
    block* pool_address;

    /* The number of available pools in the arena:  free pools + never-
     * allocated pools.
     */
    uint nfreepools;

    /* The total number of pools in the arena, whether or not available. */
    uint ntotalpools;

    /* Singly-linked list of available pools. */
    struct pool_header* freepools;

    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

static struct arena_object* usable_arenas = NULL;
  • freepools: free 풀 목록을 가리키는 포인터이며, free 풀에는 할당된 블록이 없는 상태이다.
  • nfreepools: 아레나의 free 풀 수를 나타낸다.
  • useable_arenas: CPython 은 사용 가능한(empty 또는 used 상태) 풀이 있는 모든 아레나를 추적하기 위해 useable_arena 라는 이중 연결 리스트를 유지 및 관리한다. useable_arenas 리스트는 nfreepools 값의 오름차순으로 정렬된다.
  • nextarena: 다음 사용 가능한 아레나를 가리키는 포인터
  • prevarena: useable_arenas 이중 연결 리스트에서 이전 사용 가능한 아레나를 가리키는 포인터

위 그림에서 볼 수 있듯이, useable_arenas 는 nfreepools 를 기준으로 정렬된다.

0개의 free 풀이 있는 아레나가 첫 번째 항목이고, 그 다음에는 1개의 free 풀이 있는 아레나가 위치하는 방식이다.

이는 리스트가 가장 많이 할당된 아레나부터 먼저 위치함을 의미한다. 그 이유는 다음 섹션에서 설명할 것이다.

 

사용 가능한 아레나 리스트는 어떤 아레나가 가장 많이 할당 되었는지를 기준으로 정렬되므로, 메모리 할당 요청 시 할당이 가장 많은(nfreepools가 가장 적은) 아레나에서 서비스된다.

 

Does a Python Process Release Memory?

풀에 할당된 블록이 해제되면 CPython은 메모리를 운영체제로 다시 반환하지 않는다. 이 메모리는 계속 Python 프로세스에 속하며, CPython 은 이 블록을 사용하여 새 개체에 메모리를 할당한다.

 

풀의 모든 블록이 해제되더라도 CPython 은 풀에서 운영체제로 메모리를 반환하지 않는다. CPython 은 자체 사용을 위해 할당되었던 전체 풀의 메모리를 유지한다.

 

그리고 CPython 은 블록이나 풀 수준이 아닌, 아레나 수준에서 운영체제로 메모리를 해제한다. 이때, 전체 아레나의 메모리를 한 번에 해제함에 유의하자.

 

메모리는 아레나 수준에서만 해제될 수 있으므로, CPython 은 절대적으로 필요한 경우에만 새로운 메모리 조각에서 아레나를 생성한다. 즉, 항상 이전에 만들었던 블록과 풀에서 메모리를 할당하려고 시도한다.

 

이것이 useable_arenas 가 nfreepools 의 내림차순으로 정렬된 이유이다. 

메모리에 대한 다음 요청은 할당량이 가장 많은(=남은 free 풀 수가 가장 적은) 아레나에 할당됨으로써, 최소한의 데이터가 포함된 아레나의 객체가 삭제되면 빈 공간이 될 수 있으며, 이러한 아레나는 할당 해제되어 운영체제로 메모리를 반환할 수 있다.

 

 

 

다음 내용은 Python 가비지 컬렉터에 관련된 내용이다.

운영체제는 컴퓨터 하드웨어 바로 위에 설치되어 사용자 및 다른 모든 소프트웨어와 하드웨어를 연결하는 소프트웨어 계층이다.

그리고 컴퓨터 시스템의 자원을 효율적으로 관리하는데, 이때 자원은 크게 물리적인 자원과 추상적인 자원으로 구분할 수 있다.

  • 물리적인 자원: CPU, 메모리, 디스크, 터미널, 네트워크 등 시스템을 구성하는 요소 및 주변 장치
  • 추상적인 자원:
    • 물리적 자원을 운영체제가 관리하기 위해 추상화 시킨 객체 ex) 태스크(CPU 추상화), 세그먼트와 페이지(메모리 추상화), 파일(디스크 추상화), 통신 프로토콜과 패킷(네트워크 추상화)
    • 물리적인 자원에 대응되지 않으면서 추상적인 객체로만 존재하는 자원 ex) 보안 및 사용자 ID에 따른 접근 제어

운영체제는 일반적으로 커널 뿐만 아니라 각종 주변 시스템 프로그램을 나타내는 개념이지만, 좁은 의미로는 운영체제의 핵심 부분으로 메모리에 상주하는 커널을 의미하기도 한다.

 

커널은 이러한 자원들을 관리하는 여러 관리자들로 구성되어 있다.

CS746G. Kernel Subsystem Overview

  • 태스크 관리자: 태스크의 생성, 실행, 상태 전이, 스케줄링, 시그널 처리, 프로세스 간 통신 등
  • 메모리 관리자: 물리 메모리 관리, 가상 메모리 관리, 그리고 이들을 위한 세그멘테이션, 페이징, 페이지 폴트 처리 등
  • 파일 시스템: 파일의 생성, 접근 제어, inode 관리, 디렉터리 관리, 수퍼 블록 관리 등
  • 네트워크 관리자: 소켓 인터페이스, TCP/IP 와 같은 통신 프로토콜 등의 서비스 제공
  • 디바이스 드라이버: 디스크나 터미널, CD, 네트워크 카드 등과 같은 주변 장치를 구동하는 드라이버들로 구성된다.

 

리눅스 소스와 각 디렉터리의 내용을 조금 더 살펴보자면,

linux kernel source

 

# kernel

커널 핵심 코드를 포함하는 디렉터리로, 태스크 관리자가 구현된 디렉터리이다. 태스크의 생성과 소멸, 프로그램의 실행, 스케줄링, 시그널 처리 등의 기능이 구현되어 있다.

 

# arch

아키텍쳐 종속적인 코드를 포함하는 디렉터리로, CPU 타입에 따라 하위 디렉터리로 다시 구분된다. /arch/${ARCH} 아래에도 여러 디렉터리가 있다.

  • /boot: 시스템 부팅 시 사용하는 부트스트랩 코드 구현
  • /kernel: 태스크 관리자 중에서 문맥 교환이나 스레드 관리 등 구현
  • /mm: 메모리 관리자 중에서 페이지 폴트 처리 등의 하드웨어 종속적인 부분 구현
  • /lib: 커널이 사용하는 라이브러리 함수 구현
  • /math-emu: Floating Point Unit에 대한 에뮬레이터 구현

 

# fs

리눅스에서 지원하는 다양한 파일 시스템 및 파일 시스템 콜이 구현된 디렉터리로, 다양한 파일 시스템을 사용자가 일관된 인터페이스로 접근할 수 있도록 하는 가상 파일 시스템도 이 디렉터리에 존재한다.

 

# mm

메모리 관리자가 구현된 디렉터리로, 물리 메모리 관리, 가상 메모리 관리, 태스크마다 할당되는 메모리 객체 관리 등의 기능이 구현되어 있다.

 

# driver

리눅스에서 지원하는 디바이스 드라이버가 구현된 디렉터리로, 디스크/터미널/네트워크 카드 등 주변 장치를 추상화시키고 관리한다. 

  • 블록 디바이스 드라이버: 파일 시스템을 통해 접근
  • 문자 디바이스 드라이버: 사용자 수준 응용 프로그램이 장치 파일을 통해 직접 접근
  • 네트워크 디바이스 드라이버: TCP/IP를 통해 접근

 

# net

리눅스가 지원하는 통신 프로토콜이 구현된 디렉터리로, 리눅스 커널 소스 중 상당히 많은 양을 차지한다. 

 

# ipc

리눅스 커널이 지원하는 message passing, shared memory, semaphore 등 프로세스 간 통신 기능을 구현한 디렉터리이다.

 

# init

커널 초기화 부분, 즉 커널의 메인 시작함수가 구현된 디렉터리이다.

하드웨어 종속적인 초기화가 /arch/${ARCH}/kernel 아해의 head.s 와 mics.c에서 이루어진 후,

/init 아래에 구현되어 있는 start_kernel() 으로 커널 전역적인 초기화를 수행한다.

 

# include

리눅스 커널이 사용하는 헤더 파일들이 구현된 디렉터리로,

헤더 파일들 중에서 하드웨어 독립적인 부분은 /include/linux 아래에 정의되어 있으며

하드웨어 종속적인 부분은 /include/asm-${ARCH} 디렉터리 아래에 정의되어 있다.

 

# others

  • documentation: 리눅스 커널 및 명령어들에 대한 자세한 문서 파일 등
  • lib: 커널 라이브러리 함수들이 구현
  • scripts: 커널 구성 및 컴파일 시 이용되는 스크립트 정의

 


리눅스 커널은 위 커널 소스들을 gcc로 컴파일 해서 만들 수 있으며, 

리눅스 커널을 만드는 과정은 크게 kernel configuration, kernel compile, kernel installation 세 단계로 이루어진다.

  • kernel configuration: 새로 만들어질 리눅스 커널에게 현재 시스템에 존재하는 하드웨어 특성, 커널 구성 요소 , 네트워크 특성 등의 정보를 알려주는 과정이다. 커널 구성 단계에서 사용자는 모듈 사용 여부와 시스템 정보, 그리고 시스템에 존재하는 디바이스들의 특성 등에 대한 질문에 답하게 된다. 이러한 선택 사항은 /include/linux/autoconf.h 와 .config 파일에 저장된다. 
    • make config
    • make menuconfig
    • make xconfig
    • 커널 구성은 위 명령어 들을 사용해서 할 수 있으며 텍스트 기반 설정인지 gui로 제공되는지 등에 따라 명령어가 나뉜다.
  • kernel compile: 커널 소스파일을 이용하여 실행가능한 커널을 만들게 되는데, 컴파일 과정에서 아까 만들었던 /include/linux/autoconf.h 와 .config 파일이 사용되며, 커널 컴파일이 끝나면 새로운 커널이 /kernel/arch/${ARCH}/boot 디렉터리에 생성된다.
    • make bzImage
    • make zImage
    • 커널 버전 2.6 이후부터는 그냥 `make` 명령어만 사용해도 된다. 
    • bzImage vs zImage? bzImage는 Big zImage를 의미한다. zImage는 최대 크기 512k만 가능한데, 커널이 컴파일 후 512k 크기를 넘어가면 bzImage로 만들어야 한다. 
  • kernel installation: 생성된 커널로 시스템이 부팅될 수 있도록 만드는 과정으로, 생성된 커널 이미지를 루트 파일 시스템으로 복사 후 모듈 설치 및 부트 로더 수정을 통해 이루어진다. 이로써 모듈로 구성된 커널 내부 구성 요소를 알려주고, 이후 그 구성 요소들이 사용될 때 자동으로 커널에 적재될 수 있도록 설정하는 단계이다. 
    • make modules: 커널 환경설정에서 모듈로 설정한 기능들을 컴파일
    • make modules_install: 컴파일 된 모듈을 /lib/modules 아래 설치
    • make install: 커널 설치

 

커널 소스 컴파일 시 파일 생성 단계는 아래 게시글에 잘 설명되어 있다.

http://www.iamroot.org/xe/index.php?mid=Kernel&document_srl=24595

그림의 내용을 간단히 정리하자면

(1, 3, 6) 커널 소스들을 컴파일하여 .o(오브젝트 파일) .a(라이브러리 파일)을 생성하고, 이들을 링킹하여 vmlinux 파일을 생성한다.

(2) vmlinux 파일에서 .note와 .comment 섹션을 삭제하고 재배치 정보와 심볼 정보를 삭제한 뒤 바이너리 포맷의 파일(Image)을 출력한다.

(4) 이를 gzip 압축 알고리즘을 이용하여 piggy.gz를 생성한다.

(5) 여기서 디버깅 정보 등을 삭제하여 piggy.o를 생성한다.

 

(3, 6, 7) 다음으로 커널의 압축을 해제시켜주는 코드 (head.S, misc.c)를 커널 앞부분에 덧붙여 링커를 통해 bvmlinux 또는 vmlinux 파일을 생성한다. 

(8) bvmlinux 또는 vmlinux가 bzImage 또는 zImage가 되는 것이다. 만약 물리 메모리의 1M 위치에 로드될 수 있는 작은 크기의 커널인 경우 zImage 형태로 파일을 생성하며, 그렇지 않은 경우 bzImage 형태로 파일을 생성한다.

 

 

이러한 단계를 지나 새로운 커널을 만든 후, 모듈로 선택한 커널 구성 요소를 컴파일하고 설치하면 새로 만들어진 리눅스 커널을 이용해 시스템을 부팅할 수 있다.

'CS > OS + Linux' 카테고리의 다른 글

[운영체제] 메모리 관리  (0) 2022.07.19

머신 러닝 문제의 복잡도를 평가하는 가장 좋은 방법 중 하나는, 필요한 데이터의 종류와 이런 데이터를 사용할 수 있는 기존 모델이 있는지 살펴보는 것이다.

 

데이터 패턴을 찾을 후 모델을 선택할 때

  • 특성 스케일이 차이나는 경우 ex. 나이(1~100)와 수입(1~1억) 특성의 크기와 관계 없이 가장 예측 성능이 좋은 특성을 활용해야 한다.
    • 방법1) 특성의 scale을 정규화하는 전처리를 수행해 평균이 0이고 단위 분산을 가지도록 한다. = 정규화
    • 방법2) 특성 scale의 차이에 영향을 받지 않는 모델로 바꾼다. = decision tree, random forest, gradient-boosted decision tree
  • 특성의 선형 조합만 사용하여 좋은 예측을 만들 수 있는 경우
    • 회귀 문제에는 linear regression, 분류 문제에는 logistic regression 또는 naive bayes => 간단하고 효율적이며 종종 모델의 가중치를 직접 보고 중요한 특성을 식별할 수 있다.
    • 만약 feature과 target의 관계가 더 복잡한 경우 다층 신경망 등 비선형 모델을 사용하거나 교차특성을 생성하도록 한다
  • 데이터에 시계열 특징이 있는 경우 = 현재 값이 이전 시간의 값에 의존하는 경우
    • 시계열 정보를 명시적으로 인코딩할 수 있는 모델을 사용한다. = ARIMA 모델, RNN 등
  • 각 샘플이 패턴의 조합인 경우 = 이미지 분야, 문장 분류, 음성 인식 등
    • CNN의 경우 translation invarient학습하는 능력 때문에 유용하다. 이미지에서의 위치와 상관없이 패턴 추출 가능

https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html

 

 

데이터 분할은 일반적으로 학습 70% 검증 20% 테스트 10% 사용한다.

데이터 분할 방법은 검증을 위해 매우 중요한 부분이며, 검증 셋과 테스트 셋을 본적 없는 데이터와 비슷하게 만드는 것을 목표로 해야한다.

종종 랜덤하게 샘플링하여 훈련, 검증, 테스트 셋을 나누는데, 경우에 따라 이로 인해 데이터 누수가 일어난다.

데이터 누수는 실전에서 사용자가 사용할 때 얻을 수 없는 정보를 훈련 과정에서 모델이 얻을 때 일어난다.

데이터 누수를 일으키기 쉬운 랜덤 데이터 분할

  • 시계열 데이터 누수: 모델이 과거 데이터에서 학습하여 아직 일어나지 않은 미래 이벤트를 예측하게 되는데, 이런 시계열 데이터를 랜덤하게 나누면 데이터 누수가 발생한다. 모델을 랜덤하게 나눈 데이터에서 훈련하고, 남은 데이터에서 평가하면 예측하려는 이벤트 뒤에 발생한 데이터에 모델이 노출되게 된다. => 검증 셋과 테스트 셋에는 잘 동작하지만, 실전에는 나쁜 성능
    • 실제 세상에서는 불가능한 미래 정보를 사용하여 학습했기 때문
  • 샘플 오염: 데이터 누수의 일반적인 원인은 랜덤한 작업이 일어나는 수준 때문이다. 어려운 작업에서 너무 잘 동작한다면 버그나 데이터 누수가 없는지 확인하도록 해야한다.
    • ex. 학생 수필 점수 예측 프로그램: 동일한 학생의 여러 수필이 데이터셋, 테스트셋에 모두 들어가게 되면 모델이 학생을 식별하는 특성을 감지하여 점수를 예측하게 된다 => 한 학생이 작성한 수필은 모두 비슷한 점수를 받게 된다 => 따라서 각 학생의 데이터가 훈련 셋이나 검증 셋 중 한 곳에만 나타나야 한다.

 

 

과적합을 피하는 방법 중 하나는 regularization이다. = 정보를 표현하는 모델의 능력에 벌칙을 부과하여 관련 없는 많은 패턴에 초점을 맞추는 모델의 능력을 제한하고, 적은 개수의 더 유용한 특성을 사용하게 한다.

  • 가중치의 절댓값을 cost로 부과한다 => ex. 선형 회귀나 로지스틱 회귀 같은 모델에서 L1/L2 regularization은 loss function에 큰 가중치에 대한 cost항을 추가한다
    • L1: 가중치의 절댓값의 합 => 유용하지 않은 특성을 0으로 만들어 정보가 풍부한 특성을 선택하도록 돕는다. 일부 특성에 상관관계가 있을 때 L1이 유용하다. ex) Lasso
    • L2: 가중치의 제곱합
  • drop out: 신경망이 자주 사용하는 regularization으로, 훈련 과정에서 신경망의 뉴런 중 일정 비율을 랜덤하게 무시한다. => 하나의 뉴런이 과도한 영향력을 발휘하지 못하도록
  • random forest와 같은 트리 기반 모델에서 트리의 최대 깊이를 줄이면 각 트리가 데이터에 과대적합되는 능력이 감소된다.
    • random forest에서는 트리 개수를 늘려도 regularization의 효과가 있다

 

 

머신 러닝 애플리케이션의 배포 방식에는 크게 두 가지가 있는데, 배포 방식을 선택 할 때 속도, 하드웨어, 네트워크 요구 사항, 개인 정보 보호, 비용 복잡도 등 여러 요인을 고려해야 한다. 

server-side deployment

  • server-side deployment: 클라이언트로부터 요청을 받아 inference 파이프라인을 실행하여 결과를 반환하는 웹 서버로 구성된다. 서버 측 모델 작업에는 스트리밍과 배치 두가지로 이루어질 수 있다. 호스팅 서버가 필요하여 제품의 인기가 높아지면 비용이 빠르게 증가하고, 호스팅 서버가 애플리케이션의 단일 장애점이 될 수 있다.
    • 스트리밍: 요청을 받자마자 즉시 처리 => 사용자가 개별적인 inference 요청을 보내기 떄문에 동시 사용자 수에 맞추어 선형적으로 인프라를 늘려야 한다. 서버가 처리할 수 있는 용량 이상으로 트래픽이 증가하면 지연/실패하게 된다.
      • 스트리밍 애플리케이션은 속도에 대한 요구 사항이 높을 때 필요하다. 모델에서 필요한 정보가 예측 시점에서 제공되고 모델의 예측이 즉시 필요하다면, 스트리밍 방식이 적합하다.
    • 배치: 한 번에 많은 개수의 요청을 처리 => inference 파이프라인을 여러 샘플에서 동시에 실행할 수 있는 하나의 job으로 간주한다. 배치 잡은 많은 샘플에서 모델을 실행하고 필요할 때 사용할 수 있도록 결과를 저장한다.
      • batch job: 모델의 inference가 필요하기 전에 모델의 필요한 특성을 얻을 수 있을 때 적합하다. (미리 저녁에 돌려놓고 다음날 아침에 확인한다던가)
      • 배치 방식은 스트리밍 방식과 동일한 횟수만큼 추론을 실행한다. 하지만, 자원을 더 효율적으로 사용할 수 있다. => 자원을 할당하고 병렬화 하기가 쉬움
      • 배치 방식은 미리 계산된 결과를 저장하고, inference 시에는 저장한 내용을 추출하기만 하면 되므로 추론 속도가 매우 빠르다 == 캐싱이랑 비슷하다고 생각하면 된다.
    • 가능한 많은 샘플을 계산하고, 추론 시에 사전에 계산된 결과를 추출하도록 하는 하이브리드 방식을 사용하는 것도 가능하다. 만약 사전 계산한 결과가 없거나 오래되었다면 즉시 계산을 수행한다. => 계산할 수 있는 모든 것을 미리 수행하기 때문에 결과를 신속하게 제공한다. 단 배치 파이프라인 + 스트리밍 파이프 라인을 모두 유지하기 위한 비용과 시스템 복잡도가 단점이라고 할 수 있다.

client-side deployment

  • client-side deployment: 클라이언트 측에 모델을 배포하는 목적은 모델 실행 서버가 없더라도 모든 계산을 클라이언트에서 실행하기 위해서이다. 모델은 앱에 포함되어 디바이스에서 사용되거나 웹 브라우저에 로딩될 수 있다. => 모든 사용자에 대한 inference를 실행하기 위해 인프라 구축할 필요가 없다. 또한 디바이스에서 모델 실행 시 디바이스-서버 사이 전송할 데이터 양이 줄어든다. (네트워크 지연을 줄일 수 있다) 또한 inference에 필요한 데이터가 민감한 정보를 담고 있을 때 장치에서 모델을 실행하게 되면 데이터를 원격 서버로 전송할 필요가 없으므로 제 3자가 데이터에 접근할 위험이 줄어든다. 단, 일반적으로 사용자 디바이스는 서버보다 컴퓨팅 성능이 떨어지므로, 디바이스에서 모델을 실행할 수 있도록 모델의 복잡도를 제한해야 한다.
    • on-device: 태블릿이나 핸드폰 프로세서는 일반적으로 머신러닝 모델 실행에 최적화 되어 있지 않아 inference 파이프라인을 느리게 실행한다. 배터리를 많이 소모하지 않고 클라이언트 측 모델을 빠르게 실행하려면 모델이 가능한 한 작아야 한다. => 모델 크기를 줄이려면 간단한 모델을 사용하거나, 모델의 파라미터 개수, 계산 정밀도를 줄여야 한다
      • 스마트폰과 같은 장치에서 실행하기 너무 복잡한 최첨단 모델에 의존하는 경우 서버에 배포되어야 한다. 일반적으로 장치에서 (처리 지연 시간) > (네트워크 지연 시간) 일 경우 클라우드 상에서 모델을 실행하는 것을 고려해봐야 한다.
      • 온 디바이스 배포는 속도, 인프라, 개인 정보에 대한 이득이 커서 엔지니어링 작업에 노력을 투자할만큼 충분한 가치가 있다.
    • browser: 대부분의 스마트 장치는 브라우저를 가진다. 브라우저는 종종 빠른 그래픽 계산을 지원하기 위해 최적화되어 있다. 사용자는 추가 애플리케이션 설치 없이 브라우저를 통해 모델과 상호작용할 수 있으며, 사용자 디바이스에서 계산이 수행된다. 서버 인프라는 모델의 가중치가 포함된 웹 페이지만 사용자에게 제공하면 된다. 단, 대역폭 비용을 늘린다는 단점이 있다. (애플리케이션 설치 방식은 모델을 한 번만 다운로드 하면 되지만, 브라우저 방식을 클라이언트가 웹 페이지를 열때마다 모델을 다운로드 함) => 사용하는 모델이 매우 작거나 빠르게 다운로드 가능하다면 브라우저 방식이 용이하다.
      • ex. tensorflow.js: 대부분의 미분 가능한 모델을 브라우저 상에서 자바스크립트로 훈련하거나 추론을 실행할 수 있다. 다른 언어로 훈련한 모델을 사용할 수도 있음

정리하자면, 서버 측 모델의 경우 가장 큰 latency는 종종 서버로 데이터를 전송할 때 발생한다.(네트워크 지연)

클라이언트 측 모델에서는 시간 지연 없이 실행되지만, 하드웨어 성능 제약으로 서버보다 느리게 샘플을 처리하게 된다. (처리 지연)

 

 

클라이언트 상에 배포 뿐만 아니라 훈련도 할 수 있다. 이는 특히 사용자마다 다른 모델을 원할 경우 유용하다.

이 아이디어가 federated learning(연합 학습) 의 핵심이다. 연합 학습에서는 클라이언트가 각자의 모델을 가지고, 각 모델은 사용자의 데이터에서 학습 및 요약되어 업데이트를 서버로 보낸다. 서버는 모든 업데이트를 사용해 모델을 향상시키고 새로운 모델의 업데이트를 개별 클라이언트에 전달한다. => 각 사용자는 각자의 요구에 맞는 개인화 모델을 얻을 수 있고 & 여전히 다른 사용자로부터 집계된 정보에서 도움 받을 수 있다.

연합 학습에서는 사용자 데이터를 서버로 바로 전송하지 않고, 집계된 모델 업데이트 값만 전송하기 때문에 개인 정보가 더욱 보호될 수 있다. 단, 개별 모델이 잘 동작하면서 서버로 전송된 데이터를 적절하게 익명화시키는 것은 단일 모델을 훈련하는 것보다 복잡성이 증가한다.

 

 

머신 러닝 모델을 제품으로 배포할 때 성능과 트래픽을 관리하는 것은 매우 어렵다.

머신러닝은 늘어난 트래픽을 처리하기 위해 캐싱을 사용할 수 있다. 미래에 동일한 매개변수로 함수를 호출한다면, 저장된 결과를 추출하면 되므로 함수를 빠르게 실행할 수 있다.

  • 추론 결과 캐싱하기: LRU 캐시는 가장 간단한 캐싱 방법이다. 가장 최근 사용된 모델 입력과 해당 결과를 추적하여 캐시에 유지한다. 새로운 입력에서 모델을 실행하기 전 캐시에서 이 입력을 조회하고 해당 항목을 찾으면 캐시에서 결과를 바로 제공한다.  -> 사용자가 동일한 종류의 입력을 제공하는 애플리케이션에 적합하다.
    • 캐시 확인 후 캐시에 이미지 없음을 확인 => 모델 실행(병목 지점) => 캐시에 추가 => 결과 반환
    • 캐시 확인 후 캐시에 이미지 있음을 확인 => 결과 반환
    • 주의) 캐싱을 사용할 때 부수 효과가 없는 함수만 캐싱해야 한다. ex) 결과 반환 및 데이터 저장하는 함수는 캐싱하면 데이터베이스에 값을 저장하지 않게 됨
  • 인덱스 캐싱: ex. 검색 시스템을 만들 때 일반적인 접근 방법은 먼저 모든 인덱싱 문서를 의미있는 벡터로 임베딩 -> 사용자가 검색 쿼리를 입력하면 추론 시 임베딩 벡터로 변환되어 데이터베이스에서 가장 비슷한 임베딩을 찾는다. => 이런 방싱은 대부분 계산을 미리 해 놓으므로 추론 속도를 크게 높여준다.
    • 주의) 캐싱 성능을 향상시킬 수 있지만 복잡도가 증가한다. 모델이나 내부 데이터가 업데이트 된 경우 캐시를 삭제해야 하며, 캐시 크기도 조정하며 최적의 값을 찾아야 한다.

 

 

 

 

*** 용어정리

  • 분류: 두 개 이상의 카테고리로 데이터를 분류
  • 회귀: 연속적인 값을 예측
  • 지식 추출: 큰 모델(Teacher network)에서 추출한 지식을 작은 모델(Student network)로 transfer 하는 과정
  • 카탈로그 구성: 데이터 메타데이터 관리 서비스 
  • 생성 모델: 주어진 데이터를 학습하여 데이터 분포를 따르는 유사 데이터를 생성하는 모델 ex. GAN
  • 시계열: 미래 사건을 예측하기 위해 여러 개의 과거 데이터 포인트를 사용

 

후기

이 책은 머신러닝 전반적인 내용 뿐만 아니라 실제 머신러닝 기반 애플리케이션을 만들 때 전체 파이프라인에 대해 배울 수 있다.

 

그리고 데이터의 중요성을 끊임없이 말하고 있으며 데이터셋 자체를 효과적으로 조사하는 팁들이 있다. 데이터를 수집할 때에는 대표적이고 다양성을 가진 데이터 셋을 준비해야 하며, 데이터를 살펴보고 특정 종류의 데이터가 표현되지 않으면 더 많은 데이터를 모아야 한다. 이 과정에서 데이터 셋에 클러스터링 알고리즘을 적용해 보고 이상치를 살펴보면 좋다고 한다. 

 

머신러닝에 대해 전혀 모르는 내가 처음 들어보는 내용들도 많았는데,

모델에서 특성 중요도를 평가할 때 도움이 되는 방법 중 하나인 `블랙박스 설명 도구`라는 것이 있는데, 이는 내부 동작 방식과 상관 없이 모델의 예측을 설명할 수 있다고 한다.

https://github.com/rickiepark/ml-powered-applications/blob/main/notebooks/black_box_explainer.ipynb

 

GitHub - rickiepark/ml-powered-applications: <머신러닝 파워드 애플리케이션>의 코드 저장소입니다. 원서

<머신러닝 파워드 애플리케이션>의 코드 저장소입니다. 원서 깃허브는 https://github.com/hundredblocks/ml-powered-applications 입니다. - GitHub - rickiepark/ml-powered-applications: <머신러닝 파워드 애플리케이션>의

github.com

이러한 도구의 예시로 LIME과 SHAP이 있다.

 

마지막으로 전문가 인터뷰와 모델 서빙, 모니터링쪽 내용이 많은 도움이 된다.

 

 

컴파일러의 목적은 한 언어를 다른 언어로 변환하는 것이다. 그리고 어떤 컴파일러는 시스템에서 바로 실행할 수 있는 저수준 기계어로 컴파일하지만, 어떤 컴파일러는 가상 머신에서 실행하기 위한 중간 언어로 컴파일한다.

  • 닷넷 CLR, 자바 등은 여러 시스템 아키텍쳐에서 사용할 수 있는 중간 언어로 컴파일한다.
  • C, Go, C++, 파스칼 등은 바이너리 실행 파일로 컴파일한다. 바이너리는 컴파일한 플랫폼과 동일한 플랫폼에서만 사용할 수 있다.

파이썬 애플리케이션은 보통 소스코드 형태로 배포한다. 파이썬 인터프리터는 소스 코드를 변환 후 한 줄씩 실행한다.

CPython 런타임이 첫 번째 실행될 때 코드를 컴파일 하지만, 이 단계는 사용자에게 노출되지 않는다.

 

python interpreter 내부 동작 과정

 

파이썬 코드는 기계어 대신 `바이트코드` 라는 저수준 중간 언어로 컴파일된다.

바이트코드는 .pyc 파일에 저장되고, 실행을 위해 캐싱된다.

코드를 변경하지 않고 같은 파이썬 애플리케이션을 다시 실행하면 매번 다시 컴파일하지 않고 컴파일 된 바이트코드를 불러와서 빠르게 실행할 수 있다.

 

 

 

CPython 컴파일러는 왜 C로 작성되는가?

윈도우와 리눅스 커널 api는 모두 C로 작성되었기 때문에, 여러 표준 라이브러리모듈이 저수준 OS api에 접근하기 위해 C로 작성되었다. 

 

컴파일러는 크게 두 가지 유형이 있는데,

  1. 셀프 호스팅 컴파일러는 Go 컴파일러처럼 자기 자신으로 작성한 컴파일러다. 셀프 호스팅 컴파일러는 부스트래핑이라는 단계를 통해 만들어진다.
  2. Source to Source 컴파일러는 컴파일러를 이미 가지고 있는 다른 언어로 작성한 컴파일러이다.

c.f. PyPy라는 파이썬으로 작성된 파이썬 컴파일러도 있다.

 

 

 

메모리 관리

먼저 C에서의 메모리 할당에 대해 알아보자면, 

  • 정적 메모리 할당: 필요한 메모리는 컴파일 시간에 계산되고, 실행 파일이 실행될 때 할당된다.
    • C에서는 타입 크기가 고정되어 있으며, 컴파일러는 모든 정적/전역 변수에 필요한 메모리 계산 후 필요한 메모리 양을 애플리케이션에 컴파일해 넣는다. 메모리 할당 시에는 시스템 콜을 이용한다. 
  • 자동 메모리 할당: 스코프에 필요한 메모리는 프레임에 진입 시 콜스택 내 할당되고, 프레임이 끝나면 해제된다.
    • 마찬가지로 컴파일 시간에 필요한 메모리를 계산한다.
#include <stdio.h>
static const double five_ninths = 5.0/9.0; // const: 정적 할당

double celsius(double fahrenheit){
	double c = (fahrenheit - 32) * five_ninths; // 함수 내 변수는 자동 할당
    return c;
}

int main(){
	double f = 100; // 함수 내 변수는 자동 할당
    printf("%f F is %f C\n", f, celsius(f)); // celsius(f) 반환값은 자동 할당
    return 0;
}
  • 동적 메모리 할당: 메모리 할당 api를 호출하여 런타임에 메모리를 동적으로 요청하고 할당한다
    • 사용자 입력에 따라 필요한 메모리가 결정되는 경우
    • OS는 프로세스가 메모리를 동적 할당 할 수 있도록 시스템 메모리의 일부를 예약해 두는데, 이 공간을 heap 이라 한다.
    • 동적 할당 메모리는 제대로 반환하지 않으면 메모리 누수가 발생한다.

 

 

C 기반인 CPython은 C 메모리 할당 방식들의 제약조건을 따르기 쉽지 않다. 왜냐하면

  1. 파이썬은 동적 타입 언어로, 변수의 크기를 컴파일 시간에 계산할 수 없다.
  2. 코어 타입의 크기는 대부분 동적이다. 리스트 타입은 크기를 정할 수 없고, 딕셔너리 타입은 제한없이 키를 가질 수 있다.
  3. 값의 타입이 달라도 같은 변수명을 재사용할 수 있다.

따라서 C 메모리 할당 방식의 제약을 극복하기 위해 CPython은 동적 메모리 할당에 크게 의존하며 `참조 카운팅`과 `가비지 컬렉션`을 사용하여 메모리를 자동 해제하도록 했다. Python의 객체 메모리는 개발자가 직접 할당하는 대신, 하나의 통합 api를 통해 자동으로 할당된다.

CPython은 세가지 동적 메모리 할당자 도메인을 제공한다.

  • 저수준 도메인: 시스템 힙이나 대용량 메모리 또는 비객체 메모리 할당 시 사용된다.
  • 객체 도메인: 파이썬 객체와 관련된 메모리 할당 시 사용된다.
  • PyMem 도메인: PYMEM_DOMAIN_OBJ 와 동일하며, 레거시 api 용도로 제공된다.

또한 CPython은 두가지 메모리 할당자를 사용한다.

  • malloc: 저수준 메모리 도메인을 위한 OS 할당자
  • pymalloc: PyMem 도메인과 객체 메모리 도메인을 위한 CPython 할당자
    • 기본적으로 CPython에 같이 컴파일 된다. pyconfig.h에서 WITH_PYMALLOC=0으로 설정 후 파이썬을 컴파일하면 pymalloc을 제거할 수 있으며, 이 경우 PyMeM과 객체 메모리 도메인 api도 시스템 할당자를 사용한다.

 

CPython 메모리 할당자에 대해 알아보기 전에 먼저 Python 의 메모리 구조 레벨에 대해 알아보자.

  • Arenas
    • 힙 메모리에 할당 된 고정된 크기의 256KiB, 가장 큰 메모리 단위를 나타낸다.
      • 아레나는 익명 메모리 매핑을 지원하는 시스템에서는 mmap()으로 할당한다.
    • 파이썬 할당자 pymalloc에서 사용하는 메모리 매핑이다.
    • 시스템 페이지 크기에 맞추어 정렬하며, 시스템 페이지 경계는 고정 길이의 연속 메모리 청크다.
  • Pools
    • arena 당 pool 개수는 64개로 고정되어 있다.
    • pool의 크기는 OS의 default 메모리 페이지 사이즈와 일치해야 한다.
    • 고정된 크기의 4KB 크기를 가지며, 세 가지 상태를 가질 수 있다.
      • Empty; pool이 비어있으므로 할당할 수 있다.
      • Used; pool에 비어있지도 가득차지도 않게 개체가 포함되어 있다.
      • Full; pool이 가득차서 더 이상 할당할 수 없다.
    • pool은 요청이 들어오면 할당되며, 요청된 크기 인덱스에 맞는 사용 가능한 pool이 없는 경우 새로운 pool을 생성한다.
  • Blocks
    • 블록의 크기는 고정되어 있지 않으며, 블록 사이즈는 8 ~ 512 bytes 인 8의 배수이다.
    • 각 블록은 특정 크기의 파이썬 객체를 하나만 저장할 수 있으며, 세 가지 상태를 가질 수 있다.
      • Untouched; 할당되지 않음
      • Free; 할당 되었지만, 해제되어 할당에 사용할 수 있다.
      • Allocated; 할당됨
    • 아래 그림과 같이 사용되지 않은 블록은 풀 내부의 freeblock 단일 연결 리스트에 연결된다. 할당 해제된 블록은 freeblock 리스트의 맨 앞에 추가되며, 풀이 초기화 되면 첫 두 블록은 freeblock 리스트에 연결된다.

할당된 블록과 해제된 블록, 사용되지 않은 블록들을 포함한 사용중인 풀

python 디버그 api `debugmallocstats`를 사용하면 할당된 아레나의 개수와 사용된 블록의 총 개수를 알 수 있다.

크기 인덱스 테이블과 할당 상태를 비롯한 다양한 통계 정보가 출력된다.

더보기

Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys._debugmallocstats()
Small block threshold = 512, in 32 size classes.

class   size   num pools   blocks in use  avail blocks
-----   ----   ---------   -------------  ------------
    0     16           1             128           893
    1     32           3            1143           387
    2     48          10            3147           253
    3     64          47           11794           191
    4     80          33            6697            35
    5     96           7            1102            88
    6    112           5             595           130
    7    128           3             370            11
    8    144          15            1641            54
    9    160           2             150            54
   10    176          27            2451            33
   11    192           2              96            74
   12    208           1              73             5
   13    224           3             206            10
   14    240           1              57            11
   15    256           1              52            11
   16    272           1              46            14
   17    288           1              34            22
   18    304           5             232            33
   19    320           1              31            20
   20    336           1              20            28
   21    352           1              18            28
   22    368           1              23            21
   23    384           1              17            25
   24    400           2              42            38
   25    416           2              71             7
   26    432           2              54            20
   27    448           2              48            24
   28    464           2              43            27
   29    480           2              34            34
   30    496           1              29             3
   31    512           2              48            14

# arenas allocated total           =                    3
# arenas reclaimed                 =                    0
# arenas highwater mark            =                    3
# arenas allocated current         =                    3
3 arenas * 1048576 bytes/arena     =            3,145,728

# bytes in allocated blocks        =            2,795,680
# bytes in available blocks        =              261,600
2 unused pools * 16384 bytes       =               32,768
# bytes lost to pool headers       =                9,024
# bytes lost to quantization       =               13,888
# bytes lost to arena alignment    =               32,768
Total                              =            3,145,728

arena map counts
# arena map mid nodes              =                    1
# arena map bot nodes              =                    1

# bytes lost to arena map root     =              262,144
# bytes lost to arena map mid      =              262,144
# bytes lost to arena map bot      =              131,072
Total                              =              655,360

            8 free PyDictObjects * 48 bytes each =                  384
           4 free PyFloatObjects * 24 bytes each =                   96
          6 free PyFrameObjects * 360 bytes each =                2,160
           21 free PyListObjects * 40 bytes each =                  840
   2 free 1-sized PyTupleObjects * 32 bytes each =                   64
  16 free 2-sized PyTupleObjects * 40 bytes each =                  640
   5 free 3-sized PyTupleObjects * 48 bytes each =                  240
   2 free 4-sized PyTupleObjects * 56 bytes each =                  112
   2 free 5-sized PyTupleObjects * 64 bytes each =                  128
   3 free 6-sized PyTupleObjects * 72 bytes each =                  216
   3 free 7-sized PyTupleObjects * 80 bytes each =                  240
   3 free 8-sized PyTupleObjects * 88 bytes each =                  264
   0 free 9-sized PyTupleObjects * 96 bytes each =                    0
 0 free 10-sized PyTupleObjects * 104 bytes each =                    0
 3 free 11-sized PyTupleObjects * 112 bytes each =                  336
 0 free 12-sized PyTupleObjects * 120 bytes each =                    0
 3 free 13-sized PyTupleObjects * 128 bytes each =                  384
 2 free 14-sized PyTupleObjects * 136 bytes each =                  272
 2 free 15-sized PyTupleObjects * 144 bytes each =                  288
 0 free 16-sized PyTupleObjects * 152 bytes each =                    0
 1 free 17-sized PyTupleObjects * 160 bytes each =                  160
 0 free 18-sized PyTupleObjects * 168 bytes each =                    0
 0 free 19-sized PyTupleObjects * 176 bytes each =                    0

 

또는 표준 라이브러리 `tracemalloc` 모듈로 객체 할당자의 메모리 할당 동작을 디버깅할 수도 있다. 이 모듈은 객체 할당 위치나 할당된 메모리 블록의 개수 등에 대한 정보를 제공한다. 메모리 추적을 활성화 하려면 파이썬 실행 시 -X tracemalloc=1 옵션을 추가하면 된다. 여기서 1은 추적할 프레임 수이며, 더 늘릴수도 있다.

 

 

CPython 할당자에 대해 다시 돌아가보자면, 시스템 메모리 할당자 위해 구축된 할당자로 독자적인 할당 알고리즘을 구현한다. 이 알고리즘은 시스템 할당자와 비슷하지만 CPython에 특화되어 있다.

참고💡 대부분의 메모리 할당 요청은 고정된 크기의 작은 메모리를 요구한다.
- PyObject는 16byte, PyASCIIObject는 42byte, PyCompactUnicodeObject는 72byte, PyLongObject는 32byte 크기
- pymalloc 할당자로는 메모리 블록을 최대 256kb까지 할당할 수 있고, 그보다 큰 할당 요청은 시스템 할당자로 처리한다.

그리고 중앙 레지스터가 블록 위치와 사용할 수 있는 블록 개수를 관리한다. 한 풀이 모두 사용되면 다음 풀이 사용된다.

이러한 방식은 CPython에서 주로 만들어지는 작고 수명이 짧은 객체를 할당할 때 적합하다. 그리고 힙 할당 대신 메모리 맵(mmap())을 사용하게 된다.

 

이렇게 CPython은 C의 동적 메모리 할당 시스템 위에 구축되었으며, 메모리 요구 사항은 런타임에 결정되고 PyMem api를 사용하여 메모리를 시스템에 할당한다고 했다. 파이썬은 메모리 관리를 단순화 하기 위해 `객체 메모리 관리 전략`을 도입했다. 여기에는 참조 카운팅과 가비지 컬렉션이 있다.

 

 

파이썬 프로세스 내부 구조

컴퓨터의 CPU는 프로세스를 실행할 때 다음과 같은 추가 데이터가 필요하다.

  • 실행 중인 명령이나 명령을 실행하는 데 필요한 다른 데이터를 보관하는 레지스터
  • 프로그램 시퀀스의 어떤 명령을 실행 중인지 저장하는 프로그램 카운터(명령 포인터)

CPython 프로세스는 컴파일된 CPython 인터프리터+모듈로 구성된다.

모듈을 런타임에 불러들여서 CPython 평가 루프를 통해 명령으로 변환한다.

프로그램 레지스터와 프로그램 카운터는 프로세스에 저장된 명령 중 한 명령만을 가리킨다.

즉, 한번에 하나의 명령만 실행할 수 있다. = CPython은 한 번에 하나의 파이썬 바이트 코드 명령만 실행할 수 있다.

c.f. 따라서 프로세스의 명령을 병렬로 실행하려면 다른 프로세스를 포크하거나 스레드를 스폰하는 방법을 사용한다.

 

 

 

 

후기

이 책에는 CPython 컴파일러가 어떻게 동작하는지, 파이썬 코드를 어떻게 파싱하는지에 관한 설명이 8장까지 매우매우 자세하게 나와있고,

9장 이후 파이썬에서 메모리 관리가 어떻게 구현되는지 나와있다. 9장 이후의 내용이 굉장히 많이 도움이 되었다.

 

이책을 읽다가 새롭게 알게된 사실은 python의 long과 c의 long이 다르다는 점이었다...!!

python의 long은 숫자 리스트로, 12378562834를 [1,2,3,7,8,5,6,2,8,3,4]로 표현한다고 한다.

Python 정수 표현 시 메모리 사용량 변화 - https://mortada.net/can-integer-operations-overflow-in-python.html

(그치만 numpy/pandas 같은 패키지를 사용할 때는 C 스타일이 유지되기 때문에 오버플로우 발생을 고려해야 한다.)

 

그리고 파이썬에서 가비지 컬렉터가 카운터=0일때 회수된다는것은 여러 곳에서 찾아볼 수 있었지만

어느 주기로 실행되는지는 잘 나와있지 않아 그동안 궁금했는데, 

가비지 컬렉션은 정해진 양만큼의 연산이 일어났을 때 주기적으로 실행된다는 것을 알게 되었다. 

 

이 책에는 CPython을 위해 설계된 다양한 도구를 이용하는 벤치마킹, 프로파일링, 실행 추적 등이 자세하게 나와있어서 쉽게 따라할 수 있었다.

 

 

참고:

https://scoutapm.com/blog/python-memory-management

+ Recent posts