Jak mogę profilować kod Pythona wiersz po wierszu?

116

Używałem cProfile do profilowania mojego kodu i działa świetnie. Ja też używam gprof2dot.py do wizualizacji wyników (czyni to trochę jaśniejszym).

Jednak cProfile (i większość innych profilerów Pythona, które widziałem do tej pory) wydaje się profilować tylko na poziomie wywołania funkcji. Powoduje to zamieszanie, gdy pewne funkcje są wywoływane z różnych miejsc - nie mam pojęcia, czy wywołanie nr 1, czy nr 2 zajmuje większość czasu. Sytuacja jest jeszcze gorsza, gdy dana funkcja ma sześć poziomów głębokości i jest wywoływana z siedmiu innych miejsc.

Jak uzyskać profilowanie linia po linii?

Zamiast tego:

function #12, total time: 2.0s

Chciałbym zobaczyć coś takiego:

function #12 (called from somefile.py:102) 0.5s
function #12 (called from main.py:12) 1.5s

cProfile pokazuje, ile z łącznego czasu „przenosi” do rodzica, ale znowu to połączenie jest tracone, gdy masz kilka warstw i połączonych ze sobą połączeń.

Najlepiej byłoby mieć GUI, który przeanalizowałby dane, a następnie pokazałby mój plik źródłowy z całkowitym czasem podanym dla każdej linii. Coś takiego:

main.py:

a = 1 # 0.0s
result = func(a) # 0.4s
c = 1000 # 0.0s
result = func(c) # 5.0s

Następnie mógłbym kliknąć drugie wywołanie „func (c)”, aby zobaczyć, co zajmuje czas w tym wywołaniu, niezależnie od wywołania „func (a)”.

Czy to ma sens? Czy istnieje biblioteka profili, która gromadzi tego typu informacje? Czy jest jakieś niesamowite narzędzie, które przegapiłem?

Rocketmonkeys
źródło
2
Domyślam się, że byłbyś zainteresowany pstats.print_callers. Przykładem jest tutaj .
Muhammad Alkarouri
Muhammad, to zdecydowanie pomocne! Przynajmniej rozwiązuje jeden problem: oddzielanie wywołań funkcji w zależności od pochodzenia. Myślę, że odpowiedź Joe Kingtona jest bliżej mojego celu, ale print_callers () zdecydowanie doprowadza mnie do połowy. Dzięki!
rocketmonkeys

Odpowiedzi:

120

Uważam, że właśnie do tego służy line_profiler Roberta Kerna . Z linku:

File: pystone.py
Function: Proc2 at line 149
Total time: 0.606656 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   149                                           @profile
   150                                           def Proc2(IntParIO):
   151     50000        82003      1.6     13.5      IntLoc = IntParIO + 10
   152     50000        63162      1.3     10.4      while 1:
   153     50000        69065      1.4     11.4          if Char1Glob == 'A':
   154     50000        66354      1.3     10.9              IntLoc = IntLoc - 1
   155     50000        67263      1.3     11.1              IntParIO = IntLoc - IntGlob
   156     50000        65494      1.3     10.8              EnumLoc = Ident1
   157     50000        68001      1.4     11.2          if EnumLoc == Ident1:
   158     50000        63739      1.3     10.5              break
   159     50000        61575      1.2     10.1      return IntParIO

Mam nadzieję, że to pomoże!

Joe Kington
źródło
10
Czy line_profiler współpracuje z Pythonem 3? Nie mogłem uzyskać żadnych informacji na ten temat.
user1251007
3
line_profiler nie pokazuje mi trafień i czasu. Czy ktoś może mi powiedzieć dlaczego. A jak rozwiązać?
I159
6
Oto dekorator, który napisałem: gist.github.com/kylegibson/6583590 . Jeśli uruchamiasz testy nosowe, pamiętaj, aby użyć opcji -s, aby standardowe wyjście zostało wydrukowane natychmiast.
Kyle Gibson,
5
jak wygląda skrypt w Pythonie, który generuje te dane wyjściowe? import line_profiler;i wtedy ?
Zhubarb
10
czy ktoś może pokazać, jak właściwie korzystać z tej biblioteki? Plik Readme uczy, jak zainstalować i odpowiada na różne
często
47

Można również użyć pprofile ( PyPI ). Jeśli chcesz sprofilować całe wykonanie, nie wymaga to modyfikacji kodu źródłowego. Możesz również profilować podzbiór większego programu na dwa sposoby:

  • przełącz profilowanie po osiągnięciu określonego punktu w kodzie, takiego jak:

    import pprofile
    profiler = pprofile.Profile()
    with profiler:
        some_code
    # Process profile content: generate a cachegrind file and send it to user.
    
    # You can also write the result to the console:
    profiler.print_stats()
    
    # Or to a file:
    profiler.dump_stats("/tmp/profiler_stats.txt")
  • przełączanie profilowania asynchronicznie ze stosu wywołań (wymaga sposobu wyzwolenia tego kodu w rozważanej aplikacji, na przykład programu obsługi sygnału lub dostępnego wątku roboczego) przy użyciu profilowania statystycznego:

    import pprofile
    profiler = pprofile.StatisticalProfile()
    statistical_profiler_thread = pprofile.StatisticalThread(
        profiler=profiler,
    )
    with statistical_profiler_thread:
        sleep(n)
    # Likewise, process profile content

Format wyjściowy adnotacji do kodu jest podobny do programu profilującego:

$ pprofile --threads 0 demo/threads.py
Command line: ['demo/threads.py']
Total duration: 1.00573s
File: demo/threads.py
File duration: 1.00168s (99.60%)
Line #|      Hits|         Time| Time per hit|      %|Source code
------+----------+-------------+-------------+-------+-----------
     1|         2|  3.21865e-05|  1.60933e-05|  0.00%|import threading
     2|         1|  5.96046e-06|  5.96046e-06|  0.00%|import time
     3|         0|            0|            0|  0.00%|
     4|         2|   1.5974e-05|  7.98702e-06|  0.00%|def func():
     5|         1|      1.00111|      1.00111| 99.54%|  time.sleep(1)
     6|         0|            0|            0|  0.00%|
     7|         2|  2.00272e-05|  1.00136e-05|  0.00%|def func2():
     8|         1|  1.69277e-05|  1.69277e-05|  0.00%|  pass
     9|         0|            0|            0|  0.00%|
    10|         1|  1.81198e-05|  1.81198e-05|  0.00%|t1 = threading.Thread(target=func)
(call)|         1|  0.000610828|  0.000610828|  0.06%|# /usr/lib/python2.7/threading.py:436 __init__
    11|         1|  1.52588e-05|  1.52588e-05|  0.00%|t2 = threading.Thread(target=func)
(call)|         1|  0.000438929|  0.000438929|  0.04%|# /usr/lib/python2.7/threading.py:436 __init__
    12|         1|  4.79221e-05|  4.79221e-05|  0.00%|t1.start()
(call)|         1|  0.000843048|  0.000843048|  0.08%|# /usr/lib/python2.7/threading.py:485 start
    13|         1|  6.48499e-05|  6.48499e-05|  0.01%|t2.start()
(call)|         1|   0.00115609|   0.00115609|  0.11%|# /usr/lib/python2.7/threading.py:485 start
    14|         1|  0.000205994|  0.000205994|  0.02%|(func(), func2())
(call)|         1|      1.00112|      1.00112| 99.54%|# demo/threads.py:4 func
(call)|         1|  3.09944e-05|  3.09944e-05|  0.00%|# demo/threads.py:7 func2
    15|         1|  7.62939e-05|  7.62939e-05|  0.01%|t1.join()
(call)|         1|  0.000423908|  0.000423908|  0.04%|# /usr/lib/python2.7/threading.py:653 join
    16|         1|  5.26905e-05|  5.26905e-05|  0.01%|t2.join()
(call)|         1|  0.000320196|  0.000320196|  0.03%|# /usr/lib/python2.7/threading.py:653 join

Zauważ, że ponieważ pprofile nie polega na modyfikacji kodu, może profilować instrukcje modułów najwyższego poziomu, pozwalając na profilowanie czasu uruchamiania programu (jak długo trwa import modułów, inicjalizacja globalnych, ...).

Może generować dane wyjściowe w formacie cachegrind, więc możesz użyć kcachegrind do łatwego przeglądania dużych wyników.

Ujawnienie: jestem autorem profilu.

vpelletier
źródło
1
+1 Dziękuję za Twój wkład. Wygląda na dobrze zrobioną. Mam trochę inną perspektywę - mierzenie czasu włącznie zajmowanego przez instrukcje i funkcje jest jednym z celów. Dowiedzenie się, co można zrobić, aby przyspieszyć kod, to inny cel. Różnica staje się boleśnie oczywista, gdy kod staje się duży - na przykład 10 ^ 6 linii kodu. Kod może marnować dużo czasu. Odnajduję to, pobierając niewielką liczbę bardzo szczegółowych próbek i badając je ludzkim okiem, a nie podsumowując. Problem ujawnia się przez ułamek czasu, który on marnuje.
Mike Dunlavey
1
Masz rację, nie wspomniałem o wykorzystaniu pprofile, gdy chce się profilować mniejszy podzbiór. Zredagowałem swój post, aby dodać przykłady.
vpelletier
3
Właśnie tego szukałem: nieinwazyjny i obszerny.
egpbos
1
Niezłe narzędzie, ale działa kilka razy wolniej niż oryginalny kod.
rominf
4

Możesz skorzystać z pomocy line_profiler pakietu

1. 1. zainstaluj pakiet:

    pip install line_profiler

2. Użyj magicznego polecenia, aby załadować pakiet do środowiska Python / notebook

    %load_ext line_profiler

3. Jeśli chcesz profilować kody dla funkcji,
wykonaj następujące czynności:

    %lprun -f demo_func demo_func(arg1, arg2)

otrzymasz ładne sformatowane wyjście ze wszystkimi szczegółami, jeśli wykonasz te kroki :)

Line #      Hits      Time    Per Hit   % Time  Line Contents
 1                                           def demo_func(a,b):
 2         1        248.0    248.0     64.8      print(a+b)
 3         1         40.0     40.0     10.4      print(a)
 4         1         94.0     94.0     24.5      print(a*b)
 5         1          1.0      1.0      0.3      return a/b
Sateesh
źródło
4

Tylko po to, aby poprawić wyżej wymienioną odpowiedź @Joe Kingtona .

W przypadku Pythona 3.x użyj line_profiler :


Instalacja:

pip install line_profiler

Stosowanie:

Przypuśćmy, że masz program, main.pyaw nim funkcje fun_a()i fun_b()chcesz profilować w czasie; będziesz musiał użyć dekoratora @profiletuż przed definicjami funkcji. Np.

@profile
def fun_a():
    #do something

@profile
def fun_b():
    #do something more

if __name__ == '__main__':
    fun_a()
    fun_b()

Program można sprofilować wykonując polecenie powłoki:

$ kernprof -l -v main.py

Argumenty można pobrać za pomocą $ kernprof -h

Usage: kernprof [-s setupfile] [-o output_file_path] scriptfile [arg] ...

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -l, --line-by-line    Use the line-by-line profiler from the line_profiler
                        module instead of Profile. Implies --builtin.
  -b, --builtin         Put 'profile' in the builtins. Use 'profile.enable()'
                        and 'profile.disable()' in your code to turn it on and
                        off, or '@profile' to decorate a single function, or
                        'with profile:' to profile a single section of code.
  -o OUTFILE, --outfile=OUTFILE
                        Save stats to <outfile>
  -s SETUP, --setup=SETUP
                        Code to execute before the code to profile
  -v, --view            View the results of the profile in addition to saving
                        it.

Wyniki zostaną wydrukowane na konsoli jako:

Total time: 17.6699 s
File: main.py
Function: fun_a at line 5

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    5                                           @profile
    6                                           def fun_a():
...

EDYCJA: Wyniki z profilerów można analizować za pomocą pakietu TAMPPA . Używając go, możemy uzyskać żądane wykresy linia po linii jako wątek

Pe Dro
źródło
1

PyVmMonitor ma podgląd na żywo, który może Ci w tym pomóc (możesz połączyć się z uruchomionym programem i uzyskać z niego statystyki).

Zobacz: http://www.pyvmmonitor.com/

Fabio Zadrozny
źródło