打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
这代码居然有差别?CPU友好的代码该这样写

本文用实际用例阐述了用心组织的代码也能让性能提升百倍,我们不应该停留在CRUD的漩涡中。下面来看看这个神奇的现象。

作者 | 王再军(曦峰)

来源 | 阿里开发者公众号

一、震惊,这代码居然有差别!

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

我们可以先看下面的矩阵计算。

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

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

这里停顿两秒。

/...1

/...2

现在是公布答案的时间,下图是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的访问时间:

缓存速率表

类型

缓存什么

延迟(周期数)

CPU寄存器

4字节或8字节

0

L1高速缓存

64字节块

4

L2高速缓存

64字节块

10

L3高速缓存

64字节块

50

虚拟内存/缓冲区缓存(主存)

4KB页

200

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

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

其中:

Regs,是寄存器。

d-cache,是数据缓存。

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

L1、L2、L3和主存的缓存的内容,可以参考下图:

图片来自:
https://www.deskdecode.com/what-is-cpu-central-processing-unit-and-how-its-work/

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缓存的效果,所以性能最差。

剩余60%,完整内容请点击下方链接查看:

https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247532294&idx=1&sn=1d44896f3ee88814c7ba0b4e3ba3af47&chksm=e92a4209de5dcb1fbe99a5c037867e1c9be5e5584d7b6560087c464f8d00725e8a24dece590f&token=1334496393&lang=zh_CN#rd

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
如何利用CPU Cache写出高性能代码,看这些图就够了!
优化高性能JS代码的几个要点,以及背后的原理
面试官:如何写出让 CPU 跑得更快的代码?
a[i][j]与a[j][i]性能差别的原因
JavaScript进阶系列—数组遍历与属性
高并发系统设计与时间和空间的平衡
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服