2

まず、Accelerateフレームワークを使用して周波数分析機能を調整する場合、システムの絶対時間は一貫して反復あたり225msでした。それから昨夜、2つのアレイが宣言される順序を変更しましたが、突然202msになりました。宣言の順序を変更するだけで10%増加するのは、非常識なようです。コンパイラ(最適化するように設定されている)がまだこの解決策を見つけていない理由を誰かが私に説明できますか?

追加情報:ループの前に、ループで使用される配列のセットアップがあります。これは、配列を整数から浮動小数点配列(Accelerateの場合)に変換し、時間配列(16行の長さ)のsinとcosを取得することで構成されます。すべてのfloat配列(8配列x 1000要素)は、関数で最初に宣言されます(パラメーターの健全性チェックの後)。フットプリントの縮小がほとんどないためにパフォーマンスが低下するため、これらは常に同じサイズ(定数によって)と宣言されます。それらをグローバルにすることをテストしましたが、パフォーマンスに変化がないため、コンパイラーはすでにそれを理解していると思います。ループの長さは25行です。

---追加---

はい、「-Os」がフラグです。(とにかくXcodeのデフォルト:最速、最小)

(以下はメモリからのものです。コンパイルしようとしないでください。ストライド(1)などを入れなかったためです。ただし、Accelerate呼び出しはすべてそこにあります)

渡されたパラメーター:inttimearray、intamparray、length、scale1、scale2、amp

float trigarray1[maxsize];
float trigarray2[maxsize];
float trigarray3[maxsize];
float trigarray4[maxsize];
float trigarray5[maxsize];
float temparray[maxsize];
float amparray[maxsize];    //these two make the most change
float timearray[maxsize];    //these two make the most change

vDSP_vfltu32(inttimearray,timearray,length); //convert to float array
vDSP_vflt16(intamparray,amparray,length);    //convert to float array

vDSP_vsmul(timearray,scale1,temparray,length);    //scale time and store in temp
vvcosf(temparray,trigarray3,length);     //cos of temparray
vvsinf(temparray,trigarray4,length);     //sin of temparray
vDSP_vneg(trigarray4,trigarray5,length); //negative of trigarray4

vDSP_vsmul(timearray,scale2,temparray,length); //scale time and store in temp
vvcosf(temparray,trigarray1,length);           //cos of temparray
vvsinf(temprray,trigarray2,length);            //sin of temparray

float ysum;
vDSP_sve(amparray,ysum,length);    //sum of amparray

float csum, ssum, ccsum, sssum, cssum, ycsum, yssum;

for (i = 0; i<max; i++) {

    vDSP_sve(trigarray1,csum,length);    //sum of trigarray1
    vDSP_sve(trigarray2,ssum,length);    //sum of trigarray2

    vDSP_svesq(trigarray1,ccsum,length); //sum of trigarray1^2
    vDSP_svesq(trigarray2,sssum,length); //sum of trigarray2^2

    vDSP_vmul(trigarray1,trigarray2,temparray,length); //temp = trig1*trig2
    vDSP_sve(temparray,cssum,length);                  //sum of temp array
    // 2 more sets of the above 2 lines, for the 2 remaining sums

    amp[i] = (arithmetic of sums);

    //trig identity to increase the sin/cos by a delta frequency
    //vmma is a*b+c*d=result
    vDSP_vmma (trigarray1,trigarray3,trigarray2,trigarray4,temparray,length);
    vDSP_vmma (trigarray2,trigarray3,trigarray1,trigarray5,trigarray2,length);
    memcpy(trigarray1,temparray,length*sizeof(float));
}

---現在の解決策---

私は次のようにいくつかの変更を加えました:

配列はすべて整列されていると宣言され、ゼロにされ(次に説明します)、maxsizeは16の倍数になります

__attribute__ ((align (16))) float timearray[maxsize] = {0};

長さがmaxsize未満の場合、長さを16の最も近い倍数に切り上げて、ループされたすべての関数が16で割り切れる幅で動作するように、すべての配列をゼロにしました。合計。

利点は次のとおりです。

  • わずかなパフォーマンスの向上
  • 配列宣言の順序に関係なく、速度はほぼ一定です(これは、すべてが大きなブロックではなく、必要になる直前に実行されます)。
  • 速度は、16幅の長さ(つまり、241から256、または225から240 ...)でもほぼ一定ですが、以前は、長さが256から255になると、関数のパフォーマンスが3%以上低下していました。

将来的には(おそらくこのコードでは、分析要件はまだ流動的であるため)、スタックの使用量とベクトルの整列/チャンクをさらに考慮する必要があることに気付きました。残念ながら、このコードでは、この関数を一度に複数のオブジェクトから呼び出すことができるため、これらの配列を静的またはグローバルにすることはできません。

4

5 に答える 5

3

私が最初に疑うのはアライメントです。あなたは実験したいかもしれません:

__attribute__ ((align (16))) float ...[maxsize];

またはmaxsize、16の倍数であることを確認してください。ある構成で整列し、別の構成で整列していない場合、間違いなく10%のヒットが発生する可能性があります。ベクトル演算はこれに非常に敏感になる可能性があります。

あなたが持っているかもしれない次の主要な問題は巨大なスタックmaxsizeです(かなり大きいと仮定して)。ARMは、4kを超える数値を処理するよりも、4k未満の数値をはるかに効率的に処理できます(12ビットの即値のみを処理できるため)。したがって、コンパイラがどのように最適化したかによっては、amparrayをスタックにプッシュダウンすると、アクセスするための計算がより複雑になる可能性があります。

小さな微妙なことがパフォーマンスの大きな変化につながる場合は、アセンブリをプルアップして([製品]>[出力の生成]>[アセンブリ])、コンパイラ出力の変化を確認することを常にお勧めします。また、ARMアセンブリの旋風ツアーを強くお勧めします。これにより、見ているものを理解し始めることができます。(最適化された結果が表示されるように、出力を「アーカイブ用」に設定してください。)

また、さらにいくつかのことを行う必要があります。

  • Accelerateを使用する代わりに、このルーチンを単純なCとして書き直してみてください。はい、そうでない場合を除いて、Accelerateは常に高速です。これらの関数呼び出しはすべて非常にコストがかかり、コンパイラーは、Accelerateが私の経験でできる単純な乗算と加算をより適切にベクトル化できることがよくあります。これは、ストライドが1で、ベクトルが大きくなく、iPadのような1-2コアデバイスを使用している場合に特に当てはまります。ストライドを処理するコード(ストライドが必要ない場合)があると、手作業で記述したコードよりも複雑になります(遅くなります)。私の経験では、Accelerateはランプや超越(たとえば大きなテーブルの余弦定理)には非常に優れているように見えますが、単純なベクトルや行列の計算にはあまり適していません。

  • このコードが本当に重要な場合は、アセンブリを手書きすることでコンパイラのペースを確実に上回ることができることがわかりました。私はARMアセンブラーがあまり得意ではなく、単純な行列計算でコンパイラーを2倍上回っています(そしてコンパイラーはAccelerateを粉砕しました)。ここで特に話しているのは、加算と乗算だけを行っているように見えるループです。アセンブリを手書きするのはもちろん面倒で、アセンブラ用にCバージョンを維持する必要がありますが、それが本当に重要な場合は非常に高速です。

于 2012-08-10T14:45:03.087 に答える
2

実行可能なコードがないと、パフォーマンスの障害があるかどうかを判断するのが難しい場合があります。

この回答を使用して、いくつかの可能性を提案し、この質問に対する他の回答とコメントで提起された問題のいくつかについてコメントします。

まず、それぞれ4 kBのアレイが7つあるため、ほぼL1キャッシュのサイズを使用しています。スタックなどで使用されている他の量によっては、キャッシュをスラッシングしている可能性があります。これは、ブロックサイズを小さくするとパフォーマンスが向上した理由を説明できます。ブロックが小さいほど、各反復で使用されるメモリが少なくなり、すべてがキャッシュに収まるため、反復中にほとんどまたはまったくキャストされませんでした。この種のキャッシュスラッシングに対処する別の方法は、ストリップマイニングです。全長に対してsve、svesq、vmul、vmma、およびmemcpyを実行する代わりに、長さの一部(たとえば、半分)に対してそれらすべてを実行してから実行します。それらすべてを別の部分に置き、完全に処理されるまで必要に応じて繰り返します。

trigarray5は、2番目のvmmaがtrigarray4を無効にするためにのみ存在します。trigarray5を削除し、trigarray4でvmmsb(加算ではなく減算)を呼び出します。これにより、メモリ使用量も削減されます。

キャッシュのジオメトリにより、キャッシュを埋めるよりも少ないデータが使用されている場合でも、スラッシングが発生することがあります。キャッシュはセットに分割され、各メモリアドレスは特定のセットにマップする必要があります。たとえば、32,768バイトのキャッシュには、それぞれ32バイトの1024の「行」が含まれる場合がありますが、4行の256セットに編成される場合があります。1つのメモリアドレスは1つのセットにマップされ、そのセットの4行のうちの1つを使用する必要があります。このジオメトリを法として同じアドレスで開始する(または実質的にオーバーラップする)5つのアレイがある場合、それらは各セットの4つのラインをめぐって競合し、互いにキャストし合います。配列がメモリ内で連続して割り当てられる場合、これは回避されます。これは、配列が単純に次々に宣言される場合にコンパイラが一般的に行うためですが、複雑になる可能性があります。実行可能なコードがないと、判断するのは困難です。

配列を16バイトの倍数に揃えることは問題なく、わずかに役立つ場合があります。特定の状況では、それは大いに役立ちます。可能な場合、多くのvDSPルーチンは、いくつかの初期要素を処理して適切に配置された境界に到達し、配列の終わり近くまで高速SIMDコードを使用します。この場合、別のいくつかの要素を個別に処理する必要があります。ただし、複数のベクトルを操作するルーチンに異なる配置のベクトルが渡される場合など、これが常に可能であるとは限りません。(要素を処理して1つのポインターを整列させると、他のポインターが整列しなくなります。)align属性を追加する以外に、配列を整列する別の方法は、mallocなどの標準のメモリー割り当てルーチンを使用して配列を割り当てることです。Mac OS XおよびiOSでは、mallocは16バイトに整列されたアドレスを返します。

スタックサイズとARMの即値が制限されているという事実は問題ではない可能性が高いため、ベクトルアドレスの計算は、コード内の計算の些細な部分である必要があります。(また、ARMには、単なる12ビット整数ではなく、いくつかの興味深い柔軟な即値があります。)

実際の関数呼び出しとリターン自体のコストは、おそらく取るに足らないものです。Appleが提供するコンパイラは、「Accelerateよりも単純な乗算と加算をベクトル化する方が優れている」わけではなく、関数呼び出しは「かなり高価」ではありません。</ p>

ストライドを省略しました。それらが1つでない場合は、vDSPルーチンが呼び出されたときにデータがユニットストライドを持つようにコードを書き直すことで、多くの利益が得られる可能性があります。

ここでは、分岐予測はおそらく問題ではありません。

実行可能なコードは、パフォーマンスの問題を診断するのに非常に役立ちます。

于 2012-08-11T00:10:42.067 に答える
0

おそらく、分岐予測と、配列内にある要素と関係があります。

素晴らしいリファレンスについては、この投稿を参照してください。あなたの投稿は、配列を1つの順序で宣言することでデータが「並べ替えられた」ように見えるという点でこの投稿に似ている可能性がありますが、別の順序ではそうではありません。

ソートされていない配列よりもソートされた配列を処理する方が速いのはなぜですか?

于 2012-08-10T14:25:44.873 に答える
0

ここで推測してください。アラインメント?

これらのライブラリはSIMD命令を使用することになっており、これらのタイミングは、アライメントが不要な場合でもアライメントに依存します。

また、キャッシュラインの調整が役割を果たす場合と行われない場合があります。

これらの配列はスタックに割り当てられます。つまり、sizeof(float)固有の保証と、最初のオブジェクトのアーキテクチャ上の保証を超えて、そのデータの配置をほとんど制御できません(64ビットの配置は、最初のローカル変数に対して事実上保証されます。 64ビットモードでコンパイルします)。

アドレスを印刷/記録することにより、データアライメントが何であるかを確認しようとする場合があります。また、データを保持する構造を定義し、mallocを使用してデータのメモリを取得することで、アライメントのタイミング効果を試すことができます(必要以上のメモリを取得して、メモリブロックのさまざまなオフセットに構造を配置できるようにします。キャッシュラインアライメントで遊びたい)。

于 2012-08-10T14:42:19.040 に答える
0

まず、データ配置に対するこの種の感度は、残念ながら一般的です。私たちの何人かは、複数の異なるレイアウトを試すコードを書いています

このようなパフォーマンス低下の通常の原因は次のとおりです。

  • ブランチの予測ミス

  • キャッシュ効果

    • 容量の欠落(データが多すぎる、たとえば1MBのデータが32KBのキャッシュに収まらない)

    • キャッシュの競合(たとえば、4ウェイアソシアティブ32KBキャッシュで8Kを法として同じである4つを超えるアドレス)

  • DRAM効果

    • DRAMページが欠落しています

私はあなたの言うことを解析するのに苦労しています:MAXSIZEとは何ですか?あなたは7*4KBと言います...しかしあなたは8つの配列を持っているので、MAXSIZE=1024と言っているのではないかと思います。MAXSIZEが7*1024だと言っていますか?(* 4B /フロート?)

とにかく:個々のアレイのMAXSIZEが約28KBの場合、多くのシステムのキャッシュサイズに近くなります。この場合、DRAMページの影響が疑われます。パフォーマンスの良い配置では、最もアクセスの多いアレイが別のDRAMページに配置されると思われます。

どちらが優れているかはわかりませんが、私は推測します。

float amparray[maxsize];    //these two make the most change
float timearray[maxsize];    //these two make the most change

コードに目を向けると、timearrayが最もアクセスされているようです。パフォーマンスがtimearraysecondでより良く、MAXSIZEについての私の推測が正しい場合、それはDRAMページ効果であるに違いありません。

簡単な説明:DRAMにはページとバンクの概念があります。OSページと混同しないでください。Eac DRAMチップ、つまり各DIMMには、4つまたは8つの内部バンクがあります。各銀行は1つの開いているページを持つことができます。同じページ、同じ銀行からデータにアクセスする場合、それは最速です。別の銀行のすでに開いているページからデータにアクセスする場合、同じ銀行の同じページよりも高速ですが、低速です。同じ銀行の別のページが必要な場合は、非常に低速です。ライトバックキャッシュがある場合、ライトバックはほぼランダムに発生するため、ページの動作が非常に悪くなる可能性があります。

ただし、MAXSIZEについて間違って推測した場合は、おそらくキャッシュ効果です。

赤旗:あなたは「私はストライドのようなものを入れなかった」と言います。 ストライドは、データがキャッシュ内で適切に動作しないようにすることで有名です。キャッシュは通常、アソシアティブに設定されます。つまり、キャッシュのレゾナンスを法として同じアドレスが同じセットにマップされます。あなたが連想性以上のものを持っているなら、あなたはスラッシングするでしょう。

キャッシュサイズを結合法則で割ったものとして共振を計算します。たとえば、32Kの4ウェイアソシアティブキャッシュがある場合、レゾナンスは8Kです。

とにかく...ストライドで物事にアクセスするだけの場合は、配列の配置が重要になる可能性があります。たとえば、ストライドが16であるとします。つまり、要素0、16、32、48などにアクセスします。上記で推測したように、MAXSIZEが7 * 1024の場合、要素

float trigarray1[maxsize];
float trigarray2[maxsize];
float trigarray3[maxsize];
float trigarray4[maxsize];
float trigarray5[maxsize];
float temparray[maxsize];
float amparray[maxsize];    //these two make the most change
float timearray[maxsize];    //these two make the most change

その場合、次の配列は競合します-それらのストライドアクセスパターンは同じセットにマップされます:

trigarray1, trigarray5
trigarray2, temparray
trigarray3, amparray
trigarray4, timearray,

amparrayとtimearrayを交換すると、

   trigarray3 will conflict with timearray
and
   trigarray4 with amparray

trigarray4とtimarrayが最もよく使用されているように思われるので、0、16、32、348のようなストライド、または実際に0で始まるストライドがある場合は、これら2つの配列の競合が問題になると思います。

ただし、ストライドパターンが異なる場合があります。1つのアレイでは0、16、32、48 ...、もう1つのアレイでは1,17,33、...です。次に、アレイの異なるペアが競合します。

-

ここであなたの問題を診断するのに十分な情報がありません。

優れたパフォーマンスツールにアクセスできれば、自分でそれを実行できる可能性があります。

たとえば、Intelプロセッサでは、キャッシュミスプロファイルと呼ばれるものを記録して、理想的には物理メモリアドレスを記録し、それらがキャッシュにマップするセットを計算して、ヒストグラムを生成できます。スパイクが見られる場合は、それが問題である可能性があります。同様に、DRAMページミスまたはバンクミスプロファイルを生成できます。この種のパフォーマンス測定を可能にするためにいくつかのハードウェアを設計したので、Intelについてのみ言及します。同じ種類のものがARMで利用できるはずです(そうでない場合は、それを行うための豊富な販売ツールを入手できるかもしれません... :-))。

これらが問題である場合、どのようにそれを修正できますか?

さて、あなたが上で説明したように、異なる配置を試すことによって。これは、ストライド(キャッシュセットの競合)とDRAMページの問題の両方に役立ちます。

ストライドが問題になる場合は、配列サイズを少し変えてみてください(MAXSIZE + 4、MAXSIZE 8など)。これにより、ストライドを効果的にオフセットできます。(スーパーコンピューターのコードでは、競合しないようにストライドアクセスパターンをオフセットするのと同じ理由で、サイズが255または257の配列を表示するのが一般的です。)

于 2012-08-11T04:16:21.107 に答える