Cython的一些实战

Cython 的一些测试

开头语

这篇博客本质上是对Cython探索的一些记录,当然仅供周末空闲时间的一些娱乐

基础介绍

这一段主要讲一下为什么我要做这么一个测试。主要原因是最近在工作中做了一些检测方面的内容,因为生产环境没有GPU,因此把所有模型都往CPU上挪了,但是除了模型部分的网络加速外,检测还有一些后处理比较费是时间,因此就想尝试一下是否可以对这一部分纯Python实现的内容进行加速。

这里主要针对的部分是PriorBox的生成(对,你没有看错!不是NMS就是PriorBox!😂)

实验设计

  • 先对一下四个部分进行实验:

    1. 纯Python实现的PriorBox
    2. 使用Cython直接对PriorBox类进行封装,重命名类名为PriorBoxCython
    3. 对2部分的类里的一些变量进行预定义,重命名类名为PriorBoxCythonOptimized
    4. 使用Cpp对PriorBox重新构造,并使用cython进行编译,生成PriorBoxCpp共享库。
  • 然后对四个部分的内容的耗时和内存消耗进行比对

  • 做一些分析和讨论

实验代码实现

  1. 纯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变量我这里没提供,本质就是一个参数文件,要跑这部分代码的可以看这里,复制一下测试。

  2. 使用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目录下)。

  3. 对变量进行预定义本质上只是一些心理作用,我也只是猜测会对结果产生真正的影响。修改很简单,只需要对纯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的流程,生成共享库。

  4. 这部分纯自己照着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


按照上一章的描述对所有代码处理完成之后,在写一点东西用来调用,如下:

 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
import time  # 计时
from memory_profiler import profile  # 内存消耗监视

# 我这里把2编译出来的包位置挪到当前位置了, 所以这里直接import就可以
from priorbox import PriorBox as PriBoxPyCython  

# 剩下俩都在build/lib.linux-x86_64-3.7目录下
# 要加一下PYTHONPATH才能正常import
import sys
sys.path.insert(0, 'build/lib.linux-x86_64-3.7/')
from PriorBoxCpp import PriorBoxCpp
from priorbox import PriorBox as PriorBoxCythonOptimized


#   >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>   #
#   这里把纯python的实现复制一下,我这里写的话就太长了,而且重复嘞   #
#   <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<   #

if __name__ == '__main__':
    cfg = {
        'name': 'mobilenet0.25',
		'min_sizes': [[16, 32], [64, 128], [256, 512]],
        'steps': [8, 16, 32],
		'clip': False, }
    
    @profile
    def A():  # 纯python实现
        p = PriorBox(cfg, (1024, 1024)).forward()


    @profile
    def B():  # 用cython编译直接为.c,用gcc生成共享库
        p = PriBoxPyCython(cfg, (1024, 1024)).forward()


    @profile
    def C():  # 对变量进行预定义后,用cython编译为.c,用gcc生成共享库
        p = PriorBoxCythonOptimized(cfg, (1024, 1024)).forward()


    @profile
    def D():  # 自己的垃圾cpp实现
        p = PriorBoxCpp([[16, 32], [64, 128], [256, 512]], [8, 16, 32], [1024, 1024])
	
    # 先测试内存消耗
    A()
    B()
    C()
    D()
	
    # 然后测时间,跑200次,看总时间
    def T(cmd):
        s = time.time()
        for i in range(200):
            p = eval(cmd)
        print(f'cmd: {cmd}\n{time.time() - s}')
        print('-' * 100)


    for cmd in (
            'PriorBox(cfg, (1024, 1024)).forward()',
            'PriBoxPyCython(cfg, (1024, 1024)).forward()',
            'PriorBoxCythonOptimized(cfg, (1024, 1024)).forward()',
            'PriorBoxCpp([[16, 32], [64, 128], [256, 512]], [8, 16, 32], [1024, 1024])'
    ):
        T(cmd)

测试结果如下:

 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
Filename: /mnt/c/Users/jwxie/Desktop/src/proj/cython_test/test.py
Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    67    183.5 MiB    183.5 MiB           1       @profile
    68                                             def A():
    69    186.3 MiB      2.8 MiB           1           p = PriorBox(cfg, (1024, 1024)).forward()


Filename: /mnt/c/Users/jwxie/Desktop/src/proj/cython_test/test.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    72    186.3 MiB    186.3 MiB           1       @profile
    73                                             def B():
    74    187.3 MiB      1.0 MiB           1           p = PriBoxPyCython(cfg, (1024, 1024)).forward()


Filename: /mnt/c/Users/jwxie/Desktop/src/proj/cython_test/test.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    77    187.3 MiB    187.3 MiB           1       @profile
    78                                             def C():
    79    187.6 MiB      0.2 MiB           1           p = PriorBoxCythonOptimized(cfg, (1024, 1024)).forward()


Filename: /mnt/c/Users/jwxie/Desktop/src/proj/cython_test/test.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    82    187.6 MiB    187.6 MiB           1       @profile
    83                                             def D():
    84    204.1 MiB     16.5 MiB           1           p = PriorBoxCpp([[16, 32], [64, 128], [256, 512]], [8, 16, 32], [1024, 1024])


cmd: PriorBox(cfg, (1024, 1024)).forward()
9.97243618965149 s
---------------------------------------------------------------------------------
cmd: PriBoxPyCython(cfg, (1024, 1024)).forward()
5.537133455276489 s
---------------------------------------------------------------------------------
cmd: PriorBoxCythonOptimized(cfg, (1024, 1024)).forward()
5.528991937637329 s
---------------------------------------------------------------------------------
cmd: PriorBoxCpp([[16, 32], [64, 128], [256, 512]], [8, 16, 32], [1024, 1024])
6.620768308639526 s
---------------------------------------------------------------------------------

讨论

咋说呢,意料之外又是情理之中,昨天下班前还在吹牛逼说用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 <= 不得不说垃圾百度 : (