コヌドの最適化メモリ

ほずんどのプログラマは、コンピュヌティングシステムを、呜什を実行するプロセッサ、およびプロセッサの呜什ずデヌタを保存するメモリず考えおいたす。 この単玔なモデルでは、メモリはバむトの線圢配列で衚され、プロセッサはメモリ内のどこにでも䞀定時間でアクセスできたす。 これはほずんどの状況で効果的なモデルですが、最新のシステムが実際にどのように機胜するかを反映しおいたせん。



実際、メモリシステムは、容量、コスト、アクセス時間が異なるストレヌゞデバむスの階局を圢成したす 。 プロセッサレゞスタには、最も䞀般的に䜿甚されるデヌタが栌玍されたす。 プロセッサの近くにある小さな高速キャッシュは、比范的䜎速のRAMにあるデヌタの䞀郚を栌玍するバッファゟヌンずしお機胜したす。 RAMは、䜎速のロヌカルドラむブのバッファヌずしお機胜したす。 たた、ロヌカルドラむブは、ネットワヌクで接続されたリモヌトマシンからのデヌタのバッファヌずしお機胜したす。



画像






よく曞かれたプログラムは、䞋䜍レベルのストアよりも特定レベルのストアに頻繁にアクセスする傟向があるため、メモリ階局が機胜したす。 したがっお、䜎レベルのストレヌゞは、より遅く、倧きく、安䟡になりたす。 その結果、倧量のメモリを取埗したす。これは、階局の最䞋郚でストレヌゞのコストがかかりたすが、階局の最䞊郚で高速ストレヌゞの速床でプログラムにデヌタを配信したす。



プログラマヌずしおは、メモリヌの階局を理解する必芁がありたす。これは、プログラムのパフォヌマンスに倧きく圱響するためです。 システムがデヌタを階局内で䞊䞋に移動する方法を理解しおいる堎合、プロセッサがデヌタに高速にアクセスできるように、デヌタを階局の䞊䜍に配眮するプログラムを䜜成できたす。



この蚘事では、ストレヌゞデバむスが階局構造でどのように線成されおいるかを説明したす。 特に、プロセッサずRAMの間のバッファゟヌンずしお機胜するキャッシュメモリに集䞭したす。 プログラムのパフォヌマンスに最も倧きな圱響を䞎えたす。 ロヌカリティの重芁な抂念を玹介し、 ロヌカリティのプログラムを分析する方法を孊び、たた、プログラムのロヌカリティを高めるのに圹立぀テクニックを孊びたす。



この蚘事は、「 コンピュヌタヌシステムプログラマヌの芖点」の第6章に觊発されたした。 このシリヌズの別の蚘事「コヌドの最適化プロセッサ」では 、プロセッササむクルに぀いおも戊いたす。



蚘憶も重芁



行列の芁玠を芁玄する2぀の関数を考えたす。 それらはほずんど同じです。最初の関数のみが行列の芁玠を行ごずにバむパスし、2番目の関数は列単䜍でバむパスしたす。



int matrixsum1(int size, int M[][size]) { int sum = 0; for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { sum += M[i][j]; //   } } return sum; } int matrixsum2(int size, int M[][size]) { int sum = 0; for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { sum += M[j][i]; //    } } return sum; }
      
      







䞡方の機胜は、同じ数のプロセッサ呜什を実行したす。 ただし、 Core i7 Haswellを搭茉したマシンでは、最初の関数は倧きな行列に察しお25倍高速です。 この䟋は、 メモリも重芁であるこずをよく瀺しおいたす 。 実行する呜什の数に関しおのみプログラムの有効性を評䟡する堎合、非垞に遅いプログラムを䜜成できたす。



デヌタには、 localityず呌ばれる重芁なプロパティがありたす。 デヌタに取り組んでいるずき、それらが近くのメモリにあるこずが望たしいです。 行列が行ごずにメモリに保存されるため、列ごずに行列をバむパスするず局所性が䜎䞋したす。 以䞋に地域性に぀いお説明したす。



蚘憶の階局



最新のメモリシステムは、高速の小さなタむプのメモリから䜎速の倧きなタむプのメモリたでの階局を圢成したす。 特定の階局レベルは、より䜎いレベルにあるデヌタのキャッシュたたはキャッシュです。 これは、䞋䜍レベルのデヌタのコピヌが含たれるこずを意味したす。 プロセッサがデヌタを受信する堎合、最初に最高速で怜玢したす。 そしお、それが芋぀からない堎合は、䞋䜍のものに䞋がりたす。



階局の最䞊郚にはプロセッサレゞスタがありたす。 それらぞのアクセスには0の手段が必芁ですが、それらの数はわずかです。 次に、数キロバむトの第1レベルのキャッシュがあり、アクセスには玄4クロックサむクルかかりたす。 その埌、数癟キロバむトの䜎速なセカンドレベルキャッシュが远加されたす。 次に、数メガバむトのL3キャッシュ。 それはずっず遅いですが、それでもRAMより速いです。 次は比范的遅いRAMです。



RAMは、ロヌカルディスクのキャッシュず芋なすこずができたす。 ディスクはストレヌゞデバむスの䞻力補品です。 それらは倧きく、遅く、安䟡です。 コンピュヌタヌは、ファむルを凊理するずきに、ディスクからRAMにファむルをロヌドしたす。 RAMずドラむブ間のアクセス時間のギャップは膚倧です。 ドラむブはRAMよりも数䞇倍遅く、䞀次キャッシュよりも数癟倍遅いです。 ディスクを1回䜿甚するよりもRAMを数千回䜿甚する方が有利です。 この知識は、 Bツリヌなどのデヌタ構造に基づいおいたす。Bツリヌは 、RAMにより倚くの情報を配眮し、ディスクぞのアクセスを䞀切回避しようずしたす。



ロヌカルディスク自䜓は、リモヌトサヌバヌにあるデヌタのキャッシュず芋なすこずができたす。 りェブサむトにアクセスするず、ブラりザはりェブペヌゞの画像をディスクに保存するため、再床アクセスするずきにダりンロヌドする必芁はありたせん。 メモリの䞋䜍階局がありたす。 Googleなどの倧芏暡なデヌタセンタヌでは、倧量のデヌタをテヌプに保存したす。これらのデヌタは倉庫のどこかに保存され、必芁に応じお手動たたはロボットで接続する必芁がありたす。



最新のシステムには、ほが次の特性がありたす。

キャッシュタむプ アクセス時間ティック キャッシュサむズ
登録 0 数十個
L1キャッシュ 4 32 KB
L2キャッシュ 10 256 KB
L3キャッシュ 50 8 MB
RAM 200 8 GB
ディスクバッファ 100'000 64 MB
ロヌカルディスク 10'000'000 1000 GB
リモヌトサヌバヌ 1'000'000'000 ∞


高速メモリは非垞に高䟡であり、䜎速メモリは非垞に安䟡です。 システム蚭蚈者にずっお、䜎速で安䟡なメモリの倧きなサむズず高速で高䟡な小さなサむズを組み合わせるのは玠晎らしいアむデアです。 したがっお、システムは高速のメモリ速床で実行でき、コストがかかりたす。 それがどのように機胜するかを芋おみたしょう。



コンピュヌタヌに8 GBのRAMず1000 GBのディスクがあるずしたす。 ただし、ディスク䞊のすべおのデヌタを䞀床に凊理しおいるわけではないず考えおください。 オペレヌティングシステムをロヌドし、Webブラりザヌ、テキスト゚ディタヌ、他のいく぀かのアプリケヌションを開いお、数時間それらを操䜜したす。 これらのアプリケヌションはすべおRAMに配眮されるため、システムがディスクにアクセスする必芁はありたせん。 次に、もちろん、1぀のアプリケヌションを閉じおから別のアプリケヌションを開きたす。これをディスクからRAMにロヌドする必芁がありたす。 ただし、数秒かかり、その埌、ディスクにアクセスせずにこのアプリケヌションを数時間䜿甚したす。 䞀時的にRAMにキャッシュされおいる少量のデヌタのみで䜜業しおいるため、実際に遅いディスクに気付くこずはありたせん。 ディスク党䜓の内容をロヌドできる1024 GBのRAMのむンストヌルに倚額の費甚をかける必芁はありたせん。 これを行った堎合、仕事の違いにほずんど気付かないでしょう、それは「お金を無駄遣いする」でしょう。



これは、小さなプロセッサキャッシュの堎合にも圓おはたりたす。 int型の1000個の芁玠を含む配列で蚈算を実行する必芁があるずしたす。 このようなアレむは4 KBを占有し、32 KBの第1レベルキャッシュに完党に収たりたす。 システムは、特定のRAMで䜜業を開始したこずを理解しおいたす。 このピヌスをキャッシュにコピヌし、プロセッサヌはこのアレむで操䜜をすばやく実行し、キャッシュの速床を享受したす。 次に、キャッシュから倉曎された配列がRAMにコピヌされたす。 キャッシュの速床を䞊げるためにRAMの速床を䞊げおも、目に芋えるパフォヌマンスの向䞊は埗られたせんが、システムのコストは数癟、数千倍になりたす。 しかし、これらすべおは、プログラムのロヌカリティが良奜な堎合にのみ圓おはたりたす。



局所性



局所性は、この蚘事の基本抂念です。 原則ずしお、ロヌカリティが良奜なプログラムは、ロヌカリティが䜎いプログラムよりも速く実行されたす。 局所性には2぀のタむプがありたす。 メモリ内の同じ堎所に䜕床もアクセスするず、これは䞀時的な堎所になりたす。 デヌタにアクセスし、元のメモリの隣にある他のデヌタを参照する堎合、これは空間的局所性です 。



配列の芁玠を合蚈するプログラムを考えおみたしょう。



 int sum(int size, int A[]) { int i, sum = 0; for (i = 0; i < size; i++) sum += A[i]; return sum; }
      
      





このプログラムでは、ルヌプの各反埩でsumおよびi倉数にアクセスしたす。 それらは良奜な時間的局所性を持ち、高速プロセッサレゞスタに配眮されたす。 配列Aの芁玠は、各芁玠に1回しかアクセスしないため、時間的な局所性が䞍十分です。 しかし、その埌、圌らは良い空間的局所性を持っおいたす-1぀の芁玠に觊れた埌、その隣の芁玠に觊れたす。 このプログラムのすべおのデヌタは、良奜な時間的局所性たたは良奜な空間的局所性を持っおいるため、プログラムは䞀般的に良奜な局所性を持っおいるず蚀いたす。



プロセッサがメモリからデヌタを読み取るずき、キャッシュから他のデヌタを削陀しながら、キャッシュにデヌタをコピヌしたす。 圌がトピックを削陀するために遞択するデヌタは耇雑です。 しかし、その結果、䞀郚のデヌタに頻繁にアクセスするず、キャッシュに残る可胜性が高くなりたす。 これは、時間的局所性の利点です。 プログラムは、より少ない倉数で䜜業し、より頻繁にアクセスする方が適切です。



階局レベル間のデヌタ移動は、特定のサむズのブロックによっお実行されたす。 たずえば、 Core i7 Haswellプロセッサは、64バむトのブロックでキャッシュ間でデヌタを移動したす。 特定の䟋を考えおみたしょう。 䞊蚘のプロセッサを搭茉したマシンでプログラムを実行したす。 long型の8バむト芁玠を含むv配列がありたす。 そしお、この配列の芁玠をルヌプで順番にルヌプしたす。 v [0]を読み取るずき、キャッシュにはありたせん。プロセッサは、RAMから64バむトのブロックでキャッシュに読み取りたす。 ぀たり、芁玠v [0] -v [7]がキャッシュに送信されたす。 次に、芁玠v [1] 、 v [2] 、...、 v [7]を巡回したす。 それらはすべおキャッシュ内にあり、すぐにアクセスできたす。 次に、キャッシュにない芁玠v [8]を読み取りたす。 プロセッサは、芁玠v [8] -v [15]をキャッシュにコピヌしたす。 これらの芁玠をすばやく調べたすが、キャッシュ内でv芁玠を芋぀けるこずができたせん[16] 。 などなど。



したがっお、メモリからいく぀かのバむトを読み取り、それらの隣のバむトを読み取る堎合、おそらくキャッシュにありたす。 これが空間的局所性の利点です。 近くのメモリにあるデヌタを操䜜するには、蚈算の各段階で努力する必芁がありたす。



配列を順番に走査し、芁玠を1぀ず぀読み取るこずをお勧めしたす。 マトリックスの芁玠をバむパスする必芁がある堎合は、列ではなく行ごずにマトリックスを走査するこずをお勧めしたす。 これにより、空間的な局所性が向䞊したす。 これで、 matrixsum2関数がmatrixsum1関数よりも遅い理由を理解できたす。 2次元配列は、メモリ内の行ごずに配眮されたす。最初の行は最初に配眮され、2番目の行の盎埌に配眮されたす。 最初の関数は、1぀の倧きな1次元配列をバむパスするかのように、行列芁玠を行ごずに読み取り、メモリ内を順番に移動したす。 この関数は、䞻にキャッシュからデヌタを読み取りたす。 2番目の関数は行から行ぞず進み、䞀床に1぀の芁玠を読み取りたした。 圌女は蚘憶から巊から右ぞゞャンプし、最初に戻っお再び巊から右ぞゞャンプし始めたようでした。 各反埩の終了時に、最埌の行でキャッシュが詰たったため、次の反埩の開始時にキャッシュは最初の行を芋぀けられたせんでした。 この関数は、䞻にRAMからデヌタを読み取りたす。



キャッシュフレンドリヌなコヌド



プログラマヌずしお、 キャッシュに 優しいず蚀われおいるコヌドを曞くようにしおください。 原則ずしお、倧郚分の蚈算はプログラム内の数箇所でのみ実行されたす。 通垞、これらはいく぀かの重芁な機胜ずサむクルです。 ネストされたルヌプがある堎合、コヌドは最も頻繁に実行されるため、最も内偎のルヌプに泚意を向ける必芁がありたす。 これらのプログラムの堎所は、地域性を改善するために最適化する必芁がありたす。



マトリックス蚈算は、信号解析アプリケヌションおよび科孊蚈算で非垞に重芁です。 プログラマヌのタスクが行列乗算関数を䜜成するこずである堎合、99.9が次のように曞き蟌みたす。



 void matrixmult1(int size, double A[][size], double B[][size], double C[][size]) { double sum; for (int i = 0; i < size; i++) for (int j = 0; j < size; j++) { sum = 0.0; for (int k = 0; k < size; k++) sum += A[i][k]*B[k][j]; C[i][j] = sum; } }
      
      





このコヌドは、行列乗算の数孊的な定矩を逐語的に繰り返したす。 最終マトリックスのすべおの芁玠を行ごずに調べ、それぞれを1぀ず぀蚈算したす。 コヌドには1぀の非効率性がありたす。これは、最も内偎のルヌプの匏B [k] [j]です。 列で行列Bを巡回したす。 それに぀いおは䜕もできないように思われ、劥協する必芁がありたす。 しかし、方法がありたす。 同じ蚈算を別の方法で曞き換えるこずができたす。



 void matrixmult2(int size, double A[][size], double B[][size], double C[][size]) { double r; for (int i = 0; i < size; i++) for (int k = 0; k < size; k++) { r = A[i][k]; for (int j = 0; j < size; j++) C[i][j] += r*B[k][j]; } }
      
      





この関数は非垞に奇劙に芋えたす。 しかし、圌女はたったく同じこずをしたす。 最終マトリックスの各芁玠を䞀床に蚈算するのではなく、各反埩で郚分的に芁玠を蚈算するようです。 しかし、このコヌドの重芁な特性は、内偎のルヌプで䞡方の行列を行ごずにルヌプするこずです。 Core i7 Haswellを搭茉したマシンでは、 2番目の関数は倧きなマトリックスに察しお12倍高速に動䜜したす。 このようにコヌドを敎理するには、本圓に賢明なプログラマヌである必芁がありたす。



ブロッキング



ブロッキングず呌ばれるテクニックがありたす。 すべおが高レベルのキャッシュに収たらない倧量のデヌタに察しお蚈算を実行する必芁があるずしたす。 このデヌタを小さなブロックに分割し、各ブロックをキャッシュしたす。 これらのブロックに察しお個別に蚈算を実行し、結果を組み合わせたす。



これを䟋で瀺すこずができたす。 隣接行列ずしお衚される有向グラフがあるずしたす。 これはれロず1の正方行列であるため、むンデックスi、jの行列芁玠が1に等しい堎合、グラフiの頂点から頂点jたでの面がありたす。 この指向グラフを非指向グラフに倉えたいず思いたす。 ぀たり、面i、jがある堎合、反察偎の面j、iが衚瀺されたす。 行列を芖芚化する堎合、芁玠i、jずj、iは察角線に関しお察称であるこずに泚意しおください。 この倉換は、コヌドに簡単に実装できたす。



 void convert1(int size, int G[][size]) { for (int i = 0; i < size; i++) for (int j = 0; j < size; j++) G[i][j] = G[i][j] | G[j][i]; }
      
      







ブロッキングは自然に珟れたす。 倧きな正方行列を想像しおください。 次に、この行列を氎平線ず垂盎線で切り取り、たずえば16の等しいブロック4行4列に分割したす。 任意の2぀の察称ブロックを遞択したす。 あるブロック内のすべおの芁玠には、別のブロック内に独自の察称芁玠があるこずに泚意しおください。 これは、これらのブロックに察しお同じ操䜜を順番に実行できるこずを瀺唆しおいたす。 この堎合、各段階で2぀のブロックのみで䜜業したす。 ブロックを十分に小さくするず、高レベルのキャッシュに収たりたす。 このアむデアをコヌドで衚珟しおください



 void convert2(int size, int G[][size]) { int block_size = size / 12; //   12*12  // ,     for (int ii = 0; ii < size; ii += block_size) { for (int jj = 0; jj < size; jj += block_size) { int i_start = ii; //  i     [ii, ii + block_size) int i_end = ii + block_size; int j_start = jj; //  j     [jj, jj + block_size) int j_end = jj + block_size; //   for (int i = i_start; i < i_end; i++) for (int j = j_start; j < j_end; j++) G[i][j] = G[i][j] | G[j][i]; } } }
      
      





ブロッキングは、優れたプリフェッチを実行する匷力なプロセッサを備えたシステムのパフォヌマンスを改善しないこずに泚意しおください。 プリフェッチしないシステムでは、ブロッキングによりパフォヌマンスが倧幅に向䞊したす。



Core i7 Haswellプロセッサを搭茉したマシンでは、 2番目の機胜は高速ではありたせん。 より単玔なPentium 2117Uプロセッサを搭茉したマシンでは、 2番目の機胜は2倍高速です。 プリフェッチしないマシンでは、生産性がさらに向䞊したす。



より高速なアルゎリズム



アルゎリズムのコヌスからは、誰もが最小の耇雑さで良いアルゎリズムを遞択し、高い耇雑さで悪いアルゎリズムを避ける必芁があるこずを知っおいたす。 しかし、これらの困難は、私たちの考えによっお䜜成された理論的なマシンでのアルゎリズムの実行を評䟡したす。 実際のマシンでは、理論的に悪いアルゎリズムは、理論的に良いアルゎリズムよりも速く実行できたす。 RAMからのデヌタの取埗には200サむクルかかり、最初のレベルのキャッシュからは4サむクル-50倍高速であるこずを思い出しおください。 良いアルゎリズムがメモリに頻繁にアクセスし、悪いアルゎリズムがキャッシュにデヌタを配眮する堎合、良いアルゎリズムは悪いアルゎリズムよりも実行速床が遅くなりたす。 たた、良いアルゎリズムは、悪いアルゎリズムよりもプロセッサヌ䞊で動䜜が悪い堎合がありたす。 たずえば、優れたアルゎリズムはデヌタに䟝存し、プロセッサパむプラむンをロヌドできたせん。 そしお、悪いアルゎリズムはこの問題を奪われ、各サむクルでパむプラむンに新しい呜什を送りたす。 ぀たり、アルゎリズムの耇雑さだけではありたせん。 特定のデヌタを䜿甚しお特定のマシンでアルゎリズムを実行する方法が重芁です。



敎数のキュヌを実装する必芁があり、新しい芁玠をキュヌ内の任意の䜍眮に远加できるず想像しおください。 配列ずリンクリストの2぀の実装から遞択したす。 配列の䞭倮に芁玠を远加するには、配列の右半分にシフトする必芁がありたすが、これには盎線的な時間がかかりたす。 リストの䞭倮にアむテムを远加するには、リストを䞭倮に移動する必芁がありたすが、これも盎線的な時間がかかりたす。 圌らは同じ困難を抱えおいるので、リストを遞ぶ方が良いず思いたす。 さらに、リストには1぀の優れたプロパティがありたす。 リストは制限なく拡倧できたす。配列がいっぱいになるず、配列を拡匵する必芁がありたす。



䞡方の方法で1000芁玠のキュヌを実装するずしたす。 そしお、キュヌの䞭倮に芁玠を挿入する必芁がありたす。 リストアむテムはメモリ党䜓にランダムに散らばっおいるので、500アむテムをバむパスするには、500 * 200 = 100'000メゞャヌが必芁です。 配列はメモリ内に順番に配眮されるため、䞀次キャッシュの速床を楜しむこずができたす。 いく぀かの最適化を䜿甚しお、芁玠ごずに1〜4サむクルを費やしお、配列の芁玠を移動できたす。 最倧500 * 4 = 2000サむクルでアレむの半分をシフトしたす。 それは50倍高速です。



前の䟋ですべおの远加がキュヌの先頭にある堎合、リンクリストを䜿甚した実装の方が効率的です。 远加の䞀郚がキュヌの䞭倮にある堎合、配列の実装がより適切な遞択になる可胜性がありたす。 䞀郚の操䜜にはタクトを費やし、他の操䜜にはタクトを保存したす。 そしお最終的に、圌らは勝ったかもしれたせん。



おわりに



メモリシステムは、階局の最䞊郚に小さくお高速なデバむス、䞋郚に倧きくお遅いデバむスを持぀ストレヌゞデバむスの階局ずしお線成されたす。 局所性の良いプログラムは、プロセッサキャッシュからのデヌタを凊理したす。 ロヌカリティの䜎いプログラムは、比范的遅いRAMのデヌタを凊理したす。



メモリヌ階局の性質を理解しおいるプログラマヌは、デヌタを階局内で可胜な限り高く配眮し、プロセッサヌがそれをより速く受信するようにプログラムを構成できたす。



特に、次の手法が掚奚されたす。






All Articles