行优先与列优先混淆

Row-major vs Column-major confusion

我读了很多这方面的资料,越读越糊涂。

我的理解:在行优先中,行在内存中连续存储,在列优先中,列在内存中连续存储。因此,如果我们有一个数字序列 [1, ..., 9] 并且我们想将它们存储在行主矩阵中,我们会得到:

|1, 2, 3|
|4, 5, 6|
|7, 8, 9|

而主要专栏(如果我错了请纠正我)是:

|1, 4, 7|
|2, 5, 8|
|3, 6, 9|

这实际上是前一个矩阵的转置。

我的困惑:好吧,我看不出有什么不同。如果我们对两个矩阵进行迭代(在第一个矩阵中按行,在第二个矩阵中按列),我们将以相同的顺序覆盖相同的值:1, 2, 3, ..., 9

即使矩阵乘法是相同的,我们取第一个连续元素并将它们与第二个矩阵列相乘。假设我们有矩阵 M:

|1, 0, 4| 
|5, 2, 7| 
|6, 0, 0|

如果我们将前面的行主矩阵 RM 相乘,即 R x M 我们将得到:

|1*1 + 2*0 + 3*4, 1*5 + 2*2 + 3*7, etc|
|etc.. |
|etc.. |

如果我们将列主矩阵 CM 相乘,即 C x M 通过采用 C 的列而不是它的行,我们得到来自 R x M

的相同结果

我真的很困惑,如果一切都一样,为什么这两个术语会存在?我的意思是即使在第一个矩阵 R 中,我也可以查看行并将它们视为列...

我错过了什么吗? row-major 与 col-major 实际上对我的矩阵数学意味着什么?我总是在我的线性代数 类 中学到,我们将第一个矩阵的行与第二个矩阵的列相乘,如果第一个矩阵是列优先矩阵,这会改变吗?我们现在是否必须像我在示例中所做的那样将其列与第二个矩阵的列相乘,还是完全错误?

非常感谢任何澄清!

编辑: 我遇到的另一个主要混淆来源之一是 GLM...所以我将鼠标悬停在它的矩阵类型上并按 F12 以查看它是如何实现的,在那里我看到一个向量数组,所以如果我们有一个 3x3 矩阵,我们就有一个包含 3 个向量的数组。查看我看到的那些向量的类型 'col_type' 所以我假设这些向量中的每一个都代表一列,因此我们有一个列优先系统对吗?

嗯,老实说我不知道​​。我写了这个打印函数来比较我的翻译矩阵和 glm 的,我在最后一行看到 glm 中的翻译向量,而我的在最后一列......

这只会增加混乱。您可以清楚地看到 glmTranslate 矩阵中的每个向量代表矩阵中的一行。所以...这意味着矩阵是行优先的,对吗?我的矩阵呢? (我使用的是浮点数组[16])翻译值在最后一列,这是否意味着我的矩阵是列优先的,而我现在不是? 试图阻止头部旋转

你是对的。系统将数据存储在行优先结构还是列优先结构中并不重要。它就像一个协议。计算机:"Hey, human. I'm going to store your array this way. No prob. Huh?" 但是,当涉及到性能时,它很重要。考虑以下三件事。

1.大多数数组都是按行优先顺序访问的。

2。当您访问内存时,它不是直接从内存中读取的。您首先将一些数据块从内存存储到缓存,然后将数据从缓存读取到处理器。

3。如果你要的数据在cache中不存在,cache要重新从内存中取数据

当缓存从内存中获取数据时,位置很重要。也就是说,如果你在内存中稀疏地存储数据,你的缓存应该更频繁地从内存中获取数据。此操作会破坏您的程序性能,因为访问内存比访问缓存慢得多(超过 100 倍!)。访问内存越少,程序越快。所以,这个行优先数组更有效,因为访问它的数据更有可能是本地的。

我认为您混淆了实现细节和用法(如果您愿意的话)。

让我们从二维数组或矩阵开始:

    | 1  2  3 |
    | 4  5  6 |
    | 7  8  9 |

问题在于计算机内存是一维字节数组。为了使我们的讨论更容易,让我们将单个字节分成四个一组,这样 我们有这样的东西,(每个,+-+代表一个字节,四个 bytes 表示一个整数值(假设是 32 位操作系统):

   -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    |       |       |       |       |       |       |       |       |  
   -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
       \/                   \       /
      one byte               one integer

    low memory    ------>                          high memory

另一种表示方式

所以,问题是如何将二维结构(我们的矩阵)映射到这个一维结构(即内存)上。有两种方法可以做到这一点。

  1. 行优先顺序:按照这个顺序,我们首先将第一行放入内存,然后是第二行,依此类推。这样做,我们将在内存中拥有以下内容:

    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |   1   |   2   |   3   |   4   |   5   |   6   |   7   |   8   |   9   |
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

使用此方法,我们可以通过执行以下算术找到数组的给定元素。假设我们要访问数组的 $M_{ij}$ 元素。如果我们假设我们有一个指向数组第一个元素的指针,比如ptr,并且知道列数nCol,我们可以通过以下方式找到任何元素:

     $M_{ij} = i*nCol + j$ 

要了解这是如何工作的,请考虑 M_{02}(即第一行第三列——记住 C 是从零开始的。

      $M_{02} = 0*3 + 2 = 2

所以我们访问数组的第三个元素。

  1. 列优先顺序:按照这个顺序,我们首先将第一列放入内存,然后是第二列,依此类推。这样做我们将在内存中有以下内容:

    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |   1   |   4   |   7   |   2   |   5   |   8   |   3   |   6   |   9   |
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

所以,简短的回答 - 行优先和列优先格式描述了如何将二维(或更高)维数组映射到一维内存数组。

希望这对您有所帮助。 T.

我们先看代数;代数甚至没有 "memory layout" 之类的概念。

根据代数 pov,MxN 实矩阵可以作用于其右侧的 |R^N 向量并产生 |R^M 向量。

因此,如果您参加考试并给定一个 MxN 矩阵和一个 |R^N 向量,您可以通过简单的运算将它们相乘并得到结果 - 该结果是对还是错将不取决于您的教授用来在内部检查您的结果的软件是使用列优先布局还是行优先布局;它仅取决于您是否正确计算了矩阵的每一行与向量的(单)列的收缩。

为了产生正确的输出,软件将 - 通过任何方式 - 本质上必须将矩阵的每一行与列向量收缩,就像你在考试中所做的那样。


因此,对齐列优先的软件和使用行优先布局的软件之间的区别不是它计算的内容,而是如何.

更具体地说,这些布局在topcial单行与列向量收缩方面的区别只是确定

的手段
Where is the next element of the current row?
  • 对于行优先布局,它是内存中下一个存储桶中的元素
  • 对于列优先布局,它是 M 个桶中的元素。

就是这样。


向您展示如何在实践中召唤 column/row 魔法:

您没有用 "c++" 标记您的问题,但因为您提到了“glm”,我认为您可以使用 C++。

在 C++ 的标准库中有一个臭名昭著的野兽,叫做 valarray, which, besides other tricky features, has overloads of operator[], one of them can take a std::slice(它本质上是一个非常无聊的东西,只包含三个整数类型的数字)。

然而,这个小片段拥有访问行主存储列或列主存储所需的一切——它有一个开始、一个长度和一个跨度——后者代表我提到的"distance to next bucket"

不管你使用什么:只要保持一致!

行优先或列优先只是一种约定。没关系。 C使用行专业,Fortran使用列。两者都有效。使用编程中的标准 language/environment.

两者不匹配会造成 !@#$ 混乱

如果您在存储在主列中的矩阵上使用行主寻址,您可能会得到错误的元素、读取数组末尾等等...

Row major: A(i,j) element is at A[j + i * n_columns];  <---- mixing these up will
Col major: A(i,j) element is at A[i + j * n_rows];     <---- make your code fubar

行主列主列矩阵乘法的代码相同是不正确的

(当然矩阵乘法的数学是一样的。) 假设您在内存中有两个数组:

X = [x1, x2, x3, x4]    Y = [y1, y2, y3, y4]

如果矩阵存储在 major 列中,则 X、Y 和 X*Y 为:

IF COL MAJOR: [x1, x3  *  [y1, y3    =   [x1y1+x3y2, x1y3+x3y4
               x2, x4]     y2, y4]        x2y1+x4y2, x2y3+x4y4]

如果矩阵存储在主行中,则 X、Y 和 X*Y 为:

IF ROW MAJOR:  [x1, x2    [y1, y2     = [x1y1+x2y3, x1y2+x2y4;
                x3, x4]    y3, y4]       x3y1+x4y3, x3y2+x4y4];

X*Y in memory if COL major   [x1y1+x3y2, x2y1+x4y2, x1y3+x3y4, x2y3+x4y4]
              if ROW major   [x1y1+x2y3, x1y2+x2y4, x3y1+x4y3, x3y2+x4y4]

这里没有深入的内容。这只是两个不同的约定。这就像以英里或公里为单位进行测量。两者都有效,只是不能在不转换的情况下在两者之间来回切换!

今天没有理由使用其他列优先顺序,c/c++ 中有几个库支持它(eigen、armadillo、...)。此外,列优先顺序更自然,例如。带有 [x,y,z] 的图片在文件中逐片存储,这是列优先顺序。虽然在二维中选择更好的顺序可能会造成混淆,但在更高的维度中,很明显列优先顺序是许多情况下唯一的解决方案。

C的作者创造了数组的概念,但他们可能没有想到有人将它用作矩阵。如果我看到数组是如何在已经按照 fortran 和列主要顺序组成的地方使用的,我自己也会感到震惊。我认为行优先顺序只是列优先顺序的替代,但只有在真正需要它的情况下(现在我不知道)。

奇怪的是,仍然有人创建行优先顺序的库。这是不必要的精力和时间浪费。我希望有一天一切都会成为专栏主要秩序,所有的困惑都会消失。

好的,鉴于 "confusion" 这个词在标题中是字面意思,我可以理解...混乱的程度。

首先,这个绝对是一个真正的问题

永远不要屈服于"it is used be but...PC's nowadays..."

的想法

这里的主要问题是: -Cache eviction strategy (LRU, FIFO, etc.) as @Y.C.Jung was beginning to touch on -Branch prediction -Pipelining (it's depth, etc) -Actual physical memory layout -Size of memory -Architecture of machine, (ARM, MIPS, Intel, AMD, Motorola, etc.)

本次回答将重点介绍哈佛架构,冯诺依曼机,因为它最适用于现在的PC。

内存层次结构:

https://en.wikipedia.org/wiki/File:ComputerMemoryHierarchy.svgis

成本速度的并列。

对于今天的标准 PC 系统,这类似于: SIZE: 500GB HDD > 8GB RAM > L2 Cache > L1 Cache > Registers. SPEED: 500GB HDD < 8GB RAM < L2 Cache < L1 Cache < Registers.

这引出了时间和空间局部性的想法。一种表示 如何 您的数据组织方式(代码、工作集等),另一种表示物理 您的数据在 [=] 中的组织方式157=]

鉴于今天的 "most" PC 是 little-endian(英特尔)机器,它们将数据放入特定的内存中 little-endian 订购。它与 big-endian 根本不同。

https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html (覆盖它而不是... swiftly ;))

(为了这个示例的简单性,我打算 'say' 事情发生在单个条目中,这是不正确的,整个缓存块通常被访问并且与我的制造商有很大差异,更不用说模型)。

所以,现在我们有了我们的方式,如果假设你的程序要求1GB of data from your 500GB HDD,加载到你的8GB of RAM,然后到cache 层次结构,然后最终 registers,你的程序从你最新的缓存行读取第一个条目只是为了让你的第二个(在你的代码中)所需的条目恰好位于 next cache line,(即下一个 ROW 而不是 column 你会有一个缓存 MISS.

假设缓存已满,因为它,一旦未命中,根据逐出方案,一条线将是被驱逐以为 'does' 具有您需要的下一个数据的行腾出空间。如果重复此模式,您将在 EVERY 次尝试数据检索时出现 MISS

更糟糕的是,您将驱逐实际上具有您将要需要的有效数据的行,因此您将不得不一次又一次地检索它们。

这个术语叫做:thrashing

https://en.wikipedia.org/wiki/Thrashing_(computer_science) 并且确实可以 崩溃 一个很差 written/error 容易出现的系统。 (想想windows蓝屏)....

另一方面,如果您正确地布置了数据,(即主要行)...您仍然会有失误!

但这些未命中 只会 发生在每次检索结束时,而不是在 每次尝试检索时发生。 这导致数量级系统和程序性能的差异。

非常非常简单的片段:

#include<stdio.h>

#define NUM_ROWS 1024
#define NUM_COLS 1024

int COL_MAJOR [NUM_ROWS][NUM_COLS];

int main (void){
        int i=0, j=0;
        for(i; i<NUM_ROWS; i++){
                for(j; j<NUM_COLS; j++){
                        COL_MAJOR[j][i]=(i+j);//NOTE i,j order here!
                }//end inner for
        }//end outer for
return 0;
}//end main

现在,编译: gcc -g col_maj.c -o col.o

现在,运行 有: time ./col.o real 0m0.009s user 0m0.003s sys 0m0.004s

现在重复 ROW 专业:

#include<stdio.h>

#define NUM_ROWS 1024
#define NUM_COLS 1024

int ROW_MAJOR [NUM_ROWS][NUM_COLS];

int main (void){
        int i=0, j=0;
        for(i; i<NUM_ROWS; i++){
                for(j; j<NUM_COLS; j++){
                        ROW_MAJOR[i][j]=(i+j);//NOTE i,j order here!
                }//end inner for
        }//end outer for
return 0;
}//end main

编译: terminal4$ gcc -g row_maj.c -o row.o 运行: time ./row.o real 0m0.005s user 0m0.001s sys 0m0.003s

现在,如您所见,Row Major 明显更快。

不服气? 如果你想看一个更极端的例子: 将矩阵设为 1000000 x 1000000,对其进行初始化、转置并将其打印到标准输出。 ```

(注意,在 *NIX 系统上你需要设置 ulimit unlimited)

我的回答有问题: -Optimizing compilers, they change a LOT of things! -Type of system -Please point any others out -This system has an Intel i5 processor

以上答案的简短附录。 就 C 而言,几乎直接访问内存,行优先或列优先顺序以两种方式影响您的程序: 1.它影响你的矩阵在内存中的布局 2.必须保持的元素访问顺序-以顺序循环的形式。

  1. 在前面的回答中已经解释的很透彻了,所以我再补充2。

eulerworks 回答指出,在他的示例中,使用行主矩阵导致计算速度显着降低。嗯,他是对的,但是结果可以同时反转

循环顺序是 for(over rows) { for(over columns) { do something over a matrix } }。这意味着双循环将访问一行中的元素,然后移至下一行。例如,A(0,1) -> A(0,2) -> A(0,3) -> ... -> A(0,N_ROWS) -> A(1,0) -> ...

在这种情况下,如果 A 以行主要格式存储,则缓存未命中的可能性最小,因为元素可能会在内存中以线性方式排列。否则在列优先格式中,内存访问将使用 N_ROWS 作为步幅跳跃。所以在这种情况下,行优先更快。

现在,我们实际上可以切换循环,这样它将 for(over columns) { for(over rows) { do something over a matrix } }。对于这种情况,结果将恰恰相反。列主要计算将更快,因为循环将以线性方式读取列中的元素。

因此,你不妨记住这一点: 1. 选择行优先或列优先存储格式取决于你的喜好,尽管传统的 C 编程社区似乎更喜欢行优先格式。 2. 尽管您可以自由选择您喜欢的任何内容,但您需要与索引的概念保持一致。 3. 此外,这一点非常重要,请记住,在写下您自己的算法时,请尝试对循环进行排序,以便它符合您选择的存储格式。 4. 保持一致。

鉴于上述解释,这里code snippet演示了这个概念。

//----------------------------------------------------------------------------------------
// A generalized example of row-major, index/coordinate conversion for
// one-/two-dimensional arrays.
// ex: data[i] <-> data[r][c]
//
// Sandboxed at: http://swift.sandbox.bluemix.net/#/repl/5a077c462e4189674bea0810
//
// -eholley
//----------------------------------------------------------------------------------------

// Algorithm

let numberOfRows    = 3
let numberOfColumns = 5
let numberOfIndexes = numberOfRows * numberOfColumns

func index(row: Int, column: Int) -> Int {
    return (row * numberOfColumns) + column
}

func rowColumn(index: Int) -> (row: Int, column: Int) {
    return (index / numberOfColumns, index % numberOfColumns)
}

//----------------------------------------------------------------------------------------

// Testing

let oneDim = [
       0,    1,    2,    3,    4,
       5,    6,    7,    8,    9,
      10,   11,   12,   13,   14,
]

let twoDim = [
    [  0,    1,    2,    3,    4 ],
    [  5,    6,    7,    8,    9 ],
    [ 10,   11,   12,   13,   14 ],
]

for i1 in 0..<numberOfIndexes {
    let v1 = oneDim[i1]
    let rc = rowColumn(index: i1)
    let i2 = index(row: rc.row, column: rc.column)
    let v2 = oneDim[i2]
    let v3 = twoDim[rc.row][rc.column]
    print(i1, v1, i2, v2, v3, rc)
    assert(i1 == i2)
    assert(v1 == v2)
    assert(v2 == v3)
}

/* Output:
0 0 0 0 0 (row: 0, column: 0)
1 1 1 1 1 (row: 0, column: 1)
2 2 2 2 2 (row: 0, column: 2)
3 3 3 3 3 (row: 0, column: 3)
4 4 4 4 4 (row: 0, column: 4)
5 5 5 5 5 (row: 1, column: 0)
6 6 6 6 6 (row: 1, column: 1)
7 7 7 7 7 (row: 1, column: 2)
8 8 8 8 8 (row: 1, column: 3)
9 9 9 9 9 (row: 1, column: 4)
10 10 10 10 10 (row: 2, column: 0)
11 11 11 11 11 (row: 2, column: 1)
12 12 12 12 12 (row: 2, column: 2)
13 13 13 13 13 (row: 2, column: 3)
14 14 14 14 14 (row: 2, column: 4)
*/

easy:row-major 和 column-major 是从 glUniformMatrix*() 的角度来看的。 实际上,矩阵从未改变,它总是:

区别在于矩阵class的实现。它确定如何将 16 个浮点数存储为传递给 glUniformMatrix*()

的参数

如果使用行优先矩阵,4x4 矩阵 的内存为: (a11, a12, a13, a14, a21, a22, a23, a24, a31, a32, a33, a34, a41, a42, a43, a44), else for column-major 是: (a11, a21, a31, a41, a12, a22, a32, a42, a13, a23, a33, a43, a41, a42, a43, a44)。

因为glsl是列主矩阵,所以会读取16位浮点数据 (b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16) 作为

由于在行优先中,a11=b1, a12=b2, a13=b3, a14=b4, a21=b5, a22=b6,...所以在 glsl 中矩阵更改为

而在列优先中:a11=b1, a21=b2, a31=b3, a41=b4, a12=b5, a22=b6,...所以在 glsl 矩阵中更改为

与原图相同。 所以行优先需要转置而列优先不需要。

希望能解决您的困惑。