行主要与列主要混淆

时间:2015-11-23 02:08:08

标签: c opengl matrix linear-algebra

我已经阅读了很多关于此事的内容,我读的越多,我就越感到困惑。

我的理解:在行中,主要行连续存储在内存中,列中的主要行连续存储在内存中。因此,如果我们有一系列数字[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' s,我在最后一行看到glm中的翻译向量,而我的翻译向量是最后一列......

enter image description here

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

9 个答案:

答案 0 :(得分:10)

如果您愿意,我认为您将实现细节与用法混淆。

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

    | 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   |
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    
  2. 使用此方法,我们可以通过执行以下算法找到数组的给定元素。假设我们想要访问数组的$ 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   |
      -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      
    2. 那么,简短的答案 - 行主要和列主要格式描述了两个(或更高)维数阵列如何映射到一维内存数组。

      希望这会有所帮助。 吨。

答案 1 :(得分:4)

让我们先看代数;代数甚至没有“内存布局”和东西的概念。

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

因此,如果你坐在考试中并给出一个MxN矩阵和一个| R ^ N向量,你可以通过简单的操作将它们相乘并得到一个结果 - 这个结果是对还是错不会取决于你的教授用来检查你的结果的软件是否在内部使用了列主要或行主要布局;它只取决于你是否正确计算了矢量(单)列的矩阵每一行的收缩。

为了产生正确的输出,软件将 - 无论如何 - 基本上必须用矩阵向量收缩矩阵的每一行,就像你在考试中一样。

因此,使用列主要软件和使用行主要布局的软件之间的差异不是它计算的,而只是如何

更准确地说,关于topcial单行收缩与列向量的布局之间的差异只是确定的方法

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

就是这样。

向您展示在实践中如何召唤列/行魔术:

你没有用“c ++”标记你的问题,但是因为你提到了'glm',我认为你可以与C ++相处。

在C ++的标准库中有一个名为valarray的臭名昭着的野兽,除了其他棘手的功能外,还有operator[]的重载,其中一个可以采用 std::slice (这实际上是一件非常无聊的事情,只包含三个整数类型的数字)。

然而,这个小片事物具有访问行 - 主要 - 存储列 - 行或列 - 主要 - 存储行所需的一切 - 它具有开始,长度和步幅 - 后者代表我提到的“到下一个桶的距离”。

答案 2 :(得分:3)

你是对的。如果系统将数据存储在行主结构或列主结构中,则无关紧要。它就像一个协议。电脑:"嘿,人类。我会以这种方式存储您的阵列。没问题。呵呵&#34?; 但是,在性能方面,重要的是。考虑以下三件事。

<强> 1。大多数数组都按行主要顺序访问。

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

第3。如果您想要的数据不存在于缓存中,缓存应该从内存中重新获取数据

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

答案 3 :(得分:2)

无论你使用什么:只要保持一致!

行主要或列专业只是一种惯例。无所谓。 C使用row major,Fortran使用列。两者都有效。使用编程语言/环境中的标准。

两者不匹配!@#$ stuff up

如果在colum major中存储的矩阵上使用行主要寻址,则可以获取错误的元素,读取数组的末尾等等...

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 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]

这里没什么好事。这只是两个不同的惯例。这就像以英里或公里为单位进行测量。要么工作,你就不能在没有转换的情况下在两者之间来回翻转!

答案 4 :(得分:1)

好的,所以给出了&#34;混淆&#34;在字面上,我可以理解......混乱的程度。

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

从来没有,因为&#34;它被使用的想法,但是......现在的PC ......&#34;

这里的主要问题是: -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.

这导致了时空局部性的概念。一个意味着如何您的数据组织,(代码,工作集等),另一个意味着物理 您的数据组织在&#34;内存中。&# 34;

鉴于&#34;大多数&#34;今天的PC是 little-endian (英特尔)机器,他们最近以特定的小端排序将数据存入内存。从根本上说,它确实与big-endian不同。

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

(为了简化这个例子,我要说&#39;事情发生在单个条目中,这是不正确的,整个缓存块通常被访问并且我的制造商大大改变,更不用说模型了)

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

假设缓存已满,因为它是,一旦未命中,根据逐出计划,将驱逐一条线为该线路腾出空间&#39;不&#39;拥有您需要的下一个数据。如果重复此模式,则每次尝试数据检索都会有 MISS

更糟糕的是,您将驱逐实际上具有您需要的有效数据的行,因此您必须检索它们再次和再次

这个术语称为:thrashing

https://en.wikipedia.org/wiki/Thrashing_(computer_science) 并且确实可以崩溃一个写得不好/容易出错的系统。 (想想windows BSOD )....

另一方面,如果你已经正确地布置了数据,(例如Row专业)......你仍然会错过!

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

非常非常简单的代码段

#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

现在,正如您所看到的,行专业一个明显更快。

不相信? 如果你想看一个更激烈的例子: 使矩阵1000000 x 1000000,初始化它,转置它并将其打印到stdout。 ```

(注意,在* NIX系统上,您需要设置ulimit无限制)

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

答案 5 :(得分:1)

这很容易:行主要和列主要是从glUniformMatrix *()的角度来看的。 实际上,矩阵从未改变,这是无聊的:

enter image description here

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

如果您使用行优先矩阵,则 4x4矩阵的内存为: (a11,a12,a13,a14,a21,a22,a23,a24,a31,a32,a33,a34,a41,a42,a43,a44),否则对于主要列是: (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)为

enter image description here

由于在主要行中,a11 = b1,a12 = b2,a13 = b3,a14 = b4,a21 = b5,a22 = b6,...因此glsl矩阵更改为

enter image description here 而在主要列中:a11 = b1,a21 = b2,a31 = b3,a41 = b4,a12 = b5,a22 = b6 ......因此glsl矩阵更改为

enter image description here

与原产地相同。 因此,行优先而不是列优先需要转置。

希望这可以解决您的困惑。

答案 6 :(得分:0)

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

  1. 在之前的答案中进行了相当详细的解释,因此我将添加到2。
  2. eulerworks的答案指出,在他的例子中,使用行主矩阵带来了显着的计算减速。好吧,他是对的,但结果可能会同时逆转。

    循环顺序适用于(在行上){for(在列上){在矩阵上做某事}}。这意味着双循环将访问行中的元素,然后移动到下一行。例如,A(0,1) - > A(0,2) - &gt; A(0,3) - &gt; ... - &gt; A(0,N_ROWS) - &gt; A(1,0) - &gt; ...

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

    现在,我们实际上可以切换循环,这样就可以(在列上){for(over rows){在矩阵上执行某些操作}}。对于这种情况,结果恰恰相反。由于循环将以线性方式读取列中的元素,因此列主要计算将更快。

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

答案 7 :(得分:0)

鉴于上述解释,这里有一个code snippet来证明这个概念。

User.includes(:company).order('company.name')

答案 8 :(得分:-1)

今天没有理由使用其他列主要顺序,有几个库在c / c ++中支持它(eigen,armadillo,...)。此外,列主要顺序更自然,例如。具有[x,y,z]的图片在文件中逐片存储,这是列主要顺序。虽然在二维方面,选择更好的订单可能会让人感到困惑,但在更高的维度上,很明显,在大多数情况下,列主要订单是唯一的解决方案。

C的作者创建了数组的概念,但也许他们没想到有人将它用作矩阵。如果我看到阵列的使用位置已经在fortran和列主要顺序中已经完成,我会感到震惊。我认为行主要订单只是替代列主要订单,但仅限于真正需要它的情况(现在我不知道任何事情)。

奇怪的是,仍然有人创建了具有行主要顺序的库。这是不必要的浪费能源和时间。我希望有一天,一切都将成为主要的秩序,所有的混乱都会消失。