关于JVM和OS中的指令重排以及JIT优化

news2025/4/9 14:29:13

关于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  // 这是无参构造方法,返回类型为 void
   L0
    LINENUMBER 1 L0  // 表示该字节码位置对应源代码的第 1 行
    ALOAD 0  // 将当前对象(this)加载到栈上。这里的 0 表示加载 this(当前对象)。
    INVOKESPECIAL java/lang/Object.<init> ()V  // 调用父类 Object 的构造方法(<init>),构造函数是无参的
    RETURN  // 从构造方法中返回
   L1
    LOCALVARIABLE this Ltest; L0 L1 0  // 在字节码中定义了一个局部变量 'this',类型是 test,对应的范围是 L0 到 L1,局部变量索引为 0
    MAXSTACK = 1  // 最大栈深度为 1
    MAXLOCALS = 1  // 最大局部变量数为 1
  // main 方法的声明
  public static main([Ljava/lang/String;)V  // main 方法,接受字符串数组作为参数,返回类型为 void
   L0
    LINENUMBER 3 L0  // 表示该字节码位置对应源代码的第 3 行
    ICONST_1  // 将常量 1 压入栈中
    ISTORE 1  // 将栈顶的值(1)存入局部变量 1 中
   L1
    LINENUMBER 4 L1  // 表示该字节码位置对应源代码的第 4 行
    ICONST_2  // 将常量 2 压入栈中
    ISTORE 2  // 将栈顶的值(2)存入局部变量 2 中
   L2
    LINENUMBER 5 L2  // 表示该字节码位置对应源代码的第 5 行
    ILOAD 1  // 将局部变量 1 的值(即 1)加载到栈上
    ILOAD 2  // 将局部变量 2 的值(即 2)加载到栈上
    IADD  // 将栈顶的两个整数相加(1 + 2 = 3)
    ISTORE 3  // 将结果(3)存入局部变量 3 中
   L3
    LINENUMBER 6 L3  // 表示该字节码位置对应源代码的第 6 行
    RETURN  // 返回,从 main 方法中返回
   L4
    LOCALVARIABLE args [Ljava/lang/String; L0 L4 0  // 定义了局部变量 args,类型为 String[],范围是 L0 到 L4
    LOCALVARIABLE a I L1 L4 1  // 定义了局部变量 a,类型为 int,范围是 L1 到 L4
    LOCALVARIABLE b I L2 L4 2  // 定义了局部变量 b,类型为 int,范围是 L2 到 L4
    LOCALVARIABLE c I L3 L4 3  // 定义了局部变量 c,类型为 int,范围是 L3 到 L4
    MAXSTACK = 2  // 最大栈深度为 2
    MAXLOCALS = 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:Z
 5: 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.coloradmin.cn/o/2329909.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在CPU服务器上部署Ollama和Dify的过程记录

在本指南中&#xff0c;我将详细介绍如何在CPU服务器上安装和配置Ollama模型服务和Dify平台&#xff0c;以及如何利用Docker实现这些服务的高效部署和迁移。本文分为三大部分&#xff1a;Ollama部署、Dify环境配置和Docker环境管理&#xff0c;适合需要在本地或私有环境中运行A…

【计网】TCP 协议详解 与 常见面试题

三次握手、四次挥手的常见面试题 不用死记&#xff0c;只需要清楚三次握手&#xff0c;四次挥手的流程&#xff0c;回答的时候心里要记住&#xff0c;假设网络是不可靠的 问题(1)&#xff1a;为什么关闭连接时需要四次挥手&#xff0c;而建立连接却只要三次握手&#xff1f; 关…

7.4 SVD 的几何背景

一、SVD 的几何解释 SVD 将矩阵分解成三个矩阵的乘积&#xff1a; ( 正交矩阵 ) ( 对角矩阵 ) ( 正交矩阵 ) (\pmb{正交矩阵})\times(\pmb{对角矩阵})(\pmb{正交矩阵}) (正交矩阵)(对角矩阵)(正交矩阵)&#xff0c;用几何语言表述其几何背景&#xff1a; ( 旋转 ) ( 伸缩 )…

C++的多态-上

目录 多态的概念 多态的定义及实现 1.虚函数 2. 多态的实现 2.1.多态构成条件 2.2.虚函数重写的两个例外 (1)协变(基类与派生类虚函数返回值类型不同) (2)析构函数的重写(基类与派生类析构函数的名字不同) 2.3.多态的实现 2.4.多态在析构函数中的应用 2.5.多态构成条…

内存与显存:从同根生到殊途异路的科技演进

在现代计算机的世界里&#xff0c;内存和显存是两个不可或缺的硬件组件。它们看似功能相近&#xff0c;却在发展历程中逐渐分道扬镳&#xff0c;各自服务于不同的计算需求。今天&#xff0c;我们将从一根内存条和一块显卡入手&#xff0c;深入探讨内存与显存的异同&#xff0c;…

手搓多模态-04 归一化介绍

在机器学习中&#xff0c;归一化是一个非常重要的工具&#xff0c;它能帮助我们加速训练的速度。在我们前面的SiglipVisionTransformer 中&#xff0c;也有用到归一化层&#xff0c;如下代码所示&#xff1a; class SiglipVisionTransformer(nn.Module): ##视觉模型的第二层&am…

【C++】第八节—string类(上)——详解+代码示例

hello&#xff0c;又见面了&#xff01; 云边有个稻草人-CSDN博客 C_云边有个稻草人的博客-CSDN博客——C专栏&#xff08;质量分高达97&#xff01;&#xff09; 菜鸟进化中。。。 目录 一、为什么要学习string类&#xff1f; 1.1 C语言中的字符串 1.2 面试题(暂不做讲解) …

Java 数组与 ArrayList 核心区别解析:从源码到实战!!!

&#x1f31f; Java 数组与 ArrayList 核心区别解析&#xff1a;从源码到实战 &#x1f4a1; Java 开发者必读&#xff01; 数组&#xff08;Array&#xff09;和 ArrayList 是 Java 中最常用的数据存储结构&#xff0c;但它们的底层设计、性能表现和适用场景差异显著。本文通…

【易飞】易飞批量选择品号处理方法,工作效率提升300%

开窗选择品号方式要么手动输入,要么以什么开头、包含、从A物料到B物料查询后返回的有规律的品号。对于没有规律且大量品号的处理方式是否有便捷的方法呢? 尤其在通常在查询多阶材料清单,查询库存明细表,整批变更元件等如品号无规律情况下,只能一个个选择,无法通过EXCEL方…

【最新版】啦啦外卖v64系统独立版源码+全部小程序APP端+安装教程

一.系统介绍 啦啦外卖跑腿平台独立版&#xff0c;使用的都知道该系统功能非常强大&#xff0c;应该说是目前外卖平台功能最全的一套系统。主要是功能非常多&#xff0c;拿来即用&#xff0c;包括客户端小程序、配送端小程序、商户端小程序&#xff0c;还有对应四个端的APP源码…

iproute2 工具集使用详解

目录 一、iproute2 核心命令&#xff1a;ip二、常用功能详解1. 管理网络接口&#xff08;link 对象&#xff09;2. 管理 IP 地址&#xff08;address 对象&#xff09;3. 管理路由表&#xff08;route 对象&#xff09;4. 管理 ARP 和邻居缓存&#xff08;neigh 对象&#xff0…

AD(Altium Designer)更换PCB文件的器件封装

一、确定是否拥有想换的器件PCB封装 1.1 打开现有的原理图 1.2 确定是否拥有想换的器件PCB文件 1.2.1 如果有 按照1.3进行切换器件PCB封装 1.2.2 如果没有 按照如下链接进行添加 AD(Altium Designer)已有封装库的基础上添加器件封装-CSDN博客https://blog.csdn.net/XU15…

【文献研究】含硼钢中BN表面偏析对可镀性的影响

《B 添加钢的溶融 Zn めっき性に及ぼす BN 表面析出の影響》由JFE公司田原大輔等人撰写。研究聚焦 B 添加钢在低露点退火时 BN 形成对镀锌性的影响&#xff0c;对汽车用高强度钢镀锌工艺优化意义重大。通过多组对比实验&#xff0c;结合多种分析手段&#xff0c;明确了相关因素…

React学习-css

W3Schools Tryit Editor CSS 教程 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明: p { /* 这是个注释 */ color:red; text-align:center; }选择器 CSS Id: #para1{ text-align:center; color:red; } Class: .center {text-align:center;} p…

数据分析-Excel-学习笔记Day1

Day1 复现报表聚合函数&#xff1a;日期联动快速定位区域SUMIF函数SUMIFS函数环比、同比计算IFERROR函数混合引用单元格格式总结汇报 拿到一个Excel表格&#xff0c;首先要看这个表格的构成&#xff08;包含了哪些数据&#xff09;&#xff0c;几行几列&#xff0c;每一列的名称…

树莓派PICO 设备烧录成cmsis dap

文章目录 1. 实际操作2. IO连接 1. 实际操作 2. IO连接

【数据结构】图的存储

目录 邻接矩阵 表示方法 代码定义 结构特点与度的信息 邻接表 表示方法 代码定义 结构特点与度的信息 十字链表 表示方法 第二步&#xff0c;将顶点x的firstIn域与所有headvex域为x的弧连起来。 结构特点与度的信息 邻接多重表 表示方法 结构特点与度的信息 图…

如何解决uniapp打包安卓只出现功能栏而无数据的问题

如何解决uniapp打包安卓只出现功能栏而无数据的问题 经验来自&#xff1a;关于Vue3中调试APP触发异常&#xff1a;exception:white screen cause create instanceContext failed,check js stack -> at useStore (app-service.js:2309:15)解决方案 - 甲辰哥来帮你算命 - 博客…

kotlin,数字滚动选择

用国内的通义灵码和codegeex都没有弄出来&#xff0c;最后只得用墙外的chatgpt才弄出一个满意的。kotlin真的有点难&#xff0c;好在有AI&#xff0c;让学习没这难了。 package com.example.mynumsetimport android.os.Bundle import androidx.activity.ComponentActivity imp…

【4】搭建k8s集群系列(二进制部署)之安装master节点组件(kube-apiserver)

一、下载k8s二进制文件 下载地址&#xff1a; https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG -1.20.md 注&#xff1a;打开链接你会发现里面有很多包&#xff0c;下载一个 server 包就够了&#xff0c;包含了 Master 和 Worker Node 二进制文件。…