Cython 的一些测试
开头语
这篇博客本质上是对Cython探索的一些记录,当然仅供周末空闲时间的一些娱乐。
基础介绍
这一段主要讲一下为什么我要做这么一个测试。主要原因是最近在工作中做了一些检测方面的内容,因为生产环境没有GPU,因此把所有模型都往CPU上挪了,但是除了模型部分的网络加速外,检测还有一些后处理比较费是时间,因此就想尝试一下是否可以对这一部分纯Python实现的内容进行加速。
这里主要针对的部分是PriorBox的生成(对,你没有看错!不是NMS
就是PriorBox
!😂)
实验设计
-
先对一下四个部分进行实验:
- 纯Python实现的
PriorBox
。 - 使用Cython直接对
PriorBox
类进行封装,重命名类名为PriorBoxCython
。 - 对2部分的类里的一些变量进行预定义,重命名类名为
PriorBoxCythonOptimized
。 - 使用Cpp对
PriorBox
重新构造,并使用cython进行编译,生成PriorBoxCpp
共享库。
- 纯Python实现的
-
然后对四个部分的内容的耗时和内存消耗进行比对
-
做一些分析和讨论
实验代码实现
-
纯Python的实现来自Pytorch_Retinaface仓库下的实现,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
# coding=utf-8 # file: priorbox.py # author: jwxie/jwxie.cn from itertools import product as product from math import ceil import torch class PriorBox(object): def __init__(self, cfg, image_size=None, phase='train'): super(PriorBox, self).__init__() self.min_sizes = cfg['min_sizes'] self.steps = cfg['steps'] self.clip = cfg['clip'] self.image_size = image_size self.feature_maps = [[ ceil(self.image_size[0] / step), ceil(self.image_size[1] / step)] for step in self.steps] self.name = "s" def forward(self): anchors = [] for k, f in enumerate(self.feature_maps): min_sizes = self.min_sizes[k] for i, j in product(range(f[0]), range(f[1])): for min_size in min_sizes: s_kx = min_size / self.image_size[1] s_ky = min_size / self.image_size[0] dense_cx = [x * self.steps[k] / self.image_size[1] for x in [j + 0.5]] dense_cy = [y * self.steps[k] / self.image_size[0] for y in [i + 0.5]] for cy, cx in product(dense_cy, dense_cx): anchors += [cx, cy, s_kx, s_ky] # back to torch land output = torch.Tensor(anchors).view(-1, 4) if self.clip: output.clamp_(max=1, min=0) return output
这里面有一个
cfg
变量我这里没提供,本质就是一个参数文件,要跑这部分代码的可以看这里,复制一下测试。 -
使用Cython对上面这个模块的编译很简单,用pip装好Cython之后写一个如下的
setup.py
:1 2 3 4 5 6 7 8 9 10
# coding=utf-8 # file: setup.py # author: jwxie/jwxie.cn from distutils.core import setup from Cython.Build import cythonize setup( ext_modules=cythonize("priorbox.py") )
然后用
python3 setup.py build_ext
执行编译并生成最终的共享库文件(会在当前目录下自动创建一个build目录,其中包含两个子目录分别是temp和lib,共享文件在lib目录下)。 -
对变量进行预定义本质上只是一些心理作用,我也只是猜测会对结果产生真正的影响。修改很简单,只需要对纯python实现的
PriorBox
类的forwad
函数后面跟一些cdef来进行预定义。1 2 3 4 5 6 7 8 9 10 11 12
# .... 上面和1一模一样 def forward(self): anchors = [] # >>>>>>>>>>>>>>> 添加的部分开始 cdef int i, j, k, min_size cdef double cx, cy, s_kx, s_ky, dense_cx, dense_cy cdef list f, anchors # <<<<<<<<<<<<<<< 添加的部分结束 for k, f in enumerate(self.feature_maps): # .... 下面和1一模一样
然后走一遍2的流程,生成共享库。
-
这部分纯自己照着1的实现自己翻译了一下,个人cpp只能达到大学毕业考试及格水平😂。
分成三个部分,如下表所示:
三个部分 简介 内容 第一部分 cpp实现 头文件、类实现、CmakeList.txt 第二部分 pxd文件 实际上是cython在实现交叉的时候的一个头文件,完成一些声明 第三部分 pyx文件 实际上是对cpp的实现进行一些封装,利用cpp结果实现功能并返回 -
第一部分 cpp实现
-
首先第一步分是是cpp的实现,具体的又分成三个小部分,第一部分是头文件
PriorBox.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
// // Created by JiaweiXie on 2021/3/20. // #ifndef MODEL_PRIORBOX_H #define MODEL_PRIORBOX_H #include "vector" #include "iostream" #include "math.h" class PriorBox { public: PriorBox() = default; PriorBox(std::vector<std::vector<int>> &min_size, std::vector<int> &steps, std::vector<int> &image_size); std::vector<std::vector<double>> forward(); private: std::vector<std::vector<int>> min_size; std::vector<int> steps; bool clip = false; std::vector<int> image_size; std::vector<std::vector<int>> feature_maps; std::vector<std::vector<double>> anchors; void GetFeatureMaps(); }; #endif //MODEL_PRIORBOX_H
- 然后写一个对应的实现文件
PriorBox.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
// // Created by JiaweiXie on 2021/3/20. // #include "PriorBox.h" PriorBox::PriorBox(std::vector<std::vector<int>> &min_size, std::vector<int> &steps, std::vector<int> &image_size) { this->min_size = min_size; this->steps = steps; this->image_size = image_size; this->GetFeatureMaps(); } void PriorBox::GetFeatureMaps() { for (int &step : steps) { std::vector<int> s; s.push_back(ceil(1. * image_size[0] / step)); s.push_back(ceil(1. * image_size[1] / step)); // std::cout << ceil(1. * image_size[0] / step) << std::endl; feature_maps.push_back(s); } } std::vector<std::vector<double>> PriorBox::forward() { int anchor_size = 0; for (auto f : feature_maps) { anchor_size += f[0] * f[1]; } anchors.reserve(anchor_size * 2); // std::cout << anchor_size * 2 << std::endl; for (long unsigned int k = 0; k < feature_maps.size(); ++k) { auto f = feature_maps[k]; auto ms = min_size[k]; for (int f0 = 0; f0 < f[0]; ++f0) { for (int f1 = 0; f1 < f[1]; ++f1) { for (auto elem_ms : ms) { double s_kx = 1. * elem_ms / image_size[1]; double s_ky = 1. * elem_ms / image_size[0]; double dense_cx = (f1 + 0.5) * steps[k] / image_size[1]; double dense_cy = (f0 + 0.5) * steps[k] / image_size[1]; std::vector<double> s = {dense_cx, dense_cy, s_kx, s_ky}; anchors.push_back(s); } } } } // std::cout << anchors.size() << " x " << anchors[0].size() << std::endl; return anchors; } // 做一些测试,看一下结果对不对 //int main() { // std::vector<int> steps = {8, 16, 32}; // std::vector<std::vector<int>> min_size = {{16, 32}, // {64, 128}, // {256, 512}}; // std::vector<int> image_size = {1024, 1024}; // // PriorBox p(min_size, steps, image_size); // // p.forward(); // // return 0; //}
- 最后写一个
CMakeLists.txt
用于编译测试:
cmake_minimum_required(VERSION 3.16) project(cython) set(CMAKE_CXX_STANDARD 14) find_package(OpenCV) include_directories(.) add_executable(cython PriorBox.cpp PriorBox.h)
- 然后写一个对应的实现文件
-
-
第二部分 pxd文件
引用Cython的官方文档一句话,
We now need to declare the attributes and methods for use on Cython.
We put those declarations in a file called
Rectangle.pxd
.You can see it as a header file which is readable by Cython
这部分内容可以看作是一个Cython可读取的一个头文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
# coding=utf-8 # file: PriorBoxCpp.pxd # author: jwxie/jwxie.cn # libcpp是cython自带的一个库,里面实现一些cpp的STL和内建数据结构 # 类似的也有libc是c的一些数据结构 from libcpp.vector cimport vector cdef extern from "PriorBox.cpp": pass # Declare the class with cdef cdef extern from "PriorBox.h": cdef cppclass PriorBox: # 这里实际就是把PriorBox.h的内容用python写一下 PriorBox() except + # except + 的意义是为了保证cython能捕捉到错误 # cython并不能捕捉cpp的错误 详见官方文档里面的 # "#add-public-attributes"和"#Exceptions"章节 PriorBox(vector[vector[int]], vector[int], vector[int]) except + vector[vector[double]] forward() except + """ python和cpp的错误捕捉还有一个对应表这里简单列一下,详细的可以看官方文件。 C++ Python bad_alloc MemoryError bad_cast TypeError bad_typeid TypeError domain_error ValueError invalid_argument ValueError ios_base::failure IOError out_of_range IndexError overflow_error OverflowError range_error ArithmeticError underflow_error ArithmeticError (all others) RuntimeError """
-
第三部分 pyx文件
这里的东西就是一些把cpp实现里面留下的接口再封装一下,实现与python的对接
这里有一个比较重要的东西就是,实际上cpp的一部分STL和python的数据结构是匹配的,简单列一下:
Python type => C++ type => Python type bytes std::string bytes iterable std::vector list iterable std::list list iterable std::set set iterable (len 2) std::pair tuple (len 2) 具体的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
# coding=utf-8 # file: PriorBoxCpp.pyx # author: jwxie/jwxie.cn from PriorBoxCpp cimport PriorBox def PriorBoxCpp(py_min_size, py_steps, py_image_size): cdef PriorBox cpp_prior_box cpp_prior_box = PriorBox.PriorBox(py_min_size, py_steps, py_image_size) result = cpp_prior_box.forward() return result
最后同样写一个setup.py对pyx文件进行编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# coding=utf-8 # file: setup.py # author: jwxie/jwxie.cn from Cython.Build import cythonize from setuptools import setup, Extension ext_modules = Extension( "PriorBoxCpp", sources=["PriorBoxCpp.pyx"], language='c++' # 这里不能少,默认用gcc,会报找不到ios头文件 ) setup( name='PriorBoxCpp', ext_modules=cythonize(ext_modules) )
最终生成的共享库也保存在build/lib.linux-x86_64-3.7目录下。
这里其实cimport也可以import numpy 之类的c包,读者感兴趣的可以自己玩玩 0.0,但要注意理论上不是所有的包都可以的奥。
-
一个要注意的是我是在环境 python3.7 + linux + cpython解释器 下进行的编译的,注意你自己的环境,最终生成的文件名可能会不一样
实验结果
先说感想,这个结果很气人💢,让我感觉到了我的cpp写的有多么垃圾 T。T
按照上一章的描述对所有代码处理完成之后,在写一点东西用来调用,如下:
|
|
测试结果如下:
|
|
讨论
咋说呢,意料之外又是情理之中,昨天下班前还在吹牛逼说用cpp编译一下肯定快的一比,目测秒杀级别,打脸是真快。
整体速度:python < cpp+cython < cython < cython+predefine
各个优化版本与纯python版本之间的时间差距大约是:4.43s -> 4.44s -> 3.35s 基本上就是只要做了优化,肯定都比纯Python快,当然这是废话,谁让人家是动态语言 0.0。
整体内存消耗:cpp+cython 16.5MB显著大于其他三个 0.0,这个cpp写得显得我像个憨批 😂,最小的是cython+predefined仅有0.2MB。
行吧,82.5倍秒杀我 … 哭出声… 😢
实验环境
笔记本一台 型号 XiaoXin Pro13 0.0
-
CPU: AMD Ryzen 5 4600U with Radeon Graphics - 2095.986 MHz - x86_64
-
Memory: 16GB
-
WSL linux - Ubuntu20.04 - Kernel 4.19.128-microsoft-standard
-
Anaconda Python 3.7
-
gcc - 9.3.0 / g++ - 9.3.0
总结
总的来说今天折腾了一天的目的也不是真的为了能落入生产环境里,这里还有好多路要走(其实我还是有点b数的),纯粹当作一个玩具,走一些别人走过的路,为了以后需要的时候能用得上。
另外要说的一点,其实从这个博客基础设计到一路的实现,本质上的思路和JIT很像,无非就是对一些一些常用模块进行编译实现静态化,避免多余的变量检查,达到整体加速的目的,只不过目前这个过程不需要额外的JIT引擎来进行实现,而是直接由人工来进行编译。当然啦,这也是PyPy解释器大肆宣传比CPython快的原因咯。
参考内容
[1] Cython官方文档
[2] Google <= 不得不说垃圾百度 : (