发布于2026-07-08 阅读(0)
扫一扫,手机访问
在 Ja va 里用数组做矩阵转置,关键是要吃透二维数组在内存里“按行优先”(row-major)的存储方式。如果直接逐列读取再逐行写入,内存访问就会变得断断续续,缓存命中率掉得厉害。优化的核心道理其实很简单:让读写操作尽量都顺着连续内存地址走,保持空间局部性。

先说说最基础的索引映射。对于一个 m × n 的矩阵 matrix[m][n],转置后的结果是一个 n × m 的 transposed[n][m],规则就是:原第 i 行第 j 列的元素,搬到新矩阵第 j 行第 i 列,即 transposed[j][i] = matrix[i][j]。注意,如果矩阵不是方阵,转置后行列数互换,没法原地操作,必须新建一个数组。
不少同学第一次写转置,会写出下面这种“一目了然”的版本:
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
transposed[j][i] = matrix[i][j]; // 每次读 matrix[i][j] 是跨行跳转
}
}
这段代码看着简单,但性能其实是个坑。问题出在内存访问模式上:matrix[i][j] 在内存里是按行连续存放的(matrix[0][0], matrix[0][1], ..., matrix[0][n-1], matrix[1][0], ...)。内层循环里 i 固定、j 变化,读取倒是连续的;可一旦外层 i 切换,下一次访问 matrix[i][0] 就要跳过一整行(n 个元素),导致大量缓存未命中。用专业话说,就是“跨步访问”破坏了空间局部性。
要解决这个问题,一个很有效的办法是“分块”。把矩阵切成一个个小块(比如 16×16),在每个小块内部完成局部转置,这样读写操作基本上都能落在缓存行(通常 64 字节)覆盖的范围内。为什么要选 16×16?因为 Ja va 里 int 占 4 字节,16×16 的小块总共 1024 字节,大多数 CPU 的 L1 或 L2 缓存都能轻松装下。
具体实现时,对每个块 [i..i+BS)[j..j+BS),先读入临时块,再转置写入目标位置。这样读和写的内存访问都是连续的,步长也很可控:
final int BLOCK_SIZE = 16;
for (int ii = 0; ii < m; ii += BLOCK_SIZE) {
for (int jj = 0; jj < n; jj += BLOCK_SIZE) {
int iEnd = Math.min(ii + BLOCK_SIZE, m);
int jEnd = Math.min(jj + BLOCK_SIZE, n);
for (int i = ii; i < iEnd; i++) {
for (int j = jj; j < jEnd; j++) {
transposed[j][i] = matrix[i][j];
}
}
}
}
二维数组本质上是一个对象数组的引用,每个一维数组都是独立对象,访问时会有额外的间接寻址开销。如果能用一维数组来模拟二维,不仅能提升局部性,对 GC 也更友好。做法很简单:声明 int[] matrix = new int[m * n];,然后用 matrix[i * n + j] 表示原 matrix[i][j]。
转置写入时对应写成 transposed[j * m + i] = matrix[i * n + j];。配合块划分时,一维索引的计算更稳定,JIT 编译器也更容易做向量化优化(比如配合 -XX:+UseSuperWord 选项)。
另外,如果矩阵是方阵且允许原地操作,还可以用对角线交换法——沿着主对角线两两交换元素,这样不需要额外空间。不过要记住,即使原地操作,访存模式仍然建议分块处理,才能保证缓存友好。
总的来说,矩阵转置看起来简单,但一旦数据量大了,内存访问模式就成了性能瓶颈。理解行优先存储、善用分块技巧,再配合一维数组模拟,就能写出既清晰又高效的代码。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8