" キャッシュフレンドリーでないコード "と " キャッシュフレンドリー "コードの違いは何ですか?
キャッシュ効率の良いコードを書くようにするにはどうすればいいですか?
最新のコンピューターでは、最低レベルのメモリ構造(registers)のみが1クロックサイクルでデータを移動できます。ただし、レジスタは非常に高価であり、ほとんどのコンピューターコアには数十個未満のレジスタがあります(数百から数千バイト合計)。メモリスペクトルのもう一方の端(DRAM)では、メモリは非常に安価です(つまり、文字通り何百万倍も安い =)しかし、データを受信する要求の後、数百サイクルかかります。超高速で高価なものと超低速で安価なものの間のこのギャップを埋めるのは、キャッシュメモリです。これは、実行中のコードのほとんどが小さな変数セットに頻繁にヒットし、残り(はるかに大きな変数セット)がまれにヒットするという考え方です。プロセッサがL1キャッシュでデータを見つけられない場合、L2キャッシュで検索します。存在しない場合はL3キャッシュ、存在しない場合はメインメモリ。これらの「ミス」はそれぞれ時間がかかります。
(システムメモリはハードディスクストレージであるため、キャッシュメモリはシステムメモリに対するものです。ハードディスクストレージは非常に安価ですが非常に遅いです)。
キャッシュは、latencyの影響を減らすための主要な方法の1つです。ハーブサッターを言い換えると(下のリンク):帯域幅を増やすのは簡単ですが、待ち時間から抜け出すことはできません。
データは常にメモリ階層を通じて取得されます(最小==最速から低速)。 cache hit/missは通常、CPUの最高レベルのキャッシュでのヒット/ミスを指します。最高レベルとは、最大==最も遅いことを意味します。すべてのキャッシュミスはRAM(またはさらに悪いことに)からデータをフェッチするため、キャッシュヒット率はパフォーマンスにとって重要です[lot =時間(RAMでは数百サイクル、HDDでは数千万サイクル)。それに比べて、(最高レベルの)キャッシュからのデータの読み取りには通常、ほんの数サイクルしかかかりません。
現代のコンピューターアーキテクチャでは、パフォーマンスのボトルネックはCPUダイを残しています(たとえば、RAM以上にアクセスします)。これは時間とともに悪化するだけです。現在、プロセッサ周波数の増加は、パフォーマンスの向上とは関係ありません。 問題はメモリアクセスです。CPUのハードウェア設計の取り組みは、現在、キャッシュ、プリフェッチ、パイプライン、および同時実行の最適化に重点を置いています。たとえば、最新のCPUはダイの約85%をキャッシュに、最大99%をデータの保存/移動に費やしています!
この問題については、多くのことが言われています。キャッシュ、メモリ階層、適切なプログラミングに関する優れたリファレンスを次に示します。
キャッシュフレンドリーなコードの非常に重要な側面は、ローカリティの原則であり、その目的はメモリ内のデータを閉じて、効率的なキャッシュを可能にします。 CPUキャッシュに関しては、キャッシュラインに注意して、これがどのように機能するかを理解することが重要です。 キャッシュラインはどのように機能しますか?
キャッシングを最適化するには、次の特定の側面が非常に重要です。
適切な使用 c ++ containers
キャッシュフレンドリとキャッシュフレンドリの簡単な例は、 c ++ のstd::vector
対std::list
です。 std::vector
の要素は連続したメモリに格納されます。したがって、それらへのアクセスはmuchのコンテンツを保存するstd::list
の要素にアクセスするよりもキャッシュフレンドリーです。場所。これは空間的な局所性によるものです。
この非常に素晴らしいイラストは、Bjarne Stroustrupが このyoutubeクリップ で提供しています(リンクについては@Mohammad ALi Baydounに感謝します!)。
データ構造とアルゴリズム設計でキャッシュを無視しないでください
可能な限り、キャッシュを最大限に使用できるように、データ構造と計算の順序を調整してください。これに関する一般的な手法は、 キャッシュブロッキング(Archive.orgバージョン) です。これは、高性能コンピューティングで非常に重要です(たとえば、 アトラス )。
データの暗黙的な構造を知って活用する
フィールドの多くの人が時々忘れる別の簡単な例は、列優先(例 fortran 、 matlab )対行優先順序(例 c 、 c ++ )2次元配列を保存します。たとえば、次のマトリックスを考えます。
1 2
3 4
行優先順では、これは1 2 3 4
;としてメモリに保存されます。列優先の順序では、これは1 3 2 4
として保存されます。この順序付けを利用しない実装では、キャッシュの問題がすぐに(簡単に回避可能に!)発生することがわかります。残念ながら、私のドメインではこのようなものがよく見られますvery(機械学習)。 @MatteoItaliaは、回答でこの例をより詳細に示しました。
マトリックスの特定の要素をメモリから取得する場合、それに近い要素も取得され、キャッシュラインに保存されます。順序が悪用されると、これによりメモリアクセスが少なくなります(後続の計算に必要な次のいくつかの値が既にキャッシュラインにあるため)。
簡単にするために、キャッシュは2つの行列要素を含むことができる単一のキャッシュラインで構成され、特定の要素がメモリからフェッチされると、次の要素も同じであると仮定します。上記の例の2x2マトリックスのすべての要素の合計を取得するとします(M
と呼びます):
順序付けの活用(例: c ++ で最初に列インデックスを変更する):
M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses
順序付けを活用しない(例: c ++ で最初に行インデックスを変更する):
M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses
この単純な例では、順序付けを利用すると実行速度が約2倍になります(メモリアクセスには合計の計算よりもはるかに多くのサイクルが必要になるため)。実際には、パフォーマンスの差はmuchより大きくなる可能性があります。
予測不可能な分岐を避ける
最新のアーキテクチャはパイプラインを備えており、コンパイラはメモリアクセスによる遅延を最小限に抑えるためにコードを並べ替えるのに非常に優れています。重要なコードに(予測不可能な)分岐が含まれている場合、データをプリフェッチすることは困難または不可能です。これにより、間接的にキャッシュミスが増加します。
これはveryここで説明されています(リンクの@ 0x90に感謝します): なぜソートされていない配列を処理するよりもソートされた配列を処理するのが速いのですか?
仮想機能を避ける
c ++ のコンテキストでは、virtual
メソッドはキャッシュミスに関して議論のある問題を表しています(パフォーマンスの観点から可能な場合は避けるべきであるという一般的なコンセンサスが存在します)。仮想関数はルックアップ中にキャッシュミスを引き起こす可能性がありますが、これは起こるだけですif特定の関数が頻繁に呼び出されない(そうでなければキャッシュされる可能性が高い)ので、これは問題ではないと見なされます。この問題に関する参照については、以下を確認してください。 C++クラスに仮想メソッドを使用することによるパフォーマンスコストはどれくらいですか?
マルチプロセッサキャッシュを備えた最新のアーキテクチャの一般的な問題は、 false sharing と呼ばれます。これは、個々のプロセッサがそれぞれ別のメモリ領域でデータを使用しようとし、同じcache lineにデータを保存しようとしたときに発生します。これにより、別のプロセッサが使用できるデータを含むキャッシュラインが何度も上書きされます。事実上、この状況でキャッシュミスが発生すると、異なるスレッドが互いに待機します。参照(リンクについては@Mattに感謝): キャッシュラインサイズに合わせる方法とタイミングは?
RAMメモリのキャッシングが不十分であるという極端な症状(このコンテキストではおそらくそうではないでしょう)は、いわゆる スラッシング です。これは、プロセスがディスクアクセスを必要とするページフォールトを継続的に生成する(たとえば、現在のページにないメモリにアクセスする)ときに発生します。
@Marc Claesenの答えに加えて、キャッシュにやさしいコードの典型的な有益な例は、行ごとではなく列ごとにCの2次元配列(たとえばビットマップイメージ)をスキャンするコードであると思います。
行内で隣接している要素もメモリ内で隣接しているため、順番にアクセスするということは、メモリの昇順でアクセスするということです。キャッシュは連続したメモリブロックをプリフェッチする傾向があるため、これはキャッシュに適しています。
同じ列の要素はメモリ内で互いに離れているため(特に、それらの距離は行のサイズに等しいため)、このアクセスパターンを使用すると、そのような要素に列方向にアクセスするのはキャッシュに不便です。はメモリ内で飛び跳ねているため、メモリ内の近くの要素を取得するというキャッシュの労力を無駄に消費しています。
パフォーマンスを台無しにするのに必要なのは、
// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
for(unsigned int x=0; x<width; ++x)
{
... image[y][x] ...
}
}
に
// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
for(unsigned int y=0; y<height; ++y)
{
... image[y][x] ...
}
}
この効果は、小さなキャッシュを持つシステムや大きな配列を扱うシステム(たとえば、現在のマシンでは10メガピクセル24 bppの画像)で非常に劇的(数桁のスピード)になることがあります。このため、多くの垂直スキャンを実行する必要がある場合は、最初に90度の画像を回転させ、後でさまざまな分析を実行して、キャッシュに関係のないコードを回転だけに制限することをお勧めします。
キャッシュ使用量の最適化は、主に2つの要素にかかっています。
最初の要因(他の人がすでに暗示している)は、参照地域です。ただし、参照の局所性には、実際には2つの側面があります。それは、空間と時間です。
空間の次元も2つのことになります。まず、情報を密に詰め込みたいので、より多くの情報がその限られたメモリに収まります。これは、(たとえば)ポインタで結合された小さなノードに基づいてデータ構造を正当化するために、計算上の複雑さを大幅に改善する必要があることを意味します。
第二に、一緒に処理される情報も一緒に配置したい。典型的なキャッシュは「行」で機能します。つまり、ある情報にアクセスすると、近くのアドレスにある他の情報が、触れた部分とともにキャッシュにロードされます。例えば、私が1バイトに触れると、キャッシュはその1バイトに近い128または256バイトをロードするかもしれません。それを利用するには、一般に、同時にロードされた他のデータも使用する可能性を最大にするようにデータを配置することをお勧めします。
ほんの些細な例では、これは線形検索がバイナリ検索と比べて予想以上に競争が激しくなることを意味します。キャッシュラインから1つのアイテムをロードしたら、そのキャッシュラインの残りのデータを使用するのはほとんど無料です。バイナリ検索は、データが十分に大きいためにバイナリ検索がアクセスするキャッシュラインの数を減らす場合にのみ、著しく速くなります。
時間ディメンションは、あるデータに対して何らかの操作を実行するときに、そのデータに対してすべての操作を一度に実行することを(できる限り)したいということです。
これをC++としてタグ付けしたので、比較的キャッシュに優しくない設計の典型的な例、std::valarray
を示します。 valarray
はほとんどの算術演算子をオーバーロードするので、(例えば)a = b + c + d;
(ここでa
、b
、c
、d
はすべてvalarraysです)と言って、それらの配列を要素ごとに加算することができます。
これに関する問題は、それが1対の入力を通り抜け、結果を一時的に入れ、もう1対の入力を通り抜けていくということです。大量のデータでは、ある計算の結果が次の計算で使用される前にキャッシュから消える可能性があるため、最終的な結果が得られるまで、データの読み取り(および書き込み)を繰り返します。最終結果の各要素が(a[n] + b[n]) * (c[n] + d[n]);
のようなものになる場合は、通常、a[n]
、b[n]
、c[n]
およびd[n]
をそれぞれ1回ずつ読み取り、計算を行い、結果を書き込み、n
をインクリメントして最後まで繰り返します。2
2つ目の主な要因は、回線共有を回避することです。これを理解するために、私たちはおそらくバックアップして、キャッシュがどのように編成されているかを少し見る必要があります。最も単純な形式のキャッシュは直接マッピングです。つまり、メインメモリ内の1つのアドレスは、キャッシュ内の1つの特定の場所にしか格納できません。キャッシュ内の同じ場所にマッピングされている2つのデータ項目を使用している場合、それはうまく機能しません。1つのデータ項目を使用するたびに、もう一方のデータ項目をキャッシュからフラッシュして空き容量を確保する必要があります。キャッシュの残りの部分は空の場合がありますが、それらの項目はキャッシュの他の部分を使用しません。
これを防ぐために、ほとんどのキャッシュは "セットアソシアティブ"と呼ばれるものです。たとえば、4ウェイセットアソシアティブキャッシュでは、メインメモリの任意の項目をキャッシュ内の4つの異なる場所のいずれかに格納できます。そのため、キャッシュがアイテムをロードしようとしているときは、最も使用頻度の低いものを探します。3 これら4つのうちitemをメインメモリにフラッシュし、その場所に新しいitemをロードします。
この問題はおそらくかなり明白です。直接マッピングされたキャッシュの場合、同じキャッシュ位置にマッピングされる2つのオペランドが悪い動作を引き起こす可能性があります。 Nウェイセットアソシアティブキャッシュでは、数が2からN + 1に増えます。キャッシュをより「ウェイ」に編成すると、余分な回路が必要になり、通常は実行速度が遅くなります。そのため、(たとえば)8192ウェイのセットアソシアティブキャッシュも、あまり良い方法ではありません。
結局のところ、この要素は移植可能なコードでは制御がより困難です。データの配置場所に対するあなたの管理は、通常かなり制限されています。さらに悪いことに、アドレスからキャッシュへの正確なマッピングは、他の点では類似したプロセッサ間で異なります。ただし、場合によっては、大きなバッファを割り当ててから、同じキャッシュラインを共有するデータを防ぐために割り当てたものの一部だけを使用することをお勧めします(おそらく正確なプロセッサとプロセッサを検出する必要があります)。これを行うためにそれに応じて行動する。
"false sharing"と呼ばれるもう一つの関連項目があります。これは、2つ(またはそれ以上)のプロセッサ/コアが別々のデータを持っているが同じキャッシュラインに入るマルチプロセッサまたはマルチコアシステムで発生します。これにより、2つのプロセッサ/コアがそれぞれ独自の個別のデータ項目を持っていても、データへのアクセスを調整するように強制されます。特に2つが交互にデータを変更する場合、データはプロセッサ間で絶えず往復されなければならないため、これは大幅な速度低下を招く可能性があります。これをキャッシュをもっと「方法」に、あるいはそのようなものに組織化することによって簡単に解決することはできません。これを防ぐ主な方法は、2つのスレッドが同じキャッシュラインにある可能性のあるデータを変更することがほとんどないようにすることです(データが割り当てられるアドレスを制御するのが難しいという同じ警告を伴う)。
C++をよく知っている人は、これが式テンプレートのようなものを通して最適化に開かれているかどうか疑問に思うかもしれません。私はその答えが確かにそうであると確信しています、それは成し遂げられるかもしれません、そして、もしそれが成功すれば、それはおそらくかなり実質的な勝利になるでしょう。しかし、valarray
が少ししか使われていないことを考えれば、誰かがそうするのを見て私は少なくとも少し驚きます。
valarray
(特にパフォーマンスのために設計されたもの)がこれほどひどく間違っている可能性があることを誰かが疑問に思った場合、それは1つの事柄に帰着します。彼らにとって、これは本当に理想的なデザインでした。
はい、単純化しています。ほとんどのキャッシュでは、最近使用頻度の最も低い項目を正確に測定するわけではありませんが、アクセスごとに完全なタイムスタンプを保持せずにそれに近づくことを目的としたヒューリスティックを使用します。
データ指向設計の世界へようこそ。基本的な考え方は、並べ替え、ブランチの削除、バッチ処理、virtual
呼び出しの削除です。
質問にC++のタグを付けたので、これが必須の 典型的なC++ Bullshit です。 Tony Albrecht氏の オブジェクト指向プログラミングの落とし穴 も、このテーマの優れた入門書です。
ただ積み重ねる:キャッシュにやさしくないコードとキャッシュにやさしいコードの典型的な例は、行列乗算の「キャッシュブロック」です。
単純行列乗算は次のようになります。
for(i=0;i<N;i++) {
for(j=0;j<N;j++) {
dest[i][j] = 0;
for( k==;k<N;i++) {
dest[i][j] += src1[i][k] * src2[k][j];
}
}
}
N
が大きい場合、例えばN * sizeof(elemType)
がキャッシュサイズより大きい場合、src2[k][j]
へのすべてのアクセスがキャッシュミスとなります。
これをキャッシュ用に最適化する方法はたくさんあります。これはとても簡単な例です:内側のループでキャッシュラインごとに一つのアイテムを読む代わりに、すべてのアイテムを使います:
int itemsPerCacheLine = CacheLineSize / sizeof(elemType);
for(i=0;i<N;i++) {
for(j=0;j<N;j += itemsPerCacheLine ) {
for(jj=0;jj<itemsPerCacheLine; jj+) {
dest[i][j+jj] = 0;
}
for( k==;k<N;i++) {
for(jj=0;jj<itemsPerCacheLine; jj+) {
dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
}
}
}
}
キャッシュラインサイズが64バイトで、32ビット(4バイト)フロートで操作している場合、キャッシュラインごとに16項目があります。そして、この単純な変換によるキャッシュミスの数は約16分の1に減ります。
より見栄えのする変換は、2Dタイルに対して機能し、複数のキャッシュ(L1、L2、TLB)などに最適化します。
グーグル "キャッシュブロッキング"のいくつかの結果:
http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf
http://software.intel.com/en-us/articles/cache-blocking-techniques
最適化されたキャッシュブロックアルゴリズムの素晴らしいビデオアニメーション。
http://www.youtube.com/watch?v=IFWgwGMMrh0
ループタイリングは非常に密接に関連しています。
今日のプロセッサは、さまざまなレベルのカスケードメモリ領域で動作します。そのため、CPUはCPUチップ自体の上にたくさんのメモリを持つことになります。それはこの記憶への非常に速いアクセスを持っています。 CPU上にはなく比較的アクセスが遅いシステムメモリに到達するまで、キャッシュのレベルはそれぞれ次のレベルよりも遅く(そして大きく)なります。
論理的には、CPUの命令セットに対して、巨大仮想アドレス空間のメモリアドレスを参照するだけです。単一のメモリアドレスにアクセスすると、CPUはそれを取得します。昔はそのアドレスだけを取得していました。しかし今日、CPUはあなたが要求したビットの周りにたくさんのメモリを取ってきて、それをキャッシュにコピーするでしょう。それはあなたが特定の住所を頼んだ場合それがあなたが非常に近いうちに住所を頼むつもりであるという可能性が高いということであると仮定します。たとえば、バッファをコピーしている場合、連続したアドレスから順番に読み書きします。
ですから、今日あなたがアドレスを取得するとき、それがすでにそのアドレスをキャッシュに読み込んでいるかどうかを確かめるために最初のレベルのキャッシュをチェックします。それを見つけるためにキャッシュして、それが最終的にメインメモリに出なければならなくなるまで。
キャッシュに優しいコードは、キャッシュミスを最小限に抑えるために、アクセスをメモリ内で密集させようとします。
ですから、巨大な2次元の表をコピーしたいという例が想像できるでしょう。メモリ内でリーチ行が連続して編成され、直後に1行が続きます。
要素を左から右に一度に1行ずつコピーした場合、キャッシュに適しています。一度に1列ずつテーブルをコピーすることにした場合、まったく同じ量のメモリをコピーすることになります - しかし、それはキャッシュとは無縁です。
データがキャッシュに適しているだけでなく、コードにとっても同様に重要であることを明確にする必要があります。これは、分岐予測、命令の並べ替え、実際の分割の回避、およびその他の手法に加えて行われます。
通常、コードの密度が高いほど、保存に必要なキャッシュラインは少なくなります。これにより、データに使用できるキャッシュラインが増えます。
それらが典型的にそれ自身の1つ以上のキャッシュラインを必要とするので、コードは至る所で関数を呼び出すべきではありません。
関数はキャッシュの行揃えに適したアドレスから開始する必要があります。これには(gcc)コンパイラスイッチがありますが、関数が非常に短い場合は、キャッシュライン全体を占有するのが無駄になる可能性があることに注意してください。たとえば、最も頻繁に使用される機能の3つが1つの64バイトキャッシュライン内に収まる場合、これはそれぞれが独自のラインを持っている場合よりも無駄が少なく、2つのキャッシュラインが他の用途に使用できなくなります。一般的なアライメント値は32または16です。
そのため、コードを密にするためにさらに時間をかけます。さまざまな構成をテストし、生成されたコードサイズとプロファイルをコンパイルして確認します。
@Marc Claesenが述べたように、キャッシュに優しいコードを書く方法の1つは、データが格納されている構造を利用することです。キャッシュに優しいコードを書くためのもう1つの方法は、次のとおりです。データの保存方法を変更します。その後、この新しい構造体に格納されているデータにアクセスするための新しいコードを作成します。
データベースシステムがテーブルのタプルをどのように線形化してそれらを格納するかについては、これは意味があります。テーブルのタプルを格納するには2つの基本的な方法、つまり行ストアと列ストアがあります。名前が示すように行ストアでは、タプルは行単位で格納されます。格納されているProduct
という名前のテーブルが3つの属性、すなわちint32_t key, char name[56]
とint32_t price
を持っていると仮定しましょう、それでTupleの合計サイズは64
バイトです。
サイズNのProduct
構造体の配列を作成することで、メインメモリで非常に基本的な行ストアクエリの実行をシミュレートできます。ここで、Nはテーブル内の行数です。このようなメモリレイアウトは、構造体配列とも呼ばれます。そのため、Productの構造体は次のようになります。
struct Product
{
int32_t key;
char name[56];
int32_t price'
}
/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */
同様に、Product
テーブルの各属性に1つずつ、サイズNの3つの配列を作成することで、非常に基本的な列ストアクエリの実行をメインメモリでシミュレートできます。このようなメモリレイアウトは、配列の構造体とも呼ばれます。したがって、Productの各属性の3つの配列は次のようになります。
/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */
構造体の配列(行レイアウト)と3つの別々の配列(列レイアウト)の両方をロードした後、メモリ上に存在するテーブルProduct
上に行ストアと列ストアがあります。
それでは、キャッシュフレンドリーコード部分に移ります。テーブルの作業負荷が、価格属性に対して集計クエリを実行するようなものであるとします。といった
SELECT SUM(price)
FROM PRODUCT
行ストアの場合、上記のSQLクエリを次のように変換できます。
int sum = 0;
for (int i=0; i<N; i++)
sum = sum + table[i].price;
列ストアの場合、上記のSQLクエリを次のように変換できます。
int sum = 0;
for (int i=0; i<N; i++)
sum = sum + price[i];
列ストアのコードは、属性のサブセットのみを必要とするため、このクエリでは行レイアウトのコードよりも高速になります。列レイアウトでは、それだけで、つまりprice列にのみアクセスします。
キャッシュラインサイズが64
バイトであるとします。
キャッシュ行が読み込まれるときの行レイアウトの場合、1(cacheline_size/product_struct_size = 64/64 = 1
)Tupleの価格値だけが読み込まれます。これは、構造体のサイズが64バイトで、キャッシュ行全体が埋められるため、Tupleごとにキャッシュミスが発生するためです。行レイアウトの場合。
キャッシュ行が読み取られるときの列レイアウトの場合、16(cacheline_size/price_int_size = 64/4 = 16
)タプルの価格値が読み取られます。これは、16個のタプルごとにキャッシュミスが発生するためです。列レイアウト.
そのため、与えられたクエリの場合は列レイアウトが速くなり、テーブルの列のサブセットに対するそのような集計クエリでは列レイアウトが速くなります。 TPC-H ベンチマークのデータを使用して、このような実験を自分で試すことができ、両方のレイアウトの実行時間を比較できます。列指向データベースシステムに関する wikipedia の記事もまた良いものです。
データベースシステムでは、クエリのワークロードが事前にわかっている場合は、ワークロード内のクエリに適したレイアウトにデータを格納し、これらのレイアウトのデータにアクセスできます。上記の例の場合、列レイアウトを作成し、コードをcompute sumに変更して、キャッシュに優しくなるようにしました。
キャッシュは連続メモリをキャッシュするだけではないことに注意してください。それらは複数の行(少なくとも4つ)を持っているので、不連続で重なり合ったメモリはしばしば同じくらい効率的に格納することができます。
上記のすべての例から欠けているのは、測定されたベンチマークです。パフォーマンスについては多くの神話があります。あなたがそれを測定しない限り、あなたは知りません。 - - - - の改善がない限り、コードを複雑にしないでください。