当前位置: 首页 > news >正文

数组存储 · 行主序与列主序 | 应用 / 基地址 / 选择策略

注:本文为 “数组存储 · 行主序与列主序” 相关合辑。
英文引文,机翻未校。
中文引文,略作重排。
未整理去重,如有内容异常,请看原文。


Row major and Column Major Address calculations

按行主序和按列主序的地址计算

Kolli Rohit Reddy
May 3, 2012

In this tutorial, we are going to learn about how to store elements in a two-dimensional array.
在本教程中,我们将学习如何在二维数组中存储元素。

Before starting storing elements of a multi-dimensional array, let’s say I’ve an one dimensional array having elements like this:
在开始存储多维数组的元素之前,假设我有一个一维数组,其元素如下:

A1, A2, A3….An

These elements are stored in my linear memory space as follows:
这些元素在内存空间中的存储方式如下:

A1 A2 A3 A4 ... An

One thing you need to remember is, no matter what type of array it is (1D or multi-dimensional), they will be always stored linearly in the memory space as above.
需要记住的一点是,无论数组是何种类型(一维或多维),它们始终会像上面那样线性地存储在内存空间中。

Let’s jump to the case of multi-dimensional arrays now. Imagine I’ve a 3x3 matrix like this:
现在我们来看多维数组的情况。假设我有一个 3×3 的矩阵,如下所示:

A11 A12 A13
A21 A22 A23
A31 A32 A33

The real problem of storing elements arises here, since elements have to be stored linearly in the memory space, we have many possible ways to store them. Here are some of the possibilities I could’ve had for the matrix above.
存储元素的真正问题就在这里,因为元素必须线性地存储在内存空间中,所以有多种可能的存储方式。以下是我对上述矩阵可能采用的一些存储方式。

1. A11 A13 A12 A21 A23 A22 A31 A33 A32 A33
2. A11 A22 A33 A12 A32 A13 A23 A31 A32 A21... 

and I could go on filling randomly depending on the no. of elements I’ve in my 2-D array.
我还可以根据二维数组中的元素数量随机填充。

Out of all these possible ways, there are two main ways of storing them, they are:
在所有这些可能的方式中,有两种主要的存储方式,分别是:

  • Row Major Ordering
    按行主序存储

  • Column Major Ordering
    按列主序存储

1. Row Major method

1. 按行主序存储方法

In this method, the elements of an array are filled up row-by-row such that the first row elements are stored first, then the second row and so on. Most of the high level programming languages like C/C++, Java, Pascal, etc use this method for storing multidimensional arrays.
在该方法中,数组的元素按行填充,即先存储第一行的元素,然后是第二行的元素,依此类推。大多数高级编程语言,如 C/C++、Java、Pascal 等,都使用这种方法来存储多维数组。

Offset for row major ordering

按行主序的偏移量

As you can see in the 3x3 Matrix array we have above, I want to know at what position is my element A12 located in the linear memory space. This position is called the OFFSET.
正如我们在上面的 3×3 矩阵数组中看到的,我想知道元素 A12 在线性内存空间中的位置。这个位置被称为偏移量(OFFSET)。

One way to do it is to make up a linear diagram like the one below, count manually using 0 as the starting index and go on till the element I need. But imagine you are given an array like A[20][40], definitely you can’t go over all these elements if you wanted to know, say where A[15][20] is located.
一种方法是制作一个如下面的线性图表,从 0 开始手动计数,直到我需要的元素。但是想象一下,如果你有一个像 A[20][40] 这样的数组,如果你想知道 A[15][20] 的位置,你肯定不能逐个查看所有这些元素。

在这里插入图片描述

Elements occupying the Linear memory space of 3x3 matrix in row major ordering
按行主序存储的 3×3 矩阵元素占据的线性内存空间

For this, we resort to a fairly easy mathematical method of calculating the offset.
Here is the formula to calculate the offset for row major ordering
为此,我们采用一种相对简单的数学方法来计算偏移量。
以下是按行主序计算偏移量的公式:

2. Column Major method

2. 按列主序存储方法

In Column Major ordering, all the elements of the first column are stored first, then the next column elements and so on till we are left with no columns in our 2-D array.
在按列主序存储中,先存储第一列的所有元素,然后是下一列的元素,依此类推,直到二维数组中没有列为止。

在这里插入图片描述

Elements occupying the lineary memory space in column major ordering
按列主序存储的元素占据的线性内存空间

Offset for column major method

按列主序的偏移量

Calculating address of a particular element

计算特定元素的地址

Depending on the ordering method the elements are stored in the memory, we will get different positions of an element in the linear memory and consequently a different address for each method. To calculate the address we use the following procedure:
根据元素在内存中的存储顺序,元素在线性内存中的位置会有所不同,因此每种方法的地址也会不同。我们使用以下步骤来计算地址:

Step 1: Get the offset value of the element under consideration, make sure you use the correct forumula.
步骤 1:获取所考虑元素的偏移量,确保使用正确的公式。

Step 2: Multiply the offset with the size of the element’s datatype (Like int is of 4 bytes for 32-bit, Earlier compilers like TurboC worked on 16-bit platform and for them size of int is 2 bytes).
步骤 2:将偏移量乘以元素数据类型的大小(例如,对于 32 位系统,int 类型为 4 字节;早期的编译器,如 TurboC,在 16 位平台上工作,对于它们,int 类型的大小为 2 字节)。

Step 3: Add this to the base address to get the final address
步骤 3:将此值加到基地址上,得到最终地址。

Cbse
Published in L’arome
·Last published Nov 6, 2016

Written by Kolli Rohit Reddy
Not the typical 9 to 6 Guy …


多维数组存储的两种方式

竹影半墙 原创于 2013-11-08 12:47:30 发布

1. 数组存储的要求

数组存储的核心要求:连续存储

  • 连续:数组的 nnn 个元素对应 nnn(或 n+1n+1n+1)个内存地址,且任意两个相邻元素的地址在内存中是连续的。
  • 相邻元素的定义
    • 对于一维数组,相邻元素的判定无歧义,即下标绝对值差为 1 的两个元素(如 a[0]a[0]a[0]a[1]a[1]a[1]a[2]a[2]a[2]a[3]a[3]a[3])。
    • 对于二维及以上数组,需通过“下标权重”判定:若将最左(或最右)下标视为“个位”、次左(或次右)下标视为“十位”、更左侧(或更右侧)下标视为更高位,则相邻元素对应“下标组合所构成的数字绝对值差为 1”的两个元素。

需明确:内存空间本身是连续的线性结构,不存在“矩阵”“立方体”等具象化形态。数组存储时,只需记录其基地址(首个元素的内存地址),后续元素的存储顺序由编程语言规范或编译器底层逻辑决定——即一旦确定开发平台(如特定语言、编译器版本),数组的存储方式便已固定。

对应用开发者而言,直接记录数组存储方式的场景较少(除非开发编译器中负责数组存储的模块),但探究该过程可锻炼逻辑思维,积累“将复杂结构转化为线性存储”的方法经验。

2. 数组存储的方式

数组既是构造型数据类型,也是一种基础数据结构,其存储方式需满足“连续存储”的核心要求。对于一维、二维数组,教材中常以“行优先”“列优先”描述存储方式,因二者可对应简单的具象模型(如二维数组对应二维矩阵),分析过程较直观,此处不再展开笔记。

(1)多维数组左下标优先的存储方式(n>2n > 2n>2

“左下标优先存储”的逻辑与教材中二维数组的“行优先存储”一致。需说明:对 n>2n>2n>2 的多维数组,“行”“列”的具象概念已失效,更恰当的理解是将其抽象为“多层嵌套的低维数组”,再按左下标优先级依次存储。

存储过程

以多维数组 a[s1][s2]…[sn]a[s_1][s_2] \ldots [s_n]a[s1][s2][sn]n>2n > 2n>2)为例,左下标优先的存储过程遵循“从左到右、逐层拆解”的规则,具体如下:

  1. 先顺序存储最左侧下标对应的 (n−1)(n-1)(n1) 维数组:即先存 a[0]a[0]a[0](一个 (n−1)(n-1)(n1) 维数组)、再存 a[1]a[1]a[1]、……、最后存 a[s1−1]a[s_1-1]a[s11],直至最左侧下标 s1s_1s1 遍历完毕。
  2. 存储每个 (n−1)(n-1)(n1) 维数组时,仍按左下标优先规则拆解为 (n−2)(n-2)(n2) 维数组:例如存储 a[i]a[i]a[i]0≤i<s10 \leq i < s_10i<s1)时,需先存 a[i][0]a[i][0]a[i][0](一个 (n−2)(n-2)(n2) 维数组)、再存 a[i][1]a[i][1]a[i][1]、……、最后存 a[i][s2−1]a[i][s_2-1]a[i][s21],直至次左侧下标 s2s_2s2 遍历完毕。
  3. 重复上述拆解逻辑,直至拆解为一维数组:存储任意一个一维数组时,按最右侧下标的顺序遍历,即 a[i][i2]…[in−1][0]a[i][i_2] \ldots [i_{n-1}][0]a[i][i2][in1][0]a[i][i2]…[in−1][1]a[i][i_2] \ldots [i_{n-1}][1]a[i][i2][in1][1]、……、a[i][i2]…[in−1][sn−1]a[i][i_2] \ldots [i_{n-1}][s_n-1]a[i][i2][in1][sn1](其中 0≤i<s10 \leq i < s_10i<s10≤i2<s20 \leq i_2 < s_20i2<s2,……,0≤in−1<sn−10 \leq i_{n-1} < s_{n-1}0in1<sn1)。
  4. 当所有维度的下标均遍历完毕,整个 nnn 维数组存储完成。
采用的数学逻辑

多维数组在左下标优先方式下的存储,本质遵循如下数学规则:在对第 m+1m+1m+1 个下标 imi_mimimi_mim 递增 1)进行计数前,需先将其右侧所有下标(从 im+1i_{m+1}im+1sm+1s_{m+1}sm+1)完全遍历(计满)(其中 0≤m<n−10 \leq m < n-10m<n1imi_mim 代表数组的第 m+1m+1m+1 个下标值,sm+1s_{m+1}sm+1 代表该下标的最大取值范围)。

计算 a[i1][i2]…[in]a[i_1][i_2] \ldots [i_n]a[i1][i2][in] 的地址

教材中频繁涉及“多维数组元素地址计算”,其核心逻辑是:求目标元素 a[i1][i2]…[in]a[i_1][i_2] \ldots [i_n]a[i1][i2][in] 之前的元素总数——即统计在 [i1][i2]…[in][i_1][i_2] \ldots [i_n][i1][i2][in] 这组下标之前,已存储的元素个数(再结合“基地址 + 单个元素字节数 × 前序元素总数”,即可得到目标元素地址)。

分析过程
  1. 下标 iki_kik 的权重:对于第 kkk 个下标 iki_kik,其每增加 1,需跳过“右侧所有维度的元素总数”,即 iki_kik 对应的元素数量为 ik×(Sk+1×Sk+2×…×Sn)i_k \times (S_{k+1} \times S_{k+2} \times \ldots \times S_n)ik×(Sk+1×Sk+2××Sn)(其中 StS_tSt 代表第 ttt 个维度的长度,即 sts_tst)。
  2. 前序元素的累加逻辑:当下标 iki_kik 固定时,其右侧的下标 ik+1,ik+2,…,ini_{k+1}, i_{k+2}, \ldots, i_nik+1,ik+2,,in 仍需从 0 遍历至当前值,因此需将这些下标的对应元素数量依次累加。
  3. 下标 iki_kik 之前的元素总数:综合上述逻辑,下标 iki_kik 之前已存储的元素个数为:
    ik×Sk+1×Sk+2×…×Sn+ik+1×Sk+2×…×Sn+…+in−1×Sn+ini_k \times S_{k+1} \times S_{k+2} \times \ldots \times S_n + i_{k+1} \times S_{k+2} \times \ldots \times S_n + \ldots + i_{n-1} \times S_n + i_n ik×Sk+1×Sk+2××Sn+ik+1×Sk+2××Sn++in1×Sn+in
  4. 目标元素 a[i1][i2]…[in]a[i_1][i_2] \ldots [i_n]a[i1][i2][in] 之前的总元素数:将上述逻辑推广到完整下标,目标元素前的总元素数公式为:
    i1×S2×S3×…×Sn+i2×S3×…×Sn+…+in−1×Sn+in=∑j=1n(ij×∏i=j+1nSi)i_1 \times S_2 \times S_3 \times \ldots \times S_n + i_2 \times S_3 \times \ldots \times S_n + \ldots + i_{n-1} \times S_n + i_n = \sum_{j=1}^{n} \left( i_j \times \prod_{i=j+1}^{n} S_i \right) i1×S2×S3××Sn+i2×S3××Sn++in1×Sn+in=j=1n(ij×i=j+1nSi)

若难以通过空间想象理解多维数组的“下标-存储”关系,可通过上述数学公式直接推导。需强调:数组存储本身仅依赖该数学规则,不存在天然的“行”“列”概念;“行-列对应”是为解决具象数学问题(如矩阵运算)而定义的辅助逻辑——例如用二维数组存储矩阵时,可将矩阵的“行号”“列号”对应到二维数组的两个下标,从而通过下标分析矩阵运算规则。

(2)多维数组右下标优先的存储方式(n>2n > 2n>2

与左下标优先相反,右下标优先存储的核心数学规则为:在对第 m+1m+1m+1 个下标 imi_mimimi_mim 递增 1)进行计数前,需先将其左侧所有下标(从 im−1i_{m-1}im1sm−1s_{m-1}sm1)完全遍历(计满)(其中 0<m≤n−10 < m \leq n-10<mn1imi_mim 代表数组的第 m+1m+1m+1 个下标值,sm−1s_{m-1}sm1 代表左侧下标的最大取值范围)。

计算 a[i1][i2]…[in]a[i_1][i_2] \ldots [i_n]a[i1][i2][in] 前的元素个数

遵循“右侧下标权重更高”的逻辑,目标元素 a[i1][i2]…[in]a[i_1][i_2] \ldots [i_n]a[i1][i2][in] 之前的总元素数公式为:
in×Sn−1×Sn−2×…×S1+in−1×Sn−2×…×S1+…+i2×S1+i1=∑j=1n(ij×∏i=1j−1Si)i_n \times S_{n-1} \times S_{n-2} \times \ldots \times S_1 + i_{n-1} \times S_{n-2} \times \ldots \times S_1 + \ldots + i_2 \times S_1 + i_1 = \sum_{j=1}^{n} \left( i_j \times \prod_{i=1}^{j-1} S_i \right) in×Sn1×Sn2××S1+in1×Sn2××S1++i2×S1+i1=j=1n(ij×i=1j1Si)
(注:当 j=1j=1j=1 时,乘积项 ∏i=10Si\prod_{i=1}^{0} S_ii=10Si 为空乘积,按数学惯例取值为 1,此时该项简化为 i1×1=i1i_1 \times 1 = i_1i1×1=i1,与公式左侧首尾项一致。)

3. 总结

为满足“连续存储”的核心要求,多维数组的存储方式本质仅两种,其差异体现在“下标优先级”:

  • 方式一:从最左侧下标开始,按“左→右”的顺序依次遍历存储(左下标优先);
  • 方式二:从最右侧下标开始,按“右→左”的顺序依次遍历存储(右下标优先)。

需重点注意:

  1. “行”“列”是二维数组的专属具象描述,对 n>2n>2n>2 的多维数组不适用;
  2. “左下标/右下标优先”是更通用的规则,无需依赖空间想象,可通过统一的数学公式(前序元素个数计算)推导存储逻辑,且该公式可直接用于元素地址的计算。

Row- and Column- major order(行优先和列优先顺序)

原创于 2018-04-28 11:18:33 发布

行主序与列主序存储示意图

1. 概念的核心定义与维度推广

行优先(Row-major order)和列优先(Column-major order)的术语虽起源于二维数组(矩阵)的“行”与“列”存储逻辑,但并非局限于二维场景,可自然推广到 任意维度的数组,核心差异体现在“索引变化的快慢顺序”上:

1.1 行优先(Row-major order)的维度规则

在行优先存储中,数组各维度的索引变化存在明确的“快慢层级”:

  • 核心逻辑:沿着数组 最后一个轴(维度)的索引变化最快,沿着 第一个轴(维度)的索引变化最慢

  • 二维场景验证:以二维数组 a[行][列](第一轴为“行”,第二轴为“列”)为例,行索引(第一轴)变化最慢(需先存完一行所有元素),列索引(第二轴,最后一个轴)变化最快(同一行内依次存不同列元素),即存储顺序为 a[0][0]→a[0][1]→a[0][2]→…→a[1][0]→a[1][1]→…,与“先存行、再换行”的直觉一致。

  • 三维场景推广:以三维数组 a[深度][行][列](第一轴为“深度”,第二轴为“行”,第三轴为“列”)为例,索引变化速度从慢到快依次为:深度索引(第一轴)→ 行索引(第二轴)→ 列索引(第三轴,最后一个轴)。
    存储顺序为 a[0][0][0]→a[0][0][1]→a[0][0][2]→…→a[0][1][0]→a[0][1][1]→…→a[0][2][0]→…→a[1][0][0]→…,即“先存完一个深度下的所有行与列,再切换到下一个深度”。

1.2 列优先(Column-major order)的维度规则

列优先存储的索引变化顺序与行优先完全相反:

  • 核心逻辑:沿着数组 第一个轴(维度)的索引变化最快,沿着 最后一个轴(维度)的索引变化最慢

  • 二维场景验证:仍以二维数组 a[行][列] 为例,行索引(第一轴)变化最快(需先存完一列所有行元素),列索引(第二轴,最后一个轴)变化最慢(同一列内依次存不同行元素),存储顺序为 a[0][0]→a[1][0]→a[2][0]→…→a[0][1]→a[1][1]→…,即“先存完一列、再换列”。

  • 三维场景推广:以三维数组 a[深度][行][列] 为例,索引变化速度从慢到快依次为:列索引(第三轴,最后一个轴)→ 行索引(第二轴)→ 深度索引(第一轴)。
    存储顺序为 a[0][0][0]→a[1][0][0]→a[2][0][0]→…→a[0][1][0]→a[1][1][0]→…→a[0][0][1]→a[1][0][1]→…,即“先存完一个列下的所有深度与行,再切换到下一个列”。

2. 主流编程语言的存储顺序差异

支持多维数组的编程语言或其标准库,会根据设计目标(如通用开发、科学计算)选择默认的存储顺序,这一选择直接影响数组的访问效率与代码逻辑:

存储顺序代表编程语言/工具适用场景说明
行优先(Row-major)C / C++(C风格数组)、Java、Python(列表嵌套)、JavaScript、Go适用于通用软件开发场景,符合人类“从左到右、从上到下”的读写习惯,按行遍历数组时能最大化利用CPU缓存,减少缓存未命中
列优先(Column-major)Fortran、MATLAB、R、Julia(默认)适用于科学计算、数值分析、统计分析场景,这类场景常需频繁操作数组的“列向量”(如矩阵运算、特征工程),列优先存储可让列内元素连续,提升计算效率

注意:部分编程语言支持灵活切换存储顺序,例如 Julia 可通过关键字指定数组为行优先(Array{Type, N, RowMajor})或列优先(默认);Python 的 numpy 库也允许通过 order='C'(C 风格,行优先)或 order='F'(Fortran 风格,列优先)创建数组,以适配不同计算需求。


行主序、列主序概念辨析以及基地址的含义

天射手座 原创于 2019-01-19 23:05:18 发布

1. 行主序(Row-Major Order)

在数组中,数据按照 a[0][0]a[0][1]a[0][2]a[1][0]a[1][1]a[1][2]…的顺序依次存储。
这种存储方式的核心是“先存完一行,再存下一行”,即优先填充同一行内的所有元素,行索引变化滞后于列索引变化。
例如,对于一个 2×3 的二维数组 a[2][3],行主序的存储顺序为:
a[0][0] → a[0][1] → a[0][2] → a[1][0] → a[1][1] → a[1][2]
常见于 C、C++、Java 等主流编程语言。

2. 列主序(Column-Major Order)

在数组中,数据按照 a[0][0]a[1][0]a[2][0]a[0][1]a[1][1]a[2][1]…的顺序依次存储。
这种存储方式的核心是“先存完一列,再存下一列”,即优先填充同一列内的所有元素,列索引变化滞后于行索引变化。
例如,对于同样的 2×3 二维数组 a[2][3],列主序的存储顺序为:
a[0][0] → a[1][0] → a[0][1] → a[1][1] → a[0][2] → a[1][2]
常见于 Fortran、MATLAB 等编程语言。

3. 基地址(Base Address)

基地址指的是数组 首元素的内存地址,也就是数组在计算机内存中的起始存储地址。
在内存中,数组元素按连续地址依次排列,基地址是计算数组中任意元素内存地址的“基准点”。
以行主序存储的二维数组 a[m][n] 为例,若基地址为 base,每个元素占用 size 字节内存,则元素 a[i][j] 的内存地址计算公式为:
地址 = base + (i × n + j) × size
(其中 i 为行索引,j 为列索引,m 为总行数,n 为总列数)


矩阵中“行优先”和“列优先”

paridas101 原创于 2020-01-29 21:26:17 发布

1. 行优先和列优先

顾名思义,在表示矩阵时,“行优先”以“行”为核心组织逻辑,“列优先”以“列”为核心组织逻辑,二者的核心差异体现在矩阵维度的解读与数据存储的对应关系上:

  • 维度表示差异:对于一个 m × n 的矩阵(数学中标准记法为“mn 列”):

  • 行优先:直接对应“mn 列”的物理存储,即先存储完第一行的所有元素,再存储第二行,以此类推;

  • 列优先:需将维度解读为“mn 行”的物理存储,即先存储完第一列的所有元素,再存储第二列,以此类推。

  • 平移矩阵实例(三维向量平移)
    若三维空间中的向量 p 分别沿 xyz 方向平移 abc 距离,其行优先与列优先的平移矩阵(齐次坐标下,4×4 矩阵)存在明确区别,且二者互为转置

  • 行优先平移矩阵:
    [100001000010abc1]\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ a & b & c & 1 \end{bmatrix}100a010b001c0001

  • 列优先平移矩阵:

[100a010b001c0001]\begin{bmatrix} 1 & 0 & 0 & a \\ 0 & 1 & 0 & b \\ 0 & 0 & 1 & c \\ 0 & 0 & 0 & 1 \end{bmatrix}100001000010abc1

2. 行优先与列优先:矩阵与向量的乘法形式

在通过“矩阵 × 向量”实现变换时,行优先与列优先的乘法顺序(左乘/右乘)完全不同,需匹配向量的表示形式(行向量/列向量):

2.1 行优先:行向量右乘矩阵

行优先场景下,向量需表示为行向量(1×4 矩阵),且需放在矩阵左侧,通过“行向量右乘矩阵”完成变换。
以向量 p(x, y, z, 1)(齐次坐标,行向量形式为 (x, y, z, 1))的平移为例:

(x,y,z,1)×[100001000010abc1]=(x+a,y+b,z+c,1)(x, y, z, 1) \times \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ a & b & c & 1 \end{bmatrix} = (x+a, y+b, z+c, 1)(x,y,z,1)×100a010b001c0001=(x+a,y+b,z+c,1)

(注:原文中行向量最后一位为“0”,修正为“1”,齐次坐标下点向量的最后一位应为“1”,确保平移计算正确)

2.2 列优先:列向量左乘矩阵

列优先场景下,向量需表示为列向量(4×1 矩阵),且需放在矩阵右侧,通过“矩阵左乘列向量”完成变换。
同样以向量 p(x, y, z, 1)(列向量形式为 \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix})的平移为例:

[100a010b001c0001]×(xyz1)=(x+ay+bz+c1)\begin{bmatrix} 1 & 0 & 0 & a \\ 0 & 1 & 0 & b \\ 0 & 0 & 1 & c \\ 0 & 0 & 0 & 1 \end{bmatrix} \times \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x+a \\ y+b \\ z+c \\ 1 \end{pmatrix}100001000010abc1×xyz1=x+ay+bz+c1

(注:原文中列向量最后一位为“0”,修正为“1”,原因同前,确保平移逻辑正确)

3. 行优先与列优先在 C++ 代码中的表示

在 C++ 中,二维数组(如 float mat[4][4])的物理存储默认遵循行主序,但这并不影响其同时承载“行优先”或“列优先”的矩阵逻辑——二者的代码写法完全一致,差异仅体现在“数组元素的逻辑解读”上:

3.1 代码形式与逻辑映射

以 4×4 平移矩阵为例,C++ 中统一表示为:

float mat[4][4] = {{1, 0, 0, 0}, // 第 0 行(数组索引从 0 开始,原文“第 1 行”修正为“第 0 行”){0, 1, 0, 0}, // 第 1 行{0, 0, 1, 0}, // 第 2 行{a, b, c, 1} // 第 3 行
};
  • 若按行优先解读:数组的每一行直接对应矩阵的每一行。例如 mat[3](第 3 行)对应行优先矩阵的平移分量行 (a, b, c, 1)
  • 若按列优先解读:数组的每一行对应矩阵的每一列。例如 mat[3](第 3 行)对应列优先矩阵的平移分量列 \begin{pmatrix} a \\ b \\ c \\ 1 \end{pmatrix}

3.2 核心结论

C++ 代码的数组存储形式(行主序)是“物理层面”的固定规则,而“行优先”“列优先”是“逻辑层面”的矩阵解读方式——二者不冲突,仅需在后续计算(如矩阵与向量相乘)时,匹配对应的乘法顺序(行向量右乘/列向量左乘)即可。

4. 行优先与列优先的典型应用:DirectX 与 OpenGL 的差异

在图形学领域,DirectX(DX)与 OpenGL 对矩阵的处理差异,本质是“行优先”与“列优先”的工程实践体现,核心区别体现在内存布局着色器处理逻辑的匹配上:

图形 APIC++ 内存布局(CPU 端)着色器(GPU 端)默认向量类型数据传递是否需转置核心原因
DirectX行优先(遵循 C++ 行主序)HLSL 默认使用列向量是(需转置)CPU 端行优先矩阵 → GPU 端列向量需左乘,因此需将行优先矩阵转置为列优先矩阵,确保变换逻辑正确
OpenGL列优先(需按列优先组织)GLSL 默认使用列向量否(无需转置)CPU 端直接按列优先组织矩阵 → 与 GPU 端列向量左乘逻辑完全匹配,无需额外转置操作

说明

  • DirectX 的“转置”并非改变 C++ 数组的存储,而是在传递给 HLSL 前,通过 API(如 XMMatrixTranspose)将行优先矩阵的逻辑结构转置为列优先,以适配 HLSL 的列向量计算;
  • OpenGL 要求 CPU 端直接按“列优先”逻辑组织数组(例如列优先矩阵的第一列存在数组的第一行),因此无需转置即可直接与 GLSL 的列向量匹配。

矩阵存储顺序:Row-Major 与 Column-Major 在 DirectX 中的应用与差异

在图形编程领域,矩阵作为描述坐标变换(如平移、旋转、缩放)的核心数据结构,其存储顺序(即内存中元素的排列方式)直接影响数据传输效率与 GPU 计算正确性。DirectX(简称 DX)作为主流图形开发接口,其矩阵存储与处理逻辑需结合 Row-Major(行主序)Column-Major(列主序) 的特性综合理解。本章将围绕 DirectX 环境下两种存储顺序的定义、实际应用差异及接口适配规则展开,为开发者提供清晰的理论与实践指引。

1. 核心概念:Row-Major 与 Column-Major 的定义

在深入 DirectX 具体逻辑前,需先明确两种存储顺序的本质区别——二者的核心差异在于矩阵元素在一维内存中的排列优先级,即优先按“行”存储还是按“列”存储。

以 3×3 矩阵 M=[a11a12a13a21a22a23a31a32a33]M = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{bmatrix}M=a11a21a31a12a22a32a13a23a33 为例:

  • Row-Major(行主序):优先按“行”连续存储,内存布局为 [a11,a12,a13,a21,a22,a23,a31,a32,a33][a_{11}, a_{12}, a_{13}, a_{21}, a_{22}, a_{23}, a_{31}, a_{32}, a_{33}][a11,a12,a13,a21,a22,a23,a31,a32,a33]。即先完整存储第 1 行,再存储第 2 行,以此类推。
  • Column-Major(列主序):优先按“列”连续存储,内存布局为 [a11,a21,a31,a12,a22,a32,a13,a23,a33][a_{11}, a_{21}, a_{31}, a_{12}, a_{22}, a_{32}, a_{13}, a_{23}, a_{33}][a11,a21,a31,a12,a22,a32,a13,a23,a33]。即先完整存储第 1 列,再存储第 2 列,以此类推。

需注意:存储顺序仅定义“内存排列方式”,不改变矩阵的数学意义(如矩阵的秩、逆矩阵等属性),但会直接影响 CPU 与 GPU 之间的数据交互逻辑——若存储顺序与计算端(如 GPU)的默认处理顺序不匹配,可能导致变换结果错误。

2. DirectX 中的矩阵存储与数学意义:Row-Major 存储,Column-Major 计算

DirectX 环境下的矩阵逻辑存在“存储形式”与“数学处理”的分离,这是开发者易混淆的核心点,需从两个维度拆解:

2.1 存储维度:DirectX 矩阵的内存布局为 Row-Major

根据 DirectX 官方设计规范及实践反馈(如 2008 年 Sherk 的总结),DirectX(尤其是早期的 D3DX 矩阵库)中,矩阵在 CPU 内存中的存储严格遵循 Row-Major 行主序。例如,通过 D3DX 矩阵构造函数(如 D3DXMatrixRotationY)生成的旋转矩阵、D3DXMatrixLookAtLH 生成的观察矩阵,其内存中元素均按“行优先”排列。

这一设计的核心目的是适配 CPU 端的缓存访问逻辑——CPU 缓存通常以“行”为单位预取数据,Row-Major 存储可减少缓存缺失,提升 CPU 对矩阵的计算(如矩阵乘法)效率。

2.2 数学维度:DirectX 矩阵的计算逻辑为 Column-Major

尽管 DirectX 矩阵在内存中按 Row-Major 存储,但其数学意义上遵循 Column-Major 列主序的变换逻辑(Sherk,2008)。具体表现为:

  • DirectX 中的矩阵与向量的乘法默认采用“向量右乘矩阵”(即 向量×矩阵\text{向量} \times \text{矩阵}向量×矩阵),这是 Column-Major 矩阵的典型计算方式(Row-Major 矩阵通常采用“矩阵左乘向量”)。
  • 当矩阵通过接口上传至 GPU 后,默认的着色器(Shader)代码会以 Column-Major 方式解析矩阵元素——即 GPU 会将内存中的矩阵数据“按列读取”,每一列对应一个寄存器(Register),用于后续的顶点变换或像素计算。

这种“存储(Row-Major)与计算(Column-Major)分离”的设计,是 DirectX 为平衡 CPU 缓存效率与 GPU 计算习惯而做的妥协,也是开发者需重点关注的适配点。

3. DirectX 接口的矩阵上传规则:自动转置与手动转置的差异

当 CPU 端的 Row-Major 矩阵需上传至 GPU(按 Column-Major 处理)时,DirectX 提供了不同的接口,其核心差异在于是否自动完成“Row-Major 到 Column-Major”的转置。开发者需根据接口特性选择是否手动干预,否则会导致矩阵数据解析错误。

3.1 支持自动转置的接口:ID3DXConstantTable::SetMatrix

ID3DXConstantTable::SetMatrix 是 DirectX 中用于向着色器常量表(Constant Table)上传矩阵的高频接口,其核心特性是自动处理转置(Rambler,2010):

  • 工作原理:该接口内部会检测矩阵的存储顺序(CPU 端的 Row-Major)与 GPU 处理顺序(Column-Major)的差异,自动将 Row-Major 矩阵转置为 Column-Major 格式后,再上传至 GPU。
  • 适用场景:适用于大多数常规场景(如上传模型变换矩阵、观察矩阵、投影矩阵),开发者无需手动编写转置代码,可直接传入 D3DX 库生成的 Row-Major 矩阵,降低出错概率。

3.2 不支持自动转置的接口:SetPixelShaderConstantFSetVertexShaderConstantF

SetPixelShaderConstantF(像素着色器常量设置)和 SetVertexShaderConstantF(顶点着色器常量设置)是更底层的常量设置接口,其设计目标是“轻量、直接”,因此不具备自动转置功能(Rambler,2010):

  • 工作原理:这两个接口会将 CPU 端内存中的矩阵数据“原封不动”地上传至 GPU 的着色器常量寄存器。若直接传入 Row-Major 矩阵,GPU 会按 Column-Major 规则解析,导致矩阵列与行颠倒,最终变换结果错误(如模型位置偏移、旋转方向反向)。
  • 适配要求:若使用这两个接口,开发者必须在上传前手动调用转置函数(如 D3DX 库的 D3DXMatrixTranspose),将 Row-Major 矩阵转为 Column-Major 矩阵,确保 GPU 能正确解析数据。

4. 总结:DirectX 矩阵存储与处理的核心适配逻辑

DirectX 中 Row-Major 与 Column-Major 的差异并非“非此即彼”,而是“存储-计算-接口”三者协同的逻辑体系,开发者需牢记以下核心结论:

  1. 存储层:CPU 端 DirectX 矩阵(如 D3DX 生成的矩阵)默认按 Row-Major 排列,适配 CPU 缓存效率。
  2. 计算层:GPU 端着色器默认按 Column-Major 处理矩阵,需“按列读取”数据并执行变换计算。
  3. 接口层:根据接口是否支持自动转置选择操作——ID3DXConstantTable::SetMatrix 自动转置,可直接传原矩阵;SetPixelShaderConstantF/SetVertexShaderConstantF 需手动转置,避免数据解析错误。

掌握这一逻辑体系,是避免 DirectX 图形编程中“矩阵变换错误”的关键,也是学习复杂坐标变换(如骨骼动画、视锥体裁剪)的基础。


行主序与列主序的应用优势及选择策略

在对行主序(Row-major)和列主序(Column-major)概念与主流语言支持的基础上,二者在实际工程中还存在更多与性能、兼容性、场景适配相关的优势,而选择哪种存储顺序,核心需围绕数据访问模式、硬件特性、工具链生态等维度综合判断。

一、行主序与列主序的应用优势

除了 “符合通用编程直觉”(行主序)或 “适配科学计算列操作”(列主序)的基础优势外,二者在性能优化、跨场景兼容、特殊数据处理等方面还具备以下关键价值:

1. 行主序的优势

(1)最大化 CPU 缓存命中率,降低通用场景延迟

现代 CPU 通过 “缓存(Cache)” 加速数据访问,缓存的核心特性是 “空间局部性”—— 即最近访问过的数据,其相邻数据有极高概率被再次访问。
行主序中,数组的连续内存地址对应 “同一行的相邻元素”,而通用编程场景(如遍历表格数据、处理字符串矩阵、实现二维数组的逐行初始化)中,数据访问天然以 “行” 为单位(例如读取一行用户信息、修改一行文本内容)。这种情况下,行主序能让 CPU 一次性将 “一整行数据” 加载到缓存中,后续访问同一行元素时无需再次从内存读取,大幅降低 “缓存未命中” 导致的性能损耗。
例如在 C 语言中遍历二维数组 int a [1000][1000],按行访问(for (i=0;i<1000;i++) for (j=0;j<1000;j++) a [i][j])的速度,通常比按列访问快 10 倍以上,核心原因就是行主序与缓存空间局部性的匹配。

(2)兼容多数通用数据格式与工具链

主流通用数据格式(如 CSV、JSON、TXT 表格)的存储逻辑天然是 “行优先”——CSV 文件中每一行对应一条记录,JSON 数组的嵌套结构(如 [[1,2],[3,4]])也默认按行组织数据。
行主序存储的数组与这些格式交互时,无需进行 “维度转置”,可直接按 “读一行、存一行” 的逻辑解析或生成数据,减少数据转换的代码复杂度与性能开销。例如用 Python 读取 CSV 文件到二维列表时,列表嵌套天然是行主序(外层列表对应行,内层列表对应列),直接使用无需额外处理。

(3)简化指针与内存操作逻辑

在支持指针的语言(如 C/C++)中,行主序的内存布局更符合 “线性内存连续访问” 的直觉。例如二维数组 a [m][n] 的行主序存储中,第 i 行的起始地址可直接通过 a [i](或 *(a+i))获取,而列主序中第 j 列的起始地址需要计算 a [0] + j*m(需额外传入行数 m),逻辑更复杂。
这种简化对底层内存操作(如数组拷贝、内存映射文件、硬件驱动数据交互)尤为重要,能减少代码出错概率,提升开发效率。

2. 列主序的优势

(1)适配科学计算与矩阵运算的核心需求

科学计算(如数值分析、机器学习、信号处理)中,数据操作常以 “列” 为单位:例如矩阵乘法中,左矩阵的 “列” 与右矩阵的 “行” 需逐元素相乘;机器学习中,特征矩阵的每一列对应一个特征(如 “年龄”“收入”),标准化、归一化等预处理操作需对 “列” 整体计算均值和方差。
列主序中,同一列的元素在内存中连续存储,执行 “列操作” 时可充分利用 CPU 缓存和向量指令(如 Intel AVX、ARM NEON)—— 向量指令能一次性对连续内存中的多个数据进行并行计算,大幅提升矩阵运算、特征处理的速度。例如在 MATLAB 中执行 mean (A)(计算每列均值),列主序存储让程序无需跳过行间隙查找元素,直接按连续内存块计算,效率远高于行主序下的同等操作。

(2)降低稀疏矩阵的存储与访问开销

稀疏矩阵(矩阵中大部分元素为 0)是科学计算与工程(如有限元分析、图论)中的常见数据结构,其存储需避免冗余的 0 元素。列主序更适配稀疏矩阵的主流存储格式 ——压缩列存储(Compressed Column Storage, CCS,也叫 CSC)
CCS 格式通过 “列指针数组” 记录每列非零元素的起始位置,“行索引数组” 记录非零元素的行号,“值数组” 存储非零元素的值。由于列主序下同一列元素连续,CCS 格式可直接按列遍历非零元素,无需额外转换;而若使用行主序的稀疏格式(如压缩行存储 CRS),执行列操作时需遍历所有行,效率更低。例如在求解大型线性方程组 Ax=b 时,基于列主序的 CCS 格式能显著减少内存占用(比 dense 矩阵节省 90% 以上空间),并加速迭代求解器(如共轭梯度法)的计算过程。

(3)兼容硬件加速库的底层优化

多数科学计算硬件加速库(如 BLAS、LAPACK、CUDA Math Library)的底层实现默认基于列主序优化:

  • BLAS(Basic Linear Algebra Subprograms,基础线性代数子程序库)是矩阵运算的标准接口,其核心函数(如矩阵乘法 dgemm)默认假设输入矩阵为列主序存储,若传入行主序矩阵需额外转置(增加时间开销);
  • GPU 加速(如 NVIDIA CUDA)中,列主序更适配 GPU 的内存架构 ——GPU 的全局内存访问延迟高,需通过 “合并访问”(即线程束内的线程访问连续内存地址)降低延迟,列主序下的列操作可天然实现合并访问,而行主序需额外调整线程索引逻辑。

二、行主序与列主序的选择策略

选择哪种存储顺序,本质是 “让数据存储逻辑与数据的访问模式、工具链生态、硬件特性匹配”,核心可遵循以下 4 个步骤:

1. 优先根据 “数据的核心访问模式” 判断

数据访问模式是决定存储顺序的最关键因素 ——“访问频率最高的维度,应对应存储中变化最快的维度”(即让高频访问的元素在内存中连续):

  • 若核心操作是 “按行访问”(如遍历表格记录、逐行处理文本、行内数据比较),选择行主序
    示例场景:通用软件开发(如用户数据表格处理、日志分析)、前端二维数据渲染(如 HTML 表格、Canvas 像素矩阵)。
  • 若核心操作是 “按列访问”(如计算列均值 / 方差、矩阵乘法、特征工程、列内数据筛选),选择列主序
    示例场景:科学计算(如有限元分析、信号滤波)、机器学习(特征矩阵预处理、神经网络权重更新)、统计分析(R 语言数据框操作)。

2. 适配所用编程语言与工具链的默认规则

不同语言 / 工具链的默认存储顺序已绑定其生态优化,强行违背默认规则会增加代码复杂度并损失性能:

  • 若使用通用编程语言(C/C++、Java、Python、Go、JavaScript),优先选择行主序—— 这些语言的原生数组 / 列表默认是行主序,且社区库(如 Python 的 pandas、Java 的 ArrayList)的 API 设计也基于行主序,无需额外转置即可直接使用;
    例外:Python 的 numpy 可通过 order='F' 指定列主序,但仅在调用 BLAS 库(如 numpy.linalg)时建议使用,通用场景仍推荐 order='C'(行主序)。
  • 若使用科学计算工具(MATLAB、R、Fortran、Julia),优先选择列主序—— 这些工具的底层引擎、线性代数库(如 MATLAB 的 linprog、R 的 lm)均基于列主序优化,使用默认顺序可避免 API 调用时的隐式转置(例如在 R 中用 matrix () 创建矩阵,默认是列主序,若强行按行填充需指定 byrow=TRUE,但会增加内存拷贝)。

3. 结合硬件与加速库的优化方向

若涉及高性能计算(HPC)或硬件加速(CPU 向量指令、GPU、FPGA),需匹配硬件 / 库的底层优化逻辑:

  • 若使用 CPU 向量指令(如 AVX、SSE)或通用 CPU 加速库(如 OpenBLAS),列主序更适合矩阵运算,行主序更适合通用遍历;
  • 若使用 GPU 加速(如 CUDA、OpenCL),列主序更适配 GPU 的合并访问机制,尤其在执行矩阵乘法、卷积等操作时,优先选择列主序以减少内存延迟;
  • 若使用稀疏矩阵库(如 Intel MKL Sparse、SuiteSparse),列主序适配 CCS 格式,行主序适配 CRS 格式 —— 需根据稀疏矩阵的核心操作(列操作选 CCS / 列主序,行操作选 CRS / 行主序)判断。

4. 权衡兼容性与未来扩展性

若数据需跨语言 / 工具链交互(如 C++ 程序生成数据供 MATLAB 分析,或 Python 读取 Fortran 输出的二进制文件),需提前约定存储顺序,避免数据错乱:

  • 跨语言交互时,若一方是行主序(如 C++)、另一方是列主序(如 MATLAB),需在数据传输后执行维度转置(如 C++ 中用 transpose (a) 转置矩阵,再写入文件供 MATLAB 读取);
  • 若数据未来可能扩展到更高维度(如从二维矩阵扩展到三维张量),需考虑高维下的访问模式:例如三维张量(深度 × 行 × 列)的核心操作是 “按深度 - 行遍历”,则行主序(深度变化最慢、列变化最快)更合适;若核心操作是 “按列 - 深度遍历”,则列主序(列变化最慢、深度变化最快)更合适。

三、总结

行主序与列主序的优势本质是 “存储逻辑与场景需求的匹配”:

  • 行主序:通用场景的缓存友好、工具链兼容、代码简化,适合以 “行访问” 为核心的通用开发、数据处理;
  • 列主序:科学计算的并行效率、稀疏存储优化、硬件加速适配,适合以 “列访问” 为核心的矩阵运算、机器学习、数值分析。

选择时无需绝对化 —— 部分语言 / 库(如 numpy、Julia)支持动态指定存储顺序,可根据具体操作(如 “行遍历用行主序,矩阵乘法用列主序”)灵活切换,在兼容性与性能间找到最优平衡。


via:

  • Row major and Column Major Address calculations | by Kolli Rohit Reddy | L’arome_
    https://laro.me/row-major-and-column-major-address-calculations-99e66f4fe734

  • 数组存储方式解析-CSDN博客
    https://blog.csdn.net/misskissC/article/details/14520297

  • Row- and Column- major order(行优先和列优先顺序)_column major-CSDN博客
    https://blog.csdn.net/u013608424/article/details/80118311

  • 行主序、列主序概念辨析以及基地址的含义_以行序为主序和以列序为主序-CSDN博客
    https://blog.csdn.net/tiansheshouzuo/article/details/86558120

  • 矩阵中 “行优先“ 和 “列“ 优先-CSDN博客
    https://blog.csdn.net/paxis0813/article/details/104108283

  • ……


文章转载自:

http://heY92iSG.nfqyk.cn
http://sUf4BP53.nfqyk.cn
http://BYZUjbex.nfqyk.cn
http://5JUPFTIR.nfqyk.cn
http://d255WQoQ.nfqyk.cn
http://zGk3mqZs.nfqyk.cn
http://2JryQMMo.nfqyk.cn
http://RHEvLKEx.nfqyk.cn
http://xtMPCYjn.nfqyk.cn
http://P8xWfJBs.nfqyk.cn
http://SJjn1mu2.nfqyk.cn
http://2d8guYun.nfqyk.cn
http://wbPHdqUn.nfqyk.cn
http://Y5KD8nL3.nfqyk.cn
http://T4cURY6M.nfqyk.cn
http://gmtIVD7I.nfqyk.cn
http://FwuYTmQ9.nfqyk.cn
http://k82y3rqQ.nfqyk.cn
http://Hsux66n3.nfqyk.cn
http://BC8xEYrY.nfqyk.cn
http://htgmlfun.nfqyk.cn
http://gJ3nZnHt.nfqyk.cn
http://Zh0YxLTg.nfqyk.cn
http://W0bYbrM3.nfqyk.cn
http://nbtActyV.nfqyk.cn
http://f5GmN6jr.nfqyk.cn
http://JwcfyzUm.nfqyk.cn
http://AHt2Mhbr.nfqyk.cn
http://oX4YjRop.nfqyk.cn
http://OLAcT0vm.nfqyk.cn
http://www.dtcms.com/a/383553.html

相关文章:

  • 贪心算法应用:最早截止时间优先(EDF)问题详解
  • 每天五分钟深度学习:神经网络的权重参数如何初始化
  • BisenetV1/2网络以及模型推理转换
  • Codeforces Round 1050 (Div. 4)补题
  • 【Java后端】Spring Boot 多模块项目实战:从零搭建父工程与子模块
  • c++命名空间详解
  • 第15课:知识图谱与语义理解
  • HarmonyOS图形处理:Canvas绘制与动画开发实战
  • ffmpeg 有什么用处?
  • 如何重置Gitlab的root用户密码
  • LeetCode算法日记 - Day 41: 数据流的中位数、图像渲染
  • 计算机网络(二)物理层数据链路层
  • 零基础从头教学Linux(Day 33)
  • collections模块
  • 【前端】【高德地图WebJs】【知识体系搭建】图层知识点——>热力图,瓦片图层,自定义图层
  • 关系模型的数据结构
  • Spring Boot 与前端文件上传跨域问题:Multipart、CORS 与网关配置
  • MySQL的事务特性和高可用架构
  • AI重构车载测试:从人工到智能的跨越
  • 前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)
  • 文件查找 find
  • LeetCode 2110.股票平滑下跌阶段的数目
  • 解锁仓储智能调度、运输路径优化、数据实时追踪,全功能降本提效的智慧物流开源了
  • FPGA学习篇——Verilog学习MUX的实现
  • hadoop单机伪分布环境配置
  • Vue3 响应式失效 debug:Proxy 陷阱导致数据更新异常的深度排查
  • el-table的隔行变色不影响row-class-name的背景色
  • 【深度学习新浪潮】游戏中的agents技术研发进展一览
  • Condor 安装
  • 类和对象 (中)