iBetter Books
수정

성능 프로파일링과 셀 최적화

반응형 노트북이 체감상 느리다면, 원인은 보통 두 가지 중 하나입니다. 실행 시간이 오래 걸리는 특정 셀이 자주 재실행되거나, 불필요하게 큰 데이터를 셀 간에 주고받는 경우입니다. 어느 쪽인지 측정 없이 추측으로 최적화를 시작하면 정작 병목이 아닌 곳에 시간을 쓰게 됩니다.

이 챕터에서는 느린 셀을 찾아내는 방법, PART 04에서 배운 캐싱을 운영 관점에서 배치하는 방법, lazy 모드로 자동 재실행을 억제하는 방법을 다룹니다.

느린 셀 찾기

먼저 어느 셀이 실제로 오래 걸리는지 측정합니다. 가장 간단한 방법은 셀 안에서 time 모듈로 실행 시간을 재는 것입니다.

import timestart = time.perf_counter()# 시간이 걸린다고 의심되는 연산import pandas as pddf = pd.read_csv("data/large_dataset.csv")summary = df.describe()elapsed = time.perf_counter() - startprint(f"실행 시간: {elapsed:.3f}초")summary

한 셀 전체가 아니라 특정 구간만 측정하고 싶다면 구간별로 기록합니다.

import timet0 = time.perf_counter()import pandas as pddf = pd.read_csv("data/large_dataset.csv")t1 = time.perf_counter()filtered = df[df["value"] > 100]t2 = time.perf_counter()print(f"CSV 읽기: {t1 - t0:.3f}초")print(f"필터링:  {t2 - t1:.3f}초")filtered

이 측정을 각 의심 셀에 적용해 실행 시간이 긴 셀을 특정합니다. 1초 이상 걸리는 셀이 위젯 의존성 체인 안에 있다면 최적화 대상입니다.

셀 분리로 재실행 범위 줄이기

하나의 셀에 여러 작업을 묶어두면, 일부만 바뀌어도 전체를 다시 실행해야 합니다. 비싼 작업과 자주 바뀌는 작업을 분리하는 것이 첫 번째 최적화입니다.

# 최적화 전 — 파일 읽기와 필터링이 같은 셀에 있습니다.# 필터 조건만 바꿔도 파일 읽기부터 다시 실행됩니다.import pandas as pdimport marimo as mothreshold = mo.ui.slider(0, 100, value=50, label="임계값")df = pd.read_csv("data/large_dataset.csv")   # 느린 작업filtered = df[df["value"] >= threshold.value]filtered
# 최적화 후 — 셀을 분리합니다.# 파일 읽기 셀import pandas as pddf = pd.read_csv("data/large_dataset.csv")   # 임계값이 바뀌어도 이 셀은 재실행 안 됩니다.
# 필터링 셀 — 위젯과 df에 의존import marimo as mothreshold = mo.ui.slider(0, 100, value=50, label="임계값")threshold
# 필터 결과 셀filtered = df[df["value"] >= threshold.value]filtered

셀을 분리하면 marimo는 threshold가 바뀌었을 때 파일 읽기 셀(df)은 그대로 두고 필터링 셀만 재실행합니다. 의존 관계가 없는 셀은 건드리지 않습니다.

@mo.cache로 재계산 방지

PART 04 Ch05에서 @mo.cache, @mo.lru_cache, @mo.persistent_cache 세 가지를 배웠습니다. 운영 관점에서 어느 것을 선택할지 정리합니다.

노트북 세션 내 반복 호출에는 @mo.cache.

슬라이더를 오가며 같은 값을 반복 탐색하는 상황에 씁니다. 처음 본 값은 계산하고, 돌아온 값은 캐시에서 즉시 반환합니다.

import marimo as moimport pandas as pd@mo.cachedef load_and_aggregate(data_path: str, group_col: str):    df = pd.read_csv(data_path)    return df.groupby(group_col).agg({"value": ["mean", "std", "count"]})
import marimo as mogroup_selector = mo.ui.dropdown(    ["region", "category", "year"],    value="region",    label="집계 기준")group_selector
result = load_and_aggregate("data/sales.csv", group_selector.value)result

group_selector를 바꿀 때마다 load_and_aggregate가 호출됩니다. 이전에 선택했던 값으로 돌아오면 CSV를 다시 읽지 않고 캐시에서 반환합니다.

노트북 재시작 후에도 결과를 보존해야 할 때는 @mo.persistent_cache.

데이터 전처리, 모델 학습, 임베딩 생성처럼 한 번 실행에 수 분이 걸리는 작업에 씁니다. 노트북을 껐다 켜도 캐시가 남아 있으므로 기다리지 않아도 됩니다.

import marimo as moimport numpy as np@mo.persistent_cachedef preprocess_dataset(raw_path: str, version: str):    import pandas as pd    df = pd.read_csv(raw_path)    # 오래 걸리는 전처리    df = df.dropna().reset_index(drop=True)    df["normalized"] = (df["value"] - df["value"].mean()) / df["value"].std()    return dfprocessed = preprocess_dataset("data/raw.csv", "v1")processed.head()

lazy 모드로 자동 재실행 억제

캐싱이 "같은 입력에 대한 재계산을 방지"한다면, lazy 모드는 "사용자가 원할 때만 재실행"합니다. PART 02 Ch04에서 lazy 모드 설정 방법을 다뤘습니다.

운영 관점에서 lazy 모드가 적합한 상황은 하나입니다. 여러 파라미터를 한 번에 조정하고 싶은 경우입니다.

# pyproject.toml
[tool.marimo.runtime]
on_cell_change = "lazy"

이 설정을 켜면 파라미터를 여럿 바꾼 뒤 한 번에 실행할 수 있어 불필요한 중간 계산이 사라집니다. 반면 에디터에서 코드를 수정할 때마다 수동으로 실행해야 하는 번거로움이 생깁니다. 탐색적 분석보다 배포 전 파이프라인 검토에 가까운 용도일 때 선택합니다.

대부분의 노트북은 autorun이 적합합니다. 특별히 느린 셀이 있다면 lazy 전환보다 먼저 @mo.cache로 해결을 시도합니다.

대용량 데이터 셀 운영 팁

큰 DataFrame을 셀 출력으로 그대로 표시하면 렌더링에 시간이 걸립니다. 다음 습관이 도움됩니다.

import marimo as moimport pandas as pddf = pd.read_csv("data/large.csv")# 전체 DataFrame 대신 요약 정보만 표시mo.vstack([    mo.md(f"행 수: `{len(df):,}`, 열 수: `{len(df.columns)}`"),    df.head(10),   # 처음 10행만])

marimo의 DataFrame 표시는 페이지네이션을 제공하므로 수천 행을 한 번에 출력해도 크게 문제가 없습니다. 그러나 수백만 행이라면 head()sample()로 먼저 확인하고, 전체 분석은 집계 함수로 처리하는 것이 자연스럽습니다.

정리

  • time.perf_counter()로 셀 안에서 실행 시간을 측정해 병목 셀을 특정합니다.
  • 비싼 작업과 자주 바뀌는 작업을 셀로 분리하면 재실행 범위가 좁아집니다.
  • @mo.cache는 같은 인자 재호출에서 재계산을 방지합니다. @mo.persistent_cache는 노트북 재시작 후에도 결과를 유지합니다.
  • lazy 모드는 여러 파라미터를 한 번에 조정할 때 유용하지만, 일반 탐색적 분석에는 autorun이 더 편리합니다.
  • 대용량 DataFrame 출력은 head()나 집계 함수와 함께 써서 렌더링 부하를 줄입니다.