如何减少 C API 和 Python 可执行文件之间的执行时间差异?
How to reduce execution time differences between C API and Python executable?
运行 相同的 python 脚本使用 python3
或通过嵌入式解释器使用 libpython3
给出不同的执行时间。
$ time PYTHONPATH=. ./simple
real 0m6,201s
user 1m3,680s
sys 0m0,212s
$ time PYTHONPATH=. python3 -c 'import test; test.run()'
real 0m5,193s
user 0m53,349s
sys 0m0,164s
(删除运行之间__pycache__
的内容似乎没有影响)
目前,使用脚本调用python3
速度更快;在我的实际用例中,与嵌入式解释器中的相同脚本 运行 相比,该因子快 1.5。
我想 (1) 了解差异从何而来以及 (2) 是否可以使用嵌入式解释器获得相同的性能? (目前不能使用例如 cython)。
代码
simple.cpp
#include <Python.h>
int main()
{
Py_Initialize();
const char* pythonScript = "import test; test.run()";
int result = PyRun_SimpleString(pythonScript);
Py_Finalize();
return result;
}
编译:
g++ -std=c++11 -fPIC $(python3-config --cflags) simple.cpp \
$(python3-config --ldflags) -o simple
test.py
import sys
sys.stdout = open('output.bin', 'bw')
import mandel
def run():
mandel.mandelbrot(4096)
mandel.py
来自 benchmarks-game's Mandlebrot (see License)
的调整版本
from contextlib import closing
from itertools import islice
from os import cpu_count
from sys import stdout
def pixels(y, n, abs):
range7 = bytearray(range(7))
pixel_bits = bytearray(128 >> pos for pos in range(8))
c1 = 2. / float(n)
c0 = -1.5 + 1j * y * c1 - 1j
x = 0
while True:
pixel = 0
c = x * c1 + c0
for pixel_bit in pixel_bits:
z = c
for _ in range7:
for _ in range7:
z = z * z + c
if abs(z) >= 2.: break
else:
pixel += pixel_bit
c += c1
yield pixel
x += 8
def compute_row(p):
y, n = p
result = bytearray(islice(pixels(y, n, abs), (n + 7) // 8))
result[-1] &= 0xff << (8 - n % 8)
return y, result
def ordered_rows(rows, n):
order = [None] * n
i = 0
j = n
while i < len(order):
if j > 0:
row = next(rows)
order[row[0]] = row
j -= 1
if order[i]:
yield order[i]
order[i] = None
i += 1
def compute_rows(n, f):
row_jobs = ((y, n) for y in range(n))
if cpu_count() < 2:
yield from map(f, row_jobs)
else:
from multiprocessing import Pool
with Pool() as pool:
unordered_rows = pool.imap_unordered(f, row_jobs)
yield from ordered_rows(unordered_rows, n)
def mandelbrot(n):
write = stdout.write
with closing(compute_rows(n, compute_row)) as rows:
write("P4\n{0} {0}\n".format(n).encode())
for row in rows:
write(row[1])
很明显,时间差来自于静态链接和动态链接 libpython
。在位于 python.c
旁边的 Makefile 中(来自参考实现),以下构建解释器的静态链接版本:
snake: python.c
g++ \
-I/usr/include/python3.6m \
-pthread \
-specs=/usr/share/dpkg/no-pie-link.specs \
-specs=/usr/share/dpkg/no-pie-compile.specs \
\
-Wall \
-Wformat \
-Werror=format-security \
-Wno-unused-result \
-Wsign-compare \
-DNDEBUG \
-g \
-fwrapv \
-fstack-protector \
-O3 \
\
-Xlinker -export-dynamic \
-Wl,-Bsymbolic-functions \
-Wl,-z,relro \
-Wl,-O1 \
python.c \
/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu/libpython3.6m.a \
-lexpat \
-lpthread \
-ldl \
-lutil \
-lexpat \
-L/usr/lib \
-lz \
-lm \
-o $@
将行 /usr/lib/.../libpython3.6m.a
更改为 -llibpython3.6m
构建最终变慢的版本(还需要 -L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu
)
结语
存在速度差异,但不是我原来问题的完整答案;实际上,"slower" 解释器是在特定的 LD_PRELOAD 环境下执行的,该环境改变了系统时间函数的行为方式,与 cProfile.
运行 相同的 python 脚本使用 python3
或通过嵌入式解释器使用 libpython3
给出不同的执行时间。
$ time PYTHONPATH=. ./simple
real 0m6,201s
user 1m3,680s
sys 0m0,212s
$ time PYTHONPATH=. python3 -c 'import test; test.run()'
real 0m5,193s
user 0m53,349s
sys 0m0,164s
(删除运行之间__pycache__
的内容似乎没有影响)
目前,使用脚本调用python3
速度更快;在我的实际用例中,与嵌入式解释器中的相同脚本 运行 相比,该因子快 1.5。
我想 (1) 了解差异从何而来以及 (2) 是否可以使用嵌入式解释器获得相同的性能? (目前不能使用例如 cython)。
代码
simple.cpp#include <Python.h>
int main()
{
Py_Initialize();
const char* pythonScript = "import test; test.run()";
int result = PyRun_SimpleString(pythonScript);
Py_Finalize();
return result;
}
编译:
g++ -std=c++11 -fPIC $(python3-config --cflags) simple.cpp \
$(python3-config --ldflags) -o simple
test.py
import sys
sys.stdout = open('output.bin', 'bw')
import mandel
def run():
mandel.mandelbrot(4096)
mandel.py
来自 benchmarks-game's Mandlebrot (see License)
的调整版本from contextlib import closing
from itertools import islice
from os import cpu_count
from sys import stdout
def pixels(y, n, abs):
range7 = bytearray(range(7))
pixel_bits = bytearray(128 >> pos for pos in range(8))
c1 = 2. / float(n)
c0 = -1.5 + 1j * y * c1 - 1j
x = 0
while True:
pixel = 0
c = x * c1 + c0
for pixel_bit in pixel_bits:
z = c
for _ in range7:
for _ in range7:
z = z * z + c
if abs(z) >= 2.: break
else:
pixel += pixel_bit
c += c1
yield pixel
x += 8
def compute_row(p):
y, n = p
result = bytearray(islice(pixels(y, n, abs), (n + 7) // 8))
result[-1] &= 0xff << (8 - n % 8)
return y, result
def ordered_rows(rows, n):
order = [None] * n
i = 0
j = n
while i < len(order):
if j > 0:
row = next(rows)
order[row[0]] = row
j -= 1
if order[i]:
yield order[i]
order[i] = None
i += 1
def compute_rows(n, f):
row_jobs = ((y, n) for y in range(n))
if cpu_count() < 2:
yield from map(f, row_jobs)
else:
from multiprocessing import Pool
with Pool() as pool:
unordered_rows = pool.imap_unordered(f, row_jobs)
yield from ordered_rows(unordered_rows, n)
def mandelbrot(n):
write = stdout.write
with closing(compute_rows(n, compute_row)) as rows:
write("P4\n{0} {0}\n".format(n).encode())
for row in rows:
write(row[1])
很明显,时间差来自于静态链接和动态链接 libpython
。在位于 python.c
旁边的 Makefile 中(来自参考实现),以下构建解释器的静态链接版本:
snake: python.c
g++ \
-I/usr/include/python3.6m \
-pthread \
-specs=/usr/share/dpkg/no-pie-link.specs \
-specs=/usr/share/dpkg/no-pie-compile.specs \
\
-Wall \
-Wformat \
-Werror=format-security \
-Wno-unused-result \
-Wsign-compare \
-DNDEBUG \
-g \
-fwrapv \
-fstack-protector \
-O3 \
\
-Xlinker -export-dynamic \
-Wl,-Bsymbolic-functions \
-Wl,-z,relro \
-Wl,-O1 \
python.c \
/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu/libpython3.6m.a \
-lexpat \
-lpthread \
-ldl \
-lutil \
-lexpat \
-L/usr/lib \
-lz \
-lm \
-o $@
将行 /usr/lib/.../libpython3.6m.a
更改为 -llibpython3.6m
构建最终变慢的版本(还需要 -L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu
)
结语
存在速度差异,但不是我原来问题的完整答案;实际上,"slower" 解释器是在特定的 LD_PRELOAD 环境下执行的,该环境改变了系统时间函数的行为方式,与 cProfile.