使用 ctypes 将 Python 运行速度提升 30 倍

原文链接: https://www.jianshu.com/u/8f2987e2f9fb

简介
当 Python 面临运算密集型任务时,其速度总是显得力不从心。要提升 Python 代码运行速度有多种方法,如 ctypes、cython、CFFI 等,本篇文章主要从 ctypes 方面介绍如何提升 Python 的运行速度。

ctypes 是 Python 的内置库,利用 ctypes 可以调用 C/C++ 编译成的 so 或 dll 文件 (so 存在 linux/MacOS 中,dll 存在于 windows),简单而言,就是将计算压力较大的逻辑利用 C/C++ 来实现,然后编译成 so 或 dll 文件,再利用 ctypes 加载进 Python,从而将计算压力大、耗时较长的逻辑交于 C/C++ 去执行。如 Numpy、Pandas 这些库其底层其实都是 C/C++ 来实现的。

下面代码的运行环境为:MacOS 、 Python3.7.3

纯 Python 实现

为了对比出使用 ctypes 后程序运行速度的变化,先使用纯 Python 代码实现一段逻辑,然后再利用 C 语言去实现相同的逻辑。

这里为了模仿运算密集任务,实现一段逻辑用于计算一个集合中点与点之间的距离以及实现一个操作字符串的逻辑,具体代码如下:

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
import random
import time

# 点
class Point():
  def __init__(self, x, y):
    self.x = x
    self.y = y
 
class Test():
  def __init__(self, string, nb):
    self.string = string
    self.points = []
    # 初始化点集合
    for i in range(nb):
      self.points.append(Point(random.random(), random.random()))
    self.distances = []

  # 增量字符串
  def increment_string(self, n):
    tmp = ""
    # 每个字符做一次偏移
    for c in self.string:
      tmp += chr(ord(c) + n)
    self.string = tmp

  # 这个函数计算列表中每个点之间的距离
  def distance_between_points(self):
    for i, a in enumerate(self.points):
      for b in self.points:
        # 距离公式
        self.distances.append(((b.x - a.x) ** 2 + (b.y - b.x) ** 2) ** 0.5)

if __name__ == '__main__':
  start_time = time.time()
  test = Test("A nice sentence to test.", 10000)
  test.increment_string(-5) # 偏移字符串中的每个字符
  test.distance_between_points() # 计算集合中点与点之间的距离
  print('pure python run time:%s'%str(time.time()-start_time))

上述代码中,定义了 Point 类型,其中有两个属性,分别是 x 与 y,用于表示点在坐标系中的位置,然后定义了 Test 类,其中的 increment_string () 方法用于操作字符串,主要逻辑就是循环处理字符串中的每个字符,首先通过 ord () 方法将字符转为 unicode 数值,然后加上对应的偏移 n,接着在通过 chr () 方法将数值转换会对应的字符。

此外还实现了 distance_between_points () 方法,该方法的主要逻辑就是利用双层 for 循环,计算集合中每个点与其他点的距离。使用时,创建了 10000 个点进行程序运行时长的测试。

多次执行这份代码,其运行时间大约在 39.4 左右

python 1.py
pure python run time:39.431304931640625

使用 ctypes 提速度代码

要使用 ctypes,首先就要将耗时部分的逻辑通过 C 语言实现,并将其编译成 so 或 dll 文件,因为我使用的是 MacOS,所以这里会将其编译成 so 文件,先来看一下上述逻辑通过 C 语言实现的具体代码,如下:

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
#include <stdlib.h>
#include <math.h>

#点结构
typedef struct s_point
{
  double x;
  double y;
} t_point;

typedef struct s_test
{
  char *sentence; // 句子
  int nb_points; 
  t_point *points; // 点
  double *distances; // 两点距离,指针
} t_test;

#增量字符串
char *increment_string(char *str, int n)
{
  for (int i = 0; str[i]; i++)
    // 每个字符做一次偏移
    str[i] = str[i] + n;
  return (str);
}

#随机生成点集合
void generate_points(t_test *test, int nb)
{
  #calloc () 函数用来动态地分配内存空间并初始化为 0
 #其实就是初始化变量,为其分配内存空间
  t_point *points = calloc(nb + 1, sizeof(t_point));

  for (int i = 0; i < nb; i++)
  {
    points[i].x = rand();
    points[i].y = rand();
 }
 # 将结构地址赋值给指针
  test->points = points;
  test->nb_points = nb;
}
 
#计算集合中点的距离
void distance_between_points(t_test *test)
{
 int nb = test->nb_points;
# 创建变量空间
  double *distances = calloc(nb * nb + 1, sizeof(double));

  for (int i = 0; i < nb; i++)
    for (int j = 0; j < nb; j++)
      #sqrt 计算平方根
      distances[i * nb + j] = sqrt((test->points[j].x - test->points[i].x) * (test->points[j].x - test->points[i].x) + (test->points[j].y - test->points[i].y) * (test->points[j].y - test->points[i].y));
  test->distances = distances;
}

其中具体的逻辑不再解释,可以看注释理解其中的细节,通过 C 语言实现后,接着就可以通过 gcc 来编译 C 语言源文件,将其编译成 so 文件,命令如下:

#生成 .o 文件
gcc -c fastc.c
#利用 .o 文件生成so文件
gcc -shared -fPIC -o fastc.so fastc.o

获得了 fastc.so 文件后,接着就可以利用 ctypes 将其调用并直接使用其中的方法了,需要注意的是「Windows 系统体系与 Linux/MacOS 不同,ctypes 使用方式会有差异」,至于 ctypes 的具体用法,后面会通过单独的文章进行讨论。

ctypes 使用 fastc.so 的代码如下:

'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴,
互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
import ctypes
from ctypes import *
from ctypes.util import find_library
import time


# 定义结构,继承自ctypes.Structure,与C语言中定义的结构对应
class Point(ctypes.Structure):
  _fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double)]
 
class Test(ctypes.Structure):
  _fields_ = [
    ('sentence', ctypes.c_char_p),
    ('nb_points', ctypes.c_int),
    ('points', ctypes.POINTER(Point)),
    ('distances', ctypes.POINTER(c_double)),
  ]

# Lib C functions
_libc = ctypes.CDLL(find_library('c'))
_libc.free.argtypes = [ctypes.c_void_p]
_libc.free.restype = ctypes.c_void_p

# Lib shared functions
_libblog = ctypes.CDLL("./fastc.so")
_libblog.increment_string.argtypes = [ctypes.c_char_p, ctypes.c_int]
_libblog.increment_string.restype = ctypes.c_char_p
_libblog.generate_points.argtypes = [ctypes.POINTER(Test), ctypes.c_int]
_libblog.distance_between_points.argtypes = [ctypes.POINTER(Test)]

if __name__ == '__main__':

  start_time = time.time()

  # 创建
  test = {}
  test['sentence'] = "A nice sentence to test.".encode('utf-8')
  test['nb_points'] = 0
  test['points'] = None
  test['distances'] = None
  c_test = Test(**test)
  ptr_test = ctypes.pointer(c_test)

  # 调用so文件中的c语言方法
  _libblog.generate_points(ptr_test, 10000)
  ptr_test.contents.sentence = _libblog.increment_string(ptr_test.contents.sentence, -5)
  _libblog.distance_between_points(ptr_test)
  _libc.free(ptr_test.contents.points)
  _libc.free(ptr_test.contents.distances)

  print('ctypes run time: %s'%str(time.time() - start_time))

多次执行这份代码,其运行时间大约在 1.2 左右

python 2.py
ctypes run time: 1.2614238262176514

相比于纯 Python 实现的代码快了 30 倍有余

结尾
本节简单的讨论了如何利用 ctypes 与 C/C++ 来提升 Python 运行速度,有人可能会提及使用 asyncio 异步的方式来提升 Python 运行速度,但这种方式只能提高 Python 在 IO 密集型任务中的运行速度,对于运算密集型的任务效果并不理想,最后欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。

猜你喜欢

转载自blog.csdn.net/qdPython/article/details/102665557