从代码到设计的性能优化指南

本文详述了从代码优化和设计优化两个层面提升系统性能的策略。代码优化包括预加载关联、缓存对齐、分支预测、写时复制和内联优化,通过调整代码结构、使用静态变量和内联技术提高程序执行效率。设计优化则涉及缓存、异步处理、并行和池化,如利用缓存减少数据访问延迟,采用非阻塞 IO 和协程提高并发性能,以及合理利用线程池和数据库连接池。
摘要由CSDN通过智能技术生成

目录

一 前言

二 代码优化

2.1 关联代码

预加载关联

使用线程池

使用静态变量

2.2 缓存对齐

缓存填充 (Padding)

@Contended注解

对齐内存与本地变量

2.3 分支预测

2.4 写时复制

2.5 内联优化

final 修饰符

限制方法长度

内联注解

2.6 编码优化

反射机制

异常处理

日志处理

临时对象

小结

三 设计优化

3.1 缓存

3.2 异步

非阻塞 IO

协程

3.3 并行

3.4 池化

3.5 预处理


一 前言

性能优化是一个系统开发过程中非常重要的一环,特别是在互联网应用中,性能优化往往能够决定一个应用的成败。

性能优化是个系统性工程,宏观上可分为网络,服务,存储几个方向,每个方向又可以细分为架构,设计,代码,可用性,度量等多个子项。 本文将重点从代码设计两个子项展开,谈谈那些提升性能的知识点。当然,很多性能提升策略都是有代价的,适用于某些特定场景,大家在学习和使用的时候,最好带着批判的思维,决策前,做好利弊权衡。

先简单罗列一下性能优化方向:

二 代码优化

2.1 关联代码

关联代码优化是通过预加载相关代码,避免在运行时加载目标代码,造成运行时负担。我们知道 Java 有两个类加载器:Bootstrap class loader 和 Application class loader。Bootstrap class loader 负责加载 Java API 中包含的核心类,而 Application class loader 则负责加载自定义类。关联代码优化可以通过以下几种方式来实现。

预加载关联

预加载关联类是指在程序启动时预先加载目标与关联类,以避免在运行时加载。可以通过静态代码块来实现预加载,如下所示:

public class MainClass {
    static {
        // 预加载MyClass,其实现了相关功能
        Class.forName("com.example.MyClass");
    }
    // 运行相关功能的代码
    // ...
}

使用线程池

线程池可以让多个任务使用同一个线程池中的线程,从而减少线程的创建和销毁成本。使用线程池时,可以在程序启动时创建线程池,并在主线程中预加载相关代码。然后以异步方式使用线程池中的线程来执行相关代码,可以提高程序的性能。

使用静态变量

可以使用静态变量来缓存与关联代码有关的对象和数据。在程序启动时,可以预先加载关联代码,并将对象或数据存储在静态变量中。然后在程序运行时使用静态变量中缓存的对象或数据,以避免重复加载和生成。这种方式可以有效地提高程序的性能,但需要注意静态变量的使用,确保它们在多线程环境中的安全性。

2.2 缓存对齐

在介绍缓存对齐之前,需要先普及一些 CPU 指令执行的相关知识。

  • 缓存行(Cache line)  CPU 读取内存数据时并非一次只读一个字节,一般是会读一段 64 字节(硬件决定)长度的连续的内存块 (chunks of memory),这些块我们称之为缓存行。
  • 伪共享(False Sharing):当运行在两个不同 CPU 上的两个线程写入两个不同的变量时,如果这两个变量恰好存储在同一个 CPU 缓存行中,就会发生伪共享(False Sharing)。即当第一个线程修改缓存行中其中一个变量时,其他引用此缓存行变量的线程的缓存行将会无效。如果 CPU 需要读取失效的缓存行,它必须等待缓存行刷新,这会导致性能下降。
  • CPU 停止运转(stall):当一个核心需要等待另一个核心重新加载缓存行时(出现伪共享时),它无法继续执行下一条指令,只能停止运转等待,这被称之为 stall。减少伪共享也就意味着减少了 stall 的发生。
  • IPC(instructions per cycle):它表示平均每个 CPU 周期执行的指令数量,很显然该数值越大性能越好。可以基于 IPC 指标(比如:阈值 1.0)来简单判断程序是属于访问密集型还是计算密集型。Linux 系统中可以通过 tiptop 命令来查看每个进程的 CPU 硬件数据:

如何简单来区分访存密集型和计算密集型程序?

  1. 如果 IPC < 1.0, 很可能是 Memory stall 占主导,多半意味着访存密集型。

  2. 如果 IPC > 1.0, 很可能是计算密集型的程序。

  • CPU 利用率:是指系统中 CPU 处于忙碌状态的时间与总时间的比例。忙碌状态时间又可以进一步拆分为指令(instruction)执行消耗周期 cycle(%INS) 和 stalled 的周期 cycle(%STL)。perf 采集了 10 秒内全部 CPU 的运行状态:

IPC计算

IPC = instructions/cycles
上图中,可以计算出结果为:0.79
现代处理器一般有多条流水线(比如:4核心),运行 perf 的那台机器,IPC的理论值可达到4.0。
如果我们从 IPC的角度来看,这台机器只运行到其处理器最高速度的 19.7%(0.79 / 4.0)。

总之,通过 Top 命令,看到 CPU 使用率之后,可以进一步分析指令执行消耗周期和 stalled 周期,有这些更详细的指标之后,就能够知道该如何更好地对应用和系统进行调优。

  • 缓存对齐: 是通过调整数据在内存中的分布,让数据在被缓存时,更有利于 CPU 从缓存中读取,从而避免了频繁的内存读取,提高了数据访问的速度。

缓存填充 (Padding)

减少伪共享也就意味着减少了 stall 的发生,其中一个手段就是通过填充 (Padding) 数据的形式,即在适当的距离处插入一些对齐的空间来填充缓存行,从而使每个线程的修改不会脏污同一个缓存行。

/**
 * 缓存行填充测试
 *
 * @author liuhuiqing
 * @date 2023年04月28日
 */
public class FalseSharingTest {
    private static final int LOOP_NUM = 1000000000;

    public static void main(String[] args) throws InterruptedException {
        Struct struct = new Struct();
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < LOOP_NUM; i++) {
                struct.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < LOOP_NUM; i++) {
                struct.y++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("cost time [" + (System.currentTimeMillis() - start) + "] ms");
    }

    static class Struct {
        // 共享变量
        volatile long x;
        // 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
        long p1, p2, p3, p4, p5, p6, p7;
        // long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
        // 共享变量
        volatile long y;
    }
}

经过本地测试,这种以空间换时间的方式,即实现了缓存行数据对齐的方式,在执行效率方面,比没有对齐之前,提高了 5 倍!

@Contended注解

在 Java 8 中,引入了@Contended注解,该注解可以用来告诉 JVM 对字段进行缓存对齐(将字段放入不同的缓存行),从而提高程序的性能。使用@Contended注解时,需要在

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值