如何写出CPU友好的代码,百倍提升性能?

news2024/11/25 6:36:42

作者:王再军

不管是什么样的数据,投其所好,才能够优化代码性能。本文将用一个实际用例为大家分享如何通过用心组织的代码来提升性能。

一、出现性能差别的代码

CPU友好的代码与我们平时的那些CRUD操作可能没什么关系。但是用心组织的代码其实也能让性能提升百倍。我们不应该停留在CRUD的漩涡中。今天我给大家带来一个很神奇的现象,文章不长,原理通用,还请大家耐心看完。

我们可以先看下面的矩阵计算。大家也可以自己思考一下,如果是你来实现一个矩阵的乘法,你会怎么来做。

下图是我给出的A、B、C 三个解题的思路。大家觉得在JVM里面,下面的代码性能会有区别么?如果有的话,哪一个会快一点?如果没有的话,又为什么?

下图是benchmark运行的结果(具体的运行代码和结果查看文末附件),是否和你想的一样呢。

x轴是计算数组的大小,y轴是所消耗的时间。

最上两条线是B代码块的结果,中间是A代码块的结果,最下面是C代码块的结果。

从运行时间角度看结果是:TC < TA < TB。从性能角度看结果是:PC > PA > PB。

大家猜对结果了么,是不是很你想的一样呢?如果不是的话,那就慢慢往下面看。

二、为什么会有性能差别?

要想知道这个问题的答案,我们需要知道两个知识点。

  • 首先,我们需要知道Java二维数组的存储结构是什么样子的。

  • 其次,我们需要知道CPU在计算的时候它L1、L2、L3的缓存机制。

2.1 知识点

2.1.1 知识点一:Java二维数组的存储结构

下图便是Java二维数组的一个存储方式示意图,意思是 int[][] array_A = new int[4][3]。

在一个数组里面存的都是“指针”,指向真实存放数据的地址块。

每一行的数据是连续的地址,但是行与行之间的地址就不一定连续了。这一点很重要,后面会用到。

2.1.2 知识点二:CPU的缓存机制

CPU架构是会演进的,高低端的参数也不一定相同。但我们毕竟不是CPU的制造者,不必每一个CPU都去细扣,我们只需要理解他的原理,在适当的时候做一些抽象方便理解就可以。

下图是我当前Mac的CPU参数,大家需要注意2个东西,L2缓存、L3缓存。这2个参数就是影响我们今天讨论的性能的主要因素。

下面是各个缓存的CPU的访问时间:

L1 、L2、L3、主存的大小是逐渐增大,速度是逐渐减小的。

下面是现代CPU的一个架构示意图:

其中:

  • Regs,是寄存器。

  • d-cache,是数据缓存。

  • i-cache,是指令缓存。本次我们并不讨论这个缓存快的影响。

CPU的缓存里面还有很多的细节,知道上面的信息就已经足够我们理解今天的问题了。

2.2 性能损失的原因 — 缓存命中率

有了上面的各级别的缓存参考之后,我们可以想象一下,如果把上面的图像换成是我们的二维数组呢。是不是就是下面这样(可能没有那么严谨,但是不妨碍我们理解)。

在RAM(主存)的数据是这样的:

L3缓存就是这样的(红色框选中部分):

L2缓存就是这样的(红色框选中部分):

有了这个这些层级的缓存之后,CPU在计算的时候就可以不用来回的到速度极慢的RAM(主存)中去找数组的数据了。

2.2.1 友好的遍历方式

假设上面的数据的变量名称是A,成员使用a来表述。

我们取数据按照从左到右,再从上到下的顺序来进行遍历。

对于L2缓存来说:

第一次获取数据a11(“1”)的时候其实是没有数据的,所以会耗时去把a11,a12,a13(“1,2,3”)都取回来缓存起来。

当第二次取a12、a13的时候候就直接从L2缓存取了。这样cache命中率就是 66.7%。

对于L3的情况类似。这样的遍历方式对于CPU来说是一个很友好且高效的。 

  • C代码块就是这种横向优先的访问方式。 

  • A代码块里面对arrays_A的方式是横向优先遍历的,但是在处理arrays_B的时候就是纵向遍历的(也就是下面即将提到的方式)。 

  • B代码块所有的访问都是纵向的(不友好的遍历方式)。因为发挥不出CPU缓存的效果,所以性能最差。

2.2.2 不友好的遍历方式

从上到下,再从左到右。

为什么这是一个不好的遍历方式呢?

这个得结合上一节Java的二维数组的存储结构一起看。再来回顾一下:

从上面的存储的结构图来看,其实a11,a12,a13 与 a21,a22,a23行与行之间并不是连续的。所以对于L1、L2、L3缓存来说很有可能是不能一起被缓存的(这里用了可能,具体得看L1、L2、L3的容量和数组的大小)。虽然是可能,但是通常都不会一起出现。

有了这个知识之后,我们再来看,先从上到下,再从左到右的顺序的缓存命中率。

  • 第一次,获取a11,但是缓存里面没有,找到a11之后就把a11,a12,a13缓存下来了。

  • 第二次,获取a21,但是缓存里面没有,找到a21之后就把a21,a22,a23缓存下来了,假设有CPU有两行的缓存空间。

  • 第三次,获取a31,但是缓存里面没有,找到a31之后把a31,a32,a33缓存下来,并且把a11,a12,a13替换掉(缓存的空间有限,虽然具体的替换策略有很多种,并且还和数据本身的Hash有关系,这里就假设把第一次的结果覆盖了)。

  • 后面的逻辑重复之前的步骤。最后得到的缓存命中率就是0%。

结合文章开头的缓存速率表格,我们就不难发现,如果我们每次都不命中缓存的话,那么延迟带来的耗时将会相差一个数量级。

三、总结

再来回顾一下我们之前的问题。

  • C代码块是横向优先的访问方式。 

  • A代码块里面对arrays_A的方式是横向顺序访问的,但是在处理arrays_B的时候就是纵向遍历的。 

  • B代码块所有的访问都是纵向的(不友好的遍历方式)。因为发挥不出CPU缓存的效果,所以性能最差。

Java的二维数组在内存里面是行连续的,但是行与行之间不一定连续。CPU在缓存大小有限的情况下,不可能把所有的数据都缓存下来。再加上每一层级访问速度的硬件限制,就导致了上面的性能结果。

相信大家也和我一样,知道原理之后,也不是那么迷惑了。

在实际的业务环境中,我们不一定能遇到这种纯计算的场景。但是我们还是应该尽量顺序访问数据,不管是什么样的数据。投其所好,才能够优化代码性能。

其次,我们在访问数据的时候,还是需要了解各种语言背后实际的存储结构和CPU的缓存原理,本次是讲述的是Java,但是这个思想其他语言其实也是受用的。

四、附件

4.1 运行的环境

系统参数:

JMH version: 1.36
VM version: JDK 11.0.13, Java HotSpot(TM) 64-Bit Server VM, 11.0.13+10-LTS-370

型号名称:MacBook Pro
型号标识符:MacBookPro15,2
处理器名称:四核Intel Core i5
处理器速度:2.4 GHz
处理器数目:1
核总数:4
L2缓存(每个核):256 KB
L3缓存:6 MB
超线程技术:已启用
内存:16 GB
系统固件版本:1715.60.5.0.0 (iBridge: 19.16.10647.0.0,0)

4.2 整个benchmark的java代码

ArrayTestBenchmark

import org.openjdk.jmh.annotations.*;

/**
 * 矩阵 C = AB 的计算
 *
 * @author wzj
 * @date 2023/02/09
 */
@BenchmarkMode(Mode.AverageTime)
@State(value = Scope.Benchmark)
// 预热3次
@Warmup(iterations = 3, time = 1)
// 循环 10 次
@Measurement(iterations = 10, time = 1)
public class ArrayTestBenchmark {

    private final int N = 1000;

    private final int[][] arrays_A = new int[N][N];
    private final int[][] arrays_B = new int[N][N];

    @Setup
    public void setUp() {
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                arrays_A[i][j] = i + j;
                arrays_B[i][j] = i + j;
            }
        }
    }


    @Benchmark
    public void ijk() {
        final int[][] arrays_C = new int[N][N];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                int sum = 0;
                for (int k = 0; k < N; k++) {
                    sum += arrays_A[i][k] * arrays_B[k][j];
                }
                arrays_C[i][j] += sum;
            }
        }
        assert arrays_C.length > 0;
    }

    @Benchmark
    public void jik() {
        final int[][] arrays_C = new int[N][N];
        for (int j = 0; j < N; j++) {
            for (int i = 0; i < N; i++) {
                int sum = 0;
                for (int k = 0; k < N; k++) {
                    sum += arrays_A[i][k] * arrays_B[k][j];
                }
                arrays_C[i][j] += sum;
            }
        }
        assert arrays_C.length > 0;
    }

    @Benchmark
    public void jki() {
        final int[][] arrays_C = new int[N][N];
        for (int j = 0; j < N; j++) {
            for (int k = 0; k < N; k++) {
                int r_B = arrays_B[k][j];
                for (int i = 0; i < N; i++) {
                    arrays_C[i][j] += arrays_A[i][k] * r_B;
                }
            }
        }
        assert arrays_C.length > 0;
    }

    @Benchmark
    public void kji() {
        final int[][] arrays_C = new int[N][N];
        for (int k = 0; k < N; k++) {
            for (int j = 0; j < N; j++) {
                int r_B = arrays_B[k][j];
                for (int i = 0; i < N; i++) {
                    arrays_C[i][j] += arrays_A[i][k] * r_B;
                }
            }
        }
        assert arrays_C.length > 0;
    }

    @Benchmark
    public void kij() {
        final int[][] arrays_C = new int[N][N];
        for (int k = 0; k < N; k++) {
            for (int i = 0; i < N; i++) {
                int r_A = arrays_A[k][i];
                for (int j = 0; j < N; j++) {
                    arrays_C[i][j] += r_A * arrays_B[k][j];
                }
            }
        }
        assert arrays_C.length > 0;
    }

    @Benchmark
    public void ikj() {
        final int[][] arrays_C = new int[N][N];
        for (int i = 0; i < N; i++) {
            for (int k = 0; k < N; k++) {
                int r_A = arrays_A[k][i];
                for (int j = 0; j < N; j++) {
                    arrays_C[i][j] += r_A * arrays_B[k][j];
                }
            }
        }
        assert arrays_C.length > 0;
    }
}

4.3 多次运行benchmark的结果

引用

[01] 《深入理解计算机操作系统》

[02] 《深入理解Java虚拟机》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/460132.html

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

相关文章

开源模型ModelScope的初探使用

泛AI开发者的一站式模型服务产品平台 阿里继续沿用它的平台思维&#xff0c;搞了这个ModelScope训练模型平台&#xff0c;一边开源一部分模型&#xff0c;一边在阿里云上卖自己的付费版&#xff0c;套路依旧没变&#xff0c;不过对AI相关模型感兴趣的同学&#xff0c;想做业务…

202303最新各大厂大数据核心面试题

1、 字节、阿里、拼多多、中移杭研、海亮等:Hive做过哪些实际优化?必须结合实际项目来谈,结合我实际离线数仓里做的优化? 本人回答: 1.小文件的优化(解决方法是combineHiveinput、merge、jvm重用等) 2.数据倾斜的优化:

Flutter 小技巧之横竖列表的自适应大小布局支持

今天这个主题看着是不是有点抽象&#xff1f;又是列表嵌套&#xff1f;之前不是分享过《 ListView 和 PageView 的各种花式嵌套》了么&#xff1f;那这次的自适应大小布局支持有什么不同&#xff1f; 算是某些奇特的场景下才会需要。 首先我们看下面这段代码&#xff0c;基本逻…

android studio EditText用法

1.自定义文本框 选中状态&#xff1a; <?xml version"1.0" encoding"utf-8"?> <shape xmlns:android"http://schemas.android.com/apk/res/android"><!--指定形状内部颜色--><solid android:color"#ffffff"&g…

机器学习在生态、环境经济学中的实践技术应用及论文写作

近年来&#xff0c;人工智能领域已经取得突破性进展&#xff0c;对经济社会各个领域都产生了重大影响&#xff0c;结合了统计学、数据科学和计算机科学的机器学习是人工智能的主流方向之一&#xff0c;目前也在飞快的融入计量经济学研究。表面上机器学习通常使用大数据&#xf…

点了下链接信息就泄露了,ta们是怎么做到的?

随着互联网的普及以及一系列可供上网设备的快速发展&#xff0c;截止2022年12月&#xff0c;中国网民规模达10.37亿&#xff0c;较之2021年12月增长3549万&#xff0c;互联网普及率达75.6%&#xff1b;在这么庞大的数据背后又有多少用户的个人信息被泄露呢? 一、信息泄露常见场…

2023 年最全面的 DevOps 工具列表,你用过几个?

在软件开发领域&#xff0c;DevOps已经成为越来越重要的概念。它强调了开发、测试、运维等各个环节之间的协作和自动化&#xff0c;以提高软件交付的速度和质量。随着时间的推移&#xff0c;DevOps所涉及的工具也不断更新和演进。本文将介绍一个预计在 2023 年最全面的 DevOps …

elementui中使用响应式布局实现五个盒子一行的适配

一、使用elementui中的自定义标签 自定义标签之后&#xff0c;浏览器中的css样式会出现这个类名 <el-row :gutter"30" class"row-bg"><el-col:xs"8":sm"6":md"4":lg"{ span: 24-5 }"class"headerC…

开发框架Furion之Winform+SqlSugar

目录 1.开发环境 2.项目搭建 2.1 创建WinFrom主项目 2.2 创建子项目 2.3 实体类库基础类信息配置 2.3.1 Nuget包及项目引用 2.3.2 实体基类创建 2.4 仓储业务类库基础配置 2.4.1 Nuget包及项目引用 2.4.2 Dtos实体 2.4.3 仓储基类 2.5 service注册类库基础配置 2…

【图形数据库】Neo4j简介及应用场景

文章目录 1.什么是Neo4j?2.图形数据结构3.Neo4j应用场景3.1我们可以将图领域划分成以下两部分&#xff1a;3.2目前&#xff0c;业内已经有了相对比较成熟的基于图数据库的解决方案&#xff0c;大致可以分为以下几类。3.2.1金融行业应用3.2.2社交网络图谱3.2.3企业关系图谱 总结…

Linux进程通信:存储映射mmap

1. 存储映射是什么&#xff1f; 如上图&#xff0c;存储映射是将块设备的文件映射到进程的虚拟地址空间。之后&#xff0c;进程可以直接使用指针操作其地址空间中映射的文件&#xff0c;对这块映射区操作就相当于操作文件。 2. 存储映射函数mmap的简单使用 &#xff08;1&…

网络安全岗位面试题大全:解析各个分支岗位的面试题目,帮助你上岸大厂

网络安全是一个广泛的领域&#xff0c;涵盖了许多不同的岗位和分支。我整理了网络安全各个岗位分支的面试题目&#xff1a; 安全工程师/系统管理员 您如何确保网络系统的安全性和保密性&#xff1f;您采用了哪些技术和工具&#xff1f;请描述一下您在过去工作中遇到的最具挑战…

C++ -5- 内存管理

文章目录 C语言和C内存管理的区别示例1. C/C 中程序内存区域划分2. C中动态内存管理3.operator new 与 operator delete 函数4.new 和 delete 的实现原理5.定位new表达式 C语言和C内存管理的区别示例 //C语言&#xff1a; struct SListNode {int data;struct SListNode* next; …

什么是内存?什么是内存逃逸?怎么做内存逃逸分析

内存 平时我们在电脑上听歌&#xff0c;聊天&#xff0c;或者启动某个程序&#xff0c;那么这个启动过程&#xff0c;其实就是把程序从硬盘读入到内存中去。就像安卓手机&#xff0c;内存不够了很卡&#xff0c;杀掉几个软件&#xff0c;内存就升上来了。但也不是所有的程序都…

产品经理需要了解api接口的哪些东西

一、作为产品经理&#xff0c;需要了解API接口的以下方面&#xff1a; 功能&#xff1a;API接口的功能是指它提供的业务功能&#xff0c;包括数据查询、修改、增加、删除、计算等等&#xff0c;根据产品的需求确定需要调用哪些API接口。请求方式和传参&#xff1a;API接口的请…

致力提供一站式数据可视化解决方案,支持报表、图表、大屏

一、开源项目简介 Davinci是一个DVAAS&#xff08;Data Visualization as a Service&#xff09;平台解决方案。 Davinci面向业务人员/数据工程师/数据分析师/数据科学家&#xff0c;致力于提供一站式数据可视化解决方案。既可作为公有云/私有云独立使用&#xff0c;也可作为…

Linux进程通信:信号

1. 信号的概念 Linux进程间通信的方式之一。信号也称为“软件中断”。 信号特点&#xff1a; 简单&#xff1b;携带信息有限&#xff1b;满足特定条件才发送信号&#xff1b;可进行用户空间和内核空间进程的交互&#xff1b; 2. 信号的编号 kill -l // 查看信号编号 POS…

ModelArts的使用

完整流程第一个实例&#xff1a;AI初学者&#xff1a;使用订阅算法构建模型实现花卉识别_AI开发平台ModelArts_最佳实践_模型训练&#xff08;预置算法-新版训练&#xff09;_华为云 一、支持的模型 可以在gitee上下载标准网络模型&#xff1a; models: Models of MindSpore …

Prometheus优化及高可用

Prometheus优化及高可用 概述 Prometheus几乎已成为监控领域的事实标准&#xff0c;它自带高效的时序数据库存储&#xff0c;可以让单台 Prometheus 能够高效的处理大量的数据&#xff0c;还有友好并且强大的 PromQL 语法&#xff0c;可以用来灵活的查询各种监控数据以及配置…

使用 chat_flutter 进行聊天记录展示

前言 最近需要实现一个聊天记录的页面展示&#xff0c;在网上发现没有适合自己的&#xff0c;于是自己就造了一个&#xff0c;总体感觉还不赖。 下面奉上地址、效果图和教程。 效果图 地址 github: https://github.com/xiaorui-23/chat_fluttergitee: https://gitee.com/xi…