通过C++实现对python的加速一定会遇到的几个问题

一、Pybind11,将数据从python传递到C++的两种方式:

  1. 加“壳”,能在C++中进行操作,但是不发生数据拷贝,底层的存储方式还是按照python的方式;
  2. 数据类型转换,从python中的存储方式,直接转换成C++中的数据类型,发生数据拷贝,效率较低,尤其是在数据量比较大的情况下,这种转换的代价很高。

这一点非常值得关注,当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

二、计算密集型(CPU密集型)、IO密集型,以及他们的特点:

在做速率优化时,了解这一点很有必要。观察你的程序,如果是IO密集型,那么它通过使用多线程方式的并行去提高它的效率,可能效果不会很明显。

相比于CPU的运算速度,通过IO读取数据的速度非常慢,绝对不在同一个量级上,因此IO密集型的程序,它的短板不会是CPU的运算速度,而是把事件都耗费到了数据的读取上,而CPU会有大量空闲的时间。哪怕只是涉及到从内存中读取的数据量比较大,运算量比较小的情况,也会有这样的问题,内存的读取速率也要比CPU的运行速率慢得多。单从发展来讲,我们经常提起的一个常识是存储器存取速率的发展远落后于CPU运行速率提升的速度,这导致了存储器的速率问题成为计算机速率提升的一个短板。

如果是计算密集型程序,那就可以选择通过多线程方式进行提速。如果你的计算机是4核的,就可以创建4个线程,并通过绑定核,减少上下文切换达到一个很好的提速效果。

观察CPU利用率的方式有很多种,但是我觉得下面的命令比较好用:

vmstat 1 5

上面的命令表示每秒采集一次cpu使用率,采集5次,当然可以根据你的情况调整参数,比如采集10000次:

vmstat 1 10000

三、Pybind11中多线程的使用 

在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循环前面加一个预编译处理语句,具体用法可以查找相关资料。

你可能感兴趣的:(c++,python,开发语言)