背景:
将python实现的算法转写为C++后,希望使用python调用C++实现的算法,涉及到python与C++的混合编程。这里对python和C++混合编程的多种方案进行调研。
简介
众所周之,Python的运行速度是很慢的,特别是大数据量的运算时,Python会慢得让人难以忍受。对于这种情况,“专业”的解决方案是用上numpy或者opencl。不过有时候为了一点小功能用上这种重型的解决方案很不划算,或者有时候想要实现的操作在numpy里面没有,需要我们自己用C语言来编写。总之,我们使用Python与C++的混合编程能够加快程序热点的运算速度。
首先要提醒大家注意的是,在考虑联合编程之前一定要找到程序运行的热点。简单一点地,使用标准库的profile或者cProfile模块找到最消耗CPU的位置,如果这个位置只简单的消耗IO时间,通常换成C++程序的意义也不会很大,此时做联合编程可能是事倍功半,起不到多大的效果。
还有些情况,Python程序员们想要使用操作系统或者外部模块提供的函数。这些模块一般是为C/C++程序员提供的。这时候也是Python与C++联合编程的用武之地,Python社区提供了以下几种解决方案:
1. ctyeps
ctypes是Python标准库支持的方案,直接在Python脚本中导入C的.so库进行调用,简单直接。
使用标准库ctypes直接调用C/C++编写的动态链接库。这是最简单易用的方案。C/C++程序员使用自己的丰富的经验,把预定的功能实现为动态链接库。而Python程序员只要知道这些动态链接库函数的名称、参数类型与返回值类型就能简单地调用它。当你传入参数时,ctypes模块会自动地把Python的对象成为C/C++所对应的参数类型。比如以下调用Windows的API:
#定义参数类型与函数名称
from ctypes.wintypes import UINT, DWORD
GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo
class LASTINPUTINFO(ctypes.Structure):
_fields_ = [("cbSize", UINT),
("dwTime", DWORD)]
#开始调用DLL导出的函数
def getLastInputTime_nt():
info = LASTINPUTINFO()
info.cbSize = ctypes.sizeof(info)
info.dwTime = 0
if not GetLastInputInfo(ctypes.byref(info)):
raise WindowsError("")
return info.dwTime
在这里展示了如何构造Windows的API所需要的结构体,如何填充结构体并分析返回值。
ctypes还能将Python函数提供给C/C++代码作为回调函数。
优点:
- 与其它解决方案相比。ctypes不需要程序员熟悉C/C++语言,不需要安装一个C/C++的编译器,它通过操作系统的接口直接操作C/C++代码。而且ctypes是标准库的一部分,只要安装了Python就可以直接使用。这几个原因使得它深受Python程序员的喜爱。
缺点:
- 首先,ctypes不能简单调用C++程序,因为C++在编译的时候使用了name mangling这个技术来实现函数的重载。C++会自动地为类的成员函数加上类名前缀。所以,C++程序员需要以C语言的调用约定来提供接口,没有类,没有重载函数,没有模板,没有C++异常。不能直接调用现有的C++代码可能是这个方案最大的缺点。
- 另外,对于list, set之类的数据类型,ctypes不能识别并自动地在Python与C/C++数据类型之间转换。C/C++部分不能识别Python数据类型,这时候只能用Python语言来编写转换代码。如果数据量较大,或者调用很频繁,转换代码反而会浪费很多的资源。这或许是ctypes的另一个劣势之一了。
2. cython
cython是numpy,scipy,scikit-learn等的封装方式;Cpython是官方的,但是官方都不建议使用了,
swig, pybind11是tensorflow的封装方式;
swig对结构体和回调函数都要使用typemap进行手工编写转换规则,typemap规则写起来略复杂,体验不是很好。cython在结构体和回调上也要进行手工编码处理,不过比较简单。
使用Cython语言,一种类似于Python语言的一种新型语言编写预定功能的代码,然后将这些代码转换成为C语言编译成为Python语言可以直接调用的二进制模块。Cython语言是融合Python语言与C语言的一种新型语言。它本身能够理解Python语言的语法,然后在其基础上增加了某些C语言的语法,以便更精细地控制数据类型与指针。基本兼容Python语法是这个解决方案最大的特点。很多时候,Python程序员只要在旧的代码中简单地声明一下代码中所使用的参数、变量的类型,就能把立即为旧的Python程序提速。
Cython提供了一个名为pyximporter的工具,能够在安装了C/C++编译器的计算机上面为简单的Cython程序直接生成相应的Python模块。这使得Cython的使用与普通的Python程序一样简单。比如下面这段代码,直接保存为myhello.pyx即可被调用。
#myhello.pyx
def sayHelloTenTimes():
cdef int i #只要简单地为变量标识类型即可加速循环。
for i in range(0, 10):
print("hello, world!")
$ python
>>> import pyximport; pyximport.install()
>>> import myhello
>>> myhello.sayHelloTenTimes()
优点:
- Cython非常容易使用。而且不仅能够处理C语言的模块,还能处理C++的模块——虽然没有直接支持虚函数之类的完整C++特性。因为它不直接使用C/C++语法,而是另外设计比C/C++更简洁优雅的新型语法,因此,对于不熟悉C/C++的程序员来说有很大的吸引力。相比ctypes来说,因为参数类型转换更加智能与高效,所以通常能够提升更多的效率。
缺点:
- 所谓用Python程序员所熟练的语法来编写高速的运算代码,乍一听相当地有吸引力。但是如果想要更深入地控制内存与数据结构时,程序员可能会发现,现在他不得不熟练地掌握C/C++语言,然后用Cython的语法写出来。以程序员们懒惰的性格,这反而是件难以忍受的事件。这或许是Cython本身并不大流行的主要原因吧。
cython这种Python的补丁抽象能力没有完整的C++好,对于一个倾向于只让Python成为傻瓜式接口的人,我更希望能够同时在C++层面有丰富的抽象来方便developer。
更多简介见简书
Cython 3.0 中文文档
cython documentation
3. pytran
Python->C++的转换编译器(transpiler),可以把Python的prototype转到C++,一定程度上减轻编写C++的痛点。
和Cython一样,它的目的也是提高Python代码的执行效率,但是它们在实现方法上有本质的不同。
Pythran的源代码
Pythran图示
离线训练完成,模型上线部署后,同样要用 C++ 重新实现 这些 python 的特征工程逻辑代码。我们发现,“用 C++ 重新实现” 这个步骤,给实际业务带来了大量的问题:
- 繁琐,费时费力,极容易出现 python 和 C++ 代码不一致
- 不一致会直接影响模型在线上的效果,导致大盘业务指标不如预期,产生各种 bad case
- 不一致难以发现,无法测试,无法监控,经常要靠用户投诉反馈,甚至大盘数据异常才能发现
“模型线上线下一致性问题对于模型效果非常重要。
自动翻译方案
ref: https://zhuanlan.zhihu/p/95349005
这个问题归根结底就是需要一个 “ python 到 c++ 的翻译器 ” ,发现 python 到 C++ 的翻译器也不少,其中 Pythran 是新兴比较热门的开源项目。于是一番尝试后,借助 pythran,实现了:
- 一条命令 全自动把 Python 翻译成等价 C++
- 严格等价保证改写,彻底消除不一致
- 完全去掉重新实现 这块工作量,后台开发成本降到 0 ,彻底解放生产力
- 算法同学继续使用纯 python,开发效率无影响,** 无学习成本 **
- 并能推广到其他需要 python 改写成后台 C++ 代码 的业务场景,解放生产力
然而局限性:
- python之类的自动转C++基本不用想了。除非在python基本严格按照C++写完成所有代码,那不如直接用C++写了。
- C++的智能指针和函数指针,我是想不出来python怎么转换。matlab也有这种功能,不过效果太差不实用。
- 翻译器翻译的代码可维护性差。
- 翻译器因为语法受限,不是按照编译器的测试集来搞,多多少少都会有点bug,往往经不住算法组的同学各种花式写法,debug成本相当高。能完全按照限制规则写python的同学,算上debug时间其实用C写个相同的模块速度更快。
Pythran as a Numpy backend
pytran doc
pytran user manul
4. CFFE
5. swig
一个通用的让高级脚本语言扩展支持C的工具。
swig 官网
SWIG is used in a number of ways:
-
Building more powerful C/C++ programs. Using SWIG, you can replace the main() function of a C program with a scripting interpreter from which you can control the application. This adds quite a lot of flexibility and makes the program “programmable.” That is, the scripting interface allows users and developers to easily modify the behavior of the program without having to modify low-level C/C++ code. The benefits of this are numerous. In fact think of all of the large software packages that you use every day—nearly all of them include special a macro language, configuration language, or even a scripting engine that allows users to make customizations.
-
Rapid prototyping and debugging. SWIG allows C/C++ programs to be placed in a scripting environment that can be used for testing and debugging. For example, you might test a library with a collection of scripts or use the scripting interpreter as an interactive debugger. Since SWIG requires no modifications to the underlying C/C++ code, it can be used even if the final product does not rely upon scripting.
-
Systems integration. Scripting languages work fairly well for controlling and gluing loosely-coupled software components together. With SWIG, different C/C++ programs can be turned into scripting language extension modules. These modules can then be combined together to create new and interesting applications.
-
Construction of scripting language extension modules. SWIG can be used to turn common C/C++ libraries into components for use in popular scripting languages. Of course, you will still want to make sure that no-one else has already created a module before doing this.
SWIG is sometimes compared to interface definition language (IDL) compilers such as those you find with systems such as CORBA and COM. Although there are a few similarities, the whole point of SWIG is to make it so you don’t have to add an extra layer of IDL specifications to your application. If anything, it’s much more of a rapid application development and prototyping tool. Specifically:
ISO C/C++ syntax. SWIG parses ISO C++ that has been extended with a number of special directives. As a result, interfaces are usually built by grabbing a header file and tweaking it a little bit. This particular approach is especially useful when the underlying C/C++ program undergoes frequent modification.
-
SWIG is not a stub generator. SWIG produces code that you simply compile and run. You don’t have to fill in any stubs or write special client/server code as you do with RPC-like systems.
-
SWIG does not define a protocol nor is it a component framework. SWIG does not define mechanisms or enforce rules regarding the way in which software components are supposed to interact with each other. Nor is it a specialized runtime library or alternative scripting language API. SWIG is merely a code generator that provides the glue necessary to hook C/C++ to other languages.
-
Designed to work with existing C/C++ code. SWIG requires little, if any, modifications to existing code. For the most part, it encourages you to keep a clean separation between C/C++ and its scripting interface.
-
Extensibility. SWIG provides a variety of customization options that allow you to blow your whole leg off if that’s what you want to do. SWIG is not here to enforce programming morality.
Finally, it is worth noting that even though SWIG is occasionally compared to other more specialized scripting language extension building tools (e.g., Perl XS, Python bgen, etc.), its primary audience is C/C++ programmers who want to add a scripting language component to their applications. Because of this, SWIG tends to have a slightly different focus than tools designed to build small modules for widespread use in a scripting language distribution. applications.
特点
swig对结构体和回调函数都要使用typemap进行手工编写转换规则,typemap规则写起来略复杂,体验不是很好。cython在结构体和回调上也要进行手工编码处理,不过比较简单。
swig自动化程度特别高,include头文件即可,封装做的好,而且可以同时产出Android, C#, D, Go, Guile, Java, Javascript, Lua, Octave, Perl5, PHP, Python, R, Ruby, Scilab, Tcl, MzScheme/Racket, OCaml等接口。如果需要产出多种脚本语言的binding、需要导出的接口比较多、或者希望自动化完成尽量使用swig。
pybind11灵活性更高,手撸接口比较方便,如没有自动化产出多种语言接口的需求,尽量选择pybind11.
pybind11入手简单许多。swig的门槛过高了。
可以网上参考教程写个example试一下,你就能理解使用swig的痛苦之处。
使用SWIG或者SIP,通过编写一个接口文件,使用类似于C/C++语法——声明函数、类型的信息,然后使用特殊的工具为C/C++的代码生成Python的接口代码。这些接口代码能够在Python与C/C++之间的数据结构转换。最终编译这些接口代码,成为Python的二进制模块。SWIG与SIP的接口文件与C/C++的头文件非常相似。这两种工具差不多,因为本质上他们都与Cython类似,都使用了中间语言来生成转换代码。但SWIG/SIP能够在他们的接口文件中嵌入C/C++,能够让程序员仔细地调节数据类型的转换过程。在使用上,它比Cython的层次更低,更接近于Python本身提供的API。
SWIG能够为多种脚本语言生成转换代码。而SIP则专门针对Python与C++。此外,SIP本身是作为PyQt的专门工具来开发的,因此它能够理解Qt的signal/slot。从应用项目上来看,SWIG似乎会更广泛一点。而SIP,目前所见的项目基本都与PyQt相关。据说SWIG对于C++的支持不好,不知道有没有人来说一下呢。相比之下,SIP对于C++的支持非常完善,诸如虚函数、protected member function、模版、析构函数、异常等特性都得到良好的支持。而且SIP支持Python的GIL,还拥有一个使用Python编写的编译系统。可能会更方便一点。
然而这种方案毕竟要学习一种新的语言,所以从表面上来看不如Cython和boost.python讨喜。当程序员想要仔细地调节类型转换代码的时候,需要学习SWIG/SIP的内部机制,被限定使用特殊的变量名。这使得这种方案的学习曲线相对较高。
5.2 tensorflow的接口使用swig实现的,不过现在Tensorflow已经从SWIG切到pybind11了
This document describes a system to improve:
- code readability
- build times
- binary size
- performance of the Python API
What is pybind11
Pybind11 is a header-only library that exposes C++ functions and classes to Python to create Python bindings of existing C++ code. Pybind11 is lightweight, and unlike other libraries that serve a similar purpose, it is purely focussed on binding. Pybind11 is relatively new, but is rapidly growing in contributors and adopters. It’s an adaptation of Boost.Python which is quite reputable, but has a lot of extra libraries. Pybind11 is basically the only the binding generation portions of Boost.Python.
Cons with SWIG
- SWIG auto-generated code is not optimal for performance.
- SWIG needs the swig toolchain.
- SWIG requires .i or .swig files which require knowledge of SWIG directives.
- We’ve changed .swig to .i files to get around readability issues
- SWIG combined with C++ is relatively complicated to understand.
Pro/Cons with pybind11
Pros: | Cons: |
---|---|
No need to link against any additional libraries | Less support |
Function signatures are precomputed at compile time which leads to smaller binaries | Our cases may be more complicated than their examples |
No auto-generated code | Heavy reliance on another third party library |
Much simpler and easier to debug |
6. pybind11
tensorflow的接口使用swig实现的,不过现在Tensorflow已经从SWIG切到pybind11了
pybind11本质上还是在C API外面包了一层C++(或者说利用C++的元编程能力批量产生binding)所以可定制性很好,尤其是面向我这种有非主流需求的用户。
pybind11灵活性更高,手撸接口比较方便,如没有自动化产出多种语言接口的需求,尽量选择pybind11.
pybind11入手简单许多。swig的门槛过高了。
pybind11 可实现 C++11 和 Python 之间的无缝操作。
pybind11 是一个轻量级的只包含一组头文件的 C++ 库,可以在 Python 中使用 C++ 类型。主要用于创建已有 C++ 代码的 Python 封装版本。其目的和语法类似于 Boost.Python 库。为什么要创建这个项目的原因就是因为 Boost 。作者认为 Boost 很大很复杂。而目前的 C++11 兼容的编译器使用已经非常广泛,所以希望开发一个更轻量级更具备兼容性的项目。
除去注释之外,该项目的核心头文件只有 2500 行左右代码,依赖于 Python (2.7 或者 3.x) 和标准 C++ 库。这么精简的实现有赖于新的 C++11 语言特性。特别是元组、Lambda 函数以及可变模板。自从项目创建以来,其增长已经超过了 Boost.Python。
6.1 示例
ref:
一个简单地使用教程
https://www.jianshu/p/fcded412d0bb
https://www.jianshu/p/fcded412d0bb
定义一个简单的类
class Student {
public:
std::string name;
std::string gender;
int age;
public:
Student() {}
Student(std::string name, std::string gender, int age) {
this->name = name;
this->gender = gender;
this->age = age;
}
~Student() {};
public:
//setter
void setName(std::string name) {
this->name = name;
}
void setGender(std::string gender) {
this->gender = gender;
}
void setAge(int age) {
this->age = age;
}
//getter
std::string getName() {
return this->name;
}
std::string getGender() {
return this->gender;
}
int getAge() {
return this->age;
}
};
编写python扩展
PYBIND11_MODULE(example, m) {
// Studnet
py::class_<Student>(m, "Student")
.def(py::init<std::string, std::string, int>())
.def("setName", &Student::setName)
.def("setGender", &Student::setGender)
.def("setAge", &Student::setAge)
.def("getName", &Student::getName)
.def("getGender", &Student::getGender)
.def("getAge", &Student::getAge);
}
编译生成python扩展
测试代码
推荐教程:
官方docs: pybind11
7. boost.python
使用boost.python。有意思的是,与ctypes/Cython形成鲜明的对比,boost.python倾向于让C++程序员拥有更熟悉的编程环境。它让C++程序员使用他所熟悉的C++语法直接控制Python的数据结构,调用Python的解释器。它没有像Cython那样发明新的语法,而是直接使用C++的语法,编写供Python使用的接口。与Cython同样的道理,它的效率优胜于ctypes。
与Cython/SWIG/SIP等方案相比,程序员只需要学习C/C++与Python两种语言。另外,与本文提到的几种解决方案相比,它非常适合在主要由C++编写的程序中控制Python代码。不仅功能更强大、效率还更高。如此神奇的解决方案会有什么劣势呢?某些人可能不同意吧,老鱼一听说它依赖于boost就蔫了,感觉编译与学习庞大又奇怪的boost非常浪费生命。
官方doc
Synopsis
Welcome to Boost.Python, a C++ library which enables seamless interoperability between C++ and the Python programming language. The library includes support for:
- References and Pointers
- Globally Registered Type Coercions
- Automatic Cross-Module Type Conversions
- Efficient Function Overloading
- C++ to Python Exception Translation
- Default Arguments
- Keyword Arguments
- Manipulating Python objects in C++
- Exporting C++ Iterators as Python Iterators
- Documentation Strings
The development of these features was funded in part by grants to Boost Consulting from the Lawrence Livermore National Laboratories and by the Computational Crystallography Initiative at Lawrence Berkeley National Laboratories.
8. 直接使用Python的API
可以称之为最终解决方案。Cython, SWIG, SIP的接口文件转换后所生成的C/C++代码实际上都使用Python的API。与其它方案相比,这种方案相当地繁复,必须为每次函数调用编写数据转换代码,还要操心Python对象的引用计数。我觉得这种方案一无是处,这时就不再多讲了。其它的工具pybindgen不知道什么情况。有兴趣的话可以看看。
ref
https://blog.csdn/tanmx219/article/details/86666456
https://www.zhihu/question/323926607
https://www.jianshu/p/fc5025094912
http://www.linuxdiyf/linux/23987.html
https://zhuanlan.zhihu/p/95349005
更多推荐
python/C++ 混合编程__方案调研
发布评论