python在密集计算任务上如何有很多循环,有没有相应的三方库效率将大大降低。pybind11可以让python与C++混合编程,将复杂的算法部分封装到C++库进行计算,python负责流程的串通。这样安排即避开了性能瓶颈,又实现了快速开发,是一个非常好的解决方案。但是在windows上,pybind11推荐在Visual Studio编译pyd,Visual Studio体积太大,动则10个G以上。如果只就为了给python写三方模块非常不划算,经过探索采用vscode+cmake+llvm-mingw的方式体积会小很多。
1.首先下载vscode,安装Chinese,Code runner,C/C++,Cmake,CMake Tools插件
2.再下载
cmake:Download CMake
llvm-mingw:Releases · mstorsjo/llvm-mingw (github.com)
我下载的是
llvm-mingw-20240619-msvcrt-x86_64.zip
解压后添加至环境变量
3.下载pybind11
GitHub - pybind/pybind11: Seamless operability between C++11 and Python
直接clone到本地
4.在同级目录建立CMakeLists.txt文件,写入以下内容:
cmake_minimum_required(VERSION 3.23)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 设置编译器编译模式,对于编译用的Debug模式和调试用的Release模式
# 在Debug模式中,程序运行较慢,当可以在IDE中进行断点调试
# 在Release模式中,运行速度较快,但没有调试信息。不设置默认是Debug模式
set( CMAKE_BUILD_TYPE "Debug")
# set( CMAKE_BUILD_TYPE "Release")
# 防止报错找不到DLL,用-static编译pyd减少依赖
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static")
# 设置python解释器路径,以便编译不同python版本的pyd库
set(PYTHON_EXECUTABLE "D:/Desktop/111/Python39/python.exe")
# 设置Cmake工程名字
project(HelloWorld)
# 添加依赖库路径
add_subdirectory(pybind11)
# 指定编译成pyd模块的cpp文件与模块名字
pybind11_add_module(HelloWorld hello.cpp)
5.再建立hello.cpp写一个简单的hello world程序,并用pybind11导出函数
#include <pybind11/pybind11.h>
int add(int i,int j)
{
return i + j;
}
PYBIND11_MODULE(HelloWorld,m)
{
m.doc() = "pybind11 hello world example";//module docstring
m.def("add",&add,"A function which adds two numbers");
}
6.配置Cmake的C++解释器
按control+shift+P调出命令行,输入cmake:configure,选择配置的llvm-mingw 编译器
或者点击左边的cmake-配置-编辑,再选择对应的编译器
最后用配置好的Cmake与C++编译器编译pyd库,点击下面的生成(build)。会在同级目录生成一个build目录,下面有HelloWorld.cp39-win_amd64.pyd
此时编写一个python文件导入pyd模块测试,可正确导入模块与调用add函数。此后就可以愉快的进行python/C++混合编程啦,祝你玩得愉快~
1亿次累加测试 与 图片numpy数组转换成灰度图测试
C++代码
#include<iostream>
#include<pybind11/pybind11.h>
#include<pybind11/numpy.h>
using namespace pybind11;
// 1.num次循环累加测试函数
long long int sum(int num){
long long int sum = 0;
for(int i=0;i<num;i++){
sum += i;
}
return sum;
}
// 2.图片numpy数组转换成gray灰度图测试函数
array_t<double> rgb_to_gray(array_t<double> & img_rgb)
{
if(img_rgb.ndim()!=3)
{
throw std::runtime_error("RGB image must has 3 channels!");
}
array_t<double> img_gray=array_t<double>(img_rgb.shape()[0]*img_rgb.shape()[1]);
img_gray.resize({img_rgb.shape()[0],img_rgb.shape()[1]});
auto rgb=img_rgb.unchecked<3>();
auto gray=img_gray.mutable_unchecked<2>();
for(int i=0;i<img_rgb.shape()[0];i++)
{
for(int j=0;j<img_rgb.shape()[1];j++)
{
auto R=rgb(i,j,0);
auto G=rgb(i,j,1);
auto B=rgb(i,j,2);
auto GRAY=(R*30+G*59+B*11+50)/100;
gray(i,j)=(GRAY);
}
}
return img_gray;
}
PYBIND11_MODULE(example,m)
{
m.doc()="simple demo";
m.def("rgb_to_gray",&rgb_to_gray);
m.def("sum",&sum);
}
python测试代码
import cv2
import build.example as example
import time
import numpy as np
# 1.num次循环累加测试函数
def sum(num):
sum = 0
for i in range(num):
sum += i
return sum
# 2.图片numpy数组转换成gray灰度图测试函数
def rgb_to_gray(img_rgb):
if img_rgb.shape[2]!=3:
print('image channels is 3')
h,w,c=img_rgb.shape
gray=np.zeros(shape=(h,w),dtype=np.uint8)
for i in range(h):
for j in range(w):
R=img_rgb[i,j,0]
G=img_rgb[i,j,1]
B=img_rgb[i,j,2]
GRAY=(R*30+G*59+B*11+50)/100
gray[i,j]=np.uint8(GRAY)
return gray
if __name__ == '__main__':
img_rgb=cv2.imread("./src/D65.jpg")
num = 100000000
# print(f"测试sum循环{num}次")
print(f"TEST sum loop {num} times")
t1=time.time()
re = example.sum(num)
t2=time.time()
print(f"pybind11.sum={re} time:{t2-t1}/s")
t1=time.time()
re = sum(num)
t2=time.time()
print(f"python.sum={re} time:{t2-t1}/s \n")
# print(f"图片numpy数组转换成gray灰度图")
print(f"TEST numpy array to gray image")
t1=time.time()
cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
t2=time.time()
print(f"cv2.cvtColor time:{t2-t1}/s")
t1=time.time()
example.rgb_to_gray(img_rgb)
t2=time.time()
print(f"pybind11.rgb_to_gray time:{t2-t1}/s")
t1=time.time()
rgb_to_gray(img_rgb)
t2=time.time()
print(f"python.rgb_to_gray time:{t2-t1}/s")
DUG模式测试结果,
sum累加函数:C++是python的21倍
rgb_to_gray函数:C++是python的96倍,是opencv的1/170倍
Release模式测试结果,
sum累加函数:C++是python的360倍
rgb_to_gray函数:C++是python的985倍,是opencv的1/14倍
Release速度是Debug模式的10~13倍,所以正式打包一定要用Release模式.
更重要的是C++可以开多线程碾压python的GIL锁,进一步拉开速度差异
注意:
用什么python版本编译的就得在对应版本应用pyd,否则也会报错无法找到该模块。安装的python版本最好是正式版本编译的pyd通用性更好,不要用alpha\beta等版本容易报错。不要用embeddable版本,缺少include/libs等库会报错。用正式版本的python编译好后,可以给相同或相近的alpha/beta/embeddable的python版本用。
用MinGW64的GCC编译的pyd文件无法在win上使用,有时会报错无法找到该模块。换成vs2022自带的MSC编译器就没这个问题了。有的解释是Win上的python解释器是用MSC编译的,然后它使用的二进制模块也需要是MSC编译的。Visual Studio的MSC太大,可以换成 llvm-mingw:Releases · mstorsjo/llvm-mingw (github.com)(实验发现mingw64编译的pyd库也可以被正确导入)