跨語言程式設計的探索 | 龍蜥技術

OpenAnolis小助手發表於2021-10-12

跨語言程式設計是現代程式語言中非常重要的一個方向,也被廣泛應用於複雜系統的設計與實現中。本文是 GIAC 2021(全球網際網路架構大會) 中關於 Alibaba FFI — “跨語言程式設計的探索”主題分享的內容整理。兩位分享人董登輝和顧天曉分別是龍蜥社群 Java SIG(Reliability,availability and serviceability)負責人和核心人員。

背景

前言

無疑,Java 是目前工業界最流行的應用程式語言之一。除了主流實現上(OpenJDK Hotspot)不俗的效能表現和成熟的研發生態(Spring 等)外,其成功的背後離不開語言本身較低(相比於 C/C++)的學習門檻。一個初學者可以利用現有的專案腳手架快速地搭建出一個初具規模的應用程式,但也因此許多 Java 程式設計師對程式底層的執行原理並不熟悉。本文將探討一個在大部分 Java 相關的研發工作中不太涉及到的技術  — 跨語言程式設計。

回想起多年前第一次用 Java 在控制檯上列印出 “Hello World” 時,出於好奇便翻閱 JDK 原始碼想一探實現的究竟 (在 C 語言中我們可以使用 printf 函式,而 printf 在具體實現上又依賴作業系統的介面),再一次次跳轉後最終停留在了一個“看不到實現”的 native 方法上,額,然後就沒有然後了。

我想有不少 Java 初級程式設計師對 native 方法的呼叫機制仍一知半解,畢竟在大部分研發工作中,我們直接實現一個自定義 native 方法的需求並不多,簡單來說 native 方法是 Java 進行跨語言呼叫的介面,這也是 Java Native Interface 規範的一部分。

Java 跨語言程式設計技術的應用場景

常見場景

在介紹 Java 跨語言程式設計技術之前,首先簡單地分析下實際程式設計過程中需要使用跨語言程式設計技術的場景,在這裡我羅列了以下四個場景:

1、依賴位元組碼不支援的能力

換個角度看,目前標準的位元組碼提供了哪些能力呢?根據 Spec 規範,已有的位元組碼可以實現建立 Java 物件、訪問物件欄位和方法、常規計算(加減乘除與或非等)、比較、跳轉以及異常、鎖操作等等,但是像前言中提到的列印字串到控制檯這樣的高階功能,位元組碼並不直接支援,此外,像獲取當前時間,分配堆外記憶體以及圖形渲染等等位元組碼也都不支援,我們很難寫出一個純粹的 Java 方法(組合這些位元組碼)來實現這類能力,因為這些功能往往需要和系統資源產生互動。在這些場景下,我們就需要使用跨語言程式設計技術透過其他語言的實現來整合這些功能。

2、使用系統級語言(C、C++、Assembly)實現系統的關鍵路徑

不需要顯示釋放物件是 Java 語言學習門檻低的原因之一,但因此也引入了 GC 的機制來清理程式中不再需要的物件。在主流 JVM 實現中,GC 會使得應用整體暫停,影響系統整體效能,包括響應與吞吐。

所以相對於 C/C++ ,Java 雖然減輕了程式設計師的研發負擔,提高了產品研發效率,但也引入了執行時的開銷。(Software engineering is largely the art of balancing competing trade-offs. )

當系統關鍵路徑上的核心部分(比如一些複雜演算法)使用 Java 實現會出現效能不穩定的情況下,可以嘗試使用相對底層的程式語言來實現這部分邏輯,以達到效能穩定以及較低的資源消耗目的。

3、其他語言實現呼叫 Java

這個場景可能給大部分 Java 程式設計師的第一感覺是幾乎沒有遇到過,但事實上我們幾乎每天都在經歷這樣的場景。

舉個例子:透過 java <Main-Class> 跑一個 Java 程式就會經過 C 語言呼叫 Java 語言的過程,後文還會對此做提及。

4、歷史遺留庫

公司內部或者開源實現中存在一些 C/C++ 寫的高效能庫,用 Java 重寫以及後期維護的成本非常大。當 Java 應用需要使用這些庫提供的能力時,我們需要藉助跨語言程式設計技術來複用。

Alibaba Grape

再簡單談談阿里內部的一個場景:Alibaba Grape 專案,也是在跨語言程式設計技術方向上與我們團隊合作的第一個業務方。

Grape 本身是一個並行圖計算框架的開源專案(相關論文獲得了 ACM SIGMOD Best Paper Award),主要使用 C++ 編寫,工程實現上應用了大量的模板特性。對 Grape 專案感興趣的同學可以參考 Github 上的相關文件,這裡不再詳細介紹。

該專案在內部應用中,存在很多使用 Java 作為主要程式語言的業務方,因此需要開發人員把 Grape 庫封裝成 Java SDK 供上層的 Java 應用呼叫。在實踐過程中,遇到的兩個顯著問題:

  • 封裝 SDK 的工作非常繁瑣,尤其對於像 Grape 這樣依賴模板的庫,在初期基於手動的封裝操作經常出錯
  • 執行時效能遠低於 C++ 應用

為了解決這些問題,兩個團隊展開了合作,Alibaba FFI 專案正式開始演進,該專案的實現目前也主要針對 Java 呼叫 C++ 場景。

Java 跨語言程式設計技術
下面介紹一些在工業界中相對成熟、應用較多的 Java 跨語言呼叫技術。
Java Native Interface
談到 Java 跨語言程式設計,首先不得不提的就是 Java Native Interface,簡稱 JNI。後面提到的 JNA/ JNR、JavaCPP 技術都會依賴 JNI。首先,透過兩個例子來簡單回顧一下。
控制檯輸出的例子
System.out.println("hello ffi");
透過 System.out 我們可以快速地實現控制檯輸出功能,我相信會有不少好奇的同學會關心這個呼叫到底是如何實現輸出功能的,翻閱原始碼後,我們最終會看見這樣一個 native 方法:
private native void writeBytes(byte b[], int off, int len, boolean append) throws
IOException;
(該方法由 JDK 實現,具體實現可以參考這裡:
static native void myHelloFFI();
b. 透過 javah 或者 javac -h (JDK 10)命令生成後續步驟依賴的標頭檔案(該標頭檔案可以被 C 或者 C++ 程式使用)
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloFFI */
#ifndef _Included_HelloFFI
#define _Included_HelloFFI
#ifdef __cplusplus
extern "C" {
#endif
/* 
* Class:     HelloFFI 
* Method:    myHelloFFI 
* Signature: ()V 
*/
JNIEXPORT void JNICALL Java_HelloFFI_myHelloFFI  
  (JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
c. 實現標頭檔案中的函式,在這裡我們直接使用 printf 函式在控制檯輸出 “hello ffi”
JNIEXPORT void JNICALL Java_HelloFFI_myHelloFFI  
  (JNIEnv * env, jclass c) {  
  printf("hello ffi");
}
d. 透過 C/C++ 編譯器(gcc/llvm 等)編譯生成庫檔案
e. 使用 -Djava.library.path=... 引數指定庫檔案路徑並在執行時呼叫  System.loadLibrary 載入上個步驟中生成的庫,之後 Java 程式就可以正常呼叫我們自己實現的 myHelloFFI 方法了。
C 程式呼叫 Java 方法
上面是 Java 方法呼叫 C 函式的例子,透過 JNI 技術,我們還可以實現 C 程式中呼叫 Java 方法,這裡面會涉及到 2 個概念:Invocation API 與 JNI function,在下面程式碼示例中省略了初始化虛擬機器的步驟,僅給出最終實現呼叫的兩個步驟。
// Init jvm ...// Get method idjmethodID mainID = (*env)->GetStaticMethodID(env, mainClass, "main",                                             "([Ljava/lang/String;)V");/* Invoke */(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
示例中首先透過 GetStaticMethodID 獲取方法的 “id”,之後透過 CallStaticVoidMethod 實現方法的呼叫,這兩個函式都是 JNI function。
前面我們提到過,當我們 java <Main Class> 執行 Java 程式時是其他語言呼叫 Java 語言的場景,事實上 Java 命令在實現上就是應用類似上述程式碼的流程完成主類 main 方法的呼叫。順帶提一點,我們日常研發過程中常用的一些診斷命令,比如 jcmd、jmap、jstack,和 java 命令是同一份原始碼實現(可以從圖中看出這幾個二進位制檔案的大小差不多),只是在構建過程中使用了不同的構建引數。
那麼 JNI 到底是什麼呢?以下是我的理解。

  • 首先,JNI 是 Java 跨語言訪問的介面規範,主要面向 C、C++、Assembly(為什麼沒有其他語言?我個人認為是由於這幾種語言在當時規範設計之初足以覆蓋絕大部分場景)
  • 規範本身考慮了主流虛擬機器的實現(hotspot),但本身不和任何具體的實現繫結,換句話說,Java 程式中跨語言程式設計的部分理論上可以跑在任何實現這個規範的虛擬機器上
  • 規範定義了其他語言如何訪問 Java 物件、類、方法、異常,如何啟動虛擬機器,也定義了Java 程式如何呼叫其他語言(C、C++、Assembly)
  • 在具體使用和實際執行效果的表現用一句話總結:Powerful, but slow, hard to use, and error-prone


Java Native Access & Java Native Runtime

透過前面對 Java Native Interface 的介紹,我們可以認識到使用 JNI 技術實現 Java 方法呼叫 C 語言的步驟是非常麻煩的,因此為了降低 Java 跨語言程式設計(指 Java 呼叫 C/C++ 程式)的難度,開源社群誕生了 Java Native Access(JNA) 和 Java Native Runtime(JNR)這兩個專案。本質上,這兩個技術底層仍然是基於 JNI,因此在執行時效能上不會優於 JNI。
透過 JNA/JNR 進行 C/C++ 程式的封裝,開發者就不需要主動生成或者編寫底層的膠水程式碼,快速地實現跨語言的呼叫。此外兩者還提供了其他最佳化,比如 Crash Protection(後文會有介紹)等。在實現上,JNR 會動態生成一些 Stub 最佳化執行時的效能。
JNA/JNR 和 JNI 的關係如下入:
下面是 JNR 官方給出的示例。首先建立 LibC 介面封裝目標 C 函式,然後呼叫 LibraryLoader 的 API 建立 LibC 的具體例項,最後透過介面完成呼叫:
public class HelloWorld {    
    public interface LibC { // A representation of libC in Java        
        int puts(String s); // mapping of the puts function, in C `int puts(const char *s);`   
    }
   
    public static void main(String[] args) {        
        LibC libc = LibraryLoader.create(LibC.class).load("c"); // load the "c" library into the libc variable
        
        libc.puts("Hello World!"); // prints "Hello World!" to console    
    }
}
遺憾的是,JNA 和 JNR 對 C++ 的支援並不友好,因此在呼叫 C++ 庫的場景中使用受限。
JavaCPP
The missing bridge between Java and native C++
如果說 JNA/JNR 最佳化了 Java 呼叫 C 的程式設計體驗,那麼 JavaCPP 的目標則是最佳化 Java 呼叫 C++ 的程式設計體驗,目前該專案也是工業界用得較多的SDK。
JavaCPP 已經支援大部分 C++ 特性,比如 Overloaded operators、Class & Function templates、Callback through function pointers 等。和 JNA/JNR 類似,JavaCPP 底層也是基於 JNI,實現上透過註解處理等機制自動生成類似的膠水程式碼以及一些構建指令碼。
此外,該專案也提供了利用 JavaCPP 實現的一些常用 C++ 庫的 Preset,如 LLVM、Caffe 等。
下面是使用 JavaCPP 封裝 std::vector 的的示例:
@Platform(include="<vector>")
public class VectorTest {
    @Name("std::vector<std::vector<void*> >")    
    public static class PointerVectorVector extends Pointer {        
        static { Loader.load(); }       
        public PointerVectorVector()       { allocate();  }        
        public PointerVectorVector(long n) { allocate(n); }       
        public PointerVectorVector(Pointer p) { super(p); } // this = (vector<vector<void*> >*)p        
        /**         
         other methods ....         
         */        
        public native @Index void resize(long i, long n);  // (*this)[i].resize(n)
        
        
        public native @Index Pointer get(long i, long j);  // return (*this)[i][j]       
        public native void put(long i, long j, Pointer p); // (*this)[i][j] = p    
     }
     
     
     public static void main(String[] args) {        
         PointerVectorVector v = new PointerVectorVector(13);        
         v.resize(0, 42); // v[0].resize(42)        
         Pointer p = new Pointer() { { address = 0xDEADBEEFL; } };        
         v.put(0, 0, p);  // v[0][0] = p
         
         PointerVectorVector v2 = new PointerVectorVector().put(v);       
         Pointer p2 = v2.get(0).get(); // p2 = *(&v[0][0])        
         System.out.println(v2.size() + " " + v2.size(0) + "  " + p2);
         
         
         v2.at(42);    
      }
  }
Graal & Panama
Graal 和 Panama 是目前兩個相對活躍的社群專案,與跨語言程式設計有直接的聯絡。但這兩項技術還未在生產環境中大規模使用驗證,在這裡不做具體的描述,有機會的話會單獨介紹這兩個專案。
FBJNI 
FBJNI( https://github.com/facebookincubator/fbjni )是 Facebook 開源的一套輔助 C++ 開發人員使用 JNI 的框架。前面提到的大多是如何讓Java使用者快速的訪問Native方法,實際在跨語言呼叫場景下,也存在 C++ 使用者需要安全便捷的訪問 Java 程式碼的場景。Alibaba FFI 目前關注的是如何讓 Java 快速的訪問 C++,例如假設一個需求是讓 C++ 使用者訪問 Java 的 List 介面,那麼 Alibaba FFI的做法是與其透過 JNI 介面函式來操作 Java 的 List 物件,不如將 C++的 std::vector 透過 FFI 包轉成 Java 介面。

JNI 的開銷

內聯

JVM 高效能的最核心原因是內建了強大的及時編譯器(Just in time,簡稱 JIT)。JIT 會將執行過程中的熱點方法編譯成可執行程式碼,使得這些方法可以直接執行(避免瞭解釋位元組碼執行)。在編譯過程中應用了許多最佳化技術,內聯便是其中最重要的最佳化之一。簡單來說,內聯是把被呼叫方法的執行邏輯嵌入到呼叫者的邏輯中,這樣不僅可以消除方法呼叫帶來的開銷,同時能夠進行更多的程式最佳化。

但是在目前 hotspot 的實現中,JIT 僅支援Java 方法的內聯,所以如果一個 Java 方法呼叫了 native 方法,則無法對這個 native 方法應用內聯最佳化。
說到這裡,肯定有人疑惑難道我們經常使用的一些 native 方法,比如 System.currentTimeMillis,沒有辦法被內聯嗎?實際上,針對這些在應用中會被經常使用的 native 方法,hotspot 會使用 Intrinsic 技術來提高呼叫效能(非 native 方法也可以被 Intrinsic)。個人認為 Intrinsic 有點類似 build-in 的概念,當 JIT 遇到這類方法呼叫時,能夠在最終生成的程式碼中直接嵌入方法的實現,不過方法的 Intrinsic 支援通常需要直接修改 JVM。

引數傳遞
JNI 的另一個開銷是引數傳遞(包括返回值)。由於不同語言的方法/函式的呼叫規約(Calling Convention)不同,因此在 Java 方法在呼叫 native 方法 的時候需要涉及到引數傳遞的過程,如下圖(針對 x64 平臺):
根據 JNI 規範,JVM 首先需要把 JNIEnv* 放入第一個引數暫存器(rdi)中,然後把剩下的幾個引數包括 this(receiver)分別放入相應的暫存器中。為了讓這一過程儘可能地快, hotspot 內部會根據方法簽名動態生成轉換過程的高效 stub。

狀態切換

從 Java 方法進入 native 方法,以及 native 方法執行完成並返回到 Java 方法的過程中會涉及到狀態切換。
如下圖:

在實現上,狀態切換需要引入 memory barrier 以及 safepoint check。

物件訪問
JNI 的另一個開銷存在於 native 方法中訪問 Java 物件。
設想一下,我們需要在一個 C 函式中訪問一個 Java 物件,最暴力的方式是直接獲取物件的指標然後訪問。但是由於 GC 的存在,Java 物件可能會被移動,因此需要一個機制讓 native 方法中訪問物件的邏輯與地址無關。
All problems in CS can be solved by another level of indirection
在具體的實現上,透過增加一個簡介層 JNI Handle,同時使用 JNI Functions 進行物件的訪問來解決這個問題,當然這個方案也勢必引入了開銷。
透過前面的介紹,我們知道現在主流的 Java 跨語言程式設計技術主要存在兩個問題:
1、程式設計難度
2、跨語言通訊的開銷
針對問題 1,我們可以利用 JNA/JNR 、JavaCPP 這樣技術來解決。那麼針對問題 2,我們有相應的最佳化方案麼?
下面正式介紹 Alibaba FFI 專案
Alibaba  FFI

概覽

Alibaba FFI 專案致力於解決 Java 跨語言程式設計中遇到的問題,從整體上看專案分為以下兩個模組:

a. FFI (解決程式設計難度問題)

  • 一套 Java 註解和型別
  • 包含一個註解處理器,用於生成膠水程式碼
  • 執行時支援

b. LLVM4JNI(解決執行時開銷問題)

  • 實現 bitcode 到 bytecode 的翻譯,打破 Java 方法與 Native 函式的邊界
  • 基於FFI的純 Java 介面定義,底層依賴 LLVM,透過 FFI 訪問 LLVM 的 C++ API

目前 Alibaba FFI 主要針對 C++ ,下文也主要以 C++ 作為目標通訊語言。
透過 Alibaba FFI 進行跨語言程式設計的 workflow:
1、包含使用者需要使用的 C++ API 宣告的標頭檔案
2、用 Java 語言封裝 C++ API,目前這個步驟仍需要手動進行,在未來我們會提供 SDK,使用者僅需手動編寫配置檔案即可生成這部分程式碼
3、透過 FFI 中的註解處理器生成的膠水程式碼:包括 Java 層和 native 層的程式碼
4、庫的具體實現
5、Clinet 應用在執行階段會 load 上述過程的產物
注:實線表示執行前階段原始碼與產物之間的關係,虛線表示執行階段應用與庫和產物之間的關係


FFI

FFI 模組提供了一套註解和型別,用於封裝其他語言的介面,可以在下圖中看到最頂層是一個 FFIType(FFI -> Foreign function interface)介面。

在面向 C++ 的具體實現中,一個底層的 C++ 物件會對映到一個 Java 物件,因此需要在Java 物件中包含 C++ 物件的地址。由於 C++ 物件不會被移動,所以我們可以在 Java 物件中直接儲存裸指標。
類層次圖(不完整)

本質上 FFI 模組是透過註解處理器生成跨語言呼叫中需要的相關程式碼,使用者僅需要依賴 FFI 的相關庫(外掛),並用 FFI 提供的 api 封裝需要呼叫的目標函式即可。

示例

下面是一個封裝 std::vector 的過程。
a. 透過註解和型別封裝需要呼叫的底層函式

@FFIGen(library = "stdcxx-demo")
@CXXHead(system = {"vector", "string"})
@FFITypeAlias("std::vector")
@CXXTemplate(cxx="jint", java="Integer")
@CXXTemplate(cxx="jbyte", java="Byte")
public interface StdVector<E> extends CXXPointer {
    
    @FFIFactory    
    interface Factory<E> {        
        StdVector<E> create();    
    }
    long size();
    @CXXOperator("[]") @CXXReference E get(long index);    
    @CXXOperator("[]") void set(long index, @CXXReference E value);    
    void push_back(@CXXValue E e);
    
    long capacity();    
    void reserve(long size);    
    void resize(long size);
 }

FFIGen:指定最終生成庫的名稱

CXXHead:膠水程式碼中依賴的標頭檔案
FFITypeAlias:C++ 的類名
CXXTemplate:實現 C++ 模板引數具體型別到 Java 型別的對映,相對於 JavaCPP,Alibaba FFI 提供了更靈活的配置
b. 編譯過程中,註解處理器會生成最終呼叫過程中依賴的元件
介面的真實實現:

public class StdVector_cxx_0x6b0caae2 extends FFIPointerImpl implements StdVector<Byte> {  
  static {   
    FFITypeFactory.loadNativeLibrary(StdVector_cxx_0x6b0caae2.class, "stdcxx-demo");  
  }
  
  
  public StdVector_cxx_0x6b0caae2(final long address) {    
    super(address);  
  }
  
  public long capacity() {    
    return nativeCapacity(address);  
  }
  
  public static native long nativeCapacity(long ptr);
  
  ...
  
  public long size() {    
    return nativeSize(address);  
  }
  
  public static native long nativeSize(long ptr);
  
}

JNI 的膠水程式碼:

#include <jni.h>
#include <new>
#include <vector>
#include <string>
#include "stdcxx_demo.h"
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT
jbyte JNICALL Java_com_alibaba_ffi_samples_StdVector_1cxx_10x6b0caae2_nativeGet(JNIEnv* env, jclass cls, jlong ptr, jlong arg0 /* index0 */) {    
    return (jbyte)((*reinterpret_cast<std::vector<jbyte>*>(ptr))[arg0]);
}
JNIEXPORT
jlong JNICALL Java_com_alibaba_ffi_samples_StdVector_1cxx_10x6b0caae2_nativeSize(JNIEnv* env, jclass cls, jlong ptr) {    
    return (jlong)(reinterpret_cast<std::vector<jbyte>*>(ptr)->size());
}
......
#ifdef __cplusplus
}
#endif

Crash Protection

在演進過程中,我們引入了一些最佳化機制,比如針對 C++ 函式返回臨時物件的處理、異常的轉換等。在這裡介紹一下 Crash Protection,也是針對客戶在實際場景遇到的問題的解決方案,在 JNA 和 JNR 中也有相應的處理。
有時候,Java 應用依賴的 C++ 庫需要進行版本升級,為了防止 C++ 庫中的 Bug 導致整個應用 Crash(對於 Java 中的 Bug 通常會表現為異常,多數情況下不會導致應用整體出現問題),我們需要引入保護機制。
如下:
JNIEXPORT void JNICALL Java_Demo_crash(JNIEnv* env, jclass) {  
  void* addr = 0;  
  *(int*)addr = 0; // (Crash)
}
在第 3 行會出現記憶體訪問越界的問題,如果不做特殊處理應用會 Crash。為了”隔離“這個問題,我們引入在保護機制,以下是 Linux 上的實現:
PROTECTION_START // 生成膠水程式碼中插入宏
void* addr = 0;
*(int*)addr = 0;
PROTECTION_END // 宏  
// 宏展開後的實現如下
// Pseudo code
// register signal handlers
signal(sig, signal_handler);
int sigsetjmp_rv;
sigsetjmp_rv = sigsetjmp(acquire_sigjmp_buf(), 1);
if (!sigsetjmp_rv) {
 void* addr = 0;  
 *(int*)addr = 0;
}
release_sigjmp_buf();
// restore handler ...
if (sigsetjmp_rv) {  
  handle_crash(env, sigsetjmp_rv);
}
透過實現自己的訊號處理函式和 sigsetjmp/siglongjmp 機制來實現 Crash 的保護,需要注意的是由於 Hotspot 有自定義的訊號處理器(safepoint check,implicit null check 等),為了防止衝突,需要在啟動是 preload libjsig.so(Linux 上)這個庫。最後在 handle_crash 中我們可以丟擲 Java 異常供後續排查分析。

相關專案的對比


友好地支援 C++
不需要生成和編譯原始碼
不需要執行時生成額外的 Stub
支援 c++ 模板對映 Java 泛型
JNA/JNR
N.A.
JavaCPP
Alibaba FFI

LLVM4JNI

LLVM4JNI 實現了 bitcode 到 bytecode 的翻譯,這樣一個 Native 函式就是被轉換成一個 Java 方法,從而消除前面提到的一系列開銷問題。

翻譯過程是在應用執行前完成的,其核心就是將 bitcode 的語義用 bytecode 來實現,本文不會介紹具體的實現細節(待專案開源後做詳細介紹)。下面演示幾例簡單過程的翻譯結果。
1、簡單的四則運算:

  • source

 int v1 = i + j;  
 int v2 = i - j;  
 int v3 = i * j;  
 int v4 = i / j;  
 return v1 + v2 + v3 + v4;
  • bitcode

%5 = sdiv i32 %2, %3  
  %6 = add i32 %3, 2  
  %7 = mul i32 %6, %2  
  %8 = add nsw i32 %5, %7  
  ret i32 %8
  • bytecode

Code:      
         stack=2, locals=6, args_size=3         
            0: iload_1         
            1: iload_2         
            2: idiv         
            3: istore_3         
            4: iload_2         
            5: ldc           #193                // int 2         
            7: iadd         
            8: iload_1         
            9: imul        
           10: istore        5        
           12: iload_3        
           13: iload         5        
           15: iadd        
           16: ireturn

2、JNI Functions 的轉換,目前已經支援 90+ 個。未來該功能會和fbjni等類似框架整合,打破Java和Native的程式碼邊界,消除方法呼叫的額外開銷。

  • source

 jclass objectClass = env->FindClass(“java/util/List");
 
  return env->IsInstanceOf(arg, objectClass);
  • bytecode

Code:     
         stack=1, locals=3, args_size=2        
            0: ldc           #205                // class java/util/List         
            2: astore_2        
            3: aload_1         
            4: instanceof    #205                // class java/util/List         
            7: i2b        
            8: ireturn

3、C++ 物件訪問。Alibaba FFI的另外一個好處是可以以物件導向的方式(C++是面嚮物件語言)來開發 Java off-heap 應用。當前基於Java的大資料平臺大多需要支援 off-heap 的資料模組來減輕垃圾回收的壓力,然而人工開發的 off-heap 模組需要小心仔細處理不同平臺和架構的底層偏移和對齊,容易出錯且耗時。透過 Aliabba FFI,我們可以採用 C++開發物件模型,再透過 Alibaba FFI 暴露給 Java 使用者使用。

  • source

class Pointer { 
 public:  
  int _x;  
  int _y;
  
  
  Pointer(): _x(0), _y(0) {}
  const int x() { return _x; }  
  const int y() { return _y; }
 };
 JNIEXPORT
 jint JNICALL Java_Pointer_1cxx_10x4b57d61d_nativeX(JNIEnv*, jclass, jlong ptr) {    
     return (jint)(reinterpret_cast<Pointer*>(ptr)->x());
}
 JNIEXPORT
 jint JNICALL Java_Pointer_1cxx_10x4b57d61d_nativeY(JNIEnv*, jclass, jlong ptr) {   
     return (jint)(reinterpret_cast<Pointer*>(ptr)->y());
 }
  • bitcode

define i32 @Java_Pointer_1cxx_10x4b57d61d_nativeX  
  %4 = inttoptr i64 %2 to %class.Pointer*  
  %5 = getelementptr inbounds %class.Pointer, %class.Pointer* %4, i64 0, i32 0  
  %6 = load i32, i32* %5, align 4, !tbaa !3  
  ret i32 %6
define i32 @Java_Pointer_1cxx_10x4b57d61d_nativeY
  %4 = inttoptr i64 %2 to %class.Pointer*  
  %5 = getelementptr inbounds %class.Pointer, %class.Pointer* %4, i64 0, i32 1  
  %6 = load i32, i32* %5, align 4, !tbaa !8  
  ret i32 %6
  • bytecode

     public int y();    
       descriptor: ()I    
       flags: ACC_PUBLIC    
       Code:      
         stack=2, locals=1, args_size=1         
            0: aload_0         
            1: getfield      #36                 // Field address:J         
            4: invokestatic  #84                 // Method nativeY:(J)I         
            7: ireturn      
         LineNumberTable:        
           line 70: 0
           
     public static int nativeY(long);   
       descriptor: (J)I    
       flags: ACC_PUBLIC, ACC_STATIC    
       Code:      
         stack=4, locals=2, args_size=1         
            0: lload_0         
            1: ldc2_w        #85                 // long 4l         
            4: ladd         
            5: invokestatic  #80                 // Method com/alibaba/llvm4jni/runtime/JavaRuntime.getInt:(J)I         
            8: ireturn
  • JavaRuntime

public class JavaRuntime {
    public static final Unsafe UNSAFE;
    ...
    public static int getInt(long address) {        
        return UNSAFE.getInt(address);    
    }
    
    ...
    }

在訪問 C++ 物件的欄位實現中,我們使用 Unsafe API 完成堆外記憶體的直接訪問,從而避免了 Native 方法的呼叫。

效能資料

Grape 在應用 Alibaba FFI 實現的 SSSP(單源最短路徑演算法)的效能資料如下:

SSSP (單源最短路徑演算法)
歸一化後 的 Job Time
Grape C++
1
Alibaba FFI (without LLVM4JNI)
8.3
Alibaba FFI (with LLVM4JNI)
2.4
這裡比較三種模式:
  • 純粹的 C++ 實現
  • 基於 Aibaba FFI 的 Java 實現,但是關閉 LLVM4JNI,JNI 的額外開銷沒有任何消除
  • 基於 Alibaba FFI 的 Java 實現,同時開啟 LLVM4JNI,一些 native 方法的額外開銷被消除

這裡我們以演算法完成的時間(Job Time)為指標,將最終結果以 C++ 的計算時間為單位一做歸一化處理。

結語

跨語言程式設計是現代程式語言的一個重要方向,在社群中存在許多方案來實現針對不同語言的通訊過程。
Alibaba FFI 目前主要針對 C++,在未來我們會嘗試 Java 與其他語言通訊過程的實現與最佳化,專案也會正式開源,歡迎大家持續關注。

加入SIG

歡迎更多開發者加入Java語言與虛擬機器SIG:

網址:

郵件列表: java-sig@lists.openanolis.cn

——完——

加入龍蜥社群

加入微信群:新增社群助理-龍蜥社群小龍(微信:openanolis_assis),備註【龍蜥】拉你入群;加入釘釘群:掃描下方釘釘群二維碼。歡迎開發者/使用者加入龍蜥社群(OpenAnolis)交流,共同推進龍蜥社群的發展,一起打造一個活躍的、健康的開源作業系統生態!


關於龍蜥社群

龍蜥社群 (OpenAnolis)是由 企事業單位、高等院校、科研單位、非營利性組織、個人等按照自願、平等、開源、協作的基礎上組成的非盈利性開源社群。龍蜥社群成立於2020年9月,旨在構建一個開源、中立、開放的Linux上游發行版社群及創新平臺。

短期目標是開發龍蜥作業系統(Anolis OS)作為CentOS替代版,重新構建一個相容國際Linux主流廠商發行版。中長期目標是探索打造一個面向未來的作業系統,建立統一的開源作業系統生態,孵化創新開源專案,繁榮開源生態。

龍蜥OS 8.4 已釋出,支援x86_64和ARM64架構,完善適配Intel、飛騰、海光、兆芯、鯤鵬晶片。

歡迎下載:

加入我們,一起打造面向未來的開源作業系統!



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70004278/viewspace-2795511/,如需轉載,請註明出處,否則將追究法律責任。

相關文章