
本文作者從以下三個方面講述了fastjson2 使用了哪些核心技術來提升速度,
1、用「Lambda 生成函式映射」代替「高頻的反射操作」
2、對 String 做零拷貝優化
3、常見型別決議優化

-
用「Lambda 生成函式映射」代替「高頻的反射操作」
-
對 String 做零拷貝優化
-
常見型別決議優化
一、用「 Lambda 生成函式映射」代替「高頻的反射操作」
public class Bean {
int id;
public int getId() {
return id;
}
}
Method methodGetId = Bean.class.getMethod("getId");
Bean bean = createInstance();
int value = https://www.cnblogs.com/88223100/p/(Integer) methodGetId.invoke(bean);
上面的反射執行代碼可以被改寫成這樣:
// 將getId()映射為function函式
java.util.function.ToIntFunction<Bean> function = Bean::getId;
int i = function.applyAsInt(bean);
fastjson2 中的具體實作的要復雜一點,但本質上跟上面一樣,其本質也是生成了一個 function,
//function
java.util.function.ToIntFunction<Bean> function = LambdaMetafactory.metafactory(
lookup,
"applyAsInt",
methodHanlder,
methodType(ToIntFunction.class),
lookup.findVirtual(int.class, "getId", methodType(int.class)),
methodType(int.class)
);
int i = function.applyAsInt(bean);
Method invoke elapsed: 25ms
Bean::getId elapsed: 1ms
處理速度相差居然達到 25 倍,使用 Java8 Lambda 為什么能提升這多呢?
1、反射執行的底層原理
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException,InvocationTargetException{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor;// read volatile
if (ma == null) ma = acquireMethodAccessor();
return ma.invoke(obj, args);
}
可見,經過簡單的檢查后,呼叫的是MethodAccessor.invoke(),這部分的實際實作:
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
可見,最終呼叫的是 native 本地方法(本地方法堆疊)的 invoke0(),這部分的實作:
JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)
{
return JVM_InvokeMethod(env, m, obj, args);
}
可見,呼叫的是 jvm.h 模塊的 JVM_InvokeMethod 方法,這部分的實作:
JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)
{
return JVM_InvokeMethod(env, m, obj, args);
}
2、Lambda生成函式映射的底層原理
具體來講,Bean::getId 這種 Lambda 寫法進過編譯后,會通過 java.lang.invoke.LambdaMetafactory
呼叫到
java.lang.invoke.InnerClassLambdaMetafactory#spinInnerClass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3、對比分析 & 結論
二、對 String 做零拷貝優化
1、何為零拷貝

2、fastjson2 中如何實作 0 拷貝

3、fastjson2 中的應用
static BiFunction<char[], Boolean, String> STRING_CREATOR_JDK8;
static {
//為上述String的0拷貝構造方法創建一個映射函式
CallSite callSite = LambdaMetafactory.metafactory(caller, "apply", methodType(BiFunction.class), methodType(Object.class, Object.class, Object.class), handle, methodType(String.class, char[].class, boolean.class));
STRING_CREATOR_JDK8 = (BiFunction<char[], Boolean, String>) callSite.getTarget().invokeExact();
}
static String formatYYYYMMDD(LocalDate date) {
int year = date.getYear();
int month = date.getMonthValue();
int dayOfMonth = date.getDayOfMonth();
int y0 = year / 1000 + '0';
int y1 = (year / 100) % 10 + '0';
int y2 = (year / 10) % 10 + '0';
int y3 = year % 10 + '0';
int m0 = month / 10 + '0';
int m1 = month % 10 + '0';
int d0 = dayOfMonth / 10 + '0';
int d1 = dayOfMonth % 10 + '0';
//char array
char[] chars = new char[10];
chars[0] = (char) y1;
chars[1] = (char) y2;
chars[2] = (char) y3;
chars[3] = (char) y4;
chars[4] = '-';
chars[5] = (char) m0;
chars[6] = (char) m1;
chars[7] = '-';
chars[8] = (char) d0;
chars[9] = (char) d1;
//執行「lambda函式映射」構造String
String str = STRING_CREATOR_JDK8.apply(chars, Boolean.TRUE);
return str;
}
在 JDK8 的實作中,先拼接好格式中每一個 char 字符,然后通過零拷貝的方式構造字串物件,這樣就實作了快速格式化 LocalDate 到 String,這樣的實作遠比使用 SimpleDateFormat 之類要快,這種實體化 String 的方式在fatsjson2 中的 JSONReader、JSONWritter 隨處可見,
三、常見型別決議優化
1、使用SimpleDateFormat
static final ThreadLocal<SimpleDateFormat> formatThreadLocal = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
// get format from ThreadLocal
SimpleDateFormat format = formatThreadLocal.get();
format.parse(str);
2、使用java.time.DateTimeFormatter
static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// use formatter parse Date
LocalDateTime ldt = LocalDateTime.parse(str, formatter);
ZoneOffset offset = DEFAULT_ZONE_ID.getRules().getOffset(ldt);
long millis = ldt.toInstant(offset).toEpochMilli();
Date date = new Date(millis);
這種方法比使用 SimpleDateFormat 組合 ThreadLocal 代碼更簡潔,速度也大約要快 50%,

3、針對固定格式和固定時區優化
public static Date parseYYYYMMDDHHMMSS19(String str) {
char y0 = str.charAt(0);
char y1 = str.charAt(1);
char y2 = str.charAt(2);
char y3 = str.charAt(3);
char m0 = str.charAt(4);
char m1 = str.charAt(5);
...
char s1 = str.charAt(18);
int year = (y0 - '0') * 1000 + (y1 - '0') * 100 + (y2 - '0') * 10 + (y3 - '0');
int month = (m0 - '0') * 10 + (m1 - '0');
int dom = (d0 - '0') * 10 + (d1 - '0');
int hour = (h0 - '0') * 10 + (h1 - '0');
int minute = (i0 - '0') * 10 + (i1 - '0');
int second = (s0 - '0') * 10 + (s1 - '0');
//換算成毫秒
long millis;
if (year >= 1992 && (DEFAULT_ZONE_ID == SHANGHAI_ZONE_ID || DEFAULT_ZONE_ID.getRules() == IOUtils.SHANGHAI_ZONE_RULES)) {
final int DAYS_PER_CYCLE = 146097;
final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);
long y = year;
long m = month;
long epochDay;
{
long total = 0;
total += 365 * y;
total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400;
total += ((367 * m - 362) / 12);
total += dom - 1;
if (m > 2) {
total--;
boolean leapYear = (year & 3) == 0 && ((year % 100) != 0 || (year % 400) == 0);
if (!leapYear) {
total--;
}
}
epochDay = total - DAYS_0000_TO_1970;
}
long seconds = epochDay * 86400
+ hour * 3600
+ minute * 60
+ second
- SHANGHAI_ZONE_OFFSET_TOTAL_SECONDS;
millis = seconds * 1000L;
} else {
LocalDate localDate = LocalDate.of(year, month, dom);
LocalTime localTime = LocalTime.of(hour, minute, second, 0);
LocalDateTime ldt = LocalDateTime.of(localDate, localTime);
ZoneOffset offset = DEFAULT_ZONE_ID.getRules().getOffset(ldt);
millis = ldt.toEpochSecond(offset) * 1000;
}
return new Date(millis);
}
核心邏輯就是根據位數,直接開始計算給定的時間字串,相對于參照的原點時間(1970-1-1 0點)過去了多少毫秒,這個優化,避免了parse Number的開銷,精簡了大量 Partten 的處理,處理流程非常高效,
4、性能測驗 & 結論
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
JMH測驗顯示:方法 3 的耗時遠低于其他方式,方法 3 這種針對性的型別決議優化可以使用在重度使用日期決議的優化場景,比如資料批量匯入決議日期,大資料場景的 UDF 日期決議等,
One more thing
[1]https://github.com/alibaba/fastjson2
[2]https://github.com/alibaba/fastjson2/wiki/fastjson_benchmark
[3]https://so.csdn.net/so/search?q=零拷貝&spm=1001.2101.3001.7020
[4]https://www.joda.org/joda-time/
[5]https://github.com/alibaba/fastjson2/blob/main/benchmark/src/main/java/com/alibaba/fastjson2/benchmark/DateParse.java
作者|嚴彬源(泰文)
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/Why-is-fastjson2-so-fast.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/545657.html
標籤:Java
