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

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行数据。此时超过缓存最大之后旧的数据就会被新的数据覆盖掉如下所示:

总结

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

相关推荐
像我这样帅的人丶你还15 小时前
Java 后端详解(四):分页与搜索
java·javascript·后端
她的男孩15 小时前
数据权限为什么不能只靠注解?Forge 的 Mapper 层 SQL 改写源码拆解
java·后端·架构
tntxia16 小时前
Mybatis的日志输入
java
亦暖筑序18 小时前
Java 8老系统Browser Agent实战:三层拦截把AI操作后台变成可审计流程
java·后端·设计模式
用户2986985301421 小时前
Java 实现 Word 文档加密与权限解除
java·后端
Yeats_Liao21 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构
未秃头的程序猿21 小时前
告别"if-else地狱"!Java 21模式匹配,代码优雅了10倍
java·后端·面试
鹤望兰6751 天前
字节跳动国际支付-后端开发-三面面经
java
Flittly1 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
RainCity1 天前
Java Swing 自定义组件库分享(十二)
java·笔记·后端