行主要与列主要混淆

我一直在阅读很多关于此事的内容,我读的越多,我就越困惑。

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

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

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

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

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

我的困惑:嗯,我没有看到任何区别。 如果我们迭代两个矩阵(按第一个中的行和第二个中的列),我们将以相同的顺序覆盖相同的值: 1, 2, 3, ..., 9

即使矩阵乘法是相同的,我们采用第一个连续元素并将它们与第二个矩arrays相乘。 所以说我们有矩阵M

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

如果我们将前一行主矩阵R乘以M ,即R x M我们将得到:

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

如果我们将列主要矩阵C乘以M ,即通过取C的列而不是其行的C x M ,我们得到与R x M完全相同的结果

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

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

任何澄清真的很感激!

编辑:我遇到的另一个混乱的主要原因之一是GLM ……所以我将鼠标hover在它的矩阵类型上并点击F12以查看它是如何实现的,在那里我看到了一个矢量数组,所以如果我们有一个3×3矩阵我们有一个3个向量的数组。 看看那些矢量的类型我看到’col_type’所以我假设这些矢量中的每一个都代表一个列,因此我们有一个列主系统对吗?

好吧,我不知道说实话。 我写了这个打印函数来比较我的翻译矩阵与glm的,我在最后一行看到glm中的翻译向量,而我的最后一列是…

在此处输入图像描述

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

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

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

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

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


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

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

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

就是这样。


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

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

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

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

如果愿意的话,我认为你将实现细节与用法混为一谈。

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

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

问题是计算机内存是一维字节数组。 为了使我们的讨论更容易,让我们将单个字节分组为四个组,因此我们看起来像这样,(每个单独,+ – +表示一个字节,四个字节表示一个整数值(假设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 | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 

因此,简短的答案 – 行主要和列主要格式描述了如何将两个(或更高)维数arrays映射到一维内存数组中。

希望这可以帮助。 T.

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

1.大多数数组按行主要顺序访问。

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

3.如果所需数据不存在于缓存中,则缓存应从内存中重新获取数据

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

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

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

两者不匹配!@#$填充

如果对存储在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] 

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

好的,所以鉴于“混乱”这个词在字面上是标题,我可以理解……混乱的程度。

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

永远,永远不会屈服于“它被使用但是… PC现在……”的想法。

这里的主要问题是: -Cache eviction strategy (LRU, FIFO, etc.) as @YCJung 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. SIZE: 500GB HDD > 8GB RAM > L2 Cache > L1 Cache > Registers. SPEED: 500GB HDD < 8GB RAM < L2 Cache < L1 Cache < Registers.

这导致了时空局部性的概念。 一个是指数据的组织方式,(代码,工作集等),另一个是物理上将数据组织在“内存”中的方式。

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

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

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

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

假设缓存已满,因为它很小 ,一旦未命中,根据驱逐方案,将驱逐一条线以为“确实”拥有您需要的下一个数据的线腾出空间。 如果重复这种模式,那么在每次尝试数据检索时都会有一个MISS

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

这个术语叫做: thrashing

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

另一方面,如果您已正确布置数据(即行专业)......您仍然会错过!

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

非常简单的片段:

 #include #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 

现在,使用以下命令编译: gcc -g col_maj.c -o col.o

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

现在重复ROW专业:

 #include #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 

编译: 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,初始化它,转置它并将其打印到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 -Optimizing compilers, they change a LOT of things! -Type of system -Please point any others out -This system has an Intel i5 processor

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

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

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

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

  1. 在前面的答案中解释得非常彻底,所以我将添加到2。

eulerworks回答指出,在他的例子中,使用行主矩阵带来了显着的计算减速。 嗯,他是对的,但结果可以同时逆转。

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

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

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

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

鉴于上面的解释,这里有一个代码片段,展示了这个概念。

 //---------------------------------------------------------------------------------------- // 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..