[Python] Garbage Collection Tuning
Python에서 수명이 다한 객체는 어떻게 메모리가 회수되나요?
Python에서 수명이 다한 객체는 Reference Counting을 통해 메모리가 회수됩니다. 모든 객체마다 자신이 참조되고 있는 개수를 들고 있다가 이 숫자가 0이 되면 메모리에서 삭제하는 방식 입니다.
Cyclic Garbage Collection 에 대해 설명해보세요
linked list 객체와 같이 순환 참조(reference cycle)이 있는 객체에 대해서는 Reference Counting 만으로 객체를 제거할 수 없기 때문에 cyclic garbage collection을 사용합니다. 모든 객체에 대해 reference를 graph를 그리며, 접근 불가능한 cycle을 찾아 garbage에 해당하는 객체를 찾는 전수 조사 방식을 의미합니다.
Python에서는 느린 GC 속도에 대응하기 위해 어떠한 최적화 기법을 사용하나요?
“Generation”이라는 최적화 기법을 사용합니다. GC를 수행할 때마다 모든 객체에 대해 전수조사하는 대신, 특정 Generation에 해당하는 객체들에 대해서만 Scan을 수행하는 방식을 의미합니다. 수명이 오래된 객체는 적은 빈도로 검사하고, 수명이 얼마 안된 객체는 많은 빈도로 검사하여 시간을 절약합니다.
GC 주기
>>> import gc
>>> gc.get_threshold()
(700, 10, 10)
- threshold0 : (객체 생성 수 - 객체 해제 수)가 700을 넘으면 0세대 GC 호출된다는 의미
- threshold1 : 0세대 GC가 10번 호출되면, 1세대 GC를 호출한다는 의미
- threshold2 : 1세대 GC가 10번 호출되면, 2세대 GC를 호출한다는 의미, 추가로 long_lived_pending / long_lived_total 이 25%를 넘을 경우에만 수행 가능 (gen2 GC가 너무 오래걸리기 때문에)
700개 넘게 생성되어있는데 객체가 아직 남아있어? 0세대 (가장 어린) GC 호출해!
0세대 GC가 10번이나 호출됐어? 1세대 GC 호출해!
1세대 GC가 10번 호출됐고, 오래도록 살아있는 객체 가운데 25퍼센트 이상이 살아남아있어? 2세대 GC 호출해!
GC disable 비교 실험
환경: Python 3 Google Compute Engine 백엔드 (Colab)
import time
# 사용하지 않는 import
import numpy as np
import torch
# 2세대 gc 사용 안함
# import gc
# gc.set_threshold(700, 0, 99999999)
class DummyClass:
def __init__(self):
self.foo = [[] for _ in range(4)]
def generate_objects() -> None:
bar = [DummyClass() for _ in range(1000)]
times = []
for _ in range(500):
start_time = time.time()
generate_objects()
times.append(time.time() - start_time)
avg_elapsed_time = sum(times) / len(times) * 1000
max_elapsed_time = max(times) * 1000
print(f"avg time: {avg_elapsed_time:.2f}ms, max time: {max_elapsed_time:.2f}ms")
avg time | max time | |
Base | 12.54ms | 367.95ms |
gc.disable() 추가 | 3.05ms | 31.70ms |
2세대 GC 사용 안함 | 5.04ms | 40.75ms |
사용하지 않는 import 제거 | 6.49ms | 149.15ms |
사용하지 않는 Pytorch의 Numpy를 import 하지 않을 때, 속도가 빨라진 이유?
Pytorch 및 Numpy가 상당한 양의 객체를 생성하고, 이들이 해제되지 않은 상태로 상주하여, Old generation 객체가 되어있는데, Gen2 GC가 불릴 때마다 Pytorch 및 Numpy에서 생성한 객체까지 scan하느라 시간이 더 걸렸다고 볼 수 있습니다.
ML과 GC 오버헤드가 서로 밀접한 관련이 있는 이유?
ML에서는 Pytorch, Numpy 같은 무거운 라이브러리들을 사용할 가능성이 높기 때문에, GC 오버헤드가 더 많이 발생할 수 있습니다.
GC를 튜닝하는 방법?
하이퍼커넥트에선 GC 튜닝을 통해 P99 Latency를 최대 1/3 수준까지 줄였다고 합니다.
- 2세대 GC의 threshold를 높혀서 실행 빈도를 낮게 설정
- 어플리케이션 start-up 이후 gc.freeze()를 한 번 호출하여, 라이브러리에서 생성한 Old generation 객체들에 대해서는 2세대 GC에서 scan하지 않도록 변경
- 지속적으로 요청이 들어오는 패턴이 아닌 주기적(ex. 100ms마다)으로 요청이 들어오는 어플리케이션에선, GC 호출 시점과 API 요청 시점이 겹치지 않도록 설정 (수동으로 GC 트리거 시점 조정)