Deeplearning4j 实战(3):简介Nd4j中JavaCPP技术的应用

Eclipse Deeplearning4j GiChat课程https://gitbook.cn/gitchat/column/5bfb6741ae0e5f436e35cd9f
Eclipse Deeplearning4j 系列博客https://blog.csdn.net/wangongxi
Eclipse Deeplearning4j Githubhttps://github.com/eclipse/deeplearning4j

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依赖如下:

 

   <dependency>
    	<groupId>org.bytedeco</groupId>
    	<artifactId>javacpp</artifactId>
    	<version>1.3.1</version>
   </dependency>

然后,在工程中新建Java文件,代码如下:

 

 

package cppalgo;


import java.util.Arrays;

import org.bytedeco.javacpp.IntPointer;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacpp.annotation.Namespace;
import org.bytedeco.javacpp.annotation.Platform;

@Platform(include="<algorithm>")        //include CPP header file
@Namespace("std")                       //CPP standard namespace
public class Algorithm {
    static { Loader.load(); }           //load shared library
    /***
     *  CPP sort algorithm 
     */
    public static native void sort(IntPointer first, IntPointer last);
    
    /***
     *  CPP next_permutation algorithm  
     */
    public static native boolean next_permutation(IntPointer first, IntPointer last);
    
    @SuppressWarnings({ "resource" })
    public static void main(String[] args){
        int[] ary = new int[]{10, -1, 2, 8, -9};
        IntPointer int_ptr = new IntPointer(ary);
        IntPointer end = new IntPointer(int_ptr.position(ary.length));
        IntPointer begin = new IntPointer(int_ptr.position(0));
        Algorithm.sort(begin, end);
        System.out.println("before sort: " + Arrays.toString(ary));
        int_ptr.get(ary);
        System.out.println("after sort: " + Arrays.toString(ary));
        //
        System.out.println("next permutaiton: ");
        int count = 0;
        do{
            int_ptr.get(ary);   //copy array from off-heap to on-heap
            System.out.println(Arrays.toString(ary));
            ++count;
        }while( next_permutation(begin ,end));
        System.out.println(count);
    }
}

对于这段代码我做一些补充解释。

 

@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++文件拷贝到项目的源文件目录中,并加上这一句:

 

#include "stdafx.h"

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

 

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

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

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

 

before sort: [10, -1, 2, 8, -9]
after sort: [-9, -1, 2, 8, 10]
next permutaiton: 
[-9, -1, 2, 8, 10]
[-9, -1, 2, 10, 8]
[-9, -1, 8, 2, 10]
[-9, -1, 8, 10, 2]
[-9, -1, 10, 2, 8]
[-9, -1, 10, 8, 2]
[-9, 2, -1, 8, 10]
[-9, 2, -1, 10, 8]
[-9, 2, 8, -1, 10]
[-9, 2, 8, 10, -1]
[-9, 2, 10, -1, 8]
[-9, 2, 10, 8, -1]
[-9, 8, -1, 2, 10]
[-9, 8, -1, 10, 2]
[-9, 8, 2, -1, 10]
[-9, 8, 2, 10, -1]
[-9, 8, 10, -1, 2]
[-9, 8, 10, 2, -1]
[-9, 10, -1, 2, 8]
[-9, 10, -1, 8, 2]
[-9, 10, 2, -1, 8]
[-9, 10, 2, 8, -1]
[-9, 10, 8, -1, 2]
[-9, 10, 8, 2, -1]
[-1, -9, 2, 8, 10]
[-1, -9, 2, 10, 8]
[-1, -9, 8, 2, 10]
[-1, -9, 8, 10, 2]
[-1, -9, 10, 2, 8]
[-1, -9, 10, 8, 2]
[-1, 2, -9, 8, 10]
[-1, 2, -9, 10, 8]
[-1, 2, 8, -9, 10]
[-1, 2, 8, 10, -9]
[-1, 2, 10, -9, 8]
[-1, 2, 10, 8, -9]
[-1, 8, -9, 2, 10]
[-1, 8, -9, 10, 2]
[-1, 8, 2, -9, 10]
[-1, 8, 2, 10, -9]
[-1, 8, 10, -9, 2]
[-1, 8, 10, 2, -9]
[-1, 10, -9, 2, 8]
[-1, 10, -9, 8, 2]
[-1, 10, 2, -9, 8]
[-1, 10, 2, 8, -9]
[-1, 10, 8, -9, 2]
[-1, 10, 8, 2, -9]
[2, -9, -1, 8, 10]
[2, -9, -1, 10, 8]
[2, -9, 8, -1, 10]
[2, -9, 8, 10, -1]
[2, -9, 10, -1, 8]
[2, -9, 10, 8, -1]
[2, -1, -9, 8, 10]
[2, -1, -9, 10, 8]
[2, -1, 8, -9, 10]
[2, -1, 8, 10, -9]
[2, -1, 10, -9, 8]
[2, -1, 10, 8, -9]
[2, 8, -9, -1, 10]
[2, 8, -9, 10, -1]
[2, 8, -1, -9, 10]
[2, 8, -1, 10, -9]
[2, 8, 10, -9, -1]
[2, 8, 10, -1, -9]
[2, 10, -9, -1, 8]
[2, 10, -9, 8, -1]
[2, 10, -1, -9, 8]
[2, 10, -1, 8, -9]
[2, 10, 8, -9, -1]
[2, 10, 8, -1, -9]
[8, -9, -1, 2, 10]
[8, -9, -1, 10, 2]
[8, -9, 2, -1, 10]
[8, -9, 2, 10, -1]
[8, -9, 10, -1, 2]
[8, -9, 10, 2, -1]
[8, -1, -9, 2, 10]
[8, -1, -9, 10, 2]
[8, -1, 2, -9, 10]
[8, -1, 2, 10, -9]
[8, -1, 10, -9, 2]
[8, -1, 10, 2, -9]
[8, 2, -9, -1, 10]
[8, 2, -9, 10, -1]
[8, 2, -1, -9, 10]
[8, 2, -1, 10, -9]
[8, 2, 10, -9, -1]
[8, 2, 10, -1, -9]
[8, 10, -9, -1, 2]
[8, 10, -9, 2, -1]
[8, 10, -1, -9, 2]
[8, 10, -1, 2, -9]
[8, 10, 2, -9, -1]
[8, 10, 2, -1, -9]
[10, -9, -1, 2, 8]
[10, -9, -1, 8, 2]
[10, -9, 2, -1, 8]
[10, -9, 2, 8, -1]
[10, -9, 8, -1, 2]
[10, -9, 8, 2, -1]
[10, -1, -9, 2, 8]
[10, -1, -9, 8, 2]
[10, -1, 2, -9, 8]
[10, -1, 2, 8, -9]
[10, -1, 8, -9, 2]
[10, -1, 8, 2, -9]
[10, 2, -9, -1, 8]
[10, 2, -9, 8, -1]
[10, 2, -1, -9, 8]
[10, 2, -1, 8, -9]
[10, 2, 8, -9, -1]
[10, 2, 8, -1, -9]
[10, 8, -9, -1, 2]
[10, 8, -9, 2, -1]
[10, 8, -1, -9, 2]
[10, 8, -1, 2, -9]
[10, 8, 2, -9, -1]
[10, 8, 2, -1, -9]
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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wangongxi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值