Python是一门动态解释型语言,由于GIL、GC机制等特性,python运算效率很低。
同时由于python程序没有静态编译的过程,项目代码以py源码的方式交付,任何人都可以获取和修改源代码,无法做到相应的安全保护。
针对这两个问题,可以将Python源代码编译生成C/C++扩展模块的形式(Windows平台生成.pyd文件,Linux生成.so文件),以动态链接库的方式调用。
python调用C/C++扩展函数的时候,GIL会被锁定,直到这个函数结束。由于这期间没有python的字节码被运行,所以线程不会切换,如果在扩展模块中调用阻塞式地I/O函数,python中的多线程机制就形同虚设。
Cython
使用cython编译加速python代码,cython是一门编程语言,属于python的超集,也是一个编译器,负责将cython源码(或python源码)翻译成高效的C/C++源码,最终编译成python的扩展模块,经过cython翻译编译后,程序执行效率会有明显提升。
cython先将python转为对应的C代码,然后(用gcc)编译成so(pyd),但这个so有一些特殊:
- 这个so由cython生成的C代码编译而成,可能添加了cython标注的一些静态性能优化的代码。
- 使用这个so需要依赖python vm环境(不管是程序本身就以python vm启动,还是从C或其他环境调用)
Note: 纯python源码被cython编译后,因为没有使用类型标注等cython语法,故编译后的动态链接库和可执行文件,主要依赖python的运行时,并不依赖C/C++运行时,主要由python解释器执行,多线程GIL的问题同样存在,性能提升有限,但对for循环、大的列表对象遍历性能有明显优化效果。
使用方法
先用cython将python语言代码转换为c语言代码,然后用c编译器(gcc)生成可执行文件或动态链接库。
通过shell或python脚本的方式,将项目启动的入口py编译成可执行文件,将项目的其他.py文件编译成.so(__init__.py除外)
Note:
__init__.py文件定义了python的包结构,为了使cython编译后的.so能按照正常路径import,__init__.py不能被编译,故为了保护代码,整个项目的所有__init__.py文件不建议放业务相关代码。
单个文件的编译示例
将启动main.py编译成二进制可执行文件main
# 可通过文件头的 #!/usr/bin/env python 标记是否程序启动文件
# step1: 将python代码翻译成c代码(main.py -> main.c)
cython -D -2 --directive always_allow_keywords=true --embed main.py
# step2: 将c代码编译为目标文件(main.c -> main.o)
gcc -c -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python2.7 -o main.o main.c
# step3: 将目标文件编译为二进制可执行文件(main.o -> main)
gcc -I/usr/include/python2.7 -o main main.o -lpython2.7
将test.py编译为动态链接库test.so
# step1: 将python代码翻译成c代码(test.py -> test.c)
cython -D -2 --directive always_allow_keywords=true test.py
# step2: 将c代码编译为linux动态链接库文件(test.c -> test.so)
gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python2.7 -o test.so test.c
cython参数说明;
cython参数说明:
-D, --no-docstrings, Strip docstrings from the compiled module.
-o, --output-file <filename> Specify name of generated C file
-2 Compile based on Python-2 syntax and code semantics.
-3 Compile based on Python-3 syntax and code semantics.
gcc参数说明
-shared:
编译动态库时要用到
-pthread:
在Linux中要用到多线程时,需要链接pthread库
-fPIC:
作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),
则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意
位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
-fwrapv:
它定义了溢出时候编译器的行为——采用二补码的方式进行操作
-O参数
这是一个程序优化参数,一般用-O2就是,用来优化程序用的
-O2:
会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。
-O3: 在O2的基础上进行更多的优化
-Wall:
编译时 显示Warning警告,但只会显示编译器认为会出现错误的警告
-fno-strict-aliasing:
“-fstrict-aliasing”表示启用严格别名规则,“-fno-strict-aliasing”表示禁用严格别名规则,当gcc的编译优化参数为“-O2”、“-O3”和“-Os”时,默认会打开“-fstrict-aliasing”。
-I (大写的i):
是用来指定头文件目录
-I /home/hello/include表示将/home/hello/include目录作为第一个寻找头文件的目录,寻找的顺序是:/home/hello/include-->/usr/include-->/usr/local/include
-l:
-l(小写的 L)参数就是用来指定程序要链接的库,-l参数紧接着就是库名,把库文件名的头lib和尾.so去掉就是库名了,例如我们要用libtest.so库库,编译时加上-ltest参数就能用上了
整个python项目编译示例
Usage
$ python build.py -h
usage: build.py [-h] [-c] [-f CONFIG] [-s SRC_DIR] [-o OUTPUT_DIR]
[-e EXE_FILES] [-x EXCLUDE_FILES]
optional arguments:
-h, --help show this help message and exit
-c, --using-config whether to use a configuration file
-f CONFIG, --config-file CONFIG
config file path for compile
-s SRC_DIR, --src-dir SRC_DIR
src file dir
-o OUTPUT_DIR, --output-dir OUTPUT_DIR
output file dir
-e EXE_FILES, --exe-files EXE_FILES
the executable file list, separate by comma.
-x EXCLUDE_FILES, --exclude-files EXCLUDE_FILES
the exclude file list, separate by comma.
Example
# build.yaml
compile:
src_dir: '/home/test/src'
output_dir: '/home/test/output'
exe_files:
- '{src_dir}/runserver.py'
exclude_files:
- '{src_dir}/uwsgi.py'
使用配置文件
python build.py -c -f ./build.yaml
或者
python build.py -s /home/test/src -o /home/test/output -e {src_dir}/runserver.py -x {src_dir}/uwsgi.py