앎을 경계하기

[가짜연구소3기] Data Engineer

[가짜연구소 3기] 데이터 엔지니어링 41 - Timing and profiling code

양갱맨 2021. 9. 13. 09:53

이 강의에서는...

코드 수행에 걸리는 시간과 할당되는 메모리를 프로파일링 하는 방법에 대해 배운다.


런타임은 효율성 고려 시 중요한 사항이다.

코드가 빠르다 == 더 효율적이다

매직커맨드

% 를 접두어로 사용하는 커맨드

IPython(인터랙티브 파이썬)같은 Jupyter notebook에서 사용가능하다.


%timeit

import numpy as np
%timeit rand_nums = np.random.rand(1000)
7.64 µs ± 135 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

평균 표준편차, 이 값을 얻기 위해 실행된 실행 및 루프 생성횟수를 확인할 수 있다.

실행 횟수는 -r, 루프 횟수는 -n 으로 지정해서 %timeit을 수행할 수 있다.

결과를 저장하고 싶다면 -o 를 사용하면 변수에 저장할 수 있다.

import numpy as np
times = %timeit -o -r2 -n10 rand_nums = np.random.rand(1000)
26.7 µs ± 2.13 µs per loop (mean ± std. dev. of 2 runs, 10 loops each)

각 실행에 대한 시간, 최적 또는 최악의 시간을 확인할 수 있다.

times.timings
times.best
times.worst
[1.8009999996593252e-05, 2.3329999999077698e-05]
1.8009999996593252e-05
2.3329999999077698e-05

여러 줄의 코드에서 %timeit을 사용하려면 %를 두 개를 붙이면 된다.

%%timeit
nums = []
for x in range(10):
    nums.append(x)
854 ns ± 6.72 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

파이썬에는 list, tuple, dict, set과 같은 내장 자료구조를 사용할 때, 선언 방법이 두 가지가 있다.

  1. 공식 이름 사용 - f_dict = dict()
  1. 리터럴 사용 - f_dict = {}

위 두 방법에도 시간 차이가 발생한다.

%timeit f_dict = dict()
%timeit l_dict = {}
155 ns ± 0.898 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
25.2 ns ± 0.596 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

리터럴을 사용하는 것이 더 빠르다.

연습문제

In [1]:
%timeit nums_list_comp = [num for num in range(51)]
1.96 us +- 43.1 ns per loop (mean +- std. dev. of 7 runs, 1000000 loops each)

In [2]:
%timeit nums_unpack = [*range(51)]
395 ns +- 14.4 ns per loop (mean +- std. dev. of 7 runs, 1000000 loops each)
In [1]:
%%timeit -o
hero_wts_lbs = []
for wt in wts:
    hero_wts_lbs.append(wt * 2.20462)
693 us +- 11.6 us per loop (mean +- std. dev. of 7 runs, 1000 loops each)

In [2]:
%%timeit -o
wts_np = np.array(wts)
hero_wts_lbs_np = wts_np * 2.20462
12.6 us +- 933 ns per loop (mean +- std. dev. of 7 runs, 100000 loops each)

코드 프로파일링

  • 프로그램의 다양한 부분이 실행되는 시간과 빈도를 설명하기 위해 사용한다.
  • 라인 별로 분석할 수 있다.
  • line_profiler 패키지를 사용한다.
  • 기본 내장 패키지가 아니기 때문에 설치 필요
    • pip install line_profiler

%timeit을 사용할 때 단점

해당 명령의 시간만 체크한다.

heroes = ['batman', 'superman', 'wonder woman']
hts = np.array([188.0, 191.0, 183.0])
wts = np.array([95.0, 101.0, 74.0])

def convert_units(heroes, heights, weights):
    new_hts = [ht * 0.39370 for ht in heights]
    new_wts = [wt * 2.20462 for wt in weights]

    hero_data = {}

    for i, hero in enumerate(heroes):
        hero_data[hero] = (new_hts[i], new_wts[i])

    return hero_data

%timeit convert_units(heroes, hts, wts)
5 µs ± 53.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

모든 코드마다 실행 시간을 측정하고 싶으면 각 코드마다 %timeit 또는 %%timeit을 입력해야한다.

line_profiler

line_profiler 패키지를 사용한다.

%load_ext line_profiler
%lprun -f 함수이름 함수호출명령

매직 커맨드 %lprun를 사용하면 줄 별 시간을 측정할 수 있다. -f 옵션을 사용해서 함수를 프로파일링 할 것임을 나타낸다.

%load_ext line_profiler
%lprun -f convert_units convert_units(heroes, hts, wts)
Timer unit: 1e-07 s

Total time: 2.71e-05 s
File: <ipython-input-17-bcdcaa224d33>
Function: convert_units at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def convert_units(heroes, heights, weights):
     2         1        135.0    135.0     49.8      new_hts = [ht * 0.39370 for ht in heights]
     3         1         49.0     49.0     18.1      new_wts = [wt * 2.20462 for wt in weights]
     4                                           
     5         1          8.0      8.0      3.0      hero_data = {}
     6                                           
     7         4         41.0     10.2     15.1      for i, hero in enumerate(heroes):
     8         3         32.0     10.7     11.8          hero_data[hero] = (new_hts[i], new_wts[i])
     9                                           
    10         1          6.0      6.0      2.2      return hero_data
  • Line - 코드 라인
  • Hits - 행이 실행된 횟수
  • Time - 각 행 실행의 총 시간
  • Timer unit - 타이머 단위로 마이크로초(μs, 100만분의 1초)를 사용한다.
  • Per Hit - 단일 행 실행 시 소요된 평균 시간 (Time/Hits)
  • % Time - 소요 시간 비율

%timeit%lprun 의 결과 차이

%timeit의 결과 : 5 µs ± 53.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%lprun의 결과 : Total time: 2.71e-05 s = 0.0000271s = 27.1µs

연습문제

  • new_hts list comprehension의 수행 퍼센트
In [3]:
%load_ext line_profiler
The line_profiler extension is already loaded. To reload it, use:
  %reload_ext line_profiler

In [4]:
%lprun -f convert_units_broadcast convert_units_broadcast(heroes, hts, wts)
Timer unit: 1e-06 s

Total time: 0.000524 s
File: <ipython-input-1-84e44a6b12f5>
Function: convert_units_broadcast at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def convert_units_broadcast(heroes, heights, weights):
     2                                           
     3                                               # Array broadcasting instead of list comprehension
     4         1         31.0     31.0      5.9      new_hts = heights * 0.39370
     5         1          3.0      3.0      0.6      new_wts = weights * 2.20462
     6                                           
     7         1          0.0      0.0      0.0      hero_data = {}
     8                                           
     9       481        195.0      0.4     37.2      for i,hero in enumerate(heroes):
    10       480        295.0      0.6     56.3          hero_data[hero] = (new_hts[i], new_wts[i])
    11                                                   
    12         1          0.0      0.0      0.0      return hero_data

메모리 사용 프로파일링

메모리 검사를 할 수 있는 단순한 방법은 sys 모듈을 사용하는 것이다.

import sys

nums_list = [*range(1000)]
sys.getsizeof(nums_list)
9104

단순히 메모리에 할당된 객체의 크기를 확인할 수 있다.

memory_profiler

앞서 봤던 line_profiler와 비슷한 memroy_profiler를 사용하면 라인 별 메모리 사용을 분석할 수 있다.

마찬가지로 설치가 필요하다.

pip install memory_profiler

mprun을 실행하려고 했더니 아래와 같은 에러가 떴다.

ERROR: Could not find file <ipython-input-94-8fdaa89ffea3>
NOTE: %mprun can only be used on functions defined in physical files, and not in the IPython environment.

mprun은 모듈에 정의된 함수에만 동작하기 때문에 노트북 셀에 작성된 함수에는 동작하지 않는다.

그래서 아래와 같이 별도 python 모듈을 만들어줌

%%file mprun_demo.py
import numpy as np

heroes = ['batman', 'superman', 'wonder woman']
hts = np.array([188.0, 191.0, 183.0])
wts = np.array([95.0, 101.0, 74.0])
def convert_units(heroes, heights, weights):
    new_hts = [ht * 0.39370 for ht in heights]
    new_wts = [wt * 2.20462 for wt in weights]

    hero_data = {}

    for i, hero in enumerate(heroes):
        hero_data[hero] = (new_hts[i], new_wts[i])

    return hero_data
%load_ext memory_profiler

from mprun_demo import convert_units
%mprun -f convert_units convert_units(heroes, hts, wts)
The memory_profiler extension is already loaded. To reload it, use:
  %reload_ext memory_profiler

Filename: c:\Users\didwu\Downloads\mprun_demo.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
     6     94.4 MiB     94.4 MiB           1   def convert_units(heroes, heights, weights):
     7     94.4 MiB      0.0 MiB           6       new_hts = [ht * 0.39370 for ht in heights]
     8     94.4 MiB      0.0 MiB           6       new_wts = [wt * 2.20462 for wt in weights]
     9                                         
    10     94.4 MiB      0.0 MiB           1       hero_data = {}
    11                                         
    12     94.4 MiB      0.0 MiB           4       for i, hero in enumerate(heroes):
    13     94.4 MiB      0.0 MiB           3           hero_data[hero] = (new_hts[i], new_wts[i])
    14                                         
    15     94.4 MiB      0.0 MiB           1       return hero_data
  • Line : 코드 줄
  • Memusage : 행 실행 후 사용된 메모리
  • Increment : 이전 행과 현재 행 수행의 메모리 차이 - 현재 라인의 메모리 영향을 알 수 있는 부분
  • 메모리 단위 : MiB(mebibyte, 메가 이진 바이트) MB 1,000,000byte는 10진수 기반, 이를 컴퓨터에 더 적합하게 2^20 1,048,576 byte로 표현한것
  • 작은 사이즈를 할당하는 경우 표시가 안될 수 있음
  • 운영체제 쿼리를 통해 메모리 소비를 검사한다.
  • 플랫폼, 실행 환경 등마다 결과가 다를 수 있다.

연습문제

calc_bmi_list() 에서 list comprehension 부분의 메모리 사용량은?

In [1]:
%load_ext memory_profiler

In [2]:
from bmi_lists import calc_bmi_lists

In [3]:
%mprun -f calc_bmi_lists calc_bmi_lists(sample_indices, hts, wts)
Filename: /tmp/tmp0q4pu4fb/bmi_lists.py

Line #    Mem usage    Increment   Line Contents
================================================
     1     92.7 MiB     92.7 MiB   def calc_bmi_lists(sample_indices, hts, wts):
     2                             
     3                                 # Gather sample heights and weights as lists
     4     93.2 MiB      0.3 MiB       s_hts = [hts[i] for i in sample_indices]
     5     94.0 MiB      0.3 MiB       s_wts = [wts[i] for i in sample_indices]
     6                             
     7                                 # Convert heights from cm to m and square with list comprehension
     8     95.1 MiB      0.4 MiB       s_hts_m_sqr = [(ht / 100) ** 2 for ht in s_hts]
     9                             
    10                                 # Calculate BMIs as a list with list comprehension
    11     95.8 MiB      0.3 MiB       bmis = [s_wts[i] / s_hts_m_sqr[i] for i in range(len(sample_indices))]
    12                             
    13     95.8 MiB      0.0 MiB       return bmis

배열 인덱싱과 브로드 캐스팅 부분의 메모리 사용량은?

In [1]:
%load_ext memory_profiler

In [2]:
from bmi_arrays import calc_bmi_arrays

In [3]:
%mprun -f calc_bmi_arrays calc_bmi_arrays(sample_indices, hts, wts)
ERROR! Session/line number was not unique in database. History logging moved to new session 12
Filename: /tmp/tmp9qoq92ud/bmi_arrays.py

Line #    Mem usage    Increment   Line Contents
================================================
     1     92.4 MiB     92.4 MiB   def calc_bmi_arrays(sample_indices, hts, wts):
     2                             
     3                                 # Gather sample heights and weights as arrays
     4     92.4 MiB      0.0 MiB       s_hts = hts[sample_indices]
     5     92.5 MiB      0.1 MiB       s_wts = wts[sample_indices]
     6                             
     7                                 # Convert heights from cm to m and square with broadcasting
     8     92.7 MiB      0.3 MiB       s_hts_m_sqr = (s_hts / 100) ** 2
     9                             
    10                                 # Calculate BMIs as an array using broadcasting
    11     92.7 MiB      0.0 MiB       bmis = s_wts / s_hts_m_sqr
    12                             
    13     92.7 MiB      0.0 MiB       return bmis

스타워즈 영웅 목록 얻는 함수 프로파일링

In [1]:
%load_ext line_profiler

In [2]:
%lprun -f get_publisher_heroes get_publisher_heroes(heroes, publishers, 'George Lucas')
Timer unit: 1e-06 s

Total time: 0.000247 s
File: <ipython-input-1-5a6bc05c1c55>
Function: get_publisher_heroes at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def get_publisher_heroes(heroes, publishers, desired_publisher):
     2                                           
     3         1          2.0      2.0      0.8      desired_heroes = []
     4                                           
     5       481        118.0      0.2     47.8      for i,pub in enumerate(publishers):
     6       480        117.0      0.2     47.4          if pub == desired_publisher:
     7         4          9.0      2.2      3.6              desired_heroes.append(heroes[i])
     8                                           
     9         1          1.0      1.0      0.4      return desired_heroes

In [3]:
%lprun -f get_publisher_heroes_np get_publisher_heroes_np(heroes, publishers, 'George Lucas')
Timer unit: 1e-06 s

Total time: 0.000241 s
File: <ipython-input-1-5a6bc05c1c55>
Function: get_publisher_heroes_np at line 12

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    12                                           def get_publisher_heroes_np(heroes, publishers, desired_publisher):
    13                                           
    14         1        176.0    176.0     73.0      heroes_np = np.array(heroes)
    15         1         41.0     41.0     17.0      pubs_np = np.array(publishers)
    16                                           
    17         1         23.0     23.0      9.5      desired_heroes = heroes_np[pubs_np == desired_publisher]
    18                                           
    19         1          1.0      1.0      0.4      return desired_heroes
In [6]:
%load_ext memory_profiler
The memory_profiler extension is already loaded. To reload it, use:
  %reload_ext memory_profiler

In [7]:
from hero_funcs import get_publisher_heroes, get_publisher_heroes_np

In [8]:
%mprun -f get_publisher_heroes get_publisher_heroes(heroes, publishers, 'George Lucas')
Filename: /tmp/tmp3b5yd9ze/hero_funcs.py

Line #    Mem usage    Increment   Line Contents
================================================
     4     92.6 MiB     92.6 MiB   def get_publisher_heroes(heroes, publishers, desired_publisher):
     5                             
     6     92.6 MiB      0.0 MiB       desired_heroes = []
     7                             
     8     92.6 MiB      0.0 MiB       for i,pub in enumerate(publishers):
     9     92.6 MiB      0.0 MiB           if pub == desired_publisher:
    10     92.6 MiB      0.0 MiB               desired_heroes.append(heroes[i])
    11                             
    12     92.6 MiB      0.0 MiB       return desired_heroes

In [9]:
%mprun -f get_publisher_heroes_np get_publisher_heroes_np(heroes, publishers, 'George Lucas')
Filename: /tmp/tmp3b5yd9ze/hero_funcs.py

Line #    Mem usage    Increment   Line Contents
================================================
    15     92.6 MiB     92.6 MiB   def get_publisher_heroes_np(heroes, publishers, desired_publisher):
    16                             
    17     92.6 MiB      0.0 MiB       heroes_np = np.array(heroes)
    18     92.6 MiB      0.0 MiB       pubs_np = np.array(publishers)
    19                             
    20     92.6 MiB      0.0 MiB       desired_heroes = heroes_np[pubs_np == desired_publisher]
    21                             
    22     92.6 MiB      0.0 MiB       return desired_heroes