查看原文
其他

fastFFI 官宣开源,一款高效的Java跨语言通信框架

天筱 云巅论剑 2022-05-30

在近期的云栖大会上,阿里巴巴 JVM 团队开源了其自主研发的 fastFFI 项目,目前,fastFFI 的代码和样例均在 Github (访问链接见文末)上可以访问。

fastFFI 介绍

fastFFI 是一个现代高效的 FFI 框架,其开发初衷是提高不同语言之间相互通信的易用性与性能,目前的实现主要是针对 Java 访问 C++ 代码和数据。不同程序设计语言擅长解决不同的问题,因此跨语言调用的需求在现代软件开发过程中越来越强烈。
JNI (Java Native Interface) 是 JVM 标准的  FFI  接口,开发  JNI  应用耗时且易错,除此之外,频繁的通过 Java Native 方法对 JNI 函数的调用往往会带来严重的性能问题。为了使用一个外部的函数,开发者需要首先在 Java 里声明一个 Native 方法,之后再在 Native 端开发一个 JNI 函数,在这个 JNI 函数里实现对外部函数的调用。由于 Java 和 Native 端的类型不同,JNI 函数还需要做相应的类型转换。

相比对普通的 Java 方法的调用,对 Java Native 方法的调用有额外性能开销,这些开销主要来自两部分:
1、对 Java Native 方法以及 JNI 函数调用的传参、上下文切换的开销,这是由于 JIT 编译器无法通过内联优化消除对 JNI 函数的调用。这一类开销主要是由于 Java 方法是由 JIT 编译,而 JNI 函数是由 C++ 编译器编译,不同编译器之间存在屏障。
2、Java Native 方法和 JNI 函数之间参数和返回值类型转换,例如所有的 Java String 对象需要通过 JNI 提供的接口函数转成对应的 C-style 字符串。这一类开销主要是由于 Java 和 Native 数据类型的不同造成的。
既有的 Java 的 FFI 框架大多称之为抽象 FFI 框架,是基于 JNI 之上提供一些类型支持,主要尝试解决 FFI 的易用性差的问题。事实上,既有的一些流行工具,例如 JNA (Java Native Access) 以及 JNR (Java Native Runtime) ,实现上基于通用 JNI stub 和 dlsym,以牺牲性能来追求极致的易用性。
fastFFI 同样易用,和 JavaCPP 一样,fastFFI 也是能够自动生成 JNI 代码。相比 JNR 和 JNA,fastFFI 额外需要的步骤是调用 C++ 编译器编译其自动生成的 JNI 代码。fastFFI 提供一个工具(正在内部测试中),可以自动的从 C++ 头文件中抽取 C++ 的各种定义,生成对应的 Java Binding。
在 fastFFI 中,一个 binding 是一个用来映射一个 C++ 类型定义的 Java 接口。由于接口不能被执行,因此 fastFFI 会自动生成一个 binding 类,通过在 binding 类里定义 native 方法来通过 JNI 访问 C++ 的函数。这里,我们采用 Java 接口是为了最大化的保留灵活性,未来 JVM 若提出新的 FFI 接口,fastFFI 可以无缝切换,只要在生成 binding 类的时候做相应的适配即可。采用 fastFFI 用户可以省去开发 Java Native 方法和对应的 JNI 函数的步骤。
我们认为,在现代的云部署环境下,Java 跨平台优势已经不重要,目前的云基础设施可以自动的完成对 Java 以及生成的 JNI 代码的编译、构建、打包和部署。此外,fastFFI 和 JavaCPP 都能支持 C++,而 JNR 和 JNA 只能支持对 C 构建出的动态链接库的支持,C++ 开发的库需要额外提供一套 CAPI。
顾名思义,fastFFI 相比既有的 FFI 工具和框架,其一大特点是快。由于是基于生成的 JNI 代码,最坏情况下,fastFFI 的其性能和人工开发的 JNI 一致。然而,fastFFI 青出于蓝而胜于蓝,fastFFI 能提供比 JNI 更快的访问 C++ 数据的效率,这得益于 fastFFI 内置一个工具 llvm4jni,可以将 LLVM Bitcode 转成 Java Bytecode,打破 JVM 的 JIT 编译器和 JNI 代码的 C++ 编译器之间的屏障。
我们说 fastFFI 是一个现代的 FFI 框架,这是由于,相比既有的任何 FFI 框架,fastFFI 是唯一可以将 C++ 模板映射到 Java 泛型的框架。通过 fastFFI 定义一些基本的类型信息,fastFFI 的代码生成器通过类型推导,可以为一个 C++ 模板的每一个实例化生成一个对应的 binding 类。这个 binding 类还可以用于在传参时做类型检查,用于在 Java 端尽早发现一些类型错误。此外,我们还尽可能的利用 Java 的语言结构映射 C++ 的语言结构。例如,fastFFI 采用的是 binding 接口而不是 binding 类,能够通过 Java 接口的多继承支持 C++ 的多继承。一些 Java 和 C++ 共同的高级的语言结构,例如异常处理和 Lambda,也在开发支持中。

应用场景

向 Java 中引入优秀的 C++ 库

fastFFI 的第一个应用场景是让 Java 用户享受 C++ 的高质量框架,这里以 JSON 解析为例。下图是一个典型的 JSON 解析的场景,一个用户通过 JSON 的 parser 将输入的 JSON 文件或者字节流转成 DOM 对象,之后将 DOM 对象转换成最终的用户对象。因此整个流程分为两个部分:解析和转换
我们用 simdjson (链接见文末) 和 Jackson 比较。simdjson 是一个 C++ 开发的现代 JSON 解析器,我们用 fastFFI 为其创建了一个 Java SDK,而 Jackson 则是由 Java 开发的 JSON 解析框架。我们采用 simdjson 中的四个 benchmark 中比较,从中可以看出,simdjson 的解析性能远远大于 Jackson,虽然通过 FFI 来转换 DOM 对象相比 Java 来转换 DOM 对象有些性能差异。因此,在实际的使用中,尤其针对比较大的 JSON 文件,完全可以采用 simdjson+fastFFI 的方式来处理。这里由于解析是一次复杂 JNI 调用,llvm4jni 无法优化,因此 fastFFI 和 JNI 的性能差异并不明显,仅仅略有优势。
因为不开启 llvm4jni,fastFFI 性能和 JNI 等价。本文所有表格的表头中,JNI 指的是采用 fastFFI 开发的 Java SDK,且未开启 llvm4jni,而 fastFFI 则指的是采用 fastFFI 开发的 Java SDK 且打开了 llvm4jni。

大数据

fastFFI 一大重要场景就是在大数据领域替代既有的 in-memory 跨平台数据格式(例如 Apache Arrow 和 Google FlatBuffers)。在大数据领域,一个应用一般分为数据管理平台和数据处理算法两部分。对于数据管理平台,一般用 C++ 开发更合适,可以得到灵活的内存管理,而是对于数据处理算法,用高级程序设计语言开发效率更高。
我们以 Alibaba 的图计算引擎  Grape(链接见文末) 为例,通过使用 fastFFI 创建的 Java SDK 可以显著的减少 JNI 带来的性能问题。
我们选取四种算法,BFS、SSSP、PageRank、以及 WCC,用 C++ 和 Java 分别基于 Grape C++ 和 Java API 实现。如下表所示,尤其以 SSPS 和 PageRank 为例,虽然 fastFFI 相比 C++ 的实现仍然有不少的性能差距,但是其已经大幅显著的缩小 JNI 的开销,为在 Grape 上利用 Java 开发算法提出了可能。

跨语言数据共享

fastFFI 起初是为了在跨语言调用中取代跨平台内存数据格式(例如,Apache Arrow和Google FlatBuffers)。我们认为,在 Java 和 C++ 互相调用的程序中,最优秀的内存数据格式就是 C++ 对象,C++ 代码可以高效灵活的访问和创建这些对象。然而,C++ 对象在内存中的布局在 Java 中并不能轻易获得,Java 因此无法快速的访问 C++ 对象的成员。
为了解决这些难题,fastFFI 可以根据 C++ 的对象定义生成对应的 Java binding 接口以及相应的 binding 类和 JNI 代码,这样 Java 就可以通过 fastFFI 访问 C++ 的成员。为了消除 JNI 的开销,我们可以通过 llvm4jni 将对成员访问的 JNI 函数转成 Java 字节码方法,这样 JIT 编译器就可以优化这些 JNI 函数。换句话说,fastFFI 借助 C++ 编译器(clang)生成对 C++ 对象的成员访问 LLVM Bitcode 代码,这些 Bitcode 代码蕴含了对 C++ 对象的访问以及布局信息。借助 llvm4jni,我们将这些信息通过 Java Bytecode 编码,最终通过 JIT 实现高效的对 C++ 成员的访问。
为了移除开发 C++ 数据类型的需求,fastFFI 提供一些 Annotation 可以支持从一个 Java 接口自动的生成 C++ 对象的定义,该机制称为 fastFFI mirror。
此外,一些跨平台的数据格式都会同时提供 C++ 和 Java 的 API,例如 Protocol Buffer 以及 FlatBuffers。根据之前的评估知道,一般直觉上而言,C++ 的 parser 要比 Java 的 parser 在大部分场景下要快。因此,我们用 fastFFI 来将 FlatBuffers 的 C++ API 创建了新的 Java SDK,称之为 FlatBuffers FFI。从表中可以,采用 fastFFI mirror(通过 C++ 对象交互)是最高效的方式。值得注意的是,C++ 对象仅适合数据是在本进程的 Java 和 C++ 中交互,如果涉及到跨平台的交互,则可能仍然需要将数据转成 FlatBuffers 的格式。不过即使如此,采用 fastFFI 包裹 C++ API 得到的 Java SDK 仍然是最为高效的。

结语

fastFFI 项目仍在不断演进中,期待更多开发者的使用和讨论。
访问链接地址如下:
Github访问链接:https://github.com/alibaba/fastffi
simdjson访问链接:https://github.com/simdjson/simdjson
Grape访问链接:https://github.com/alibaba/libgrape-lite
往期精彩推荐
1.双十一咋省钱?KeenTune助你业务资源省省省
2.LifseaOS 悄然来袭,一款为云原生而生的 OS
3.6位大咖坐在一起聊了聊:龙蜥社区做了什么、以及能带来什么?
4.龙蜥操作系统将捐赠开放原子开源基金会
5.6秒拉起3000个!阿里云Severeless产品背后的底层技术究竟有多硬核?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存