web-dev-qa-db-ja.com

行優先と列優先の混乱

私はこれについて多くのことを読んできましたが、読むほど混乱してしまいます。

私の理解:行優先の行ではメモリに連続して保存され、列優先の列ではメモリに連続して保存されます。したがって、一連の数値[1, ..., 9]があり、それらを行優先行列に格納する場合、次のようになります。

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

一方、列のメジャー(間違っている場合は修正してください)は次のとおりです。

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

これは事実上前の行列の転置です。

私の混乱:まあ、私は違いを見ません。両方の行列を(最初の行で、2番目の列で)繰り返す場合、同じ値を同じ順序でカバーします:1, 2, 3, ..., 9

行列の乗算も同じです。最初の連続する要素を取得し、2番目の行列の列で乗算します。したがって、マトリックス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の列を取ることでC x Mになり、まったく同じになります。 R x Mの結果

すべてが同じである場合、なぜこれらの2つの用語が存在するのでしょうか。つまり、最初の行列Rでも、行を見て列を検討することができます...

何か不足していますか? row-majorとcol-majorは、実際に私の行列演算に何を意味しますか?線形代数クラスでは、最初の行列の行と2番目の行列の列を乗算することを常に学びましたが、最初の行列が列優先の場合、それは変わりますか?私の例で行ったように、列を2番目の行列の列で乗算する必要がありますか?

どんな説明でも本当に感謝しています!

EDIT:私が抱えている他の主な混乱の原因の1つはGLM ...です。そのため、マトリックスタイプにカーソルを合わせてF12キーを押して、それは実装されており、そこにはベクトル配列があります。したがって、3x3マトリックスがある場合、3つのベクトルの配列があります。それらのベクトルのタイプを見て、「col_type」を見たので、それらのベクトルのそれぞれが列を表すと仮定しました。

まあ、私は正直であることを知りません。翻訳行列をglmと比較するためにこの印刷関数を書きました。最後の行にglmの翻訳ベクトルが表示され、最後の列に私のものがあります...

enter image description here

これにより、混乱が増えるだけです。 glmTranslate行列の各ベクトルが行列の行を表していることがはっきりとわかります。だから...それは、行列が行優先であるということですか?私のマトリックスはどうですか? (フロート配列[16]を使用しています)変換値は最後の列にありますが、これは私の列が列優先であり、今はそうしなかったことを意味していますか? 頭の回転を止めようとする

15
vexe

まず代数を見てみましょう。代数には、「メモリレイアウト」などの概念さえありません。

代数povから、MxN実数行列は右側の| R ^ Nベクトルに作用して| R ^ Mベクトルを生成できます。

したがって、試験に座ってMxNマトリックスと| R ^ Nベクトルを与えられた場合、簡単な操作でそれらを掛け合わせて結果を得ることができます-その結果が正しいか間違っているかは、あなたの教授のソフトウェアに依存しません結果を確認するために内部で列優先レイアウトまたは行優先レイアウトを使用します。行列の各行の収縮をベクトルの(単一の)列で適切に計算したかどうかにのみ依存します。

正しい出力を生成するために、ソフトウェアは、どのような手段であっても、試験で行ったように、行列の各行を列ベクトルで基本的に縮小する必要があります。


したがって、列優先を調整するソフトウェアと行優先レイアウトを使用するソフトウェアの違いは、計算するwhatではなく、単にhow

もっと具体的に言うと、列ベクトルとの単一の行の縮約に関するこれらのレイアウトの違いは、単にを決定する手段です。

Where is the next element of the current row?
  • 行優先レイアウトの場合、メモリ内の次のバケットの要素になります
  • Column-major-layoutの場合、バケット内の要素はMバケット離れています。

以上です。


実際にその列/行の魔法がどのように呼び出されるかを示すには:

質問に「c ++」のタグを付けていませんが、「 glm 」と言ったため、できると思いますC++と仲良くなりましょう。

C++の標準ライブラリには、 valarray と呼ばれる悪名高い獣があります。これは、他のトリッキーな機能に加えて、オーバーロードを持っていますof operator [] 、そのうちの1つはstd::slice(本質的に非常に退屈なもので、3つの整数型の数字のみで構成されています)。

ただし、この小さなスライスには、行単位で行優先ストレージまたは行単位で列優先ストレージにアクセスするために必要なすべてのものがあります。開始、長さ、およびストライドがあり、後者は「次のバケツまでの距離」と言いました。

6
decltype_auto

実装の詳細と使用方法を混同していると思います。

2次元の配列または行列から始めましょう。

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

問題は、コンピューターのメモリがバイトの1次元配列であるということです。議論を簡単にするために、1バイトを4つのグループにグループ化します。したがって、次のようになります(各シングル、+-+はバイトを表し、4バイトは整数値を表します(32ビットオペレーティングシステムを想定))。

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

    low memory    ------>                          high memory

別の表現方法

したがって、問題は、2次元構造(マトリックス)をこの1次元構造(つまりメモリ)にマッピングする方法です。これを行うには2つの方法があります。

  1. 行優先順:この順序では、最初の行を最初にメモリに配置し、次に2番目の行を配置します。これを行うと、メモリ内に次のようになります。

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

このメソッドを使用すると、次の算術演算を実行して、配列の特定の要素を見つけることができます。配列の$ M_ {ij} $要素にアクセスするとします。配列の最初の要素へのポインタ、たとえばptrがあり、列の数がnColであるとわかっている場合、次の方法で任意の要素を見つけることができます。

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

これがどのように機能するかを確認するには、M_ {02}(つまり、最初の行、3番目の列を考慮してください。Cはゼロベースであることを思い出してください。

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

したがって、配列の3番目の要素にアクセスします。

  1. 列優先順:この順序では、最初の列を最初にメモリに配置し、次に2番目の列を配置します。これを行うと、メモリ内に次のようになります。

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

SO、簡潔な答え-行優先および列優先形式は、2つ(またはそれ以上)の次元配列がメモリの1次元配列にどのようにマッピングされるかを説明します。

お役に立てれば。 T.

11
thurizas

あなたが正しいです。システムがデータを行優先構造または列優先構造に格納したかどうかは関係ありません。これはプロトコルのようなものです。コンピューター:「ねえ、人間。このようにあなたのアレイを保管するつもりだ。問題ないよね?」ただし、パフォーマンスに関しては重要です。次の3つのことを考慮してください。

1。ほとんどの配列は行優先順でアクセスされます。

2。メモリにアクセスするとき、メモリから直接読み取られません。最初にメモリからキャッシュにデータのブロックをいくつか保存し、次にキャッシュからプロセッサにデータを読み取ります。

。必要なデータがキャッシュに存在しない場合、キャッシュはメモリからデータを再フェッチする必要があります

キャッシュがメモリからデータを取得するとき、局所性が重要です。つまり、データをメモリにまばらに格納する場合、キャッシュはより頻繁にメモリからデータをフェッチする必要があります。メモリへのアクセスはキャッシュへのアクセスよりはるかに遅い(100倍以上)ため、このアクションはプログラムのパフォーマンスを損ないます。メモリへのアクセスが少ないほど、プログラムは高速になります。そのため、この行優先配列は、そのデータへのアクセスがローカルである可能性が高いため、より効率的です。

3
Y.C.Jung

何を使用しても構いません。一貫性を保ってください。

行メジャーまたは列メジャーは単なる規則です。関係ありません。 Cは行メジャーを使用し、Fortranは列を使用します。両方とも機能します。プログラミング言語/環境の標準を使用してください。

2つが一致しないと、!@#$が詰まってしまいます。

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

行列の乗算を行うコードが行メジャーと列メジャーで同じであると言うのは誤りです

(もちろん、行列乗算の数学は同じです。)メモリに2つの配列があると想像してください。

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

行列が列メジャーに格納されている場合、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]

ここでは何も深く進行していません。これは、2つの異なる規則です。それは、マイルまたはキロメートルで測定するようなものです。どちらも機能しますが、変換せずに2つの間を行き来することはできません。

3
Matthew Gunn

わかりました。「混乱」という言葉が文字通りタイトルにあるので、混乱のレベルを理解できます。

まず、これは絶対に本当の問題です

「これは使われているが... PCは最近だ...」という考えに決して屈することはありません。

主な問題は次のとおりです:-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

costspeed.の並置

今日の標準的なPCシステムでは、これは次のようになります:SIZE: 500GB HDD > 8GB RAM > L2 Cache > L1 Cache > Registers. SPEED: 500GB HDD < 8GB RAM < L2 Cache < L1 Cache < Registers.

これは、時間的および空間的局所性のアイデアにつながります。 1つはhowデータが整理されていることを意味します(コード、ワーキングセットなど)、もう1つは物理的にwhereデータは「メモリ」に整理されます。

今日のPCの「ほとんど」がlittle-endian(Intel)マシンであるため、特定のリトルエンディアンの順序でデータをメモリに配置します。基本的にビッグエンディアンとは異なります。

https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html (むしろカバー... swiftly;))

(この例の単純化のために、私は物事が単一のエントリで起こると「言う」つもりです、これは間違っています、キャッシュブロック全体は通常アクセスされ、私のメーカー、大幅に少ないモデルで大幅に異なります)。

hypotheticallyプログラムが1GB of data from your 500GB HDD、あなたの8GB of RAM,その後cache階層に入り、最終的にregistersになります。ここで、プログラムは2番目(コード内)の目的のエントリが発生するように、最新のキャッシュラインから最初のエントリを読み取ります。 next cache line,(つまり、次の[〜#〜] row [〜#〜]columnの代わりにキャッシュがあります[〜#〜] miss [〜#〜]

cacheがいっぱいであると仮定します。なぜなら、それはsmallであるためです。必要な次のデータを「持っている」行のためのスペースを空けるために、行が削除されます。このパターンが繰り返されると、[〜#〜] miss [〜#〜]on[〜#〜] every [ 〜#〜]データ取得を試みました!

さらに悪いことに、実際に必要な有効なデータがある行を削除するので、それらを取得する必要がありますAGAINおよびAGAIN.

この用語は次のように呼ばれます:thrashing

https://en.wikipedia.org/wiki/Thrashing_(computer_science) そして、実際にcrash書きにくい/エラーが発生しやすいシステムです。 (ウィンドウを考える[〜#〜] bsod [〜#〜])....

一方、データを適切にレイアウトしていた場合(行メジャー)... youWOULDにはまだミスがあります!

しかし、これらのミスはonlyであり、すべての検索試行ではなく、各検索の最後に発生します。システムとプログラムのパフォーマンスの違いの桁。

非常に単純なスニペット:

#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.oreal 0m0.009suser 0m0.003ssys 0m0.004s

行メジャーの繰り返し:

#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

Compile:terminal4$ gcc -g row_maj.c -o row.oRun:time ./row.oreal 0m0.005suser 0m0.001ssys 0m0.003s

ご覧のとおり、Row Majorの方がかなり高速でした。

納得できない?もっと劇的な例をご覧になりたい場合:行列を1000000 x 1000000にして、初期化し、転置して標準出力に出力します。 `` `

(注、* NIXシステムでは、ulimitを無制限に設定する必要があります)

ISSUESと私の回答:-Optimizing compilers, they change a LOT of things! -Type of system -Please point any others out -This system has an Intel i5 processor

1
eulerworks

上記の説明を考えると、概念を示す コードスニペット がここにあります。

//----------------------------------------------------------------------------------------
// 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)
*/
0
eholley

上記の回答に対する短い補遺。 Cでは、メモリがほぼ直接アクセスされるため、行優先または列優先の順序は、2つの方法でプログラムに影響します。1.メモリ内のマトリックスのレイアウトに影響します。2.保持する必要のある要素アクセスの順序-順序付けループの形式。

  1. 前の回答で非常に徹底的に説明されているので、2に追加します。

eulerworksの回答は、彼の例では、行メジャーマトリックスを使用すると計算が大幅に遅くなると指摘しています。まあ、彼は正しいですが、結果は同時に逆転する可能性があります。

ループの順序は、for(over rows){for(over columns){matrix over何かを行う}}でした。つまり、デュアルループは行の要素にアクセスしてから、次の行に移動します。たとえば、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){行列に対して何かを行う}}のようになります。この場合、結果はまったく逆になります。ループは列の要素を線形に読み取るため、列の主要な計算は高速になります。

したがって、これを覚えておいてください。1.従来のCプログラミングコミュニティは行優先形式を好むように見えますが、行優先形式または列優先形式を選択するのはあなたの好み次第です。 2.好きなものを自由に選択できますが、インデックス付けの概念と一致する必要があります。 3.また、これは非常に重要です。独自のアルゴリズムを書き留めるときは、選択したストレージ形式を尊重するようにループを順序付けてください。 4.一貫性を保つ。

0
Juwhan Kim

今日、列優先以外の順序を使用する理由はありません。c/ c ++でそれをサポートするいくつかのライブラリがあります(eigen、armadillo、...)。さらに、列優先の順序はより自然です。 [x、y、z]を含む写真は、ファイルのスライスごとに保存されます。これは列優先順です。 2次元ではより良い順序を選択するのは混乱するかもしれませんが、より高い次元では、多くの状況で列優先順序が唯一の解決策であることは明らかです。

Cの作成者は配列の概念を作成しましたが、おそらく誰かがそれを行列として使用することを期待していなかったでしょう。すでにすべてがフォートランと列優先の順序で構成されている場所で配列がどのように使用されるかを見た場合、私はショックを受けます。行優先順序は列優先順序の単なる代替であると思いますが、本当に必要な状況でのみです(今のところ、私は何も知りません)。

奇妙なことに、まだ誰かが行優先の順序でライブラリを作成しています。それは不必要なエネルギーと時間の浪費です。いつかすべてが列優先の順序になり、すべての混乱が単に消えることを願っています。

0
Paul