内外大小循环耗时分析
一、概述
在实际开发中经常会碰到需要写双层 for 循环的场景,那么这个时候就有一个问题了,在不影响结果的前提下,双层循环是大循环写在外面好还是小循环写在外面好呢,针对这个问题我们来简单的探究一下
二、案例代码
代码 1、双层循环时把小循环写在外面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | @Slf4j public class Demo { // 测试次数 private static final int test_number = 100 ; // 小循环次数 100 private static final int SMALL_CIRCLE = 10 ; // 大循环次数 100000000 private static final int BIG_CIRCLE = 1000 * 1000 ; // 数组元素的初始化值 private static final int INITIALIZE_NUMBER = 2 ; // 统计总数 private static int sum = 0 ; public static void main(String[] args) { // 定义一个二维数组并为每个元素设置初始化值 2 int [][] twoDimensional = new int [BIG_CIRCLE][SMALL_CIRCLE]; initial(twoDimensional); long startTime = System.currentTimeMillis(); for ( int n = 0 ; n < test_number; n++) { // 小循环写在外面 for ( int j = 0 ; j < SMALL_CIRCLE; j++) { for ( int i = 0 ; i < BIG_CIRCLE; i++) { sum += twoDimensional[i][j]; } } } long endTime = System.currentTimeMillis(); log.info( "sum: {}" , sum); log.info( "总耗时: {} 毫秒" , endTime - startTime); } public static void initial( int [][] twoDimensional) { for ( int i = 0 ; i < BIG_CIRCLE; i++) { for ( int j = 0 ; j < SMALL_CIRCLE; j++) { twoDimensional[i][j] = INITIALIZE_NUMBER; } } } } |
5 次抽样统计耗时
代码 2、改变一下循环遍历的顺序,把大循环写在外面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | @Slf4j public class Demo { // 测试次数 private static final int test_number = 100 ; // 小循环次数 100 private static final int SMALL_CIRCLE = 10 ; // 大循环次数 100000000 private static final int BIG_CIRCLE = 1000 * 1000 ; // 数组元素的初始化值 private static final int INITIALIZE_NUMBER = 2 ; // 统计总数 private static int sum = 0 ; public static void main(String[] args) { // 定义一个二维数组并为每个元素设置初始化值 2 int [][] twoDimensional = new int [BIG_CIRCLE][SMALL_CIRCLE]; initial(twoDimensional); long startTime = System.currentTimeMillis(); for ( int n = 0 ; n < test_number; n++) { // 大循环写在外面 for ( int i = 0 ; i < BIG_CIRCLE; i++) { for ( int j = 0 ; j < SMALL_CIRCLE; j++) { sum += twoDimensional[i][j]; } } } long endTime = System.currentTimeMillis(); log.info( "sum: {}" , sum); log.info( "总耗时: {} 毫秒" , endTime - startTime); } public static void initial( int [][] twoDimensional) { for ( int i = 0 ; i < BIG_CIRCLE; i++) { for ( int j = 0 ; j < SMALL_CIRCLE; j++) { twoDimensional[i][j] = INITIALIZE_NUMBER; } } } } |
5 次抽样统计耗时
结论: 从上面多次的统计结果可以看出,大循环写在外面耗时要远小于小循环写在外面,大循环写在外面的性能更好
三、原因分析
现代计算机组成结构
CPU 需要频繁的与内存进行交互以便存取数据,但是 CPU 的性能是呈指数增长的,而内存的性能却是线性增长的,随着时间的推移这便会带来一个问题,那就是 CPU 和内存的速度差距越来越大,高速的 CPU 大量的时间都在等待 IO,严重影响了计算机的性能
虽然可以通过双端口/多模块的设计方式来提升主存的存取速度,但是主存的速度还是远远跟不上 CPU 的速度(速度差值为 10^3 左右)
后来在编程过程中,发现数据与指令的访问有局部性原理,关于局部性原理的概述如下
时间局部性:在最近的未来要用到的信息,很可能是现在正在使用的信息
空间局部性:在最近的未来要用到的信息(指令和数据),很可能与现在正在使用的信息在存储空间上是邻近的
有了上述的场景,为了解决 CPU 和主存的速度差异性,才引入了缓存,缓存使用特殊的硬件电路设计,使得它的速度与 CPU 差距不大,并且与主存的交互可以以块为单位
引入缓存后计算机的结构
一台计算机一般都有多个 CPU,每个 CPU 上存在多个核心,每个核心有自己独占的 L1、L2 缓存,同一个 CPU 内公用同一个 L3 缓存,需要注意的是不同 CPU 之间的 L3 是不能共用的
为了解释上述内外循环的问题,我这里简单的画了一张缓存和主存数据存储的关系图,实际上一般的缓存有 8 个缓存行,每个缓存行的大小为 64B,主存的一个存储单元也不止存放一个数组元素,但是为了更简单的描述问题,主存、缓存的图都做了简化
假设主存和缓存采用全相连映射进行交互
有了上图再看一下代码,对于大循环写在外面的情况
1 2 3 4 5 6 | // 大循环写在外面 for ( int i = 0 ; i < BIG_CIRCLE; i++) { for ( int j = 0 ; j < SMALL_CIRCLE; j++) { sum += twoDimensional[i][j]; } } |
当 i = 0 时,每一个缓存行数据都为空,需要从主存中将数据读入缓存中
当 i = 1 时,每个缓存行中已经存在数据了,直接使用缓存,不需要再从主存中读入缓存了
...........
当 i = 999999 时,每个缓存行中已经存在数据了,直接使用缓存,不需要再从主存中读入缓存了
可以看出每个循环只需要从主存中读取一次数据进入缓存,随后 CPU 与缓存进行数据交互即可,缓存命中概率极高
小循环写在外面
1 2 3 4 5 6 | // 小循环写在外面 for ( int j = 0 ; j < SMALL_CIRCLE; j++) { for ( int i = 0 ; i < BIG_CIRCLE; i++) { sum += twoDimensional[i][j]; } } |
当 j = 0 时,每一个缓存行数据都为空,缓存行大小为 10,第一次将 arr[0][0]、arr[1][0]、arr[2][0]、arr[3][0]、arr[4][0]、arr[5][0]、arr[6][0]、arr[7][0]、arr[8][0]、arr[9][0] 10 个元素从主存读入缓存,紧接着需要使用 arr[10][0]、arr[11][0]、arr[12][0]、arr[13][0]、arr[14][0]、arr[15][0]、arr[16][0]、arr[17][0]、arr[18][0]、arr[19][0] 十个元素,但是这 10 个元素在缓存中不存在,那么又要重复去主存将这 10 个元素覆盖掉之前的 10 个元素,依次类推,缓存命中的次数极低
四、总结
对于双层循环,需要把大循环写在外面,因为根据局部性原理,这样写命中缓存的概率会大一些
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?