编译Faiss
安装OpenBlas
OpenBlas是科学计算库,是基于BLAS 和 Lapack的接口规范进行了实现,实现了BLAS 全部功能,以及Lapack的部分功能。
-
解压进入目录
-
执行make或者gmake
# gmake
-
install
# mkdir /opt/openblas-0.3.4 # make PREFIX=/opt/openblas-0.3.4 install # ln -s /opt/openblas-0.3.4/lib/libopenblas.so /usr/lib64/libopenblas.so
安装Lapack
LAPACK 是用fortran语言编写的线性代数库,它的底层也是BLAS,所以我们在安装LAPACK 前需要先安装BLAS。
安装gfortran
# yum install gcc-gfortran
安装BLAS
-
下载 BLAS-3.8.0
-
解压进入安装目录
-
执行make命令,生成.a的静态文件,之后设置BLAS环境变量
# make # mv blas_LINUX.a libblas.a # cp libblas.a /usr/local/lib/
安装LAPACK
-
解压压缩包
-
创建lapack-build目录,目录结构如下:
|- lapack-3.8.0 |- lapack-build
-
make & install
# cd lapack-build # cmake -DCMAKE_INSTALL_PREFIX=/usr/local/lapack-3.8.0 -DCMAKE_BUILD_TYPE=RELEASE -DBUILD_SHARED_LIBS=ON ../lapack-3.8.0 # make && make install
-
在/usr/local/lib下创建软链
# ln -s /usr/local/lapack-3.8.0/lib64/liblapack.so /usr/local/lib/liblapack.so
安装libgfortran4
yum install libgfortran4
编译Faiss
-
下载Faiss-1.4.0
-
解压压缩包,并进入解压目录
-
configure
# ./configure --prefix=/opt/faiss-1.4.0 --with-blas=/usr/lib64/libopenblas.so --with-lapack=/usr/local/lib/liblapack.so
这里主要指定目标地址,以及openblas、lapack库地址。
-
make && make install
# make && make install
到这里我们就可以在
/opt/faiss-1.4.0
中找到我们想要的.so文件了。
JNI开发
这里不给出具体的细节,关于JNI开发的一些内容可参考我的另外一篇博客JNI开发整理,下面拎出来两个在开发过程中遇到的问题:
uint8_t到Java类型的转换
先看一下Faiss中OnDiskInvertedLists
结构体的一个方法:
const uint8_t * get_codes (size_t list_no) const override;
该方法返回了一个uint8_t
类型的数组对象,这可咋整?在JNI Types and Data Structures一文中我们发现:
Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
一开始很惊喜,但是仔细一想不对啊,这只能说明java中的boolean映射到Native中是用uint8_t
存储的,不能说明boolean
就是无符号8位实现,翻阅The Java Virtual Machine Specification,也提到了对于boolean array,每个boolean元素使用8bits,看到这这,我差点在定义native方法时就用boolean[]作为返回值了:
public native boolean[]get_codes(int list_no);
但是看上去总感觉有那么点怪怪的,脑子中一直有一个byte在盘旋,但是java中byte是有符号8bit的实现,不敢用啊,纠结了一会之后发现,好像陷入了一个误区啊,OnDiskInvertedLists中codes也就是uint8_t *
并不是用来参与数值运算的啊,有符号无符号有啥关系呢,作为编码存储的话有效位还是8位啊,是果断改为byte[]实现:
//java class OnDiskInvertedLists
public native byte[] get_codes(int list_no);
//JNIOnDiskInvertedLists.cpp
JNIEXPORT jbyteArray JNICALL Java_xxx_xxx_OnDiskInvertedLists_get_1codes(
JNIEnv *env, jobject obj, jint list_no) {
OnDiskInvertedLists* ilist = currentIList(env, obj);
const uint8_t* codes = ilist->get_codes(list_no);
int len = ilist->list_size(list_no) * ilist->code_size;
return newArray(env, len, codes);
}
最后再贴一段无符号转到有符号之后,到底有损还是无损的测试代码?
uint8_t uint_15 = 15;
signed char int_15 = (signed char) uint_15;
cout << "1111转有符号后的高位:" << (short) (int_15 >> 3) << endl;
uint8_t uint_7 = 7;
signed char int_7 = (signed char) uint_7;
cout << "0111转有符号后的高位:" << (short) (int_7 >> 3) << endl;
输出:
1111转有符号后的高位:1
0111转有符号后的高位:0
多例的实现
这可能是在JNI开发中遇到的最多的问题,因为JNI开发实际上是面向函数的,看下面一段代码:
//Java class A
public class A{
public A(int v){
init(int v);
}
public native init(int v);
public native int invokeCMethod();
public static void main(String[]args){
A a1 = new A(1);
System.out.println(a1.invokeCMethod());
A a2 = new A(2);
System.out.println(a2.invokeCMethod());
}
}
//A.cpp
class A{
private int value;
public A(int v){value=v;}
public int invokeCMethod(){ return value;}
}
//JNI_A.cpp
static A* a = NULL;
JNIEXPORT void JNICALL Java_A_init(JNIEnv *env, jobject obj, jint v){
a = new A(v);
}
JNIEXPORT jint JNICALL Java_A_invokeCMethod(JNIEnv *env, jobject obj){
return a->invokeCMethod();
}
当只有单个线程的时候,我们确实可以做到了多例的访问,但是如果有多个线程,或者在new完a2之后,我们再想使用a1时,这就不太可能了,那么怎么办呢?这里有两种解决思路:
第一种,在JNI_A.cpp定义一个全局的map对象,同时在new Java对象时,生成一个key值,每次调用native函数时都传递该key值,在JNI_A.cpp中根据key值从map中添加/获取对象进行调用:
//Java class A
public class A{
private String key;
public A(int v){
key = UUID.randomUUID().toString();
init(key,v);
}
public native init(String key, int v);
public native int invokeCMethod(String key);
}
//JNI_A.cpp
static map<string, A> *aMap = new map<string, A>();
JNIEXPORT void JNICALL Java_A_init(JNIEnv *env, jobject obj,jstring key, jint v){
const char *c_str = env->GetStringUTFChars(key, 0);
A* a = new A(v);
(*aMap)[c_str]=*a;
env->ReleaseStringUTFChars(key, c_str);
}
JNIEXPORT jint JNICALL Java_A_invokeCMethod(JNIEnv *env,jstring key, jobject obj){
const char *c_str = env->GetStringUTFChars(key, 0);
map<string, A>::iterator iter = aMap->find(c_str);
env->ReleaseStringUTFChars(key, c_str);
if (iter == aMap->end()) {
jclass exClass = env->FindClass("java/lang/NullPointerException");
env->ThrowNew(exClass, "can not find instnce A for key!");
}
return &(iter->second)->invokeCMethod();
}
第二种实现比较粗暴,但是代码比较精简,主要借助c/c++中的reinterpret_cast
进行实现,它可以将将对应的long类型地址转为实际的对象类型,我们直接看代码:
//Java class A
public class A{
private long point;//保存c++中A对象的地址空间
public A(int v){init(v);}
public native init(int v);
public native int invokeCMethod(long point,String key);
}
JNIEXPORT void JNICALL Java_A_init(JNIEnv *env, jobject obj, jint v){
A* a = new A(v);
jclass jcls = env->FindClass("com/focustech/fsp/faiss/jni/CBase");
jfieldID pointField = env->GetFieldID(jcls, "point", "J");
env->SetLongField(obj, pointField, point);
}
JNIEXPORT jint JNICALL Java_A_invokeCMethod(JNIEnv *env,jobject obj,jlong point){
A* a = reinterpret_cast<A*>(point);
return a->invokeCMethod();
}
创建C++中的A对象,然后将地址反写回Java对象中,这样下回调用时直接通过地址映射到对应的C++对象,其实上面两种方法在调用invokeCMethod()时都可以不显示的传递key值或地址参数,因为JNI查找并调用函数时,已经将对应的Java对象作为参数传递到Native函数中,但是这样每次通过该对象取属性效率到底高还是不高,最终还是得通过测试来确认(目前在我的项目中,主要通过该方式进行实现)。
对Faiss的Java调用实现,在我的github上已经创建项目(faiss-java),目前只实现了IndexFlat接口,后续还会持续添加。
最后的最后,贴出我项目中编译JNI实现用的CMakeLists.txt
:
# https://cmake.org/cmake/help/v3.0/index.html
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.6)
add_definitions(-std=c++11)
# 变量设置
set (FAISS_HOME "/opt/faiss-1.4.0")
set (OPENBLAS "/opt/openblas-0.3.3")
set (JAVA_HOME "/opt/jdk1.8.0_40")
MESSAGE(STATUS "JAVA_HOME = ${JAVA_HOME}")
MESSAGE(STATUS "CMAKE_HOME_DIRECTORY = ${CMAKE_HOME_DIRECTORY}")
MESSAGE(STATUS "FAISS_HOME = ${FAISS_HOME}.")
# 项目信息
project ("faiss4j")
#头文件查找路径,相当于GCC的-I参数
include_directories("${JAVA_HOME}/include")
include_directories("${JAVA_HOME}/include/linux")
include_directories("${CMAKE_HOME_DIRECTORY}/src/main/resources/fass-jni")
include_directories("${FAISS_HOME}/include/faiss")
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory("${CMAKE_HOME_DIRECTORY}/src/main/resources/fass-jni" DIR_SRCS)
MESSAGE(STATUS "DIR_SRCS = ${DIR_SRCS}.")
link_libraries("${FAISS_HOME}/lib/libfaiss.so")
add_library(${PROJECT_NAME} SHARED ${DIR_SRCS} "${FAISS_HOME}/lib/libfaiss.so")