代码结构
总体架构
Faiss 采用分层架构,并使用了多种计算机编程语言。
编译方式
Faiss 及其 Python 接口的编译通过一个 Makefile 完成。这个 Makefile 依赖于若干变量(如 BLAS 库、优化选项等),这些变量设置在 makefile.inc 文件中。关于如何正确设置这些参数,详见 INSTALL 文档。
依赖项
Faiss 唯一的强制依赖是 BLAS/Lapack 线性代数库。开发时使用的是 Intel MKL,但只要实现了相同接口且支持 Fortran 调用约定的 BLAS/Lapack 实现都可支持。当前许多 BLAS 实现正在从 32 位整数转向 64 位整数,因此需要在编译时用 -DFINTEGER=int 或 -DFINTEGER=long 参数指定整数类型。
BLAS (Basic Linear Algebra Subprograms) 是基础线性代数子程序库,用于高效处理向量和矩阵的基本运算。
CPU 端 Faiss C++ 编码规范
Faiss 的 CPU 端代码设计为易于脚本语言封装。GPU 端代码独立开发,有不同的规范(参见下文)。
无公有/私有成员
所有对象都为 C++ 的 struct 结构体,没有公有(public)或私有(private)字段的区分,全部字段均可直接访问。这意味着无法通过 getter/setter(取值/赋值器)实现安全检查。
这种设计使得外部代码可以更轻松访问索引的内部结构,无需修改 Faiss 代码或重新编译库。
对象所有权(C++)
Faiss 类尽量保持简单,默认拷贝构造函数有效,析构函数为空。但有部分例外情况:当对象 A 持有对象 B 的指针时,A 中会有一个布尔变量 own_fields,指示销毁 A 时是否也应销毁 B。
- 构造函数默认将
own_fields设为 false(不拥有指针内存)。 - 若调用代码丢失了对 B 的引用,可能出现内存泄漏。
- 若希望销毁 A 时自动释放 B,请将
own_fields设置为 true。 - 通过如
index_factory、load_index和clone_index这类函数构建 A/B 时,会自动将own_fields设为 true。
| 类名 | 字段名 |
|---|---|
| IndexIVF | quantizer |
| IndexPreTransform | chain |
| IndexIDMap | index |
| IndexRefineFlat | base_index |
例如,在 C++ 中创建 IndexIVFPQ 索引的典型写法如下:
faiss::Index *index;
if (...) { // 该代码块结束后,只需追踪 index 对象
faiss::IndexFlatL2 *coarseQuantizer = new faiss::IndexFlatL2(targetDim);
// 这里必须用指针
faiss::IndexIVFPQ *index1 = new faiss::IndexIVFPQ(
coarseQuantizer, targetDim, numCentroids, numQuantizers, 8);
index1->own_fields = true; // 索引析构时自动释放 coarse quantizer
index1->nprobe = 5;
index = index1;
}
Faiss 并没有引入 shared_ptr(智能指针),主要是因为要兼容 Lua/Python 的引用计数机制较难实现。
对象所有权(Python)
通过 SWIG(Simplified Wrapper and Interface Generator,简化封装与接口生成器)可以为每个 C++ 对象自动生成 Python 包装对象:
- 若对象在 Python 中新建(比如通过构造函数或
read_index),Python 对象生命周期结束时会自动销毁对应的 C++ 对象。 - 如果 Python 中构建了组合对象,示例如下:
def make_index():
coarseQuantizer = faiss.IndexFlatL2(targetDim)
index = faiss.IndexIVFPQ(
coarseQuantizer, targetDim, numCentroids, numQuantizers, 8)
index.nprobe = 5
return index
- 这时 C++ 对象的
own_fields默认为 false,Python 层需要追踪对象所有权。 - 构造函数接收对象参数时,Python 层会把该对象加入
referenced_objects(Python 对象的动态列表)。 - 通过
add_ref_in_constructor实现引用管理。
判断对象所有权是否正常的简单方法:
b = B()
a = a(b)
a.do_something()
以及
a = a(B())
a.do_something()
如果第二种方式崩溃(比如段错误,而第一种没问题),则说明对象所有权管理有问题。
编码规范
Faiss 采用 C++17 编写。对外接口不要使用模板(template)。可以使用虚函数(virtual function)。
- 类名采用驼峰式(CamlCase)
- 方法和字段用下划线分隔的小写(如 Python,lower_case_with_underscores)
- 各类都需有无参构造函数,并初始化参数为可复现的默认值,便于读写 I/O
- 缩进:C++、Python 代码均为 4 空格
- 强制使用 clang-format 进行代码格式化,提交前请先运行
- 不要用
long、long long、char(除字符串外),这些类型可移植性差。建议用uint8_t、int64_t这类明确大小的类型
写简单的代码。C++ 语法复杂,很多功能可有多种表达方式,请选择最易理解的一种。
几条建议:
- 除非需要,避免过多的抽象。
- 避免使用算法库中的大多数算法(如
copy_if、count_if),优先写显式循环。 - 避免写无实际作用的代码(如未被使用的 getter/setter)。
优化相关
优化后的代码会变得冗长重复且难读。
在优化前请先编写一个可读性好的非优化版本,这样可以:
- 更好地理解算法原理
- 为优化效果作对比参考
- 方便新硬件上的优化基线
建议即便有优化版本,也要让非优化版本可供调用(不要仅写进注释)。
避免代码膨胀
由于 Faiss 已较为庞大,需要尽可能避免冗余代码:
- 出现大量复制粘贴,多半是不合理的设计
- 冗余代码维护成本高,更新需多处手动同步
- 以代码行数(LOC)评估开发绩效是无效的
避免膨胀的技巧:
- 经典的函数重构(将重复逻辑提取为函数)
- 模板和预处理宏(虽可复用,但易致可读性、编译错误问题)
- 审慎决定哪些功能该放入 Faiss 内部:Faiss 主要做向量搜索,不相关功能可考虑移至
contrib目录;也正持续推进模块化,便于在库外编写高效专用代码 - 构建脚本和测试脚本(如 Python)可以用宏、循环生成,减少冗余
GPU 端 Faiss 编码规范
GPU 端 Faiss 索引对象从 CPU 版本继承,实现部分(但不是全部)相同接口。出于多种原因,CPU 接口并非全部都能迁移到 GPU,但主要功能都已覆盖。建议多用 getters/setters,因为在 GPU 上更改状态可能有副作用(且需要做特殊错误检查)。因此,现有 CPU 与 GPU API 有一定差异,未来会尝试进一步统一。
.cu和.cuh文件用于 CUDA 编译与包含,由nvcc编译器处理。.h和.cpp由主机编译器编译。- 最佳链接方式是用
nvcc,因为它能自动链接所需 CUDA 库。
测试
所有新提交的代码都应有对应测试。
推荐用 Python 进行测试,因为这样可以完整测试所有层次的调用(C++ 测试无法覆盖 Python 包装部分)。
请将测试拆分,示例如下:
# 错误示例
class Test1(unittest.TestCase):
def test_all(self):
for k in 1, 2, 3:
self.assertEqual(faiss.add_10(k), k + 10)
# 推荐写法
class Test1(unittest.TestCase):
def do_test(self, k): # unittest 不会自动执行,不以 "test_" 开头
self.assertEqual(faiss.add_10(k), k + 10)
def test_1(self):
self.do_test(1)
def test_2(self):
self.do_test(2)
def test_3(self):
self.do_test(3)
拆分后的测试更易定位具体失败用例。
外部项目集成
按惯例,Faiss 的头文件以 <faiss/...> 形式引用。例如:
#include <faiss/IndexIVFPQ.h>
#include <faiss/gpu/GpuIndexFlat.h>
C++ 代码封装与 Python 调用
Faiss 的 C++ 代码可被 Python 调用。这主要通过 SWIG 实现:
- SWIG 解析 Faiss 头文件,为每个 C++ 类生成对应 Python 类。
- Python 下的低层模块名为
swigfaiss,所有 C++ 类和方法可直接 Python 调用。 faiss为更高一层封装,导出全部 swigfaiss 包内容,自动选择 GPU/CPU 版本,并为类增添附加方法和函数。
C++ 指针的处理
C++ 类如果需要数组指针参数(如 float* x),SWIG 默认并不支持。为此,Faiss 的 Python 封装(如 faiss.py)添加了一些辅助工具:
- 用
swig_ptr(numpy.array)函数可从 numpy 数组提取 SWIG 指针。会检查类型与数据布局(必须为 C-连续),但不会防止 NULL/None 或校验数组长度,仍可能造成 C++ 崩溃或内存错误。 - 常用方法(如
Index::train,Index::add,Index::search等)提供单独的 Python 封装,能自动传递并校验数组。原始 C++ 方法(如train)被重命名为train_c,由新封装方法代理。 - 若想将
std::vector<> v转为 numpy 数组,用faiss.vector_to_array(v)(会复制数据,仅适用于标量元素的 vector)。 - 要把 numpy 数组拷贝到
std::vector<> v,用faiss.copy_array_to_vector(a, v)(vector 会重分配并完全存放 numpy 数据)。 - 若要创建指向
float* x的 numpy 数组,用rev_swig_ptr(x, 125)(新数组长为 125,不复制数据也不做长度检查)。若指针来自std::vector<float>,请用.data()获取底层数据指针。
上述操作请务必注意指针生命周期与数据一致性,否则会有野指针、内存泄漏或崩溃风险。