關于JVM和OS中的指令重排以及JIT優化

關于JVM和OS中的指令重排以及JIT優化


前言:

這東西應該很重要才對,可是大多數博客都是以訛傳訛,全是錯誤,尤其是JVM會對字節碼進行重排都出來了,明明自己測一測就出來的東西,寫出來誤人子弟…
研究了兩天,算是有點名堂了,只是不能看到到CPU的重排過程有點可惜
紙上得來終覺淺,建議手動截一下字節碼以及匯編自己研究一下,肯定會有不一樣的收獲
關于JMM和JIT可以嘗試看一下油管Jakob Jenkov的教程,很不錯!


? 通俗易懂的說,指令重排是為了最大化執行效率,會在保證語意不變的情況下,調整代碼的順序。
而JIT會修改優化代碼中的熱點部分,使其效率大幅提升

OS中的指令重排:

比如:

a = b + c;
b = a + c;
d = e + f;
e = d + f;

這段代碼可能會被調整為:

a = b + c;
d = e + f;
b = a + c;
e = d + f;

但是肯定不會調整為:

b = a + c;
a = b + c;
d = e + f;
e = d + f;

因為這樣改變了代碼語意

具體會調成什么樣取決于JVM和OS

實際上來說,指令重排并不是以一行java代碼為單位進行的,也就是說,我舉的例子并不恰當

一行代碼是由多句指令構成的,比如一個簡單的Java程序:

public class test {public static void main(String[] args) {int a = 1;int b = 2;int c = a + b;}
}

其轉化為字節碼:

public class test {// 構造函數的聲明public <init>()V  // 這是無參構造方法,返回類型為 voidL0LINENUMBER 1 L0  // 表示該字節碼位置對應源代碼的第 1 行ALOAD 0  // 將當前對象(this)加載到棧上。這里的 0 表示加載 this(當前對象)。INVOKESPECIAL java/lang/Object.<init> ()V  // 調用父類 Object 的構造方法(<init>),構造函數是無參的RETURN  // 從構造方法中返回L1LOCALVARIABLE this Ltest; L0 L1 0  // 在字節碼中定義了一個局部變量 'this',類型是 test,對應的范圍是 L0 到 L1,局部變量索引為 0MAXSTACK = 1  // 最大棧深度為 1MAXLOCALS = 1  // 最大局部變量數為 1// main 方法的聲明public static main([Ljava/lang/String;)V  // main 方法,接受字符串數組作為參數,返回類型為 voidL0LINENUMBER 3 L0  // 表示該字節碼位置對應源代碼的第 3 行ICONST_1  // 將常量 1 壓入棧中ISTORE 1  // 將棧頂的值(1)存入局部變量 1 中L1LINENUMBER 4 L1  // 表示該字節碼位置對應源代碼的第 4 行ICONST_2  // 將常量 2 壓入棧中ISTORE 2  // 將棧頂的值(2)存入局部變量 2 中L2LINENUMBER 5 L2  // 表示該字節碼位置對應源代碼的第 5 行ILOAD 1  // 將局部變量 1 的值(即 1)加載到棧上ILOAD 2  // 將局部變量 2 的值(即 2)加載到棧上IADD  // 將棧頂的兩個整數相加(1 + 2 = 3)ISTORE 3  // 將結果(3)存入局部變量 3 中L3LINENUMBER 6 L3  // 表示該字節碼位置對應源代碼的第 6 行RETURN  // 返回,從 main 方法中返回L4LOCALVARIABLE args [Ljava/lang/String; L0 L4 0  // 定義了局部變量 args,類型為 String[],范圍是 L0 到 L4LOCALVARIABLE a I L1 L4 1  // 定義了局部變量 a,類型為 int,范圍是 L1 到 L4LOCALVARIABLE b I L2 L4 2  // 定義了局部變量 b,類型為 int,范圍是 L2 到 L4LOCALVARIABLE c I L3 L4 3  // 定義了局部變量 c,類型為 int,范圍是 L3 到 L4MAXSTACK = 2  // 最大棧深度為 2MAXLOCALS = 4  // 最大局部變量數為 4
}

可以看到,轉換成字節碼多出了很多操作

其實字節碼轉成機器碼/匯編時還會接著細分,為了演示就不再向下分析了

那么轉換成字節碼后我們會發現什么?

一個簡單的 **int a = 1;**被轉化成了

    LINENUMBER 3 L0  // 表示該字節碼位置對應源代碼的第 3 行ICONST_1  // 將常量 1 壓入棧中ISTORE 1  // 將棧頂的值(1)存入局部變量 1 中LOCALVARIABLE a I L1 L4 1  // 定義了局部變量 a,類型為 int,范圍是 L1 到 L4

展示的目的在于,每一行代碼把其溯源到底層的機器碼,都是由一系列操作組成的

一般可以分為:

  • 取指(IF):從存儲器中讀取指令,并將指令送入指令寄存器IR;同時更新程序計數器PC,指向下一條指令的地址。
  • 譯碼(ID):對IR中的指令進行譯碼,確定操作碼、操作數和功能;同時從寄存器文件中讀取源操作數,并放入臨時寄存器A和B中;如果有立即數,還要進行符號擴展,并放入臨時寄存器Imm中。
  • 執行(EX):根據操作碼和功能,對A、B或Imm中的操作數進行算術或邏輯運算,并將結果放入臨時寄存器ALUOutput中;或者根據操作碼和功能,對A和Imm中的操作數進行有效地址計算,并將結果放入臨時寄存器ALUOutput中。
  • 訪存(MEM):如果是加載指令,從存儲器中讀取數據,并放入臨時寄存器LMD中;如果是存儲指令,從B中讀取數據,并寫入存儲器中;如果是分支指令,根據條件判斷是否跳轉,并更新PC。
  • 寫回(WB):如果是運算指令或加載指令,將ALUOutput或LMD中的結果寫回目標寄存器;如果是其他類型的指令,則不進行寫回操作。

(這些操作在上述字節碼不太能看出來,因為這是針對匯編/機器碼而設計的)

知道指令是由這么一個順序來的了,那這和指令重排有什么關系呢?

你或許會發現,或許我們可以不用從上到下執行完所有指令,而是挑一些可以并發一起執行?

比如我們可以在執行一條指令的IF時還能執行別的指令的ID

但有人不就會問CPU不就單核怎么做到?

CPU是單核,但每條指令用到的單元不一樣啊,取指用PC寄存器,計算時就用ALU,互不干擾!

所以我們完全可以調整這些指令的執行順序來做到最大化效率

而這種技術稱之為指令流水線

而擁有像上面五層指令執行類別的CPU的流水線稱之為五層流水線

在這里插入圖片描述

這張圖展示的處理器就能同時執行五條指令,原理就是充分利用了CPU中的其他單元,形成了一種“偽并行”

能夠“預測”到后面的指令,并能找到可以提前用空閑的處理單元處理的指令提取執行

這樣對計算機的提升非常大,以至于有CPU擁有1000多層的流水線

那CPU是怎么知道該怎么樣找到可以并發的指令?

是通過分析“數據依賴”來發現那些可以并行運行

那既然存在數據依賴,那就一定會存在一種情況:下個指令必須用到上個指令的結果,且沒有其他指令能插進來

那這樣就會產生氣泡,也稱為**“打嗝”**:

在這里插入圖片描述

一旦產生了氣泡,會讓后續操作周期延誤,所以,為了維持流水線的高效率,CPU會盡力去進行指令重排來填補氣泡

讓能并發執行(互不干擾)的指令提前執行來填補氣泡,避免延誤執行周期

那么古爾丹,代價是什么呢?

盡管指令重組能保證語義不變,但不能保證在高并發條件下不會出錯!

畢竟提前和延后修改共享變量都可能會引起不可預測的錯誤!

所以會采用synchronized 或 volatile 來針對性的避免這種情況


JVM中的指令重排與優化:

? JVM中的指令重排和優化是發生在JIT編譯階段,而不是翻譯成字節碼階段,網上很多博客都說錯了!

? 仔細想一下也很正常,指令重排針對的是匯編機器碼層面的操作,字節碼根本接觸不到

? 想要驗證很簡單,你找個程序,挑個能體現指令重組的程序,比較一下加了volatile和不加volatile的字節碼,你會發現除了那個加了volatile的變量之外根本沒區別

? 而且Java是解釋+編譯,一般情況下是由JVM一句一句照著字節碼翻譯成機器碼走一步看一步,遇到有循環,執行多次的代碼塊就會用JIT對其進行編譯優化,下次執行就直接調用JIT編譯出來的機器碼

? 所以很容易理解JVM的指令重排發生在JIT編譯階段

? 那JIT會干什么呢?

  • JIT會根據JVM的不同(也就是底層的不同),適當的修改代碼,調整順序來迎合OS的流水線和指令重排。
  • JIT也會給你寫的屎山做優化,優化一些不必要的操作

? 舉個例子:

package com.jitTest;public class test {static boolean noUse = true;public static void main(String[] args) {int cnt = 0;while(noUse){cnt++;if(cnt == 10000)break;}}
}

這里循環了10000次,肯定會觸發JIT的熱點代碼優化

我們先下一個JITWatch

使用教程參考JITWatch很折騰?有這篇文章在可以放心大多數情況下,通過諸如javap等反編譯工具來查看源碼的字節碼已經能夠滿足我 - 掘金

但是別照著它去自己編譯dll,可直接在atzhangsan/file_loaded下載,JITWatch要下載源碼手動編譯,不能下jar!

之后我們用JITWatch截取其字節碼和匯編代碼:

字節碼:

 0: iconst_0        1: istore_1        2: getstatic       #2   // Field noUse:Z5: ifeq            21   8: iinc            1, 1 
11: iload_1         
12: sipush          10000
15: if_icmpne       2    
18: goto            21   
21: return    

可以發現根本沒有優化掉noUse變量,這也證明之前的“代碼重排發生在JIT編譯而不是JVM編譯成字節碼

接下來看匯編部分:

# {method} {0x000001a4d3fd44f0} 'main' '([Ljava/lang/String;)V' in 'com/jitTest/test'
# parm0:    rdx:rdx   = '[Ljava/lang/String;'
#           [sp+0x20]  (sp of caller)
[Entry Point]
0x000001a4b23e6140: sub $0x18,%rsp
0x000001a4b23e6147: mov %rbp,0x10(%rsp)  ;*synchronization entry; - com.jitTest.test::main@-1 (line 6)
0x000001a4b23e614c: add $0x10,%rsp
0x000001a4b23e6150: pop %rbp
0x000001a4b23e6151: test %eax,-0x1e36157(%rip)  # 0x000001a4b05b0000;   {poll_return} *** SAFEPOINT POLL ***
0x000001a4b23e6157: retq
0x000001a4b23e6158: hlt
0x000001a4b23e6159: hlt
0x000001a4b23e615a: hlt
0x000001a4b23e615b: hlt
0x000001a4b23e615c: hlt
0x000001a4b23e615d: hlt
0x000001a4b23e615e: hlt
0x000001a4b23e615f: hlt
[Exception Handler]
[Stub Code]
0x000001a4b23e6160: jmpq 0x000001a4b2264620  ;   {no_reloc}
[Deopt Handler Code]
0x000001a4b23e6165: callq 0x000001a4b23e616a
0x000001a4b23e616a: subq $0x5,(%rsp)
0x000001a4b23e616f: jmpq 0x000001a4b2006f40  ;   {runtime_call}
0x000001a4b23e6174: hlt
0x000001a4b23e6175: hlt
0x000001a4b23e6176: hlt
0x000001a4b23e6177: hlt

其中的:

[Entry Point]
0x000001a4b23e6140: sub $0x18,%rsp
0x000001a4b23e6147: mov %rbp,0x10(%rsp)  ;*synchronization entry; - com.jitTest.test::main@-1 (line 6)
0x000001a4b23e614c: add $0x10,%rsp
0x000001a4b23e6150: pop %rbp
0x000001a4b23e6151: test %eax,-0x1e36157(%rip)  # 0x000001a4b05b0000;   {poll_return} *** SAFEPOINT POLL ***
0x000001a4b23e6157: retq

便是函數部分,我們可以看到,其中唯一的比較函數就是 0x000001a4b23e6151: test %eax,-0x1e36157(%rip) ,代表著比較cnt是否到了10000,根本沒有看見判斷noUse變量是否為真

說明JIT編譯時就已經發現noUse變量很no use,就將其刪去了

對于指令重排,其實不太好測出來,復雜程序的匯編你看不出來,簡單程序的匯編又被JIT優化后因為太簡單就會按順序執行

而且具體重組的方法是由你的底層決定,大頭也是CPU的指令重排,JIT也是打個下手

但由此我們完全可以看出JIT可以對代碼進行修改優化和重構來提升效率

JIT完全是Java的大爹


總結:

? OS中的指令重排極大的提升了CPU性能,但也帶來了并發風險

? JVM中的JIT會在字節碼轉機器碼時對代碼進行優化修改以及重排,極大的提升了Java的速度,使其與編譯執行語言速度相媲美

? JIT太猛了…寫的一個一百多行的測試屎山給優化到只有十幾行…

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/75544.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/75544.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/75544.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

VS2022遠程調試Linux程序

一、 1、VS2022安裝參考 VS Studio2022安裝教程&#xff08;保姆級教程&#xff09;_visual studio 2022-CSDN博客 注意&#xff1a;勾選的時候&#xff0c;要勾選下方的選項&#xff0c;才能調試Linux環境下運行的程序&#xff01; 2、VS2022遠程調試Linux程序測試 原文參…

WPF設計學習記錄滴滴滴4

<Button x:Name"btn"Content"退出"Width" 100"Height"25"Click"btn_Click" IsDefault"True"/> <Button x:Name"btn" <!-- 控件標識&#xff1a;定義按鈕的實例名稱為"btn&…

JVM 有哪些垃圾回收器

垃圾收集算法 標記-復制算法(Copying): 將可用內存按容量劃分為兩個區域,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面, 然后再把已使用過的內存空間一次清理掉。 標記-清除算法(Mark-Sweep): 算法分為“標記” 和“清除”兩個…

React DndKit 實現類似slack 類別、頻道拖動調整位置功能

一周調試終于實現了類 slack 類別、頻道拖動調整位置功能。 歷經四個版本迭代。 實現了類似slack 類別、頻道拖動調整功能 從vue->react &#xff1b;更喜歡React的生態及編程風格&#xff0c;新項目用React來重構了。 1.zustand全局狀態 2.DndKit 拖動 功能視頻&…

新浪財經股票每天10點自動爬取

老規矩還是先分好三步&#xff0c;獲取數據&#xff0c;解析數據&#xff0c;存儲數據 因為股票是實時的&#xff0c;所以要加個cookie值&#xff0c;最好分線程或者爬取數據時等待爬取&#xff0c;不然會封ip 廢話不多數&#xff0c;直接上代碼 import matplotlib import r…

使用Android 原生LocationManager獲取經緯度

一、常用方案 1、使用LocationManager GPS和網絡定位 缺點&#xff1a;個別設備,室內或者地下停車場獲取不到gps定位,故需要和網絡定位相結合使用 2、使用Google Play服務 這種方案需要Android手機中有安裝谷歌服務,然后導入谷歌的第三方庫&#xff1a; 例如&#xff1a;i…

驗證碼實現

驗證碼案例 學了Spring MVC &#xff0c;配置 相關章節&#xff0c; 現可以嘗試寫一個前后端交互的驗證碼 文章目錄 驗證碼案例前言一、驗證碼是什么&#xff1f;二、需求1.引入依賴2.導入前端頁面3.約定前后段交互接口 三、代碼解析Controllermodelapplication.xml 四丶結果五…

查詢當前用戶的購物車和清空購物車

業務需求&#xff1a; 在小程序用戶端購物車頁面能查到當前用戶的所有菜品或者套餐 代碼實現 controller層 GetMapping("/list")public Result<List<ShoppingCart>> list(){List<ShoppingCart> list shoppingCartService.shopShoppingCart();r…

(多看) CExercise_05_1函數_1.2計算base的exponent次冪

題目&#xff1a; 鍵盤錄入兩個整數&#xff1a;底(base)和冪指數(exponent)&#xff0c;計算base的exponent次冪&#xff0c;并打印輸出對應的結果。&#xff08;注意底和冪指數都可能是負數&#xff09; 提示&#xff1a;求冪運算時&#xff0c;基礎的思路就是先無腦把指數轉…

【nacos安裝指南】

Nacos安裝指南 1.Windows安裝 開發階段采用單機安裝即可。 1.1.下載安裝包 在Nacos的GitHub頁面&#xff0c;提供有下載鏈接&#xff0c;可以下載編譯好的Nacos服務端或者源代碼&#xff1a; GitHub主頁&#xff1a;https://github.com/alibaba/nacos GitHub的Release下載…

通過發音學英語單詞:從音到形的學習方法

&#x1f4cc; 通過發音學英語單詞&#xff1a;從音到形的學習方法 英語是一種 表音語言&#xff08;phonetic language&#xff09;&#xff0c;但不像拼音文字&#xff08;如漢語拼音、西班牙語等&#xff09;那么規則&#xff0c;而是 部分表音部分表意。這意味著我們可以通…

列表某個字段由多個值組成,使用id匹配展示

說明&#xff1a;列表中字段A的值由多個值組成&#xff0c;但是后端返回的是這多個值的id字符串&#xff0c;需要前端拿著多個id組成的字符串去另一個接口數據源匹配展示 列表后端返回多個字符串如下&#xff1a; sectorName: "1899292545382895618,1907311191514636289…

MQL5教程 05 指標開發實戰:雙色線、雙線變色MACD、跨時間周期均線

文章目錄 一、雙色線指標二、雙線變色MACD指標三、跨時間周期均線 一、雙色線指標 這里的類型中&#xff0c;Color開頭的&#xff0c;是可以選擇多個顏色的。 #property indicator_chart_window #property indicator_buffers 18 #property indicator_plots 7 //--- plot xian…

Java全棧面試寶典:線程安全機制與Spring Boot核心原理深度解析

目錄 一、Java線程安全核心原理 &#x1f525; 問題1&#xff1a;線程安全的三要素與解決方案 線程安全風險模型 線程安全三要素 synchronized解決方案 &#x1f525; 問題2&#xff1a;synchronized底層實現全解析 對象內存布局 Mark Word結構&#xff08;64位系統&…

【Cursor】設置語言

Ctrl Shift P 搜索 configure display language選擇“中文-簡體”

【新能源汽車整車動力學模型深度解析:面向MATLAB/Simulink仿真測試工程師的硬核指南】

1. 前言 作為MATLAB/Simulink仿真測試工程師,掌握新能源汽車整車動力學模型的構建方法和實現技巧至關重要。本文將提供一份6000+字的深度技術解析,涵蓋從基礎理論到Simulink實現的完整流程。內容經過算法優化設計,包含12個核心方程、6大模塊實現和3種驗證方法,滿足SEO流量…

Java 線程池與 Kotlin 協程 高階學習

以下是Java 線程池與 Kotlin 協程 高階學習的對比指南&#xff0c;結合具體代碼示例&#xff0c;展示兩者在異步任務處理中的差異和 Kotlin 的簡化優勢&#xff1a; 分析&#xff1a; 首先&#xff0c;我們需要回憶Java中線程池的常見用法&#xff0c;比如通過ExecutorService創…

嵌入式EMC設計面試題及參考答案

目錄 解釋 EMC(電磁兼容性)的定義及其兩個核心方面(EMI 和 EMS) 電磁兼容三要素及相互關系 為什么產品必須進行 EMC 設計?列舉至少三個實際工程原因 分貝(dB)在 EMC 測試中的作用是什么?為何采用對數單位描述干擾強度? 傳導干擾與輻射干擾的本質區別及典型頻率范圍…

實操(進程狀態,R/S/D/T/t/X/Z)Linux

1 R 狀態并不直接代表進程在運行&#xff0c;而是該進程在運行隊列中進行排隊&#xff0c;由操作系統在內存維護的隊列 #include <stdio.h> #include <unistd.h>int main() {while(1){printf("我在運行嗎\n");sleep(1);}return 0; }查看狀態&#xff08…

React 文件上傳新玩法:Aliyun OSS 加持的智能上傳組件

文件上傳是前端開發中的“老朋友”&#xff0c;但如何讓它既簡單又強大&#xff0c;還能無縫對接云端存儲&#xff1f;今天&#xff0c;我要帶你認識一個超酷的 React 組件 AliUploader&#xff0c;它不僅支持拖拽上傳、批量編輯和文件排序&#xff0c;還直接把文件傳到 Aliyun…