如何减少 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.