作者:来自 Elastic Chris Hegarty
在 Lucene 领域,我们一直热切地采用新版本 Java 的功能。这些功能使 Lucene 更接近 JVM 和底层硬件,从而提高了性能和稳定性。这使得 Lucene 保持现代化和具有竞争力。
Lucene 的下一个主要版本,Lucene 10,将需要至少 Java 21。让我们看看我们为什么要这样做以及它将如何惠及 Lucene。
外部内存
出于效率考虑,索引及其各种支持结构被存储在 Java 堆之外 - 它们被存储在磁盘上,并映射到进程的虚拟地址空间中。直到最近,在 Java 中执行此操作的方法是使用直接字节缓冲区,这正是 Lucene 一直在做的。
直接字节缓冲区具有一些固有的限制。例如,它们最多只能寻址 2GB,需要更多的结构和代码来跨越更大的尺寸。然而,最重要的是缺乏确定性关闭,我们通过调用 Unsafe::invokeCleaner 来绕过此问题,有效地关闭缓冲区并释放内存。正如其名称所暗示的那样,这是一种不安全的操作。Lucene 在此周围添加了保障,但根据定义,在释放内存后仍然存在微小的失败风险。
更近些时候,Java 添加了 MemorySegment,它克服了我们在直接字节缓冲区中遇到的限制。我们现在拥有了安全的确定性关闭,并且可以处理远远超过以前限制的内存。虽然 Lucene 9.x 已经可选地支持由内存段支持的映射目录实现,但即将推出的 Lucene 10 放弃了对字节缓冲区的支持。所有这些意味着,Lucene 10 只能使用内存段,因此最终在一个安全的模型中运行。
外部函数
不同的工作负载,如搜索或索引,或者不同类型的数据,比如文档值或向量嵌入,具有不同的访问模式。正如我们所见,由于 Lucene 映射其索引数据的方式,与操作系统页面缓存的交互对性能至关重要。
多年来,人们在围绕内存使用和页面缓存进行优化方面付出了大量的努力和考虑。首先是通过调用 madvise 的本地 JNI 代码,然后是使用直接 I/O 的目录实现。然而,尽管在当时表现良好,但这两种解决方案都略显不理想。前者需要特定于平台的构建和构件,后者则利用了可选的 JDK 特定 API。因此,出于这些原因,这两种解决方案都不是 Lucene 核心的一部分,而是存在于更远的 misc 模块中。Mike McCandless 在 2010 年写了一篇很好的博客!
在现代 Java 中,我们现在可以使用 Panama Foreign Function Interface (FFI) 来调用系统上的本地库函数。我们在 Lucene 核心中直接使用它来从 Java 中调用标准 C 库中的 posix_madvise - 而无需任何 JNI 代码或非标准功能。通过这个,我们现在可以告诉系统我们打算使用的内存访问模式。
向量化
并行性和并发性,虽然是不同的概念,但通常都可以转化为 "将任务拆分以便更快地执行",或者 "同时执行更多任务"。Lucene 不断研究新的算法,并努力以更高效和有效的方式实现现有算法。现在在 Java 中对我们来说更加简单的一个领域是数据级并行性 - 即使用 SIMD(单指令多数据)向量指令来提高性能。
Lucene 使用最新的 JDK Vector API 来实现向量距离计算,从而产生高效的硬件特定的 SIMD 指令。这些指令在支持的硬件上运行时,可以比等效的标量代码快 8 倍执行浮点点积计算。这篇博客包含了关于这个特定优化的更具体信息。
随着向 Java 21 最低版本的转变,我们可以更加直接地看到在更多地方使用 JDK Vector API 的可能性。我们甚至正在尝试使用 FFI 调用定制的 SIMD 实现,因为现在本地调用的开销已经非常小了。
结论
尽管最新的 Lucene 9.x 版本能够受益于许多最近的 Java 功能,但需要在 Java 11 之类的早期版本上运行的要求意味着我们在 9.x 中已经达到了一定的复杂性水平,虽然今天可能还可以,但这并不是我们未来想要的状态。
即将推出的 Lucene 10 将比以往任何时候都更接近 JVM 和硬件。通过要求至少使用 Java 21,我们能够放弃旧的直接字节缓冲区目录实现,通过 posix_madvise 可靠地向系统建议关于内存访问模式的信息,并继续努力利用硬件加速的指令。
准备将 RAG 构建到你的应用中吗?想要尝试使用向量数据库的不同 LLM 吗? 在 Github 上查看我们的 LangChain、Cohere 等示例笔记本,即将开始的 Elasticsearch 工程师培训!
原文:Lucene Speed: How Vectorization and FFI/madvise Make Lucene Faster --- Elastic Search Labs