二维数组的遍历(详解两种不同遍历方式的好坏)

ini 复制代码
int array[][] = {
	{11, 12, 13, 14, 15},
	{21, 22, 23, 24, 25},
	{31, 32, 33, 34, 35}
};

局部性原理

这里只讨论空间局部性

  • CPU 读取内存 (速度慢) 数据后,会将其放入高速缓存 (速度快) 当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了
  • 缓存的最小存储单位是缓存行 (cacheline),一般是 64 bytes,一次读取的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其邻近的数据,这就是所谓空间局部性

下面代码中StopWatch是spring-core-5.2.3.RELEASE.jar 导入即可使用

ini 复制代码
public class TestCacheLine
{
    public static void ij(int a[][], int rows, int columns)
    {
        long sum = 0L;
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < columns; j++)
            {
                sum += a[i][j];
            }
        }
        System.out.println(sum);
    }

    public static void ji(int a[][], int rows, int columns)
    {
        long sum = 0L;
        for (int i = 0; i < columns; i++)
        {
            for (int j = 0; j < rows; j++)
            {
                sum += a[j][i];
            }
        }
        System.out.println(sum);
    }

    /*
    CPU 缓存 内存
    皮秒     毫秒
       64字节
       缓存行  cache line
       空间局部性
     */

    public static void main(String[] args)
    {
        int rows = 1_000_000;
        int columns = 14;
        int a[][] = new int[rows][columns];
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("ij");
        ij(a, rows, columns);
        stopWatch.stop();

        stopWatch.start("ji");
        ji(a, rows, columns);
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }
}

打印结果:

markdown 复制代码
0
0
StopWatch '': running time = 192431100 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
041449500  022%  ij
150981600  078%  ji

为什么两者之间有这么大的差异?

我们分析一下如下的代码

ini 复制代码
public static void ij(int a[][], int rows, int columns)
    {
        long sum = 0L;
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < columns; j++)
            {
                sum += a[i][j];
            }
        }
        System.out.println(sum);
    }

当循环a[0][0]时,它除了读取0,0索引的元素外,还会将邻近的(后续)元素也顺便读取进来(局部性原理) 凑读 64 个字节给它填满一个缓存行。所以不光读了0,0 就连 0,1到0,13也都读取进来了。

当读取0,1元素是也就是a[0][1]时,就不需要当内存中找了,因为已经缓存起来了所以直接找缓存就行了速度就比较快了

蓝色背景代表需要找内存,灰色背景代表不需要找内存直接找缓存,可以充分利用缓存来提高效率

直到最后的0,13,都直接找缓存中读取数据就可以了,速度很快

0~n的这一轮就循环读取完毕了,该下一行了就是 1,0了这时 缓存中是没有的就需要再到内存中读取了,同理读取一次后就会将邻近的元素都加载到缓存中去

总结

我们发现,外层循环数组行,内层循环数组列 就能充分利用提升读取效率

下面再分析速度比较慢的情况,就是外层循环数组列,内层循环数组行

ini 复制代码
public static void ji(int a[][], int rows, int columns)
    {
        long sum = 0L;
        for (int i = 0; i < columns; i++)
        {
            for (int j = 0; j < rows; j++)
            {
                sum += a[j][i];
            }
        }
        System.out.println(sum);
    }

第一次将0,0的元素和邻近的元素到0,13的所有元素都读取到缓存了

但是第二次读取的是1,0因为i不变j变1 a[j][i] 也就是a[1][0]。这时缓存中并没有1,0 又要去读取内存了

第三次读取3,0。同样的缓存中没有需要找内存读取加载到缓存中。但是缓存是有限的,而缓存中的0,1 1,1 2,1 都没有用上 ,数据没用上接下来超过了缓存的最大值后它就会把旧的数据被新的数据覆盖掉

假设我们缓存最大值是7行数据,而我们需要遍历10行数据。此时超过缓存最大之后旧的数据就会被新的数据覆盖掉如下所示:

总结

我们发现,外层循环数组列,内层循环数组行 会浪费掉每行缓存的数据 而 旧数据会被新数据覆盖因为缓存有限 这使得效率很低

相关推荐
计算机学姐10 分钟前
基于SpringBoot的咖啡店管理系统【个性化推荐+数据可视化统计+配送信息】
java·vue.js·spring boot·后端·mysql·信息可视化·tomcat
My的梦想已实现20 分钟前
关于JAVA Springboot集成支付后打包JAR之后报安全错误的处理
java·spring boot·jar
ooseabiscuit35 分钟前
SpringBoot3整合FastJSON2如何配置configureMessageConverters
java
ok_hahaha44 分钟前
java从头开始-黑马点评-Redission
java·开发语言
无巧不成书02181 小时前
Java面向对象零基础实战:从Employee类吃透自定义类核心,掌握封装精髓
java·开发语言·java入门·面向对象·自定义类·employee类·java核心技术
小江的记录本1 小时前
【注解】常见 Java 注解系统性知识体系总结(附《全方位对比表》+ 思维导图)
java·前端·spring boot·后端·spring·mybatis·web
跃上青空1 小时前
Java如何优雅的使用fastjson2进行枚举序列化/反序列化,欢迎探讨
java·开发语言
Mr.45671 小时前
Spring Boot 集成 PostgreSQL 表级备份与恢复实战
java·spring boot·后端·postgresql
架构师沉默1 小时前
为什么一个视频能让全国人民同时秒开?
java·后端·架构
生命不息战斗不止(王子晗)2 小时前
mysql基础语法面试题
java·数据库·mysql