分析 GIL
Profiling the GIL
有没有办法剖析 python 进程对 GIL 的使用?基本上,我想找出 GIL 持有时间的百分比。该进程是单线程的。
我的动机是我有一些用 Cython 编写的代码,它使用 nogil
。理想情况下,我想在多线程进程中 运行 它,但为了知道这是否可能是个好主意,我需要知道 GIL 是否有大量时间空闲。
我在 8 年前找到了 this related question。唯一的答案是 "No"。希望从那时起情况有所改变。
我不知道有这样的工具。
但是有一些试探法可以帮助您猜测使用多线程是否有帮助。正如您可能知道的那样,GIL 将在 IO 操作期间释放,并且一些调用本机代码,尤其是第 3 方本机模块。如果您没有太多这样的代码,那么多线程可能对您没有帮助。
如果您有 IO/native 代码,那么您可能需要尝试一下。根据代码库,将整个事情转换为利用多线程可能需要大量工作,因此您可能想要尝试将多线程应用于您知道 IO/native 代码被调用的部分,并测量到看看你是否有任何改进。
根据您的用例,多处理可能适用于主要 CPU 绑定的情况。多处理确实会增加开销,因此它通常是 CPU 持续相对较长时间(几秒或更长时间)的绑定任务的好方法。
完全是偶然,我发现了一个工具可以做到这一点:gil_load。
实际上是在我发布问题后发布的。
干得好,@chrisjbillington。
>>> import sys, math
>>> import gil_load
>>> gil_load.init()
>>> gil_load.start(output = sys.stdout)
>>> for x in range(1, 1000000000):
... y = math.log(x**math.pi)
[2017-03-15 08:52:26] GIL load: 0.98 (0.98, 0.98, 0.98)
[2017-03-15 08:52:32] GIL load: 0.99 (0.99, 0.99, 0.99)
[2017-03-15 08:52:37] GIL load: 0.99 (0.99, 0.99, 0.99)
[2017-03-15 08:52:43] GIL load: 0.99 (0.99, 0.99, 0.99)
[2017-03-15 08:52:48] GIL load: 1.00 (1.00, 1.00, 1.00)
[2017-03-15 08:52:52] GIL load: 1.00 (1.00, 1.00, 1.00)
<...>
>>> import sys, math
>>> import gil_load
>>> gil_load.init()
>>> gil_load.start(output = sys.stdout)
>>> for x in range(1, 1000000000):
... with open('/dev/null', 'a') as f:
... print(math.log(x**math.pi), file=f)
[2017-03-15 08:53:59] GIL load: 0.76 (0.76, 0.76, 0.76)
[2017-03-15 08:54:03] GIL load: 0.77 (0.77, 0.77, 0.77)
[2017-03-15 08:54:09] GIL load: 0.78 (0.78, 0.78, 0.78)
[2017-03-15 08:54:13] GIL load: 0.80 (0.80, 0.80, 0.80)
[2017-03-15 08:54:19] GIL load: 0.81 (0.81, 0.81, 0.81)
[2017-03-15 08:54:23] GIL load: 0.81 (0.81, 0.81, 0.81)
[2017-03-15 08:54:28] GIL load: 0.81 (0.81, 0.81, 0.81)
[2017-03-15 08:54:33] GIL load: 0.80 (0.80, 0.80, 0.80)
<...>
如果你想知道GIL被取了多少次,你可以使用gdb断点。例如:
> cat gil_count_example.py
import sys
import threading
from threading import Thread
def worker():
k=0
for j in range(10000000):
k+=j
return
num_threads = int(sys.argv[1])
threads = []
for i in range(num_threads):
t = Thread(target = worker)
t.start()
threads.append(t)
for t in threads:
t.join()
对于 3.X 中断 take_gil
> cgdb --args python3 gil_count_example.py 8
(gdb) b take_gil
(gdb) ignore 1 100000000
(gdb) r
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00007ffff7c85f10 in take_gil
at Python-3.4.3/Python/ceval_gil.h:208
breakpoint already hit 1886 times
对于 2.X 中断 PyThread_acquire_lock
> cgdb --args python2 gil_count_example.py 8
(gdb) b PyThread_acquire_lock
(gdb) ignore 1 100000000
(gdb) r
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000039bacfd410
breakpoint already hit 1584561 times
一个高效的穷人分析器也可以用来分析函数花费的时间,我使用https://github.com/knielsen/knielsen-pmp
> ./get_stacktrace --max=100 --freq=10 `/sbin/pidof python2`
...
292 71.92% sem_wait:PyThread_acquire_lock
.
> ./get_stacktrace --max=100 --freq=10 `/sbin/pidof python3`
...
557 77.68% pthread_cond_timedwait:take_gil
有没有办法剖析 python 进程对 GIL 的使用?基本上,我想找出 GIL 持有时间的百分比。该进程是单线程的。
我的动机是我有一些用 Cython 编写的代码,它使用 nogil
。理想情况下,我想在多线程进程中 运行 它,但为了知道这是否可能是个好主意,我需要知道 GIL 是否有大量时间空闲。
我在 8 年前找到了 this related question。唯一的答案是 "No"。希望从那时起情况有所改变。
我不知道有这样的工具。
但是有一些试探法可以帮助您猜测使用多线程是否有帮助。正如您可能知道的那样,GIL 将在 IO 操作期间释放,并且一些调用本机代码,尤其是第 3 方本机模块。如果您没有太多这样的代码,那么多线程可能对您没有帮助。
如果您有 IO/native 代码,那么您可能需要尝试一下。根据代码库,将整个事情转换为利用多线程可能需要大量工作,因此您可能想要尝试将多线程应用于您知道 IO/native 代码被调用的部分,并测量到看看你是否有任何改进。
根据您的用例,多处理可能适用于主要 CPU 绑定的情况。多处理确实会增加开销,因此它通常是 CPU 持续相对较长时间(几秒或更长时间)的绑定任务的好方法。
完全是偶然,我发现了一个工具可以做到这一点:gil_load。
实际上是在我发布问题后发布的。
干得好,@chrisjbillington。
>>> import sys, math
>>> import gil_load
>>> gil_load.init()
>>> gil_load.start(output = sys.stdout)
>>> for x in range(1, 1000000000):
... y = math.log(x**math.pi)
[2017-03-15 08:52:26] GIL load: 0.98 (0.98, 0.98, 0.98)
[2017-03-15 08:52:32] GIL load: 0.99 (0.99, 0.99, 0.99)
[2017-03-15 08:52:37] GIL load: 0.99 (0.99, 0.99, 0.99)
[2017-03-15 08:52:43] GIL load: 0.99 (0.99, 0.99, 0.99)
[2017-03-15 08:52:48] GIL load: 1.00 (1.00, 1.00, 1.00)
[2017-03-15 08:52:52] GIL load: 1.00 (1.00, 1.00, 1.00)
<...>
>>> import sys, math
>>> import gil_load
>>> gil_load.init()
>>> gil_load.start(output = sys.stdout)
>>> for x in range(1, 1000000000):
... with open('/dev/null', 'a') as f:
... print(math.log(x**math.pi), file=f)
[2017-03-15 08:53:59] GIL load: 0.76 (0.76, 0.76, 0.76)
[2017-03-15 08:54:03] GIL load: 0.77 (0.77, 0.77, 0.77)
[2017-03-15 08:54:09] GIL load: 0.78 (0.78, 0.78, 0.78)
[2017-03-15 08:54:13] GIL load: 0.80 (0.80, 0.80, 0.80)
[2017-03-15 08:54:19] GIL load: 0.81 (0.81, 0.81, 0.81)
[2017-03-15 08:54:23] GIL load: 0.81 (0.81, 0.81, 0.81)
[2017-03-15 08:54:28] GIL load: 0.81 (0.81, 0.81, 0.81)
[2017-03-15 08:54:33] GIL load: 0.80 (0.80, 0.80, 0.80)
<...>
如果你想知道GIL被取了多少次,你可以使用gdb断点。例如:
> cat gil_count_example.py
import sys
import threading
from threading import Thread
def worker():
k=0
for j in range(10000000):
k+=j
return
num_threads = int(sys.argv[1])
threads = []
for i in range(num_threads):
t = Thread(target = worker)
t.start()
threads.append(t)
for t in threads:
t.join()
对于 3.X 中断 take_gil
> cgdb --args python3 gil_count_example.py 8 (gdb) b take_gil (gdb) ignore 1 100000000 (gdb) r (gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x00007ffff7c85f10 in take_gil at Python-3.4.3/Python/ceval_gil.h:208 breakpoint already hit 1886 times
对于 2.X 中断 PyThread_acquire_lock
> cgdb --args python2 gil_count_example.py 8 (gdb) b PyThread_acquire_lock (gdb) ignore 1 100000000 (gdb) r (gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x00000039bacfd410 breakpoint already hit 1584561 times
一个高效的穷人分析器也可以用来分析函数花费的时间,我使用https://github.com/knielsen/knielsen-pmp
> ./get_stacktrace --max=100 --freq=10 `/sbin/pidof python2` ... 292 71.92% sem_wait:PyThread_acquire_lock
.
> ./get_stacktrace --max=100 --freq=10 `/sbin/pidof python3` ... 557 77.68% pthread_cond_timedwait:take_gil