尝试: Cython 和 inline 优化

3 分钟读完

2018 年 12 月 14 日

学习 Cython 的时候, 出于好奇对比了一下 Cython 和 ctypes 调用 C 编写动态链接库的效率, 有一些发现. 运行的 C 代码是:

/* int1.c */
double f(double x){
	return x * ( x - 1.0);
}

double integrate(double a, double b, int N){
	int i;
	double tot = 0;
	double step = (b - a)/N;
	for(i=0; i<N; i++){
		tot += f(a + i*step);
	}
	return tot;
}

double integrateRep(double a, double b, int N, int rep) {
	double tot=0;
	int i;
	for(i=0; i<rep; i++) {
		/* tot += integrate(a, b, N); */
		double step = (b - a)/N;
		int j;
		for(j=0; j<N; j++){
			tot += f(a + i*step);
		}
	}
	return tot/rep;
}

Cython 则是:

# intpy.pyx
cdef double f(double x):
    return x * (x - 1.0)

def integrate(double a, double b, int N):
    cdef int i
    cdef double tot, dx
    tot = 0.0
    dx = (b - a) / N
    for i in range(N):
        tot += f(a + i*dx)
    return tot

def integrateRep(double a, double b, int N, int rep):
    cdef int i
    cdef double tot
    tot = 0.0
    for i in range(rep):
        tot += integrate(a, b, N)
    return tot/rep

为了避免载入时间上的影响, 把循环写在 C 里面了. 计时:

# timeit testing
import timeit
setup = """
import intpy
from ctypes import CDLL, c_double
clib1 = CDLL("./int1.so")
"""
parm = (0, 5, 1000, 5000000)

if __name__ == "__main__":
    print("test cython")
    print("  Result = ", timeit.timeit(
        "intpy.integrateRep(%f, %f, %d, %d)"%parm,
        setup = setup, number = 1))
    print("test C Lib V1")
    print("  Result = ", timeit.timeit(
        "clib1.integrateRep(c_double(%f), c_double(%f), %d, %d)"%parm,
        setup = setup, number = 1))

如上的结果竟然是 Cython 快了近一倍…! 为了弄明白是什么情况, 手工编译 Cython 使用相同的优化策略:

# Makefile
CFLAGS += -shared -fPIC -Ofast

all : int1.so intpy.so

intpy.c : intpy.pyx
	cython -3 $^

intpy.so : intpy.c
	$(CC) $(CFLAGS) -pthread -I/usr/include/python3.7m -o [email protected] $^

%.so : %.c
	$(CC) $(CFLAGS) -o [email protected] $^

经过一些尝试, 发现如果把 f 函数在 C 里面定义为内联函数, 或者宏, C 库立马就和 Cython 速度快了 (快得不多). 纯 C 当然还是最快的, 即便不使用内联定义.

记得曾经看到别人说, 不需要自己声明内联, 因为编译器几乎总是能做出最好的判断来进行内联化的优化. 不过显然在 Linux 下, 编译为动态链接库时, 内联优化并没有启动, 即便 -O3-Ofast 都包含了内联的 flag.

比较让人讨厌的是, 手工加入 inline 关键词会让编译出来的 .so 文件失去相应的接口…

留下评论