1. 程式人生 > >Deeplearning4j 實戰(3):簡介Nd4j中JavaCPP技術的應用

Deeplearning4j 實戰(3):簡介Nd4j中JavaCPP技術的應用

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。