AboutPython

[Python] Garbage Collection Tuning

scone 2024. 4. 7. 17:23

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 수준까지 줄였다고 합니다.

  1. 2세대 GC의 threshold를 높혀서 실행 빈도를 낮게 설정
  2. 어플리케이션 start-up 이후 gc.freeze()를 한 번 호출하여, 라이브러리에서 생성한 Old generation 객체들에 대해서는 2세대 GC에서 scan하지 않도록 변경
  3. 지속적으로 요청이 들어오는 패턴이 아닌 주기적(ex. 100ms마다)으로 요청이 들어오는 어플리케이션에선, GC 호출 시점과 API 요청 시점이 겹치지 않도록 설정 (수동으로 GC 트리거 시점 조정)

 

 

출처 : 고성능 ML 백엔드를 위한 Python 성능 최적화 팁 1번 항목