关于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.mzph.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…