之所以想研究下这类技术,是因为平时在调用 tensorflow 或者 sklearn 这些封装较好得机器学习库的时候,很好奇他们前端应用接口和后端计算的交互方式,再者想科普一下一个概念,我相信很多刚接触编程语言,或者没有太深层次计算机基础的朋友都会对编程语言本身理解有偏差,这里甚至包括一些所谓的人工智能专家。
python 这门语言本身比较古老了,之所以被人工智能这波浪潮吹鼓,其实不是因为语言本身有什么特殊性,而是因为它后端有一些优秀的机器学习库,而 python 本身简洁的语法和解释性语言的优势,让它可以快速实现模型原型,对于模型开发这项任务来说,没有比它更合适的了。但是对应用部署来说,它的性能或许会成为障碍。这里我之前都存在一个误区,会盲目的把应用部署性能的问题矛头完全归结于python语言本身,现在想来非也,因为训练好的模型也是编译后加载到内存中进行计算的,而python只是起到前端调用的作用,并不会真正参与核心计算。
其实这里还想说明,如果numpy这个库用的好,可并行的算法实现性能上不应该成为技术瓶颈。因为python根本就没有参与核心的计算,只是语法上调用了它而已,下面通过一个实验来说明 numpy计算是无视python GIL而能利用多核心开启多线程的!
- 我的机器 4核心8线程
先实验证明一下,python GIL的现象
import threading
# 功能函数,死循环为了查看CPU的利用情况
def run(i):
print("SUB {}".format(i))
while True:
pass
if __name__ == '__main__':
# 开启4线程
for i in range(4):
t = threading.Thread(target=run, args=(i, ))
t.start()
print("MAIN")
如图所示,明显看到尽管开启了4线程并且设置非阻塞情况,但是CPU的还是没有被充分利用。这里再对照执行下C++代码看是否可以充分利用CPU。
#include <iostream>
#include <thread>
using namespace std;
# 死循环,查看CPU利用率
void run(int i)
{
cout << i << endl;
while (1){
}
}
int main()
{
# 开启4个线程
for (uint8_t i = 0; i < 4; i++) {
thread t(run, i);
t.detach();
}
getchar();
return 0;
}
如图所示,CPU利用率是之前的2.5倍,当然语言之间性能的差异远远高于这个,这里只是为了对比说明python 的GIL机制让python的多线程如同鸡肋。
直接调用numpy进行矩阵计算
import numpy as np
# 定义个随机矩阵
a = np.random.random(2500000000).reshape(50000, 50000)
# 两个矩阵dot运算
np.dot(a, a)
可以看到,计算完全是利用了多核的,因为是随机生成的矩阵,所以不像死循环一样一直高占用CPU,那么也就是说numpy既具备了python简洁的语法,又具备了很好的计算性能,所以回应前面所得出的结论,单纯做算法实现性能不是瓶颈,不过也不是所有算法都是可以并行执行的,如果算法涉及到必须要迭代,当前计算结果依赖于前一步计算,那怎么办?比如GBDT,有没有办法,既可以享受python简洁的语法,又能利用底层语言的高性能呢?答案是肯定的,那就是本文的主题,这里就还原xgboost的调用方式,采用python ctypes模块,加载C++编译后的动态链接库。
- 首先编写C++代码,例子还采用上述方式。此处需要定义特殊符号,修饰要作为API的函数。
#ifndef UNTITLED_LIBRARY_H
#define UNTITLED_LIBRARY_H
#define EXTERN_C extern "C"
#define CALL_DLL EXTERN_C __declspec(dllexport)
#include <iostream>
#include <thread>
void run(int i);
CALL_DLL void go();
#endif
using namespace std;
void run(int i)
{
cout << i << endl;
while (1){
}
}
CALL_DLL void go(){
for (uint8_t i = 0; i < 4; i++) {
thread t(run, i);
t.detach();
}
}
- python 调用
import ctypes
# 加载动态库hello.dll,这里我项目名为hello,IDE用的clion,
# 编译后dll文件在cmake-build-debug文件下
libc = ctypes.cdll.LoadLibrary("cmake-build-debug/hello.dll")
# 调用C++函数
libc.go()
- 歪瑞古德!这种实现方式相对比较简单,还有种import pyd的模式,tensorflow 和 sklearn都采用 pyd的模式,这种模式在实现C++代码时候要到一些特殊函数做导入模块的处理。
特别说明:文中的死循环是查看CPU的占用情况,在真正写项目代码时候,要格外注意这种隐藏风险!