[python] 파이썬에서 메모리 사용량을 어떻게 프로파일합니까?

나는 최근 알고리즘에 관심을 가지게되었고 순진한 구현을 작성한 다음 다양한 방식으로 최적화함으로써 알고리즘을 탐색하기 시작했습니다.

나는 이미 프로파일 링 런타임을위한 표준 파이썬 모듈에 익숙하다. (대부분의 경우 IPython에서 timeit magic 함수가 충분하다는 것을 알았지 만) 메모리 사용에 관심이있어서 트레이드 오프를 탐색 할 수있다. 예를 들어, 이전에 계산 된 값의 테이블을 캐싱하는 비용과 필요에 따라 다시 계산하는 비용). 주어진 기능의 메모리 사용량을 프로파일 링하는 모듈이 있습니까?



답변

이것은 이미 여기에 답변되었습니다 : Python memory profiler

기본적으로 당신은 그런 식으로 일합니다 ( Guppy-PE 에서 인용 ) :

>>> from guppy import hpy; h=hpy()
>>> h.heap()
Partition of a set of 48477 objects. Total size = 3265516 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  25773  53  1612820  49   1612820  49 str
     1  11699  24   483960  15   2096780  64 tuple
     2    174   0   241584   7   2338364  72 dict of module
     3   3478   7   222592   7   2560956  78 types.CodeType
     4   3296   7   184576   6   2745532  84 function
     5    401   1   175112   5   2920644  89 dict of class
     6    108   0    81888   3   3002532  92 dict (no owner)
     7    114   0    79632   2   3082164  94 dict of type
     8    117   0    51336   2   3133500  96 type
     9    667   1    24012   1   3157512  97 __builtin__.wrapper_descriptor
<76 more rows. Type e.g. '_.more' to view.>
>>> h.iso(1,[],{})
Partition of a set of 3 objects. Total size = 176 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0      1  33      136  77       136  77 dict (no owner)
     1      1  33       28  16       164  93 list
     2      1  33       12   7       176 100 int
>>> x=[]
>>> h.iso(x).sp
 0: h.Root.i0_modules['__main__'].__dict__['x']
>>> 


답변

Python 3.4에는 새로운 모듈이 포함되어 있습니다 tracemalloc. 메모리를 가장 많이 할당하는 코드에 대한 자세한 통계를 제공합니다. 다음은 메모리를 할당하는 맨 위 세 줄을 표시하는 예입니다.

from collections import Counter
import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


tracemalloc.start()

counts = Counter()
fname = '/usr/share/dict/american-english'
with open(fname) as words:
    words = list(words)
    for word in words:
        prefix = word[:3]
        counts[prefix] += 1
print('Top prefixes:', counts.most_common(3))

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

결과는 다음과 같습니다.

Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
Top 3 lines
#1: scratches/memory_test.py:37: 6527.1 KiB
    words = list(words)
#2: scratches/memory_test.py:39: 247.7 KiB
    prefix = word[:3]
#3: scratches/memory_test.py:40: 193.0 KiB
    counts[prefix] += 1
4 other: 4.3 KiB
Total allocated size: 6972.1 KiB

메모리 누수가 언제 누수가 아닌가?

이 예제는 계산이 끝날 때 메모리가 계속 유지되는 경우에 유용하지만 많은 메모리를 할당 한 다음 모두 해제하는 코드가있는 경우가 있습니다. 기술적으로 메모리 누수가 아니지만 생각보다 많은 메모리를 사용하고 있습니다. 메모리가 모두 해제 될 때 어떻게 메모리 사용량을 추적 할 수 있습니까? 코드 인 경우 실행 중에 스냅 샷을 찍기 위해 디버깅 코드를 추가 할 수 있습니다. 그렇지 않은 경우 기본 스레드가 실행되는 동안 백그라운드 스레드를 시작하여 메모리 사용량을 모니터링 할 수 있습니다.

다음은 코드가 모두 count_prefixes()함수 로 이동 된 이전 예 입니다. 해당 함수가 반환되면 모든 메모리가 해제됩니다. 또한 sleep()장기 실행 계산을 시뮬레이션하기 위해 일부 호출을 추가했습니다 .

from collections import Counter
import linecache
import os
import tracemalloc
from time import sleep


def count_prefixes():
    sleep(2)  # Start up time.
    counts = Counter()
    fname = '/usr/share/dict/american-english'
    with open(fname) as words:
        words = list(words)
        for word in words:
            prefix = word[:3]
            counts[prefix] += 1
            sleep(0.0001)
    most_common = counts.most_common(3)
    sleep(3)  # Shut down time.
    return most_common


def main():
    tracemalloc.start()

    most_common = count_prefixes()
    print('Top prefixes:', most_common)

    snapshot = tracemalloc.take_snapshot()
    display_top(snapshot)


def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


main()

해당 버전을 실행하면 메모리 사용량이 6MB에서 4KB로 줄어 들었습니다. 기능이 끝나면 메모리가 모두 해제 되었기 때문입니다.

Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
Top 3 lines
#1: collections/__init__.py:537: 0.7 KiB
    self.update(*args, **kwds)
#2: collections/__init__.py:555: 0.6 KiB
    return _heapq.nlargest(n, self.items(), key=_itemgetter(1))
#3: python3.6/heapq.py:569: 0.5 KiB
    result = [(key(elem), i, elem) for i, elem in zip(range(0, -n, -1), it)]
10 other: 2.2 KiB
Total allocated size: 4.0 KiB

다음은 메모리 사용량을 모니터링하기 위해 두 번째 스레드를 시작하는 다른 답변에서 영감을 얻은 버전 입니다.

from collections import Counter
import linecache
import os
import tracemalloc
from datetime import datetime
from queue import Queue, Empty
from resource import getrusage, RUSAGE_SELF
from threading import Thread
from time import sleep

def memory_monitor(command_queue: Queue, poll_interval=1):
    tracemalloc.start()
    old_max = 0
    snapshot = None
    while True:
        try:
            command_queue.get(timeout=poll_interval)
            if snapshot is not None:
                print(datetime.now())
                display_top(snapshot)

            return
        except Empty:
            max_rss = getrusage(RUSAGE_SELF).ru_maxrss
            if max_rss > old_max:
                old_max = max_rss
                snapshot = tracemalloc.take_snapshot()
                print(datetime.now(), 'max RSS', max_rss)


def count_prefixes():
    sleep(2)  # Start up time.
    counts = Counter()
    fname = '/usr/share/dict/american-english'
    with open(fname) as words:
        words = list(words)
        for word in words:
            prefix = word[:3]
            counts[prefix] += 1
            sleep(0.0001)
    most_common = counts.most_common(3)
    sleep(3)  # Shut down time.
    return most_common


def main():
    queue = Queue()
    poll_interval = 0.1
    monitor_thread = Thread(target=memory_monitor, args=(queue, poll_interval))
    monitor_thread.start()
    try:
        most_common = count_prefixes()
        print('Top prefixes:', most_common)
    finally:
        queue.put('stop')
        monitor_thread.join()


def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


main()

resource모듈을 사용하면 현재 메모리 사용량을 확인하고 최대 메모리 사용량에서 스냅 샷을 저장할 수 있습니다. 대기열은 주 스레드가 보고서를 인쇄하고 종료 할시기를 메모리 모니터 스레드에 알려줍니다. 실행되면 list()호출 에 사용중인 메모리가 표시됩니다 .

2018-05-29 10:34:34.441334 max RSS 10188
2018-05-29 10:34:36.475707 max RSS 23588
2018-05-29 10:34:36.616524 max RSS 38104
2018-05-29 10:34:36.772978 max RSS 45924
2018-05-29 10:34:36.929688 max RSS 46824
2018-05-29 10:34:37.087554 max RSS 46852
Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
2018-05-29 10:34:56.281262
Top 3 lines
#1: scratches/scratch.py:36: 6527.0 KiB
    words = list(words)
#2: scratches/scratch.py:38: 16.4 KiB
    prefix = word[:3]
#3: scratches/scratch.py:39: 10.1 KiB
    counts[prefix] += 1
19 other: 10.8 KiB
Total allocated size: 6564.3 KiB

Linux를 사용하는 경우 모듈 /proc/self/statm보다 유용 할 수 있습니다 resource.


답변

객체의 메모리 사용량 만보고 싶다면 ( 다른 질문에 대답하십시오 )

모듈 을 포함하는 Pympler 라는 asizeof
모듈이 있습니다.

다음과 같이 사용하십시오 :

from pympler import asizeof
asizeof.asizeof(my_object)

달리 sys.getsizeof, 그것은 당신이 직접 만든 개체에 작동합니다 .

>>> asizeof.asizeof(tuple('bcd'))
200
>>> asizeof.asizeof({'foo': 'bar', 'baz': 'bar'})
400
>>> asizeof.asizeof({})
280
>>> asizeof.asizeof({'foo':'bar'})
360
>>> asizeof.asizeof('foo')
40
>>> asizeof.asizeof(Bar())
352
>>> asizeof.asizeof(Bar().__dict__)
280
>>> help(asizeof.asizeof)
Help on function asizeof in module pympler.asizeof:

asizeof(*objs, **opts)
    Return the combined size in bytes of all objects passed as positional arguments.


답변

폭로:

  • Linux에만 해당
  • 현재 프로세스에서 사용한 메모리를 전체 기능으로보고 합니다.

그러나 단순성으로 인해 좋습니다.

import resource
def using(point=""):
    usage=resource.getrusage(resource.RUSAGE_SELF)
    return '''%s: usertime=%s systime=%s mem=%s mb
           '''%(point,usage[0],usage[1],
                usage[2]/1024.0 )

using("Label")진행 상황을보고 싶은 곳에 삽입 하십시오. 예를 들어

print(using("before"))
wrk = ["wasting mem"] * 1000000
print(using("after"))

>>> before: usertime=2.117053 systime=1.703466 mem=53.97265625 mb
>>> after: usertime=2.12023 systime=1.70708 mem=60.8828125 mb


답변

수락 된 답변과 그다음으로 가장 높은 투표 응답이 내 의견으로는 몇 가지 문제가 있기 때문에 작지만 중요한 수정 사항이있는 Ihor B.의 답변에 밀접하게 기반한 답변을 하나 더 제공하고 싶습니다.

이 솔루션은 사용자가 프로파일 실행할 수 있습니다 중 하나 와 함수 호출을 래핑하여 profile, 기능을 호출 하거나 와 함수 / 메소드를 장식으로 @profile장식.

첫 번째 기법은 소스를 망치지 않고 일부 타사 코드를 프로파일 링 할 때 유용하지만, 두 번째 기법은 “깨끗한”기능이며 함수 / 방법의 소스를 수정하지 않아도 더 잘 작동합니다. 프로필을 작성하고 싶습니다.

RSS, VMS 및 공유 메모리를 얻을 수 있도록 출력도 수정했습니다. 나는 “이전”과 “이후”값에 신경 쓰지 않고 델타 만 신경 쓰지 않으므로 값을 제거했습니다 (Ihor B.의 답변과 비교하는 경우).

프로파일 링 코드

# profile.py
import time
import os
import psutil
import inspect


def elapsed_since(start):
    #return time.strftime("%H:%M:%S", time.gmtime(time.time() - start))
    elapsed = time.time() - start
    if elapsed < 1:
        return str(round(elapsed*1000,2)) + "ms"
    if elapsed < 60:
        return str(round(elapsed, 2)) + "s"
    if elapsed < 3600:
        return str(round(elapsed/60, 2)) + "min"
    else:
        return str(round(elapsed / 3600, 2)) + "hrs"


def get_process_memory():
    process = psutil.Process(os.getpid())
    mi = process.memory_info()
    return mi.rss, mi.vms, mi.shared


def format_bytes(bytes):
    if abs(bytes) < 1000:
        return str(bytes)+"B"
    elif abs(bytes) < 1e6:
        return str(round(bytes/1e3,2)) + "kB"
    elif abs(bytes) < 1e9:
        return str(round(bytes / 1e6, 2)) + "MB"
    else:
        return str(round(bytes / 1e9, 2)) + "GB"


def profile(func, *args, **kwargs):
    def wrapper(*args, **kwargs):
        rss_before, vms_before, shared_before = get_process_memory()
        start = time.time()
        result = func(*args, **kwargs)
        elapsed_time = elapsed_since(start)
        rss_after, vms_after, shared_after = get_process_memory()
        print("Profiling: {:>20}  RSS: {:>8} | VMS: {:>8} | SHR {"
              ":>8} | time: {:>8}"
            .format("<" + func.__name__ + ">",
                    format_bytes(rss_after - rss_before),
                    format_bytes(vms_after - vms_before),
                    format_bytes(shared_after - shared_before),
                    elapsed_time))
        return result
    if inspect.isfunction(func):
        return wrapper
    elif inspect.ismethod(func):
        return wrapper(*args,**kwargs)

위의 코드가 다음과 같이 저장되었다고 가정하면 사용 예 profile.py:

from profile import profile
from time import sleep
from sklearn import datasets # Just an example of 3rd party function call


# Method 1
run_profiling = profile(datasets.load_digits)
data = run_profiling()

# Method 2
@profile
def my_function():
    # do some stuff
    a_list = []
    for i in range(1,100000):
        a_list.append(i)
    return a_list


res = my_function()

결과는 아래와 비슷합니다.

Profiling:        <load_digits>  RSS:   5.07MB | VMS:   4.91MB | SHR  73.73kB | time:  89.99ms
Profiling:        <my_function>  RSS:   1.06MB | VMS:   1.35MB | SHR       0B | time:   8.43ms

몇 가지 중요한 최종 메모 :

  1. 이 프로파일 링 방법은 다른 많은 것들이 기계에서 발생할 수 있기 때문에 대략적인 것입니다. 가비지 수집 및 기타 요인으로 인해 델타가 0 일 수도 있습니다.
  2. 알려지지 않은 이유로 매우 짧은 함수 호출 (예 : 1 또는 2ms)은 메모리 사용량이 0으로 표시됩니다. 메모리 통계가 얼마나 자주 업데이트되는지에 대한 하드웨어 / OS (Linux의 기본 랩톱에서 테스트)의 제한 사항이라고 생각합니다.
  3. 예제를 단순하게 유지하기 위해 함수 인수를 사용하지 않았지만 예상대로 작동해야
    profile(my_function, arg)합니다.my_function(arg)

답변

아래는 간단한 함수 데코레이터로, 함수 호출 전, 함수 호출 후 프로세스가 소비 한 메모리 양과 차이점을 추적 할 수 있습니다.

import time
import os
import psutil


def elapsed_since(start):
    return time.strftime("%H:%M:%S", time.gmtime(time.time() - start))


def get_process_memory():
    process = psutil.Process(os.getpid())
    return process.get_memory_info().rss


def profile(func):
    def wrapper(*args, **kwargs):
        mem_before = get_process_memory()
        start = time.time()
        result = func(*args, **kwargs)
        elapsed_time = elapsed_since(start)
        mem_after = get_process_memory()
        print("{}: memory before: {:,}, after: {:,}, consumed: {:,}; exec time: {}".format(
            func.__name__,
            mem_before, mem_after, mem_after - mem_before,
            elapsed_time))
        return result
    return wrapper

모든 세부 사항을 설명하는 내 블로그 가 있습니다. ( 보관 된 링크 )


답변

아마도 도움이 될 것입니다 :
< 추가 참조 >

pip install gprof2dot
sudo apt-get install graphviz

gprof2dot -f pstats profile_for_func1_001 | dot -Tpng -o profile.png

def profileit(name):
    """
    @profileit("profile_for_func1_001")
    """
    def inner(func):
        def wrapper(*args, **kwargs):
            prof = cProfile.Profile()
            retval = prof.runcall(func, *args, **kwargs)
            # Note use of name from outer scope
            prof.dump_stats(name)
            return retval
        return wrapper
    return inner

@profileit("profile_for_func1_001")
def func1(...)