程序编码优化-JAVA篇
lipiwang 2025-05-23 18:24 3 浏览 0 评论
之前一篇博客介绍了C语言中一些基础的编码优化,实际上涉及到编译优化,所有语言进行编译时,相应的编译器都可以进行对应的优化;
1. 字段访问相关优化
基于逃逸分析的优化方式:进行锁消除、栈上分配、标量替换等;标量替换:将对象本身拆散为一个个字段,把原本对象字段的访问,替换为一个个局部变量的访问; 若对象没有逃逸,则:
static int bar(int x) {
Foo foo = new Foo();
foo.a = x;
return foo.a;
}
static int bar(int x) {
int a = x;
return a;
}
即使JIT有这种逃逸分析的功能,但是有时因为内联不够彻底而被即时编译器当成是逃逸的,无法进行标量替换,所以此时需要程序员优化字段访问:
字段读取优化
static int bar(Foo o,int x){
int y = o.a+x;
return o.a+y;
}
static int bar(Foo o,int x){
int t = o.a;
int y = t+x;
return t+y;
}
字段存储优化
class Foo {
int a = 0;
void bar() {
a = 1;
int t = a;
a = t + 2;
}
}
// 优化为
class Foo {
int a = 0;
void bar() {
a = 1;
int t = 1;//少一次访存
a = t + 2;
}
}
// 进一步优化为
class Foo {
int a = 0;
void bar() {
a = 3;
}
}
死代码消除
int bar(int x, int y) {
int t = x*y;
t = x+y;
return t;
}
涉及两个存储局部变量的操作:
int bar(int x, int y) {
return x+y;
}
int bar(boolean f, int x, int y) {
int t = x*y;
if (f)
t = x+y;
return t;
}
优化为:
int bar(boolean f, int x, int y) {
int t;
if (f)
t = x+y;
else
t = x*y;
return t;
}
//精简数据流:
int bar(int x) {
if (false)
return x;
else
return -x;
}
总结起来:字段访问优化主要是减少访存操作;
2. 循环优化
这个优化在优化程序性能中也有提到。
循环无关代码外提
循环中中值不变的表达式,如果不改变程序予以,将这些循环无关代码提出循环外;
int foo(int x, int y, int[] a) {
int sum = 0;
for (int i = 0; i < a.length; i++) {
sum += x * y + a[i];
}
return sum;
}
// 对应的字节码
int foo(int, int, int[]);
Code:
0: iconst_0
1: istore 4
3: iconst_0
4: istore 5
6: goto 25
// 循环体开始
9: iload 4 // load sum
11: iload_1 // load x
12: iload_2 // load y
13: imul // x*y
14: aload_3 // load a
15: iload 5 // load i
17: iaload // a[i]
18: iadd // x*y + a[i]
19: iadd // sum + (x*y + a[i])
20: istore 4 // sum = sum + (x*y + a[i])
22: iinc 5, 1 // i++
25: iload 5 // load i
27: aload_3 // load a
28: arraylength // a.length
29: if_icmplt 9 // i < a.length
// 循环体结束
32: iload 4
34: ireturn
优化为:
int fooManualOpt(int x, int y, int[] a) {
int sum = 0;
int t0 = x * y;
int t1 = a.length;
for (int i = 0; i < t1; i++) {
sum += t0 + a[i];
}
return sum;
}
循环展开
int foo(int[] a) {
int sum = 0;
for (int i = 0; i < 64; i++) {
sum += (i % 2 == 0) ? a[i] : -a[i];
}
return sum;
}
int foo(int[] a) {
int sum = 0;
for (int i = 0; i < 64; i += 2) { // 注意这里的步数是2
sum += (i % 2 == 0) ? a[i] : -a[i];
sum += ((i + 1) % 2 == 0) ? a[i + 1] : -a[i + 1];
}
return sum;
}
在C2中,只有计数循环才能被展开,要满足四个条件:
- 维护一个循环计数器,基于计数器的循环出口只有一个
- 循环计数器类型为int、short或char
- 每个迭代循环计数器增量为常数
- 循环计数器上限或下限是循环无关的数值
循环外提
int foo(int[] a) {
int sum = 0;
for (int i = 0; i < a.length; i++) {
if (a.length > 4) {
sum += a[i];
}
}
return sum;
}
//优化为:
int foo(int[] a) {
int sum = 0;
if (a.length > 4) {
for (int i = 0; i < a.length; i++) {
sum += a[i];
}
} else {
for (int i = 0; i < a.length; i++) {
}
}
return sum;
}
// 进一步优化为:
int foo(int[] a) {
int sum = 0;
if (a.length > 4) {
for (int i = 0; i < a.length; i++) {
sum += a[i];
}
}
return sum;
}
循环剥离
将循环的前几个迭代或者后几个迭代剥离出循环的优化方式。一般来说,循环的前几个迭代或者后几个迭代都包含特殊处理。通过将这几个特殊的迭代剥离出去,可以使原本的循环体的规律性更加明显,从而触发进一步的优化。
int foo(int[] a) {
int j = 0;
int sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[j];
j = i;
}
return sum;
}
int foo(int[] a) {
int sum = 0;
if (0 < a.length) {
sum += a[0];
for (int i = 1; i < a.length; i++) {
sum += a[i - 1];
}
}
return sum;
}
3. 向量化
如何优化如下代码:
void foo(byte[] dst, byte[] src) {
for (int i = 0; i < dst.length - 4; i += 4) {
dst[i] = src[i];
dst[i+1] = src[i+1];
dst[i+2] = src[i+2];
dst[i+3] = src[i+3];
}
... // post-loop
}
会产生4条读指令以及4条4条写指令;可以优化为:
void foo(byte[] dst, byte[] src) {
for (int i = 0; i < dst.length - 4; i += 4) {
dst[i:i+3] = src[i:i+3];
}
... // post-loop
}
SIMD指令
上面的byte数组,四个数组元素合起来才4字节,如果换成int、long,合起来是16字节、32字节;但是x86_64上通用寄存器大小为64位(8字节),无法暂存这些数据,需要借助XMM寄存器,byte数组向量化读取、写入操作同样适用XMM寄存器;
XMM寄存器由SSE指令集引入,一开始为128位,11年X86上的CPU开始支持AVX指令集,XMM寄存器升级为256位,并更名为YMM寄存器;后又将YMM寄存器升级至512位,更名为ZMM寄存器;
SSE及AVX指令都涉及一个概念:单指令流多数据流(SIMD);SIMD指令:PADDB、PADDW、PADDD以及PADDQ,分别实现byte、short、int、long的向量加法;
void foo(int[] a, int[] b, int[] c) {
for (int i = 0; i < c.length; i++) {
c[i] = a[i] + b[i];
}
}
内存的右边是高位,寄存器的左边是高位,上面这段代码经过向量化优化后,使用PADDD来实现:
c[i:i+3] = a[i:i+3]+b[i:i+3],可以看做是CPU指令级别并行
c.length/4是理论值,现实中C2还考虑缓存行对齐因素,能够应用向量化加法的仅有数组中间部分元素;
使用SIMD的hotspot intrinsic
SIMD虽然高效,使用麻烦,主要因为不同CPU支持的SIMD指令可能不同,越新的SIMD指令,支持的寄存器长度越大,功能越强;几乎所有x86_64支持SSE指令集,绝大部分支持AVX指令集;
但是JAVA虚拟机执行的java字节码是平台无关的,首先被解释执行,而后返回执行的部分才会被java虚拟机编译为机器码;进行编译时,已经知道java虚拟机的目标CPU,可以知道其所支持的指令集;
Java字节码的平台无关性引发另一个问题,Java程序无法像C++程序那样,直接使用Intel提供的,被替换为具体SIMD指令的intrinsic方法;HotSpot提供的替代方案:Java层面的intrinsic方法,这些intrinsic语义比单个SIMD指令复杂,运行过程,hotspot虚拟机根据当前体系架构来决定是否对该intrinsic方法的调用替换为另一种高效的实现,否则使用原本的java实现;
由于开发成本及维护成本高,这种intrinsic数量少,如System.arraycopy和Arrays.copyOf、Arrays.equals及Java9的Arrays.compare和Arrays.mismatch以及String.indexOf、StringLatin1.inflate。
这些intrinsic只能做点点覆盖,不少情况,并不会用到intrinsic,又存在向量化优化机会,这时候需要借助编译器中的自动向量化;
自动向量化
JIT的自动向量化针对能够展开的计数循环,进行向量优化,即JIT能够自动展开优化成使用PADDD指令的向量加法; 自动向量化条件:
- 循环变量增量为1
- 循环变量不能为long类型,C2无法将循环识别为计数循环
- 循环迭代之间最好不要有数据依赖,如a[i]=a[i-1]
- 不能有分支跳转
- 不要手工进行循环展开
自动向量化条件较为苛刻,C2支持的整数向量化操作不多,只有向量加法、向量减法、按位与、或、异或以及批量移位、批量乘法;C2还支持向量点积的自动向量化(两两香橙再求和);
为了解决intrinsic以及自动向量化覆盖面过窄的问题,openJDK尝试引入开发人员可控的向量化抽象;
HotSpot运用向量优化的方式有两种:1)使用HotSpot intrinsic,在调用特定方法时候替换为使用了SIMD指令的高效实现,属于点覆盖;2)依赖即时编译器进行自动向量化,在循环展开优化之后将不同迭代的运算合并为向量运算。自动向量化的触发条件较为苛刻,因此也无法覆盖大多数用例。
注解
- RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
- RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
- RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码;
生命周期:source<class<runtime;一般如果需要在运行时主动获取注解信息,只能用runtime注解;编译时进行一些预处理操作,比如生成一些辅助代码,用class注解;只做一些检查工作,如@override和@supresswarnings,可用source注解;
这里很重要的一点是编译多个Java文件时的情况:假如要编译A.java源码文件和B.class文件,其中A类依赖B类,并且B类上有些注解希望让A.java编译时能看到,那么B.class里就必须要持有这些注解信息才行
4. 性能测试中的坑
普通测试方法,影响因素:java虚拟机堆空间的自适配、即时编译;还有一些指令优化、计数循环优化(把i为int改为long,即可避免这个优化);操作系统和硬件系统带来的影响,一个较为常见的例子便是电源管理策略,许多机器特别是笔记本,会动态配置CPU频率,而CPU频率直接影响到性鞥测试的数据,短时间的性能测试未必可靠;
OpenJdk开源项目JMH,内置许多方法提供了标准测试;
Java 中的native方法的链接方式主要有两种。一是按照 JNI 的默认规范命名所要链接的 C 函数,并依赖于Java 虚拟机自动链接。另一种则是在 C 代码中主动链接;(第二种还是要依赖第一种)
JNI 中的引用可分为局部引用和全局引用。这两者都可以阻止垃圾回收器回收被引用的 Java对象。不同的是,局部引用在native方法调用返回之后便会失效。传入参数以及大部分JNIAPI函数的返回值都属于局部引用。
- 上一篇:java实现10种排序算法
- 下一篇:Java零基础入门常见的学习知识详解
相关推荐
- httpclient+jsoup实现小说线上采集阅读
-
前言 用过老版本UC看小说的同学都知道,当年版权问题比较松懈,我们可以再UC搜索不同来源的小说,并且阅读,那么它是怎么做的呢?下面让我们自己实现一个小说线上采集阅读。(说明:仅用于技术学习、研究) ...
- Python3+requests+unittest接口自动化测试实战
-
一、Requests介绍RequestsisanelegantandsimpleHTTPlibraryforPython,builtforhumanbeings.翻译过来就是...
- 授权码 + PKCE 模式|OIDC & OAuth2.0 认证协议最佳实践系列【03】
-
在上一篇文章中,我们介绍了OIDC授权码模式,本次我们将重点围绕授权码+PKCE模式(AuthorizationCodeWithPKCE)进行介绍,从而让你的系统快速具备接入用户认...
- JWT 在 Java Web 开发中的奇妙应用
-
JWT在JavaWeb开发中的奇妙应用在当今的互联网世界里,安全始终是一个绕不开的话题。而当我们谈论到Web应用的安全性时,认证和授权绝对是其中的核心部分。说到这,我忍不住要给大家讲个笑话...
- 动手操作:一个 OAuth 2 应用程序(2) - 配置 Keycloak 为授权服务器
-
接上一篇《动手操作:一个OAuth2应用程序(1)-应用程序场景》进行场景分析后,本篇就开始动手实现授权服务器。在本文中,我们将Keycloak配置为系统的授权服务器(图3)。...
- JSON Web Token是什么?
-
JSONWebToken(缩写JWT)是目前最流行的跨域认证解决方案。传统的session认证http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证...
- Keycloak Servlet Filter Adapter使用
-
KeycloakClientAdapters简介Keycloakclientadaptersarelibrariesthatmakeitveryeasytosecurea...
- 使用JWT生成token
-
一、使用JWT进行身份验证1、传统用户身份验证Internet服务无法与用户身份验证分开。一般过程如下:用户向服务器发送用户名和密码。验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中...
- 在word中通过VBA调用百度翻译API在线翻译
-
一天的时间,借助各种AI终于解决了这个问题:在word中通过VBA调用百度翻译API进行在线翻译。给我的word又添加了一项神技。先上代码:Sub宏5()''宏5宏Dimapp...
- API 安全之认证鉴权
-
作者:半天前言API作为企业的重要数字资源,在给企业带来巨大便利的同时也带来了新的安全问题,一旦被攻击可能导致数据泄漏重大安全问题,从而给企业的业务发展带来极大的安全风险。正是在这样的背景下,Ope...
- 用WordPress建站哪些插件会拖慢速度影响排名?
-
你是否发现网站加载总慢半拍,SEO排名死活上不去?八成是插件惹的祸!80%的站长不知道,WordPress插件用错类型或配置不当,分分钟让网站速度暴跌,爬虫抓取效率直接砍半。缓存插件没装对,越用越卡你...
- JavaScript报错了?不要慌!怎么看怎么处理都在这里
-
在开发中,有时,我们花了几个小时写的JS代码,在游览器调试一看,控制台一堆红,瞬间一万头草泥马奔腾而来。至此,本文主要记录JS常见的一些报错类型,以及常见的报错信息,分析其报错原因,并给予处理...
-
- 跨站脚本攻击(四)
-
04XSS漏洞挖掘技巧4.1常见的绕过姿势实际应用中web程序往往会通过一些过滤规则来阻止带有恶意代码的用户输入被显示,但由于HTML语言的松散性和各种标签的不同优先级,使得我们绕过过滤规则成为了可能。4.1.1利用大小写绕过HTML标签...
-
2025-05-24 15:21 lipiwang
- WAF-Bypass之SQL注入绕过思路总结
-
过WAF(针对云WAF)寻找真实IP(源站)绕过如果流量都没有经过WAF,WAF当然无法拦截攻击请求。当前多数云WAF架构,例如百度云加速、阿里云盾等,通过更改DNS解析,把流量引入WAF集群,流量经...
- Springboot之登录模块探索(含Token,验证码,网络安全等知识)
-
简介登录模块很简单,前端发送账号密码的表单,后端接收验证后即可~淦!可是我想多了,于是有了以下几个问题(里面还包含网络安全问题):1.登录时的验证码2.自动登录的实现3.怎么维护前后端登录状态在这和大...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)