Python'da bellek kullanımını nasıl profilleştirebilirim?


230

Son zamanlarda algoritmalar ile ilgilenmeye başladım ve naif bir uygulama yazıp çeşitli şekillerde optimize ederek bunları keşfetmeye başladım.

Ben zaten profilleme zamanı için standart Python modülüne aşinayım (çoğu şey için IPython'da timeit sihirli işlevini yeterli buldum), ama aynı zamanda bellek kullanımıyla da ilgileniyorum, böylece bu dengeleri de keşfedebilirim ( örneğin önceden hesaplanmış değerler tablosunu önbelleğe alma maliyeti ve gerektiğinde yeniden hesaplama maliyeti). Bana verilen bir fonksiyonun bellek kullanımını belirleyecek bir modül var mı?


Yanıtlar:


118

Bu cevap zaten burada: Python bellek profiler

Temel olarak böyle bir şey yaparsınız ( Guppy-PE'den alıntılanmıştır ):

>>> 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']
>>> 

6
Resmi lepistes belgeleri biraz minimiyal; diğer kaynaklar için bu örneğe ve kalın denemeye bakın .
tutuDajuju

14
Guppy artık korunmuyor gibi görünüyor, bu yüzden bu cevabın indirgenmesini ve bunun yerine diğer cevaplardan birinin kabul edilmesini öneriyorum.
robguinness

1
@robguinness Aşağı düşürüldüğünü mu demek istediniz? Bu adil görünmüyor çünkü zamanın bir noktasında değerliydi. Bence X düzenlemesi için geçerli olmadığını ve bunun yerine Y veya Z cevabını görmek için en üstte bir düzenleme olduğunu düşünüyorum. Bu hareket tarzının daha uygun olduğunu düşünüyorum.
WinEunuuchs2Unix

1
Elbette, bu da işe yarıyor, ancak bir şekilde kabul edilen ve en yüksek oyu alan cevabın hala çalışan ve sürdürülen bir çözümü içermesi iyi olurdu.
robguinness

92

Python 3.4 yeni bir modül içerir: tracemalloc. Hangi kodun en fazla belleği ayırdığı hakkında ayrıntılı istatistikler sağlar. Bellek tahsis eden ilk üç satırı gösteren bir örnek.

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)

İşte sonuçlar:

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

Bellek sızıntısı ne zaman sızıntı değildir?

Bu örnek, hesaplamanın sonunda bellek hala tutulurken harika, ancak bazen çok fazla bellek ayıran ve sonra hepsini serbest bırakan bir kodunuz var. Teknik olarak bir bellek sızıntısı değil, düşündüğünüzden daha fazla bellek kullanıyor. Her şey yayınlandığında bellek kullanımını nasıl takip edebilirsiniz? Kodunuz buysa, çalışırken anlık görüntüler çekmek için bazı hata ayıklama kodları ekleyebilirsiniz. Değilse, ana iş parçacığı çalışırken bellek kullanımını izlemek için bir arka plan iş parçacığı başlatabilirsiniz.

Kodun tümünün count_prefixes()işleve taşındığı önceki örnek aşağıdadır . Bu işlev döndüğünde, tüm bellek serbest bırakılır. sleep()Uzun süren bir hesaplamayı simüle etmek için bazı çağrılar da ekledim .

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()

Bu sürümü çalıştırdığımda, bellek kullanımı 6MB'tan 4KB'ye geçti, çünkü işlev bittiğinde tüm belleğini serbest bıraktı.

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

Şimdi, bellek kullanımını izlemek için ikinci bir konu başlatan başka bir cevaptan esinlenilmiş bir versiyon .

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()

resourceModül geçerli bellek kullanımını kontrol etmek ve tepe bellek kullanımı gelen anlık tasarruf sağlar. Sıra, ana iş parçacığının bellek monitörü iş parçacığına raporunu ne zaman yazdıracağını ve kapatılacağını bildirmesini sağlar. Çalıştığında, list()çağrı tarafından kullanılan belleği gösterir :

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'taysanız modülden /proc/self/statmdaha kullanışlı bulabilirsiniz resource.


Bu harika, ancak anlık görüntüleri yalnızca "count_prefixes ()" içindeki işlevler geri döndüğünde aralıklarla yazdırılıyor gibi görünüyor. Diğer bir deyişle, örneğin işlev long_running()içinde uzun süren bir çağrınız varsa, count_prefixes()maksimum RSS değerleri long_running()döndürülene kadar yazdırılmaz . Yoksa yanılıyor muyum?
robguinness

Sanırım yanılıyorsun, @robguinness. memory_monitor()ayrı bir iş parçacığında çalışıyor count_prefixes(), bu yüzden diğerini etkilemenin tek yolu GIL ve ilettiğim ileti kuyruğu memory_monitor(). count_prefixes()Arama yaparken sleep(), iş parçacığı bağlamının değişmesini teşvik ettiğinden şüpheleniyorum . Eğer senin long_running()aslında çok uzun sürüyor değil sen çarpana kadar, daha sonra iplik bağlam geçiş olmayabilir sleep()çağrı geri count_prefixes(). Bu mantıklı değilse, yeni bir soru gönderin ve buradan bağlantı kurun.
Don Kirkby

Teşekkürler. Yeni bir soru göndereceğim ve buraya bir bağlantı ekleyeceğim. (Kodun özel bölümlerini paylaşamadığım için yaşadığım sorunun bir örneğini oluşturmam gerekiyor.)
robguinness

31

Yalnızca bir nesnenin bellek kullanımına bakmak istiyorsanız, ( diğer soruya cevap verin )

Modülü içeren Pympler adlı bir modül var asizeof .

Aşağıdaki gibi kullanın:

from pympler import asizeof
asizeof.asizeof(my_object)

Bunun aksine sys.getsizeof, kendi oluşturduğunuz nesneler için çalışır .

>>> 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.

1
Bu asizeof RSS ile ilgili mi?
pg2455

1
@mousecoder: en.wikipedia.org/wiki/RSS_(disambiguation adresinde hangi RSS ) ? Web beslemeleri? Nasıl?
serv-inc

2
@ serv-inc Yerleşik set boyutu , ancak Pympler'in kaynağında sadece bir söz bulabiliyorum ve bu söz doğrudan bağlı görünmüyorasizeof
jkmartindale

1
@mousecoder tarafından bildirilen bellek asizeofRSS'ye katkıda bulunabilir, evet. "İlgili" ile başka ne demek istediğinden emin değilim.
OrangeDog

1
@ serv-inc bu duruma çok özel olabilir. ama büyük boyutlu çok büyük bir sözlüğü ölçen tracemalloc
usecase'im

22

Bilgilendirme:

  • Yalnızca Linux'ta geçerlidir
  • Geçerli işlem tarafından kullanılan belleği, içindeki bağımsız işlevleri değil bir bütün olarak rapor eder

Ama basitliği nedeniyle güzel:

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")Neler olup bittiğini görmek istediğiniz yere ekleyin . Örneğin

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

6
"belirli bir işlevin bellek kullanımı" yani yaklaşımınız yardımcı olmuyor.
Glaslos

Bakarak usage[2]size bakıyor ru_maxrssise sürecin yalnızca kısmı olan mukim . Bu işlem, kısmen de olsa diske aktarılmışsa çok yardımcı olmaz.
Louis

8
resourceWindows altında çalışmayan Unix'e özgü bir modüldür.
Martin

1
Birimleri ru_maxrss(yani, usage[2]bu sayıyı çarpmak gerek yoktur bu yüzden), sayfalar değil kB vardır resource.getpagesize().
Tey '

1
Bu benim için hiçbir şey basmadı.
kuantumpotato

7

Kabul edilen cevap ve bir sonraki en yüksek oyu alan cevabın bana göre bazı sorunları olduğu için, Ihor B.'nin bazı küçük ama önemli değişikliklerle cevabına yakından dayanan bir cevap daha sunmak istiyorum.

Bu çözüm üzerinde profil çalışmasını sağlar ya sahip bir işlev çağrısı sararak profile, işlev ve onu arayarak ya ile işlev / yöntem dekorasyon tarafından @profiledekoratör.

İlk teknik, kaynak ile uğraşmadan bazı üçüncü taraf kodlarını profillemek istediğinizde kullanışlıdır, oysa ikinci teknik biraz "daha temiz" ve fonksiyonun / yöntemin kaynağını değiştirmediğinizde daha iyi çalışır profil istiyorum.

Ayrıca çıktıyı değiştirdim, böylece RSS, VMS ve paylaşılan bellek elde edersiniz. Ben önce "önce" ve "sonra" değerleri umurumda değil, sadece delta, bu yüzden (Ihor B. cevap ile karşılaştırıyorsanız) bunları kaldırdım.

Profil kodu

# 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)

Yukarıdaki kodun kaydedildiği varsayılarak örnek kullanım 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()

Bu, aşağıdakine benzer çıktılarla sonuçlanmalıdır:

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

Birkaç önemli son not:

  1. Unutmayın, bu profil oluşturma yöntemi yalnızca yaklaşık olacaktır, çünkü makinede başka birçok şey olabilir. Çöp toplama ve diğer faktörler nedeniyle deltalar sıfır bile olabilir.
  2. Bilinmeyen bir nedenden ötürü, çok kısa işlev çağrıları (örneğin 1 veya 2 ms) sıfır bellek kullanımı ile ortaya çıkar. Bunun, donanım istatistiklerinin (Linux ile temel dizüstü bilgisayarda test edildi) bellek istatistiklerinin ne sıklıkla güncellendiği konusunda bir sınırlama olduğundan şüpheleniyorum.
  3. Örnekleri basit tutmak için herhangi bir işlev argümanı kullanmadım, ancak beklendiği gibi çalışmalı, yani profile(my_function, arg)profil içinmy_function(arg)

7

Aşağıda, işlev çağrısından önce, işlev çağrısından sonra işlemin ne kadar bellek harcadığını ve farkın ne olduğunu izlemenizi sağlayan basit bir işlev dekoratörü bulunmaktadır:

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

İşte tüm detayları açıklayan blogum . ( arşivlenmiş bağlantı )


4
Olması gereken process.memory_info().rssdeğil process.get_memory_info().rss, en azından ubuntu ve piton 3.6. İlgili stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki

1
3.x konusunda haklısın. Müşterim en yeni sürümü değil, Python 2.7 kullanıyor.
Ihor B.

4

belki yardımcı olur:
<eke bakın >

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(...)

1

İşlevin sonucunu döndürürken, memory_profile kullanarak bir kod / işlev bloğunun bellek kullanımını hesaplamak için basit bir örnek:

import memory_profiler as mp

def fun(n):
    tmp = []
    for i in range(n):
        tmp.extend(list(range(i*i)))
    return "XXXXX"

kodu çalıştırmadan önce bellek kullanımını hesaplayın ve ardından kod sırasında maksimum kullanımı hesaplayın:

start_mem = mp.memory_usage(max_usage=True)
res = mp.memory_usage(proc=(fun, [100]), max_usage=True, retval=True) 
print('start mem', start_mem)
print('max mem', res[0][0])
print('used mem', res[0][0]-start_mem)
print('fun output', res[1])

fonksiyonu çalıştırırken örnekleme noktalarındaki kullanımı hesaplar:

res = mp.memory_usage((fun, [100]), interval=.001, retval=True)
print('min mem', min(res[0]))
print('max mem', max(res[0]))
print('used mem', max(res[0])-min(res[0]))
print('fun output', res[1])

Kredi: @skeept

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.