编译Faiss

安装OpenBlas

OpenBlas是科学计算库,是基于BLAS 和 Lapack的接口规范进行了实现,实现了BLAS 全部功能,以及Lapack的部分功能。

  1. 下载 OpenBLAS-0.3.4

  2. 解压进入目录

  3. 执行make或者gmake

    # gmake
    
  4. 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

  1. 下载 BLAS-3.8.0

  2. 解压进入安装目录

  3. 执行make命令,生成.a的静态文件,之后设置BLAS环境变量

    # make 
    # mv blas_LINUX.a libblas.a
    # cp libblas.a /usr/local/lib/
    

安装LAPACK

  1. 下载 blas-3.8.0.tgz

  2. 解压压缩包

  3. 创建lapack-build目录,目录结构如下:

    |- lapack-3.8.0
    |- lapack-build
    
  4. 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
    
  5. 在/usr/local/lib下创建软链

    # ln -s /usr/local/lapack-3.8.0/lib64/liblapack.so /usr/local/lib/liblapack.so
    

安装libgfortran4

yum install libgfortran4

编译Faiss

  1. 下载Faiss-1.4.0

  2. 解压压缩包,并进入解压目录

  3. configure

    # ./configure --prefix=/opt/faiss-1.4.0 --with-blas=/usr/lib64/libopenblas.so --with-lapack=/usr/local/lib/liblapack.so
    

    这里主要指定目标地址,以及openblas、lapack库地址。

  4. 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")