<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          fastjson2為什么這么快?

          共 10357字,需瀏覽 21分鐘

           ·

          2023-02-25 10:13

          導(dǎo)讀


          本文作者從以下三個(gè)方面講述了fastjson2 使用了哪些核心技術(shù)來提升速度。

          1、用「Lambda 生成函數(shù)映射」代替「高頻的反射操作」

          2、對(duì) String 做零拷貝優(yōu)化

          3、常見類型解析優(yōu)化

          fastjson 是很多企業(yè)應(yīng)用中處理 json 數(shù)據(jù)的基礎(chǔ)工具,其憑借在易用性、處理速度上的優(yōu)越性,支撐了很多數(shù)據(jù)處理場景。fastjson 的作者「高鐵」已更新推出 2.0 版本的 fastjson,即 fastjosn2[1]
          據(jù) “相關(guān)數(shù)據(jù)” [2]顯示,fastjson2 各方面性能均有提升,常規(guī)數(shù)據(jù)序列化相比 1.0 系列提升達(dá)到 30%,那么,fastjson2 使用了哪些核心技術(shù)來提升速度的呢?筆者總結(jié)包含但不限于以下幾個(gè)方面:
          • 用「Lambda 生成函數(shù)映射」代替「高頻的反射操作」

          • 對(duì) String 做零拷貝優(yōu)化

          • 常見類型解析優(yōu)化

          一、用「 Lambda 生成函數(shù)映射」代替「高頻的反射操作」

          我們來看一段最簡單的反射執(zhí)行代碼:
          public class Bean {    int id;    public int getId() {        return id;    }}
          Method methodGetId = Bean.class.getMethod("getId");Bean bean = createInstance();int value = (Integer) methodGetId.invoke(bean);

          上面的反射執(zhí)行代碼可以被改寫成這樣:

          // 將getId()映射為function函數(shù)java.util.function.ToIntFunction<Bean> function = Bean::getId; int i = function.applyAsInt(bean);

          fastjson2 中的具體實(shí)現(xiàn)的要復(fù)雜一點(diǎn),但本質(zhì)上跟上面一樣,其本質(zhì)也是生成了一個(gè) function。

          //functionjava.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 和 Lambda 函數(shù)分別執(zhí)行 10000 次來看下處理速度差異:
          Method invoke elapsed: 25msBean::getId elapsed: 1ms

          處理速度相差居然達(dá)到 25 倍,使用 Java8 Lambda 為什么能提升這多呢?

          答案就是:Lambda 利用 LambdaMetafactory 生成了函數(shù)映射代替反射。
          下面我們?cè)敿?xì)分析下 Java反射 與 Lambda 函數(shù)映射 的底層區(qū)別。

          1、反射執(zhí)行的底層原理

          注:以下只是想表達(dá)出反射調(diào)用本身的繁雜性,大可不必深究這些代碼細(xì)節(jié)
          從代碼角度,我們從 Java 方法反射 Method.invoke 的源碼入口來深入:
          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);}

          可見,經(jīng)過簡單的檢查后,調(diào)用的是MethodAccessor.invoke(),這部分的實(shí)際實(shí)現(xiàn):

          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);

          可見,最終調(diào)用的是 native 本地方法(本地方法棧)的 invoke0(),這部分的實(shí)現(xiàn):

          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);}

          可見,調(diào)用的是 jvm.h 模塊的 JVM_InvokeMethod 方法,這部分的實(shí)現(xiàn):

          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);}
          更詳細(xì)的細(xì)節(jié):https://www.zhihu.com/question/464985077/answer/1940021614

          2、Lambda生成函數(shù)映射的底層原理

          具體來講,Bean::getId 這種 Lambda 寫法進(jìn)過編譯后,會(huì)通過 java.lang.invoke.LambdaMetafactory 

          調(diào)用到

          java.lang.invoke.InnerClassLambdaMetafactory#spinInnerClass

          最終實(shí)現(xiàn)是調(diào)用 JDK 自帶的字節(jié)碼庫 jdk.internal.org.objectweb.asm 動(dòng)態(tài)生成一個(gè)內(nèi)部類,上層 call 內(nèi)部類的方法執(zhí)行調(diào)用。
          所以 Lambda 生成函數(shù)映射的方式,核心消耗就在于生成函數(shù)映射,那生成函數(shù)映射的效率究竟如何呢?
          我們和反射獲取 Method 做個(gè)對(duì)比,Benchmark 結(jié)論:
          Benchmark 
          Mode
          Cnt
          Score
          Error
          Units
          genMethod(反射獲取方法)
          avgt(平均耗時(shí))
          5
          0.125
          0.015
          us/op
          genLambda(生成方法的函數(shù)映射)
          avgt
          5
          51.880
          40.040
          us/op
          從數(shù)據(jù)來看,生成函數(shù)映射的耗時(shí)遠(yuǎn)高于反射獲取 Method。那為我們不禁要問,既然生成函數(shù)映射的性能遠(yuǎn)低于反射獲取方法,那為什么最終用生成函數(shù)的方式的執(zhí)行速度比反射要快?
          答案就在于——函數(shù)復(fù)用,將一個(gè)固定簽名的函數(shù)緩存起來,下次調(diào)用就可以省去函數(shù)創(chuàng)建的過程。
          比如 fastjson2 直接將常用函數(shù)的初始化緩存放在 static 代碼塊,這就將函數(shù)創(chuàng)建的消耗就被前置到類加載階段,在數(shù)據(jù)處理階段的耗時(shí)進(jìn)一步降低。

          3、對(duì)比分析 & 結(jié)論

          從原理上來說,反射方式,在獲取 Method 階段消耗較少,但 invoke 階段則是每次都用都調(diào)用本地方法執(zhí)行,先是在 jvm 層面多出一些檢查,而后轉(zhuǎn)到 JNI 本地庫,除了有額外的 jvm 堆棧與本地方法棧的 context 交換 ,還多出一系列 C 體系額外操作,在性能上自然是不如 Lambda 函數(shù)映射;
          Lambda 生成函數(shù)映射的方式,在生成代理類的過程中有部分開銷,這部分開銷可以通過緩存機(jī)制大量減少,而后的調(diào)用則全部屬于 Java 范疇內(nèi)的堆棧調(diào)用(即拿到代理類后,調(diào)用效率和原生方法調(diào)用幾乎一致)。

          二、對(duì) String 做零拷貝優(yōu)化

          1、何為零拷貝

          零拷貝[3]是老生常談的問題,Kafka 和 Netty 等都用到了零拷貝的知識(shí),這里簡單介紹一下其概念以便生疏的讀者理解上流暢。
          零拷貝:是指計(jì)算機(jī)執(zhí)行IO操作時(shí),CPU不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域,進(jìn)而減少上下文切換以及CPU的拷貝時(shí)間。它是一種IO操作優(yōu)化技術(shù)。
          JDK8 中的 String 是如何拷貝的?
          為了實(shí)現(xiàn)字符串是不可變的特性,JDK 在構(gòu)造 String 構(gòu)造字符串的時(shí)候,會(huì)有拷貝的過程,比如上圖是 JDK8 的 String 的一個(gè)構(gòu)造函數(shù)的實(shí)現(xiàn),其在堆內(nèi)存中重新開辟了一塊內(nèi)存區(qū)域。
          如果要提升構(gòu)造字符串的開銷,就要避免這樣的拷貝,即零拷貝。

          2、fastjson2 中如何實(shí)現(xiàn) 0 拷貝

          在 JDK8 中,String 有一個(gè)構(gòu)造函數(shù)是不做拷貝的:
          但這個(gè)方法不是 public,不能直接訪問到,可以反射執(zhí)行,也可以使用 LambdaMetafactory 創(chuàng)建函數(shù)映射來調(diào)用,前面有介紹這個(gè)技巧。
          生成的函數(shù)映射可以緩存起來復(fù)用,而這個(gè)構(gòu)造方法的簽名是固定不變的,這意味著,只需要生成一次,后續(xù)所有需要初始化 String 的時(shí)候都可以復(fù)用。

          3、fastjson2 中的應(yīng)用

          將 LocalDate 格式化為 “yyyy-MM-dd” 的 String 源碼(注:針對(duì) JDK8 的實(shí)現(xiàn),此處對(duì)源碼精簡整理以方便閱讀):
          static BiFunction<char[], Boolean, String>  STRING_CREATOR_JDK8;static {    //為上述String的0拷貝構(gòu)造方法創(chuàng)建一個(gè)映射函數(shù)    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;
          //執(zhí)行「lambda函數(shù)映射」構(gòu)造String String str = STRING_CREATOR_JDK8.apply(chars, Boolean.TRUE); return str;}

          在 JDK8 的實(shí)現(xiàn)中,先拼接好格式中每一個(gè) char 字符,然后通過零拷貝的方式構(gòu)造字符串對(duì)象,這樣就實(shí)現(xiàn)了快速格式化 LocalDate 到 String,這樣的實(shí)現(xiàn)遠(yuǎn)比使用 SimpleDateFormat 之類要快。這種實(shí)例化 String 的方式在fatsjson2 中的 JSONReader、JSONWritter 隨處可見。

          三、常見類型解析優(yōu)化

          fastjson2 里針對(duì)各種類型的優(yōu)化處理很多,不能一一列舉,這里僅以 Date 類型舉例,我們前面舉例了將 Date 格式化為 String,這次我們反過來,將 String 轉(zhuǎn)換為 Date —— 如何快速將字符串解析成日期?以下給出幾種實(shí)現(xiàn)方式,隨后我們來做個(gè)對(duì)比。

          1、使用SimpleDateFormat

          SimpleDateFormat 是我們使用最廣泛、最容易想到的方式,需要注意的是 SimpleDateFormat 不是線程安全的,并發(fā)場景下要 sync 同步處理。
          static final ThreadLocal<SimpleDateFormat> formatThreadLocal = ThreadLocal.withInitial(        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
          // get format from ThreadLocalSimpleDateFormat format = formatThreadLocal.get();format.parse(str);

          2、使用java.time.DateTimeFormatter

          JDK8 提供了 java.time API,吸收了 joda-time[4]的部分精華,功能更強(qiáng)大,性能也更好。同時(shí),DateTimeFormatter 是線程安全的。
          static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
          // use formatter parse DateLocalDateTime 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%。

          圖片源自github

          3、針對(duì)固定格式和固定時(shí)區(qū)優(yōu)化

          我們?cè)谌粘L幚?Date 數(shù)據(jù)時(shí),在國內(nèi)最常見的格式就是 "yyyy-MM-dd HH:mm:ss",默認(rèn)的時(shí)區(qū)為東 8 區(qū),在 java.time 中的 ZonedId 是 "Asia/Shanghai"(而不是 Asia/Beijing),而東 8 區(qū)在1992年之后,不在使用夏令時(shí),固定的 zoneOffset 是 +8,根據(jù)這個(gè)情況,我們可以針對(duì)性做優(yōu)化,如下(為方便理解,以下為源碼的簡化版,去掉了影響閱讀的邊界處理等邏輯):
          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);}


          核心邏輯就是根據(jù)位數(shù),直接開始計(jì)算給定的時(shí)間字符串,相對(duì)于參照的原點(diǎn)時(shí)間(1970-1-1 0點(diǎn))過去了多少毫秒,這個(gè)優(yōu)化,避免了parse Number的開銷,精簡了大量 Partten 的處理,處理流程非常高效。

          4、性能測試 & 結(jié)論

          benchmark[5]
          Benchmark 
          Mode
          Cnt
          Score
          Error
          Units
          DateParse.simpleDateFormatParse
          avgt(平均耗時(shí))
          5
          11.540
          4.170
          us/ms
          DateParse.dateTimeFormatterParse
          avgt
          5
          7.594
          0.200
          us/ms
          DateParse.parseYYYYMMDDHHMMSS19
          avgt
          5
          0.425
          0.098
          us/ms

          JMH測試顯示:方法 3 的耗時(shí)遠(yuǎn)低于其他方式,方法 3 這種針對(duì)性的類型解析優(yōu)化可以使用在重度使用日期解析的優(yōu)化場景,比如數(shù)據(jù)批量導(dǎo)入解析日期,大數(shù)據(jù)場景的 UDF 日期解析等。

          One more thing

          fastjson 系列相比同類 json 處理工具,雖然在安全性、魯棒性等方面還可以提升,但其最大優(yōu)勢——處理速度,卻使其他同類競品望塵莫及。我們也可以在日常業(yè)務(wù)處理中,學(xué)習(xí)其精華部分,運(yùn)用其中的技術(shù)亮點(diǎn),優(yōu)化業(yè)務(wù)處理速度,提升用戶體驗(yàn)。
          參考鏈接:

          [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

          瀏覽 131
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲电影欧美片日韩 | 三级片男人天堂 | 爱爱天堂网 | 久久中文字幕7区 | 日本中文不卡在线 |