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

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);
    }

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

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

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

直到最后的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 aji 也就是a10。这时缓存中并没有1,0 又要去读取内存了

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

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

总结

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

相关推荐
骄马之死4 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
郑洁文5 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
螺丝钉code6 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code
摇滚侠7 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown7 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
折哥的程序人生 · 物流技术专研8 小时前
Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?
java·设计模式·架构·原型模式·单一职责原则
装不满的克莱因瓶8 小时前
基于 OpenResty 扩展开发实现动态服务注册与发现能力
java·开发语言·架构·openresty
程序员小羊!8 小时前
06Java 异常机制与常用类
java
weixin_523185328 小时前
Java基础知识总结(四):引用数据类型与参数传递机制
java·开发语言·python
宸津-代码粉碎机9 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring