这一点非常值得关注,当Python中考虑调用C++接口时,大多情况下都是为了提高程序的运行速率(有时候可能是为了能够调用C++的库)。因此,通过以上哪一种方式传递数据需要衡量。
如果你有现成的C++接口,它已经被实现并且调试过,能够在C++中很好的运行了,这时候你想在python中调用它。在这种情况下,通过Pybind11要做的是将数据传递进来,通过方式(2)可以避免大规模的修改C++代码。当然,如果因为数据拷贝带来的代价抵消了你在python中调用C++所带来的效率提升时,这也就没有必要了。
在一个需要重头实现C++接口的项目中,那么就更需要衡量一下,采用方式(2)带来的代价是否能在编码的便捷性,对代码运行效率的提升方面做好平衡。
当然,pybind11对两种方式都做到了很好的支持,类型的转换方式也十分友好,下面的两个接口分别展示了这两种情况,但是如果你的入参数的据量比较大,在python中对这两个接口分别测时,可以看到第二种方式的耗时比第一种大得多,即使里面啥都没干。
void BomTree::timeTest_1(const py::dict &inv_dict)
{
}
void BomTree::timeTest_2(const map &inv_dict)
{
}
在python中的调用方式,二者完全一样,这是因为pybind11已经隐性的将数据类型做了转换。
a.timeTest_1(inv_dict)
a.timeTest_2(inv_dict)
用我自己的数据进行测试,可以看出完全不是一个量级:
Time_1: 1.9073486328125e-05(注意后面的e-05)
Time_2: 0.06657171249389648
在做速率优化时,了解这一点很有必要。观察你的程序,如果是IO密集型,那么它通过使用多线程方式的并行去提高它的效率,可能效果不会很明显。
相比于CPU的运算速度,通过IO读取数据的速度非常慢,绝对不在同一个量级上,因此IO密集型的程序,它的短板不会是CPU的运算速度,而是把事件都耗费到了数据的读取上,而CPU会有大量空闲的时间。哪怕只是涉及到从内存中读取的数据量比较大,运算量比较小的情况,也会有这样的问题,内存的读取速率也要比CPU的运行速率慢得多。单从发展来讲,我们经常提起的一个常识是存储器存取速率的发展远落后于CPU运行速率提升的速度,这导致了存储器的速率问题成为计算机速率提升的一个短板。
如果是计算密集型程序,那就可以选择通过多线程方式进行提速。如果你的计算机是4核的,就可以创建4个线程,并通过绑定核,减少上下文切换达到一个很好的提速效果。
观察CPU利用率的方式有很多种,但是我觉得下面的命令比较好用:
vmstat 1 5
上面的命令表示每秒采集一次cpu使用率,采集5次,当然可以根据你的情况调整参数,比如采集10000次:
vmstat 1 10000
在C/C++中,计算密集型的情况,通过采用多线程的方式实现并行是一种很好的选择。
由于在python最流行的解释器CPython中采用了GIL(Global Interpreter Lock,全局解释器锁),通常采用多线程其实只是实现了形式上的并行,而实际上只是通过交替执行的方式占用CPU,而不是将各个线程分布到多个核上运行,因此这其实是所谓的并发,而不是并行。
这样不但不会提升程序的运行效率,反而会由于上下文切换,开启线程本身的对资源的消耗等原因增加程序的耗时。
至于CPython解释器为什么要采用GIL,可以参考其他介绍(其实可以通过采用其他python解释器来避开GIL)。
在python中实现真正的并行有点麻烦(当然还是有办法),所以通过C++实现真正意义上的并行,是一个不错的选择。
下面的代码是一个在C++类内实现多线程的例子,并且实现了线程与核的绑定。
// forFilerWrite.h
#ifndef _FORFILERWRITE_H_
#define _FORFILERWRITE_H_
#include
#include
#include
#include
#include
#define MOST_THREAD_NUM 10
struct MyThreadTest
{
MyThreadTest();
~MyThreadTest();
static void *ThreadFunc(void *args); //注意这里的静态定义
void DoTheThing();
void Process();
private:
pthread_t _pid[MOST_THREAD_NUM];
};
#endif
// forFilerWrite.cpp
#include "forFilerWrite.h"
namespace py = pybind11;
MyThreadTest::MyThreadTest()
{
}
MyThreadTest::~MyThreadTest()
{
for (int i = 1; i < MOST_THREAD_NUM; i++)
{
pthread_kill(_pid[i], SIGKILL);
}
}
void MyThreadTest::DoTheThing()
{
sleep(2);
}
//作为静态函数,它不能随便调用对象中的非静态变量和方法,通过args的传入,可以实现类中另一个方法的调用,
//而在所调用的方法DoTheThing中也可以实现对非静态变量的调用。
void *MyThreadTest::ThreadFunc(void *args)
{
MyThreadTest *pMyThreadTest = (MyThreadTest *)args;
pMyThreadTest->DoTheThing();
return NULL;
}
void MyThreadTest::Process()
{
int sysRet = -1;
cpu_set_t mask;
int cpu_num = get_nprocs();
std::cout << cpu_num << std::endl;
for (int i = 1; i < MOST_THREAD_NUM; i++)
{
memset(&mask, 0, sizeof(mask));
pthread_create(&_pid[i], NULL, &MyThreadTest::ThreadFunc, this);
printf("newthread pid = %lu\n", _pid[i]);
CPU_ZERO(&mask);
CPU_SET(i, &mask);
sysRet = pthread_setaffinity_np(_pid[i], sizeof(mask), &mask);
if (-1 == sysRet)
{
printf("pthread_setaffinity_np err\n");
return;
}
}
for (int j = 1; j <= MOST_THREAD_NUM; j++)
{
if (pthread_join(_pid[j], NULL))
{
printf("pthread_join err\n");
}
}
return;
}
const double PyhonInterface()
{
// py::gil_scoped_release release; //通过测试,这里是否释放GIL,都可以达到并行的目的。
MyThreadTest *c_mytest = new MyThreadTest();
c_mytest->Process();
// py::gil_scoped_acquire acquire;
return 0;
}
PYBIND11_MODULE(mutithread, m)
{
m.doc() = "thread test";
m.def("PyhonInterface", &PyhonInterface);
}
PyhonInterface方法中调用py::gil_scoped_release release;和py::gil_scoped_acquire acquire;可以实现对python解释器中的GIL进行释放和重新获取,但是在像上面这样,并行也是由C++实现的情况下,这两个语句可以不用,也能够实现真正的并行。
我们也可以通过编写C++接口实现GIL的释放,这时候就可以通过py::gil_scoped_release release;语句。然后回到python中并行执行python代码,并行的操作完成后,再调用GIL重新获取的C++接口,通过py::gil_scoped_acquire acquire;实现。 我没有这样实操过,但理论上是可以的,并且看网上有人实现过。
值得一提的是,如果需要并行,首先可以考虑OpenMP,它可能会让你的并行实现起来相当简单。只需要在你的for循环前面加一个预编译处理语句,具体用法可以查找相关资料。