Deeplearning4j 实战(3):简介Nd4j中Java与CPP技术的应用【转】

来源:互联网 发布:淘宝论文查重可靠吗 编辑:程序博客网 时间:2024/06/06 01:40

from:http://blog.csdn.net/wangongxi/article/details/54970231

Deeplearning4j中张量的计算是由一个叫Nd4j的库来完成的。它类似于Python中的numpy,对高维向量的计算有比较好的支持。并且,为了提高运算的性能,很多计算任务是通过调用C++来完成的。具体来说,底层C++运行张量计算可以选择的backend有:BLAS,OpenBLAS, Intel MKL等,上层Java逻辑是通过JavaCPP技术来调用这些库。JavaCPP是也是一个开源库(https://github.com/bytedeco/javacpp),和大部分其他JNI的技术一样,它的目的也是为了实现JVM on-heap memory 到 off-heap memory的映射和操作。它通过一些助记符可以将自己编写的C++类或者C++标准库中文件进行编译并自动生成C++ JNI代码,不需要手动编写。到目前为止,JavaCPP已经封装了包括OpenCV,ffmpeg等多个优秀的C++项目,方便了很多Java程序员对这些开源库的调用。小弟我自己看到网上对于JavaCPP的使用介绍并不是特别多,所以写了这篇博客,简单介绍下JavaCPP的基本使用,作为一篇入门的文章供大家参考,其中代码在windows 7上可以正常运行。

使用JavaCPP技术主要可以分为以下几个步骤:

1.编写Java的逻辑代码:可以是自己实现的Java类,也可以通过助记符引用C++的标准库

2.编译你所写的Java文件,生成字节码文件

3.运行步骤2中生成的字节码文件,自动生成C++ JNI代码

4.利用步骤3中生成的JNI代码,生成本地共享库/动态链接库

5.指定shared library路径,加载shared library并运行字节码(自动调用shared library)


由于C++中有很多高效的算法实现,比如排序、查找、全排列等等,而且据我了解Java中并没有全排列算法的实现,所以这里就结合以上说的5个步骤,以Java调用C++中的全排列算法(next_permutataion)为目标来具体说说JavaCPP技术的应用。

首先,我们在IDE环境中新建一个Maven工程,加入JavaCPP的Maven依赖如下:

[html] view plain copy
  1. <dependency>  
  2.     <groupId>org.bytedeco</groupId>  
  3.     <artifactId>javacpp</artifactId>  
  4.     <version>1.3.1</version>  
  5. </dependency>  
然后,在工程中新建Java文件,代码如下:

[java] view plain copy
  1. package cppalgo;  
  2.   
  3.   
  4. import java.util.Arrays;  
  5.   
  6. import org.bytedeco.javacpp.IntPointer;  
  7. import org.bytedeco.javacpp.Loader;  
  8. import org.bytedeco.javacpp.annotation.Namespace;  
  9. import org.bytedeco.javacpp.annotation.Platform;  
  10.   
  11. @Platform(include="<algorithm>")        //include CPP header file  
  12. @Namespace("std")                       //CPP standard namespace  
  13. public class Algorithm {  
  14.     static { Loader.load(); }           //load shared library  
  15.     /*** 
  16.      *  CPP sort algorithm  
  17.      */  
  18.     public static native void sort(IntPointer first, IntPointer last);  
  19.       
  20.     /*** 
  21.      *  CPP next_permutation algorithm   
  22.      */  
  23.     public static native boolean next_permutation(IntPointer first, IntPointer last);  
  24.       
  25.     @SuppressWarnings({ "resource" })  
  26.     public static void main(String[] args){  
  27.         int[] ary = new int[]{10, -128, -9};  
  28.         IntPointer int_ptr = new IntPointer(ary);  
  29.         IntPointer end = new IntPointer(int_ptr.position(ary.length));  
  30.         IntPointer begin = new IntPointer(int_ptr.position(0));  
  31.         Algorithm.sort(begin, end);  
  32.         System.out.println("before sort: " + Arrays.toString(ary));  
  33.         int_ptr.get(ary);  
  34.         System.out.println("after sort: " + Arrays.toString(ary));  
  35.         //  
  36.         System.out.println("next permutaiton: ");  
  37.         int count = 0;  
  38.         do{  
  39.             int_ptr.get(ary);   //copy array from off-heap to on-heap  
  40.             System.out.println(Arrays.toString(ary));  
  41.             ++count;  
  42.         }while( next_permutation(begin ,end));  
  43.         System.out.println(count);  
  44.     }  
  45. }  
对于这段代码我做一些补充解释。

@Platform和@Namespace都是对应于C++里的一些概念或语言特性做的Java级别的支持。目的也是简化JNI C++代码的开发,当后面编译生成JNI文件的时候,这些信息都会自动添加到.cpp文件中。代码中的sort和next_permutation是对应于C++标准库中的这两个算法的名称,也就是快速排序和全排列算法。注意,名称务必保持一致。在声明这两个方法的时候,IntPointer是作为入参的。它其实是C++指针的一个wrapper。在C++中,算法的入参一般是迭代器,当然指针也是一种迭代器,或者说迭代器是指针一种wrapper。这里我们的目的是对整型数组进行排序和全排列。在main方法中,就是具体的逻辑了。有一点要注意,就是必须先声明end IntPointer再声明begin IntPointer。原因我在最后会做些分析。

接下来,我们对Java代码进行编译,生成字节码文件。具体的命令是:javac -cp javacpp-1.3.1.jar cppalgo/Algorithm.java。这个命令在控制台完成,或者用IDE的outputjar应该也行。结果会生成.class文件。javacpp-1.3.1.jar这个jar包,就是Maven依赖加入后,从Maven仓库里下载的jar。

再接下来,我们生成C++ JNI文件。具体的命令是:java -jar javacpp-1.3.1.jar cppalgo.Algorithm。一般来说,运行这个命令它首先会生成JNI C++文件,然后调用C++编译器生成shared library。但是,在windows上,自动链接编译器,貌似是蛮麻烦的还容易配置出错。所以,实际上运行这个命令后,是可以生成.cpp文件,但也会报连接编译器的错误:


虽然报了错,但JNI的CPP文件是正常生成的,如下图中的红框:


既然我们有了JNI的C++文件,那么其实我们可以利用编译器,比如Visual Studio来对其进行编译生成shared library。我们在VS中新建dll项目(具体这里不详细讲了,和一般的dll项目一样,可网上查阅),项目命名为jniAlgorithm,也就是和生成的C++文件同名。将之前生成的JNI C++文件拷贝到项目的源文件目录中,并加上这一句:

[cpp] view plain copy
  1. #include "stdafx.h"  
此外,由于编译的时候需要调用jni.h之类的头文件,所以需要在项目的头文件引用配置中,将JDK中jni.h所在的目录路径添加进去。我自己的在VS2012中的额外头文件配置路径如下:


此外,如果是64位的系统,还需要将整个项目配置成64位的dll输出,具体可网上搜索相关内容。

然后编译整个项目,如果成功,就会生成shared library,也就是windows平台上的dll文件。我这边生成的文件如下:

到此,整个工作已经接近完成。最后,在Java IDE中,执行那个文件,当然最好加上这一句:-Djava.library.path=动态链接库路径。也就是指定刚才动态链接库生成的路径,这样程序就可以找得到那个dll文件。执行的结果如下:

[plain] view plain copy
  1. before sort: [10, -1, 2, 8, -9]  
  2. after sort: [-9, -1, 2, 8, 10]  
  3. next permutaiton:   
  4. [-9, -1, 2, 8, 10]  
  5. [-9, -1, 2, 10, 8]  
  6. [-9, -1, 8, 2, 10]  
  7. [-9, -1, 8, 10, 2]  
  8. [-9, -1, 10, 2, 8]  
  9. [-9, -1, 10, 8, 2]  
  10. [-9, 2, -1, 8, 10]  
  11. [-9, 2, -1, 10, 8]  
  12. [-9, 2, 8, -1, 10]  
  13. [-9, 2, 8, 10, -1]  
  14. [-9, 2, 10, -1, 8]  
  15. [-9, 2, 10, 8, -1]  
  16. [-9, 8, -1, 2, 10]  
  17. [-9, 8, -1, 10, 2]  
  18. [-9, 8, 2, -1, 10]  
  19. [-9, 8, 2, 10, -1]  
  20. [-9, 8, 10, -1, 2]  
  21. [-9, 8, 10, 2, -1]  
  22. [-9, 10, -1, 2, 8]  
  23. [-9, 10, -1, 8, 2]  
  24. [-9, 10, 2, -1, 8]  
  25. [-9, 10, 2, 8, -1]  
  26. [-9, 10, 8, -1, 2]  
  27. [-9, 10, 8, 2, -1]  
  28. [-1, -9, 2, 8, 10]  
  29. [-1, -9, 2, 10, 8]  
  30. [-1, -9, 8, 2, 10]  
  31. [-1, -9, 8, 10, 2]  
  32. [-1, -9, 10, 2, 8]  
  33. [-1, -9, 10, 8, 2]  
  34. [-1, 2, -9, 8, 10]  
  35. [-1, 2, -9, 10, 8]  
  36. [-1, 2, 8, -9, 10]  
  37. [-1, 2, 8, 10, -9]  
  38. [-1, 2, 10, -9, 8]  
  39. [-1, 2, 10, 8, -9]  
  40. [-1, 8, -9, 2, 10]  
  41. [-1, 8, -9, 10, 2]  
  42. [-1, 8, 2, -9, 10]  
  43. [-1, 8, 2, 10, -9]  
  44. [-1, 8, 10, -9, 2]  
  45. [-1, 8, 10, 2, -9]  
  46. [-1, 10, -9, 2, 8]  
  47. [-1, 10, -9, 8, 2]  
  48. [-1, 10, 2, -9, 8]  
  49. [-1, 10, 2, 8, -9]  
  50. [-1, 10, 8, -9, 2]  
  51. [-1, 10, 8, 2, -9]  
  52. [2, -9, -1, 8, 10]  
  53. [2, -9, -1, 10, 8]  
  54. [2, -9, 8, -1, 10]  
  55. [2, -9, 8, 10, -1]  
  56. [2, -9, 10, -1, 8]  
  57. [2, -9, 10, 8, -1]  
  58. [2, -1, -9, 8, 10]  
  59. [2, -1, -9, 10, 8]  
  60. [2, -1, 8, -9, 10]  
  61. [2, -1, 8, 10, -9]  
  62. [2, -1, 10, -9, 8]  
  63. [2, -1, 10, 8, -9]  
  64. [2, 8, -9, -1, 10]  
  65. [2, 8, -9, 10, -1]  
  66. [2, 8, -1, -9, 10]  
  67. [2, 8, -1, 10, -9]  
  68. [2, 8, 10, -9, -1]  
  69. [2, 8, 10, -1, -9]  
  70. [2, 10, -9, -1, 8]  
  71. [2, 10, -9, 8, -1]  
  72. [2, 10, -1, -9, 8]  
  73. [2, 10, -1, 8, -9]  
  74. [2, 10, 8, -9, -1]  
  75. [2, 10, 8, -1, -9]  
  76. [8, -9, -1, 2, 10]  
  77. [8, -9, -1, 10, 2]  
  78. [8, -9, 2, -1, 10]  
  79. [8, -9, 2, 10, -1]  
  80. [8, -9, 10, -1, 2]  
  81. [8, -9, 10, 2, -1]  
  82. [8, -1, -9, 2, 10]  
  83. [8, -1, -9, 10, 2]  
  84. [8, -1, 2, -9, 10]  
  85. [8, -1, 2, 10, -9]  
  86. [8, -1, 10, -9, 2]  
  87. [8, -1, 10, 2, -9]  
  88. [8, 2, -9, -1, 10]  
  89. [8, 2, -9, 10, -1]  
  90. [8, 2, -1, -9, 10]  
  91. [8, 2, -1, 10, -9]  
  92. [8, 2, 10, -9, -1]  
  93. [8, 2, 10, -1, -9]  
  94. [8, 10, -9, -1, 2]  
  95. [8, 10, -9, 2, -1]  
  96. [8, 10, -1, -9, 2]  
  97. [8, 10, -1, 2, -9]  
  98. [8, 10, 2, -9, -1]  
  99. [8, 10, 2, -1, -9]  
  100. [10, -9, -1, 2, 8]  
  101. [10, -9, -1, 8, 2]  
  102. [10, -9, 2, -1, 8]  
  103. [10, -9, 2, 8, -1]  
  104. [10, -9, 8, -1, 2]  
  105. [10, -9, 8, 2, -1]  
  106. [10, -1, -9, 2, 8]  
  107. [10, -1, -9, 8, 2]  
  108. [10, -1, 2, -9, 8]  
  109. [10, -1, 2, 8, -9]  
  110. [10, -1, 8, -9, 2]  
  111. [10, -1, 8, 2, -9]  
  112. [10, 2, -9, -1, 8]  
  113. [10, 2, -9, 8, -1]  
  114. [10, 2, -1, -9, 8]  
  115. [10, 2, -1, 8, -9]  
  116. [10, 2, 8, -9, -1]  
  117. [10, 2, 8, -1, -9]  
  118. [10, 8, -9, -1, 2]  
  119. [10, 8, -9, 2, -1]  
  120. [10, 8, -1, -9, 2]  
  121. [10, 8, -1, 2, -9]  
  122. [10, 8, 2, -9, -1]  
  123. [10, 8, 2, -1, -9]  
  124. 120  
我们看到,无论是排序还是全排列,结果都符合我们的预期。也就是说,到此为止,一个简单的JavaCPP应用就完成了。

最后,我说下可能存在的坑:

1.之前讲的,end Pointer需要比begin Pointer先声明的原因:在程序中,调用的position接口会改变指针的位置,并且这个位置信息会传到C++中。因此,要先声明end Pointer。

2.Pointer的get方法:将off-heap memory中的数据copy到on-heap中,这步不可缺少。

3.Pointer中存在deallocate方法,用于释放C++的内存。但是,Java中对象并不会被立刻gc,其实也不可能被立刻gc


总结一下。其实在Java调用C++的场景在算法中还是比较多的。原因可能在于

1.已经有很多高效的C++的算法库存在,如opencv等等

2.理论上,C++的执行效率会高于Java。毕竟JVM有些操作也是通过调用C++来做的。因此直接将大量运算就放在off-heap上进行,也是一种选择

3.内存利用率可能会更高。C++的缺点在于程序员需要自己管理内存,管理不当,可能会造成内存泄漏。但是这恰恰也是其有点,因为及时地释放内存,可以提高使用效率。不像Java,基本只能依赖gc。


0 0