Search

메모리 관리 비교 - python vs JVM

가. GIL in CPython

1) GIL이란

GIL(Global Interpreter Lock): 멀티 쓰레드 환경에서 하나의 쓰레드만 인터프리터의 자원을 독점 사용하는 제약
→ GIL은 자원점유 측면에서 OS의 mutex(or lock)의 기능과 유사하다
GIL은 C 언어로 개발된 Cpython 구현체의 인터프리터에만 존재함
→ C 외 언어로 구현된 Python 구현체의 인터프러터에는 GIL 방식을 채택하지 않음
용어정리: CPU bound, I/O bound, CPU burst, I/O burst
→ CPU bound: 프로그램 성능이 CPU burst에 의해 묶인(제한받는) 것
→ I/O bound: 프로그램 성능이 I/O burst에 의해 묶인(제한받는) 것
→ CPU burst: cpu 연산이 연속되는 것
→ I/O burst: 네트워크나 Disk Drive에서 I/O waiting이 연속되는 것
→ burst: 한 번에 연산(또는 전송)되는 데이터 블럭

2) GIL 도입 이유

CPython의 메모리 관리방식(reference counting)이 thread-safe하지 않기 때문에 이를 보완하기 위한 방법으로 GIL이 도입되었음
→ 멀티 쓰레드 환경에서 참조 카운팅(reference counting)의 메모리 관리 방식에 따른 부작용을 제거하기 위한 솔루션
→ 예를 들어, a라는 객체 인스턴스를 참조한 경우가 2번이라면 참조 카운트는 2가 됨. 두 개의 쓰레드가 동시에 관련 참조를 해제하여 다른 쓰레드의 참조해제를 카운트에 반영하지 못하여 참조 카운트가 1이 된다면 객체 참조와 관련된 다양한 이슈가 발생할 수 있음.

3) GIL context switching

in common: 특정 쓰레드의 I/O 작업이 끝이나면 GIL을 다른 쓰레드로 넘겨줌
in Python2 way: 일련의 코드 실행 후 GIL을 넘겨주는 방식 추가
→ 추가 설명: 100 byte-code의 instruction 후에 다른 쓰레드에게 GIL을 점유할 기회 전달
→ 배경: CPU Bound 작업의 경우 I/O 작업이 거의 없기 때문에 특정 쓰레드가 매우 긴 시간 동안 대기상태로 남겨지는 경우에 대한 솔루션 필요
in Python3 way: 시간을 주기로 GIL을 다른 쓰레드로 넘겨주는 방식 추가
→ 추가 설명: 5ms를 기준으로 다른 쓰레드에게 GIL을 점유할 기회 전달
→ 배경: byte-code instruction에 따라 실행 시간이 매우 다르기 때문에 시간적인 면에서 비효과적으로 GIL 점유하게 됨

3) GIL 성능 이슈

CPU Bound Process 성능 이슈
→ Multi CPU core 환경에서 여러 프로세스가 존재하지만 오직 하나의 쓰레드에게만 자원 점유를 허용함

5) GIL 성능 이슈 극복 방법

CPU Bound Process 성능 이슈
→ MultiProcessing 라이브러리 활용
→ Celery와 브로커를 조합한 분산 비동기 기반의 메시지 큐 방식 활용
→ CPython 외의 Python 구현체를 사용하는 인터프리터 사용
참고) CPython 계열의 라이브러리 사용 불가(에를 들어, Numpy 등)

4) GIL 유지하는 이유

CPython 기반 API가 reference counting 방식에 의존되어 있으므로 GIL을 제거하는데 어려움이 있음
→ CPython은 C 언어의 API에 의존하고 있음. 해당 API의 주요 로직은 reference counting 방식 설계 원칙에 의존되어 있음.
싱글쓰레드 기반의 작업에 있어, GIL 제거 후 보다 제거 전의 성능이 뛰어남
멀티쓰레드 기반으로 작업 시 쓰레드 간 자원공유 실패를 걱정하지 않아도 됨

6) AI 라이브러리는 GIL에 영향을 받지 않나?

검증 필요
GIL은 CPU 자원 스위칭에 이슈가 있지만 GPU에는 관련 이슈가 없음
AI 라이브러리는 주로 GPU 자원에 의존한 작업이므로 GIL 이슈는 큰 제약사항이 되지 않음

나. Mark and Sweep vs Reference Counting

1) Mark and Sweep

동작원리: 루트 객체를 시작으로 참조하고 있는 모든 객체를 순회하며 표시(Marking), 힙 영역의 객체 중 Marking 되어 있지 않은 객체는 제거(Sweep)
→ Marking하는 시점에서 모든 쓰레드의 동작을 중지시킴(it is called ‘stop the world’)
장점: 쓰레드의 동시접근을 허용하므로 멀티쓰레딩 동작에 따른 성능 향상 가능
단점: 객체 제거 전까지 불필요한 객체가 메모리에 남아있기 때문에 메모리 낭비 이슈 존재
Mark and Sweep in Python: 메모리 관리 방식에서 보조로 사용함. 주 방식은 Reference Counting
Mark and Sweep in Java, Node: 메모리 관리 방식에서 주 수단으로 사용함

2) Reference Counting

동작원리: 객체별로 참조에 횟수를 저장함, 참조횟수가 0이된다면 메모리에서 제거하는 방식
장점: 불필요 객체가 즉시 제거됨으로써 메모리를 효율적으로 사용할 수 있음
→ 메모리에 큰 제약사항이 있는 경우 유리
단점: 한 시점에 오직 하나의 쓰레드만 자원을 점유할 수 있으므로 멀티쓰레딩 환경에서 성능 이슈 발생

3) Python 메모리 관리 방식

Python은 주로 Reference Counting 방식으로 메모리를 관리하지만 Mark and Sweep 방식을 보조 방식으로 사용함
순환참조의 경우, Reference Counting 방식으로 메모리 누수를 방지할 수 없으므로 Mark and Sweep 방식으로 보조가 필요함
→ 순환참조가 발생할 수 있는 경우: 자기 자신 참조, 서로 참조 등이 있음
# 자기 참조 >>> l = [] >>> l.append(l) >>> del l # 서로 참조 >>> a = Foo() # 0x60 >>> b = Foo() # 0xa8 >>> a.x = b # 0x60의 x는 0xa8를 가리킨다. >>> b.x = a # 0xa8의 x는 0x60를 가리킨다. # 이 시점에서 0x60의 레퍼런스 카운터는 a와 b.x로 2 # 0xa8의 레퍼런스 카운터는 b와 a.x로 2다. >>> del a # 0x60은 1로 감소한다. 0xa8은 b와 0x60.x로 2다. >>> del b # 0xa8도 1로 감소한다.
Python
복사
code from https://blog.winterjung.dev/2018/02/18/python-gc

4) 객체의 메모리 효율적 관리 방법

slot 활용에 따른 메모리 절약: 인스턴스 객체의 속성의 키 값만 list로 관리하여 메모리 효율을 높일 수 있음
→ 인스턴스 객체의 속성값은 기본적으로 키와 값을 모두 포함한 dict로 관리됨
→ 속성값의 키만을 list로 관리하면 메모리를 절약할 수 있음
→ 리팩토링 시 객체에 대해 __dict__ 메소드를 활용이 필요하지 않다면 dict 대신 slot의 형태로 관리하는 방법 권장
slot 적용: 클래스 객체의 상단에 __slots__ 정의
class Human: __slots__ = ["name", "weight"] def __init__(self, name, weight): self.name = name self.weight = weight mj = Human("MJ", 12) print(mj.__slots__) # ['name', 'weight'] print(mj.__dict__) # AttributeError: 'Human' object has no attribute '__dict__'
Python
복사

Reference