web-dev-qa-db-ja.com

C ++では、変数をキャッシュする必要がありますか、それともコンパイラに最適化を行わせる必要がありますか? (エイリアス)

次のコードを検討してください(punsigned char*およびbitmap->widthは整数型であり、正確には未知であり、使用している外部ライブラリのバージョンに依存します):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

最適化する価値はありますか[..]

これにより、より効率的な結果が得られる場合があります。

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

...または、コンパイラが最適化するのは簡単なことですか?

「より良い」コードとは何だと思いますか?

編集者からのメモ(Ike):取り消し線のテキストについて疑問に思っている人のために、フレーズとしての元の質問は、トピック外の領域に非常に近く、肯定的なフィードバックにもかかわらず非常に閉じられていました。ただし、これらの問題のセクションに対処した回答者を罰しないでください。

114
Yaron Cohen-Tal

一見したところ、コンパイラーは、最適化フラグを有効にすると、両方のバージョンで同等のアセンブリを生成できると思いました。私がそれをチェックしたとき、私は結果を見て驚いた:

ソースunoptimized.cpp

注:このコードは実行するためのものではありません。

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

ソースoptimized.cpp

注:このコードは実行するためのものではありません。

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

編集

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

アセンブリ(unoptimized.s)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

アセンブリ(optimized.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

diff

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

最適化されたバージョン用に生成されたアセンブリは、各反復でleaオフセットを計算する最適化されていないバージョンとは異なり、実際にwidth定数をロードします( width ) ( movq )。

時間ができたら、最終的にはベンチマークを投稿します。良い質問。

81
YSC

実際には、コードスニペットから伝えることができる情報が不十分であり、考えられることの1つはエイリアスです。私たちの観点からは、pbitmapがメモリ内の同じ場所を指すことを望まないことはかなり明らかですが、コンパイラはそれを知らず(pはタイプchar*pbitmapが重複していても、コンパイラはこのコードを機能させる必要があります。

これは、この場合、ループがbitmap->widthポインタpを介して、再読み込み時に確認する必要がありますbitmap->width後で、つまりローカル変数に格納することは違法になることを意味します。

そうは言っても、実際にはいくつかのコンパイラが同じコードの2つのバージョンを実際に生成することがあると思います(この状況の証拠を見てきましたが、この場合にコンパイラが何をしているかについての情報を直接探したことがありません)エイリアスを作成し、問題がないと判断した場合はより高速なコードを実行します。

そうは言っても、2つのバージョンのパフォーマンスを単純に測定するというコメントに賛成です。2つのバージョンのコード間で一貫したパフォーマンスの違いが見られないことにお金がかかっています。

私の意見では、コンパイラの最適化の理論と手法を学ぶことが目的であればこのような質問は問題ありませんが、ここでの最終目標がプログラムの高速化である場合は時間の無駄(無駄なマイクロ最適化)です。

38
SirGuy

他の答えは、ポインタをループから引き上げると、charが何でもエイリアスできるようにするエイリアスルールにより定義された動作が変更される可能性があり、ほとんどの場合は明らかに人間にとって正しいにもかかわらず、コンパイラの許容可能な最適化ではないことを指摘していますプログラマー。

また、ループ外への操作の巻き上げは、通常、パフォーマンスの観点からの改善であるとは限りませんが、読みやすさの観点からは否定的であることが多いと指摘しました。

しばしば「第三の方法」があることを指摘したいと思います。必要な反復回数までカウントするのではなく、ゼロまでカウントダウンできます。つまり、繰り返しの回数はループの開始時に一度だけ必要であり、その後保存する必要はありません。アセンブラレベルでは、デクリメント操作は通常、カウンターがデクリメントの前(キャリーフラグ)と後(ゼロフラグ)の両方でゼロであったかどうかを示すフラグを設定するため、明示的な比較の必要性を排除します。

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

このバージョンのループは、0 ..(width-1)の範囲ではなく、1..widthの範囲のx値を与えることに注意してください。実際にはxを何にも使用していないので、これは問題ではありませんが、注意する必要があります。範囲0 ..(width-1)のx値を持つカウントダウンループが必要な場合は、できます。

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

また、ビットマップ->幅で行うことは変数に直接代入するだけなので、比較ルールへの影響を心配することなく、上記の例のキャストを取り除くこともできます。

24
plugwash

わかりました、だから私はGCC -O3(Linux x64でGCC 4.9を使用)で測定しました。

結局のところ、2番目のバージョンは54%速く動作します!

ですから、エイリアシングは問題だと思います。私はそれについて考えていませんでした。

[編集]

__restrict__で定義されたすべてのポインターを使用して最初のバージョンを再試行しましたが、結果は同じです。奇妙なことです。エイリアシングは問題ではないか、何らかの理由で、コンパイラは__restrict__を使用してもそれを適切に最適化しません。

[編集2]

エイリアシングが問題であることを証明できたと思います。元のテストを繰り返しましたが、今回はポインターではなく配列を使用しました。

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

そして、測定しました(「-mcmodel = large」を使用してリンクする必要がありました)。それから私は試しました:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

測定結果は同じでした-コンパイラはそれ自体で最適化できたようです。

次に、pのタイプがstd::uint16_t*であるときに、元のコード(ポインターp)を試しました。繰り返しますが、結果は同じでした-エイリアシングが厳しいためです。それから、「-fno-strict-aliasing」でビルドしてみましたが、再び時間の違いがわかりました。

23
Yaron Cohen-Tal

ここで最適化を防ぐことができる唯一のものは、 strict aliasing rule です。 要するに

「C(またはC++)コンパイラによって行われた厳密なエイリアスは、異なる型のオブジェクトへのポインターを逆参照することは、同じメモリー位置を参照しない(つまり、互いにエイリアスする)という仮定です」

[…]

ルールの例外はchar*で、これは任意の型を指すことが許可されています。

例外は、unsignedおよびsignedcharポインターにも適用されます。

これはコードの場合です。*pであるpを介してunsigned char*を変更しているため、コンパイラmustbitmap->widthを指す可能性があると想定しています。したがって、bitmap->widthのキャッシングは無効な最適化です。この最適化を防ぐ動作は、 YSCの答え に示されています。

pが非charおよび非decltype(bitmap->width)型を指している場合にのみ、キャッシングは可能な最適化になります。

11
emlai

最初に尋ねられた質問:

最適化する価値はありますか?

そして、それに対する私の答え(賛成票と反対票の両方をうまく組み合わせて..)

コンパイラーに心配させてください。

コンパイラはほぼ確実にあなたよりも良い仕事をします。そして、あなたの「最適化」が「明白な」コードよりも良いという保証はありません-それを測定しましたか??

さらに重要なことは、最適化するコードがプログラムのパフォーマンスに影響を与えるという証拠はありますか?

ダウン投票にもかかわらず(そして現在エイリアシングの問題が見られますが)、私はそれが有効な答えであることにまだ満足しています。 何かを最適化する価値があるかどうかわからない場合、おそらくそうではありません。

もちろん、かなり異なる質問は次のとおりです。

コードの断片を最適化する価値があるかどうかはどうすればわかりますか?

まず、アプリケーションまたはライブラリを現在よりも高速に実行する必要がありますか?ユーザーは長く待たされていますか?あなたのソフトウェアは、明日の天気ではなく昨日の天気を予測していますか?

ソフトウェアの目的とユーザーの期待に基づいて、あなただけが本当にこれを伝えることができます。

ソフトウェアに最適化が必要であると仮定して、次に行うことは測定を開始することです。プロファイラーは、コードが時間を費やしている場所を教えてくれます。フラグメントがボトルネックとして表示されていない場合は、そのままにしておくのが最善です。プロファイラーやその他の測定ツールも、変更によって違いが生じたかどうかを示します。コードを最適化するために何時間も費やすことは可能ですが、識別可能な違いが生じていないことがわかります。

とにかく、「最適化」とはどういう意味ですか?

「最適化された」コードを書いていない場合、コードはできる限り明確で、クリーンで簡潔でなければなりません。 「時期尚早な最適化は悪」という議論は、ずさんなまたは非効率的なコードの言い訳ではありません。

最適化されたコードは通常、パフォーマンスのために上記の属性の一部を犠牲にします。追加のローカル変数を導入したり、予想よりも広いスコープを持つオブジェクトを使用したり、通常のループ順序を逆にすることもできます。これらのすべてはあまり明確または簡潔ではない可能性がありますので、コードを(簡潔に!)文書化してください。

しかし、多くの場合、「遅い」コードでは、これらのマイクロ最適化が最後の手段です。最初に確認する場所は、アルゴリズムとデータ構造です。仕事をすることをまったく避ける方法はありますか?線形検索をバイナリ検索に置き換えることはできますか?ここでは、リンクリストはベクターよりも高速ですか?それともハッシュテーブル?結果をキャッシュできますか?ここで適切な「効率的な」意思決定を行うと、パフォーマンスに1桁以上影響することがよくあります。

10
Roddy

このような状況では、次のパターンを使用します。それはあなたの最初のケースとほぼ同じくらい短く、一時変数をループのローカルに保持するため、2番目のケースよりも優れています。

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

これは、スマートコンパイラ、デバッグビルド、または特定のコンパイルフラグよりも高速です。

Edit1:ループの外側に定数演算を配置することは、goodプログラミングパターンです。特にC/C++でのマシン操作の基礎の理解を示しています。自分自身を証明するための努力は、この慣習に従わない人々にすべきだと主張します。コンパイラーが良いパターンで罰する場合、それはコンパイラーのバグです。

Edit2::vs2013の元のコードに対する提案を測定したところ、%1の改善が得られました。もっと良くできますか?単純な手動最適化により、エキゾチックな命令に頼ることなく、x64マシンの元のループに比べて3倍の改善が得られます。以下のコードは、リトルエンディアンシステムと適切に配置されたビットマップを想定しています。テスト0はオリジナル(9秒)、テスト1は高速(3秒)です。私は誰かがこれをさらに速くすることができると確信しており、テストの結果はビットマップのサイズに依存するでしょう。間違いなく近い将来、コンパイラは一貫して最速のコードを生成できるようになります。コンパイラーがプログラマーAIになるのはこれからになるのではないかと思います。しかし、今のところは、ループ内の余分な操作が不要であることを知っていることを示すコードを書いてください。

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}
6
0kcats

考慮すべきことが2つあります。

A)最適化はどのくらいの頻度で実行されますか?

ユーザーがボタンをクリックしたときのように答えがあまり頻繁でない場合は、コードが読めなくても気にしないでください。答えが1秒間に1000回である場合、おそらく最適化を行うことをお勧めします。少しでも複雑な場合は、コメントを書いて、次に来る人を助けるために何が起こっているのかを説明してください。

B)これにより、コードの維持/トラブルシューティングが難しくなりますか?

パフォーマンスの大幅な向上が見られない場合、コードを単純に数クロックの刻みを節約するために暗号化することは良い考えではありません。多くの人が、優れたプログラマーはコードを見て、何が起こっているのかを把握できるはずだと言うでしょう。これは本当です。問題は、ビジネスの世界ではそれを理解するのに余分な時間がお金がかかるということです。したがって、読みやすくすることができれば、それを実行できます。あなたの友人はそれに感謝します。

それは私が個人的にBの例を使用するということです。

5
soulsabr

比較は間違っています2つのコードスニペット

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

そして

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

は同等ではありません

最初のケースでは、widthはconstでなく依存しており、後続の反復間で変化しないと仮定することはできません。 したがって、最適化することはできませんが、 ループごとにチェックするにする必要があります。

最適化された場合、ローカル変数には、プログラム実行中のある時点でbitmap->widthの値が割り当てられます。 コンパイラは、これが実際に変わらないことを確認できます。

マルチスレッドについて考えましたか、または値が揮発性であるように値が外部的に依存している可能性があります。あなたが言わないなら、コンパイラがこれらすべてのことを理解することをどのように期待するでしょうか?

コンパイラは、コードで許可されている範囲内でのみ実行できます。

4
g24l

原則として、引き継ぐ必要があると判断するまで、コンパイラに最適化を行わせます。このロジックは、パフォーマンスとは関係なく、人間の可読性とは関係ありません。 vast大部分の場合、プログラムの可読性はパフォーマンスよりも重要です。人間が読みやすいコードを作成し、パフォーマンスがコードの保守性よりも重要であると確信している場合にのみ最適化について心配する必要があります。

パフォーマンスが重要であることを確認したら、コードでプロファイラーを実行して、どのループが非効率的であるかを判断し、それらを個別に最適化する必要があります。確かに、その最適化を行いたい場合もあります(特に、STLコンテナーが関与するC++に移行する場合)が、読みやすさの点で大きなコストがかかります。

さらに、実際にコードの速度が低下する可能性がある病理学的状況を考えることができます。たとえば、コンパイラがbitmap->widthがプロセスを通して一定であることを証明できなかった場合を考えてみましょう。 width変数を追加することにより、コンパイラーにそのスコープ内のローカル変数を維持するように強制します。何らかのプラットフォーム固有の理由により、その余分な変数がスタックスペースの最適化を妨げた場合、バイトコードの出力方法を再編成し、より効率の低いものを生成する必要があります。

たとえば、Windows x64では、関数がローカル変数の複数のページを使用する場合、関数のプリアンブルで特別なAPI呼び出し__chkstkを呼び出す必要があります。この関数は、必要なときにスタックを拡張するために使用するガードページを管理する機会をウィンドウに与えます。余分な変数がスタック使用量を1ページ未満から1ページ以上に押し上げる場合、関数は入力されるたびに__chkstkを呼び出すようになりました。遅いパスでこのループを最適化する場合、実際には遅いパスで保存したよりも速いパスを遅くすることがあります!

確かに、それは少し病理学的ですが、その例のポイントは、実際にコンパイラーを遅くすることができるということです。それは、最適化がどこに行くかを決定するためにあなたの仕事をプロファイルしなければならないことを示しているだけです。とりあえず、重要な場合もそうでない場合もある最適化のために、読みやすさを犠牲にしないでください。

4
Cort Ammon

コンパイラは多くのことを最適化できます。あなたの例では、読みやすさ、操作性、そしてコード標準に従うものを探してください。 (GCCで)最適化できるものの詳細については、 このブログ投稿 を参照してください。

4

コンパイラーがどのようにコードを最適化するかを正確に把握していない限り、コードの可読性と設計を維持して独自の最適化を行うことをお勧めします。実際には、新しいコンパイラバージョン用に記述するすべての関数のアセンブリコードをチェックすることは困難です。

2
Vinayak S M

widthの値は反復間で変更できるため、コンパイラは_bitmap->width_を最適化できません。最も一般的な理由がいくつかあります。

  1. マルチスレッド。コンパイラは、他のスレッドが値を変更しようとしているかどうかを予測できません。
  2. ループ内の変更。変数がループ内で変更されるかどうかを判断するのが簡単でない場合があります。
  3. 関数呼び出しです。 iterator::end()またはcontainer::size()なので、常に同じ結果を返すかどうかを予測するのは困難です。

高度な最適化が必要な場所について(私の個人的な意見)をまとめると、それを自分で行う必要があり、他の場所ではそのままにしておくと、コンパイラーが最適化するかどうかは、大きな違いがない場合、コードの可読性が主なターゲットです。

1
ST3