Numba 5분 가이드 — @jit의 두 모드와 성능 측정

Numba 5분 가이드

Numba는 NumPy 배열·함수·루프를 많이 쓰는 코드에 가장 잘 맞아요. 가장 흔한 사용법은 데코레이터를 함수에 붙여 컴파일을 지시하는 거예요. 호출 시점에 "just-in-time"으로 머신코드로 컴파일되고, 이후에는 전부 또는 일부가 네이티브 머신코드 속도로 돌아요.

출처: https://numba.readthedocs.io/en/stable/user/5minguide.html

설치

conda install numba
pip install numba

object 모드 vs nopython 모드

@jit 데코레이터는 기본적으로 nopython 모드로 동작해요. 이 모드는 데코레이터된 함수를 파이썬 인터프리터 개입 없이 온전히 실행되도록 컴파일하므로 최고 성능을 내는 권장 방식이에요. nopython 모드 컴파일이 실패하면 object 모드로 컴파일할 수 있는데, forceobj=True로 지정해요.

from numba import jit
import pandas as pd

x = {'a': [1, 2, 3], 'b': [20, 30, 40]}

@jit(forceobj=True, looplift=True)  # object mode 사용, 루프만 컴파일 시도
def use_pandas(a):
    # 파이썬 객체 위주
    pass

looplift=True는 루프를 머신코드 함수로 컴파일하려 시도해 순수 object 모드보다 나을 수 있어요.

성능 측정 주의

Numba는 주어진 인자 타입에 대해 함수를 먼저 컴파일해야 해요. 컴파일은 시간이 걸리지만, 한 번 컴파일되면 캐시해서 같은 타입으로 다시 부르면 재사용해요. 첫 호출에 컴파일 시간이 포함돼 측정을 왜곡할 수 있으니 주의해야 해요.

from numba import jit
import numpy as np
import time

x = np.arange(100).reshape(10, 10)

@jit(nopython=True)
def go_fast(a):
    trace = 0.0
    for i in range(a.shape[0]):
        trace += np.tanh(a[i, i])
    return a + trace

start = time.perf_counter()
go_fast(x)          # 컴파일 포함
end = time.perf_counter()
print("Elapsed (with compilation) = {} s".format((end - start)))

start = time.perf_counter()
go_fast(x)          # 캐시에서 실행
end = time.perf_counter()
print("Elapsed (after compilation) = {} s".format((end - start)))

얼마나 빠른가

nopython 모드(또는 루프 컴파일)가 되면 특정 CPU에 맞춰 컴파일해요. 애플리케이션에 따라 다르지만 보통 1~2자릿수(order of magnitude) 속도 향상을 볼 수 있어요.

원리

Numba는 데코레이터된 함수의 파이썬 바이트코드를 읽고 입력 인자 타입 정보와 결합해 분석·최적화 후, LLVM 컴파일러 라이브러리로 CPU에 맞춘 머신코드를 생성해요. 컴파일된 버전은 이후 매 호출에 사용돼요.

다른 데코레이터

  • @njit@jit(nopython=True)의 별칭(매우 흔함)
  • @vectorize — NumPy ufunc 생성
  • @guvectorize — 일반화 ufunc 생성
  • @stencil — 스텐실 연산 커널
  • @jitclass — JIT 인지 클래스
  • @cfunc — C/C++에서 호출할 네이티브 콜백
  • @overload — nopython 모드용 함수 구현 등록

더 알아보기