Bagaimana cara profil penggunaan memori di Python?


230

Saya baru-baru ini menjadi tertarik pada algoritma dan mulai mengeksplorasi mereka dengan menulis implementasi yang naif dan kemudian mengoptimalkannya dengan berbagai cara.

Saya sudah terbiasa dengan modul Python standar untuk profil runtime (untuk sebagian besar hal saya sudah menemukan fungsi sihir timeit di IPython sudah mencukupi), tapi saya juga tertarik dengan penggunaan memori sehingga saya dapat menjelajahi pengorbanan tersebut juga ( misalnya biaya caching tabel dari nilai yang sebelumnya dihitung versus mengkomputasi ulang sesuai kebutuhan). Apakah ada modul yang akan membuat profil penggunaan memori dari fungsi yang diberikan untuk saya?


Duplikat profiler memori Python mana yang disarankan? . IMHO jawaban terbaik pada 2019 adalah memory_profiler
vladkha

Jawaban:


118

Yang ini sudah dijawab di sini: Python memory profiler

Pada dasarnya Anda melakukan hal seperti itu (dikutip dari 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']
>>> 

6
Dokumentasi guppy resmi agak minim; untuk sumber lain lihat contoh ini dan esai yang besar .
tutuDajuju

14
Guppy tampaknya tidak lagi dipertahankan, jadi saya sarankan jawaban ini diturunkan dan salah satu jawaban lainnya diterima.
robguinness

1
@robguinness Dengan menurunkan versi Anda berarti down-vote? Itu tidak adil karena itu berharga pada satu titik waktu. Saya pikir edit di atas menyatakan itu tidak lagi berlaku untuk alasan X dan untuk melihat jawaban Y atau Z sebagai gantinya. Saya pikir tindakan ini lebih tepat.
WinEunuuchs2Unix

1
Tentu, itu berhasil juga, tetapi entah bagaimana akan lebih baik jika jawaban yang diterima dan dipilih tertinggi melibatkan solusi yang masih berfungsi dan dipertahankan.
robguinness

92

Python 3.4 termasuk modul baru: tracemalloc. Ini memberikan statistik terperinci tentang kode mana yang mengalokasikan sebagian besar memori. Berikut adalah contoh yang menampilkan tiga baris teratas mengalokasikan memori.

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)

Dan inilah hasilnya:

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

Kapan kebocoran memori bukan kebocoran?

Contoh itu bagus ketika memori masih ditahan di akhir perhitungan, tetapi kadang-kadang Anda memiliki kode yang mengalokasikan banyak memori dan kemudian melepaskan semuanya. Secara teknis ini bukan kebocoran memori, tetapi menggunakan lebih banyak memori dari yang Anda kira seharusnya. Bagaimana Anda bisa melacak penggunaan memori ketika semuanya dilepaskan? Jika ini kode Anda, Anda mungkin dapat menambahkan beberapa kode debug untuk mengambil snapshot saat sedang berjalan. Jika tidak, Anda dapat memulai utas latar belakang untuk memantau penggunaan memori saat utas utama berjalan.

Inilah contoh sebelumnya di mana kode semuanya telah dipindahkan ke count_prefixes()fungsi. Ketika fungsi itu kembali, semua memori dilepaskan. Saya juga menambahkan beberapa sleep()panggilan untuk mensimulasikan perhitungan yang sudah berjalan lama.

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

Ketika saya menjalankan versi itu, penggunaan memori telah dari 6MB ke 4KB, karena fungsi ini melepaskan semua memorinya ketika selesai.

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

Sekarang inilah versi yang terinspirasi oleh jawaban lain yang memulai utas kedua untuk memantau penggunaan memori.

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

The resourceModul memungkinkan Anda memeriksa penggunaan memori saat ini, dan menyimpan snapshot dari penggunaan memori puncak. Antrian memungkinkan utas utama memberitahu utas monitor memori kapan harus mencetak laporannya dan dimatikan. Saat dijalankan, ini menunjukkan memori yang digunakan oleh list()panggilan:

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

Jika Anda menggunakan Linux, Anda mungkin menemukan /proc/self/statmlebih berguna daripada resourcemodul.


Ini bagus, tetapi sepertinya hanya mencetak snapshot selama interval ketika fungsi di dalam "count_prefixes ()" kembali. Dengan kata lain, jika Anda memiliki panggilan yang berjalan lama, misalnya long_running()di dalam count_prefixes()fungsi, nilai RSS maks tidak akan dicetak sampai long_running()kembali. Atau saya salah?
robguinness

Saya pikir Anda salah, @robguinness. memory_monitor()sedang berjalan di utas terpisah dari count_prefixes(), jadi satu-satunya cara yang satu dapat mempengaruhi yang lain adalah GIL dan antrian pesan yang saya sampaikan memory_monitor(). Saya menduga bahwa ketika count_prefixes()panggilan sleep(), itu mendorong konteks utas untuk beralih. Jika Anda long_running()tidak terlalu lama, maka konteks utas mungkin tidak beralih sampai Anda menekan sleep()panggilan kembali count_prefixes(). Jika itu tidak masuk akal, posting pertanyaan baru dan tautkan ke sini dari sini.
Don Kirkby

Terima kasih. Saya akan memposting pertanyaan baru dan menambahkan tautan di sini. (Saya perlu membuat contoh dari masalah yang saya alami, karena saya tidak bisa membagikan bagian-bagian dari kode tersebut.)
robguinness

31

Jika Anda hanya ingin melihat penggunaan memori suatu objek, ( jawab pertanyaan lain )

Ada modul yang disebut Pympler yang berisi asizeof modul.

Gunakan sebagai berikut:

from pympler import asizeof
asizeof.asizeof(my_object)

Tidak seperti sys.getsizeofitu, ini berfungsi untuk objek yang Anda buat sendiri .

>>> 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
Apakah jumlah ini terkait dengan RSS?
pg2455

1
@mousecoder: RSS mana di en.wikipedia.org/wiki/RSS_(disambiguation) ? Umpan web? Bagaimana?
serv-inc

2
@ serv-inc Resident menetapkan ukuran , meskipun saya hanya dapat menemukan satu menyebutkannya di sumber Pympler dan menyebutkan bahwa tampaknya tidak terikat langsung keasizeof
jkmartindale

1
@mousecoder, memori yang dilaporkan oleh asizeofdapat berkontribusi untuk RSS, ya. Saya tidak yakin apa lagi yang Anda maksud dengan "terkait".
OrangeDog

1
@ serv-inc mungkin saja spesifik kasus. tetapi untuk penggunaan saya mengukur satu kamus multidimensi yang besar, saya menemukan tracemallocsolusi di bawah ini lebih cepat
ulkas

22

Penyingkapan:

  • Hanya berlaku untuk Linux
  • Melaporkan memori yang digunakan oleh proses saat ini secara keseluruhan, bukan fungsi individu di dalamnya

Tapi bagus karena kesederhanaannya:

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 )

Cukup masukkan di using("Label")mana Anda ingin melihat apa yang terjadi. Sebagai contoh

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
"Penggunaan memori dari fungsi yang diberikan" sehingga pendekatan Anda tidak membantu.
Glaslos

Dengan melihat usage[2]Anda melihat ru_maxrss, yang hanya merupakan bagian dari proses yang merupakan penduduk . Ini tidak akan banyak membantu jika proses telah ditukar ke disk, bahkan sebagian.
Louis

8
resourceadalah modul khusus Unix yang tidak berfungsi di Windows.
Martin

1
Unit ru_maxrss(yaitu, usage[2]) adalah kB, bukan halaman sehingga tidak perlu untuk mengalikan angka itu dengan resource.getpagesize().
Mereka '

1
Ini tidak menghasilkan apa-apa untuk saya.
quantumpotato

7

Karena jawaban yang diterima dan juga jawaban tertinggi berikutnya memiliki, menurut pendapat saya, beberapa masalah, saya ingin menawarkan satu jawaban lagi yang didasarkan erat pada jawaban Ihor B. dengan beberapa modifikasi kecil tapi penting.

Solusi ini memungkinkan Anda untuk menjalankan profil pada baik dengan membungkus panggilan fungsi dengan profilefungsi dan menyebutnya, atau dengan menghias fungsi / metode dengan @profiledekorator.

Teknik pertama berguna ketika Anda ingin profil beberapa kode pihak ketiga tanpa mengacaukan sumbernya, sedangkan teknik kedua agak "bersih" dan bekerja lebih baik ketika Anda tidak keberatan memodifikasi sumber fungsi / metode yang Anda gunakan. ingin profil.

Saya juga memodifikasi output, sehingga Anda mendapatkan RSS, VMS, dan memori bersama. Saya tidak terlalu peduli dengan nilai "sebelum" dan "setelah", tetapi hanya delta, jadi saya menghapusnya (jika Anda membandingkan dengan jawaban Ihor B.).

Kode profil

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

Contoh penggunaan, dengan asumsi kode di atas disimpan sebagai 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()

Ini akan menghasilkan output yang mirip dengan di bawah ini:

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

Beberapa catatan akhir yang penting:

  1. Ingat, metode pembuatan profil ini hanya merupakan perkiraan, karena banyak hal lain yang mungkin terjadi pada mesin. Karena pengumpulan sampah dan faktor-faktor lain, delta-delta mungkin bahkan nol.
  2. Untuk beberapa alasan yang tidak diketahui, panggilan fungsi yang sangat singkat (mis. 1 atau 2 ms) muncul tanpa penggunaan memori. Saya menduga ini adalah beberapa keterbatasan perangkat keras / OS (diuji pada laptop dasar dengan Linux) pada seberapa sering statistik memori diperbarui.
  3. Untuk menjaga contoh sederhana, saya tidak menggunakan argumen fungsi apa pun, tetapi mereka harus berfungsi seperti yang diharapkan, yaitu profile(my_function, arg)untuk profilmy_function(arg)

7

Di bawah ini adalah dekorator fungsi sederhana yang memungkinkan untuk melacak berapa banyak memori proses yang dikonsumsi sebelum panggilan fungsi, setelah panggilan fungsi, dan apa bedanya:

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

Ini blog saya yang menjelaskan semua detail. ( tautan yang diarsipkan )


4
seharusnya process.memory_info().rsstidak process.get_memory_info().rss, setidaknya di ubuntu dan python 3.6. terkait stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki

1
Anda benar untuk 3.x. Pelanggan saya menggunakan Python 2.7, bukan versi terbaru.
Ihor B.

4

mungkin ini membantu:
< lihat tambahan >

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

Contoh sederhana untuk menghitung penggunaan memori dari blok kode / fungsi menggunakan memory_profile, sambil mengembalikan hasil fungsi:

import memory_profiler as mp

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

menghitung penggunaan memori sebelum menjalankan kode kemudian menghitung penggunaan maks selama kode:

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

menghitung penggunaan dalam titik pengambilan sampel saat menjalankan fungsi:

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

Kredit: @skeept

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.