web-dev-qa-db-ja.com

構造体で小さな値を表す最も効率的な方法は何ですか?

非常に小さな値で構成される構造を表現しなければならないことがよくあります。たとえば、Fooには4つの値があります。a, b, c, dそれ、範囲は0 to 3。通常は気にしませんが、時々、それらの構造は

  1. タイトループで使用されます。

  2. それらの値は10億回/秒で読み取られます。これは、プログラムのtheボトルネックです。

  3. プログラム全体は、数十億のFoosの大きな配列で構成されています。

その場合、Fooを効率的に表現する方法を決めるのに苦労しています。基本的に4つのオプションがあります。

struct Foo {
    int a;
    int b;
    int c;
    int d;
};

struct Foo {
    char a;
    char b;
    char c;
    char d;
};

struct Foo {
    char abcd;
};

struct FourFoos {
    int abcd_abcd_abcd_abcd;
};

Fooごとにそれぞれ128、32、8、8ビットを使用し、スパースから高密度パックまでの範囲です。最初の例はおそらく最も言語的な例ですが、それを使用すると、プログラムのサイズが基本的に16倍になり、正しく聞こえません。さらに、ほとんどのメモリはゼロで埋められ、まったく使用されないため、これは無駄ではないかと思います。一方、それらを密に梱包すると、それらを読み取るための追加のオーバーヘッドが発生します。

構造体の小さな値を表すための計算上「最速」の方法は何ですか?

38
MaiaVictor

読み取りのオーバーヘッドが大きくない高密度のパッキングには、ビットフィールドを使用した構造体をお勧めします。 0から3の範囲の4つの値がある例では、構造体を次のように定義します。

struct Foo {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
}

これのサイズは1バイトで、フィールドには簡単にアクセスできます。つまり、foo.afoo.bなど。

構造体をより密にパックすることで、キャッシュの効率が向上するはずです。

編集:

コメントを要約すると:

ビットフィールドではまだ少し手間がかかりますが、コンパイラによって行われ、手動で書くよりも効率的である可能性があります(言うまでもなく、ソースコードがより簡潔になり、バグが発生しにくくなります)。また、処理する構造体の量が多い場合、このようなパック構造体を使用することで得られるキャッシュミスの削減は、構造体が課すビット操作のオーバーヘッドを補う可能性があります。

34
dbush

スペースが考慮される場合にのみそれらをパックします(たとえば、1,000,000構造体の配列)。それ以外の場合、シフトとマスキングを行うために必要なコードは、データ用のスペースの節約よりも大きくなります。したがって、DキャッシュよりもIキャッシュでキャッシュミスが発生する可能性が高くなります。

20
stark

明確な答えはなく、「正しい」選択を行うのに十分な情報を提供していません。トレードオフがあります。

I/O時間(ファイルからデータを読み取るなど)が計算効率(計算のセットにかかる時間など)よりも重要かどうかを指定していないため、「主な目標は時間効率」であるというあなたの声明は不十分です。ユーザーが「実行」ボタンを押した後)。

したがって、データを単一の文字として書き込む(読み取りまたは書き込みの時間を短縮するため)が、4つのintの配列に解凍することが適切な場合があります(後続の計算が高速になります)。

また、intが32ビットであるという保証はありません(最初のパッキングが128ビットを使用するとステートメントで想定しています)。 intは16ビットにすることができます。

11
Peter

Fooには、0から3の範囲のa、b、c、dの4つの値があります。通常は気にしませんが、これらの構造は...

別のオプションがあります。値0 ... 3はある種の状態を示している可能性が高いため、「フラグ」の使用を検討できます。

enum{
  A_1 = 1<<0,
  A_2 = 1<<1,
  A_3 = A_1|A_2,
  B_1 = 1<<2,
  B_2 = 1<<3,
  B_3 = B_1|B_2, 
  C_1 = 1<<4,
  C_2 = 1<<5,
  C_3 = C_1|C_2,
  D_1 = 1<<6,
  D_2 = 1<<7,
  D_3 = D_1|D_2,
  //you could continue to  ... D7_3 for 32/64 bits if it makes sense
}

これは、ほとんどの状況でビットフィールドを使用することと大差ありませんが、条件付きロジックを大幅に減らすことができます。

if ( a < 2 && b < 2 && c < 2 && d < 2) // .... (4 comparisons)
//vs.
if ( abcd & (A_2|B_2|C_2|D_2) !=0 ) //(bitop with constant and a 0-compare)

データに対して実行する操作の種類に応じて、4セットまたは8セットのabcdを使用し、必要に応じて末尾を0で埋めることが理にかなっている場合があります。これにより、最大32の比較をbitopおよび0-compareに置き換えることができます。たとえば、64ビット変数の4の8セットすべてに「1ビット」を設定したい場合は、uint64_t abcd8 = 0x5555555555555555ULL;を実行してから、2ビットすべてを設定するにはabcd8 |= 0xAAAAAAAAAAAAAAAAULL;を実行してすべての値を作成できます。今3


補遺:さらに検討すると、タイプとしてユニオンを使用し、charと@dbushのビットフィールドでユニオンを実行するか(これらのフラグ操作はunsigned charでも機能します)、各a、b、c、dにcharタイプを使用できます。そしてそれらをunsignedintと結合します。これにより、使用するユニオンメンバーに応じて、コンパクトな表現と効率的な操作の両方が可能になります。

union Foo {
  char abcd; //Note: you can use flags and bitops on this too
  struct {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
  };
};

またはさらに拡張

union Foo {
  uint64_t abcd8;  //Note: you can use flags and bitops on these too
  uint32_t abcd4[2];
  uint16_t abcd2[4];
  uint8_t  abcd[8];
  struct {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
  } _[8];
};
union Foo myfoo = {0xFFFFFFFFFFFFFFFFULL};
//assert(myfoo._[0].a == 3 && myfoo.abcd[0] == 0xFF);

この方法では、エンディアンの違いがいくつか発生します。これは、ユニオンを使用して他の方法の他の組み合わせをカバーする場合にも問題になります。

union Foo {
  uint32_t abcd;
  uint32_t dcba; //only here for endian purposes
  struct { //anonymous struct
    char a;
    char b;
    char c;
    char d;
  };
};

さまざまな共用体タイプとアルゴリズムを試して測定し、共用体のどの部分を保持する価値があるかを確認してから、役に立たない部分を破棄することができます。複数のchar/short/int型を同時に操作すると、AVX/simd命令の組み合わせに合わせて自動的に最適化されるのに対し、ビットフィールドを手動で展開しない限り、ビットフィールドを使用することはできません...テストと測定それら。

9
technosaurus

データセットをキャッシュに収めることは重要です。ハイパースレッディングはハードウェアスレッド間(Intel CPU上)でコアごとのキャッシュを競合的に共有するため、小さい方が常に優れています。 この回答 に関するコメントには、キャッシュミスのコストの数値が含まれています。

x86では、符号またはゼロ拡張子を持つ8ビット値を32ビットまたは64ビットレジスタ(movzxまたはmovsx)にロードするのは、文字通り、のプレーンなmovと同じくらい高速です。バイトまたは32ビットのdword。 32ビットレジスタの下位バイトを格納することにもオーバーヘッドはありません。 (Agner Fogの ここの説明表とC/asm最適化ガイド を参照してください)。

それでもx86固有:[u]int8_t一時も問題ありませんが、[u]int16_t一時は避けてください。 (メモリ内の[u]int16_tからのロード/ストアは問題ありませんが、レジスタ内の16ビット値を操作すると、Intel CPUでのオペランドサイズのプレフィックスのデコードが遅くなるため、大きなペナルティが発生します。)それらを配列インデックスとして。 (8ビットレジスタを使用しても上位24/56ビットがゼロになるわけではないため、8ビットレジスタを配列インデックスとして使用するか、より広い型の式で使用するには、ゼロまたは符号拡張するための追加の命令が必要です( int。)

ARMまたは他のアーキテクチャが、シングルバイトロードからの効率的なゼロ/符号拡張、またはシングルバイトストアに対して何ができるかわかりません。

これを考えると、私の推奨事項は、ストレージにpackを使用し、一時intを使用することです。 (またはlongですが、64ビットのオペランドサイズを指定するにはREXプレフィックスが必要なため、x86-64ではコードサイズがわずかに増加します。)例:.

int a_i = foo[i].a;
int b_i = foo[i].b;
...;
foo[i].a = a_i + b_i;

ビットフィールド

ビットフィールドにパックするとオーバーヘッドが大きくなりますが、それでも価値があります。 1バイトまたは32/64ビットのメモリチャンクでのコンパイル時定数ビット位置(または複数ビット)のテストは高速です。実際にいくつかのビットフィールドをintsに解凍し、それらを非インライン関数呼び出しなどに渡す必要がある場合は、シフトしてマスクするためにいくつかの追加の命令が必要になります。これによりキャッシュミスが少しでも減少する場合は、それだけの価値があります。

ビットまたはビットのグループのテスト、設定(1)またはクリア(0)は、ORまたはANDを使用して効率的に実行できますが、未知のブール値をビットフィールドに割り当てる より多くの命令が必要 他のフィールドのビットと新しいビット。変数をビットフィールドに頻繁に割り当てると、コードが大幅に肥大化する可能性があります。したがって、構造体でint foo:6などを使用すると、fooは上位2ビットを必要としないことがわかっているため、役に立たない可能性があります。それぞれを独自のbyte/short/intに入れるのに比べて多くのビットを節約していない場合、キャッシュミスの削減は、追加の命令(I-cache/uop-cacheミスにつながる可能性があります)を上回りません。直接の余分な待ち時間と命令の作業も同様です。)

X86 BMI1/BMI2(ビット操作)命令セット拡張レジスタからいくつかの宛先ビットにデータをコピーする (周囲のビットを壊すことなく)より効率的になります。 BMI1:Haswell、まんぐり返し。 BMI2:Haswell、掘削機(未リリース)。 SSE/AVXと同様に、これは、関数のBMIバージョンと、それらの命令をサポートしないCPU用のフォールバック非BMIバージョンが必要になることを意味することに注意してください。 AFAIK、コンパイラには、これらの命令のパターンを表示して自動的に使用するオプションがありません。それらは組み込み関数(またはasm)を介してのみ使用できます。

Dbushの答え 、フィールドの使用方法にもよりますが、ビットフィールドにパックすることはおそらく良い選択です。 4番目のオプション(4つの別々のabcd値を1つの構造体にパックする)はおそらく間違いですnless 4つの連続したabcd値(ベクタースタイル)で何か便利なことができます。

一般的にコードを作成するには、両方の方法を試してください

コードが広範囲に使用するデータ構造の場合、ある実装から別の実装に切り替えてベンチマークできるように設定することは理にかなっています。 Nir Friedmanの答え、ゲッター/セッター付き は良い方法です。ただし、int temporariesを使用し、構造体の個別のメンバーとしてフィールドを操作するだけで問題なく動作するはずです。パックされたビットフィールドについて、バイトの正しいビットをテストするコードを生成するのはコンパイラー次第です。

必要に応じてSIMDの準備をする

各構造体の1つまたは2つのフィールドだけをチェックするコードがある場合は、特に。連続する構造体の値をループすると、 cmasterによって与えられた配列の構造体の答え が役立ちます。 x86ベクトル命令は最小粒度として1バイトを持っているため、各値が別々のバイトにある配列構造体を使用すると、a == somethingを使用して、PCMPEQB / PTESTの最初の要素をすばやくスキャンできます。

9
Peter Cordes

まず、「最も効率的」とはどういう意味かを正確に定義します。最高のメモリ使用率?最高のパフォーマンス?

次に、アルゴリズムを両方の方法で実装し、配信後に実行する予定の実際の条件下で実行する予定の実際のハードウェアで実際にプロファイリングします。

「最も効率的」という元の定義により適したものを選択してください。

それ以外は単なる推測です。どちらを選択してもおそらく問題なく動作しますが、実際に測定ソフトウェアを使用する正確な条件下での違いがなければ、どの実装が「より効率的」であるかはわかりません。

7
Andrew Henle

唯一の本当の答えは、コードを一般的に記述してから、プログラム全体をそれらすべてでプロファイリングすることだと思います。もう少し厄介に見えるかもしれませんが、これにはそれほど時間がかかるとは思いません。基本的に、私は次のようなことをします:

template <bool is_packed> class Foo;
using interface_int = char;

template <>
class Foo<true> {
    char m_a, m_b, m_c, m_d;
 public: 
    void setA(interface_int a) { m_a = a; }
    interface_int getA() { return m_a; }
    ...
}

template <>
class Foo<false> {
  char m_data;
 public:
    void setA(interface_int a) { // bit magic changes m_data; }
    interface_int getA() { // bit magic gets a from m_data; }
}

生データを公開する代わりにこのようにコードを書くだけで、実装とプロファイルを簡単に切り替えることができます。関数呼び出しはインライン化され、パフォーマンスに影響を与えません。参照を返す関数の代わりにsetAとgetAを記述しただけであることに注意してください。これは、実装がより複雑です。

5
Nir Friedman

大規模な配列とメモリ不足エラー

  1. プログラム全体は、数十億のFoosで構成されています。

まず最初に、#2の場合、ギガバイトにまたがる場合、自分自身またはユーザー(他のユーザーがソフトウェアを実行している場合)がこの配列を正常に割り当てることができない場合があります。ここでよくある間違いは、メモリ不足エラーは「使用可能なメモリがありません」を意味し、代わりにOSが要求されたメモリサイズに一致する未使用ページの連続セット。このため、30ギガバイトの物理メモリが空いている場合でも、1ギガバイトのブロックを割り当てて失敗させるように要求すると、混乱することがよくあります。たとえば、使用可能なメモリの通常の量の1%を超えるサイズでメモリの割り当てを開始したら、全体を表す1つの巨大な配列を避けることを検討することがよくあります。

したがって、おそらく最初に行う必要があるのは、データ構造を再考することです。数十億の要素の単一の配列を割り当てる代わりに、小さなチャンク(小さな配列が一緒に集約されたもの)に割り当てることで、問題が発生する可能性を大幅に減らすことができます。たとえば、アクセスパターンが本質的にシーケンシャルのみである場合は、展開されたリスト(互いにリンクされた配列)を使用できます。ランダムアクセスが必要な場合は、それぞれが4キロバイトに及ぶ配列へのポインターの配列のようなものを使用できます。これには、要素のインデックスを作成するためにもう少し作業が必要ですが、この種の数十億の要素のスケールでは、多くの場合必要になります。

アクセスパターン

質問で指定されていないものの1つは、メモリアクセスパターンです。この部分はあなたの決定を導くために重要です。

たとえば、データ構造は順番にトラバースされるだけですか、それともランダムアクセスが必要ですか? abcdのすべてのフィールドは常に一緒に必要ですか、それとも1つ、2つ、または3つアクセスできますか一度に?

すべての可能性をカバーしてみましょう。私たちが話している規模では、これは:

struct Foo {
    int a1;
    int b1;
    int c1;
    int d1
};

...役に立たない可能性があります。この種の入力スケールでは、タイトなループでアクセスされるため、通常、時間は上位レベルのメモリ階層(ページングとCPUキャッシュ)によって支配されます。階層の最下位レベル(レジスターおよび関連する命令)に焦点を合わせることが、もはやそれほど重要ではなくなりました。別の言い方をすれば、処理する要素が数十億である場合、最後に心配する必要があるのは、このメモリをL1キャッシュラインからレジスタに移動するコストと、ビット単位の命令のコストです。 (それがまったく問題ではないと言っているのではなく、はるかに低い優先順位であると言っているだけです)。

ホットデータ全体がCPUキャッシュに収まり、ランダムアクセスが必要な十分に小さいスケールでは、この種の単純な表現は、階層の最下位レベル(レジスタと命令)での改善によるパフォーマンスの改善を示すことができます。 、それでも、私たちが話しているものよりも大幅に小規模な入力が必要になります。

したがって、これでもかなりの改善になる可能性があります。

struct Foo {
    char a1;
    char b1;
    char c1;
    char d1;
};

...そしてこれはさらに:

// Each field packs 4 values with 2-bits each.
struct Foo {
    char a4; 
    char b4;
    char c4;
    char d4;
};

*上記のビットフィールドを使用できますが、使用されているコンパイラによっては、ビットフィールドに関連する警告がある傾向があることに注意してください。あなたの場合は不要かもしれませんが、私は一般的に説明されている移植性の問題のためにそれらを避けるようにしばしば注意しました。ただし、以下のSoAおよびホット/コールドフィールド分割領域に挑戦すると、ビットフィールドを使用できなくなるポイントに到達します。

このコードは、すでにミニチュアSoA形式になっているため、さらに最適化パスを簡単に探索できるようにする水平ロジックにも焦点を当てています(例:SIMDを使用するようにコードを変換する)。

データ「消費」

特にこの種の規模では、さらにメモリアクセスが本質的にシーケンシャルである場合は、データの「消費」(マシンがデータをロードし、必要な計算を実行し、結果を保存する速度)の観点から考えると役立ちます。 。私が役立つと思う単純な心のイメージは、コンピューターが「大きな口」を持っていると想像することです。小さじ1杯ではなく、スプーン一杯の十分な量のデータを一度にフィードし、より関連性の高いデータを連続したスプーン一杯にしっかりと詰め込むと、処理速度が速くなります。

Hungry Computer

ホット/コールドフィールド分割

これまでの上記のコードは、これらのフィールドすべてが等しくホット(頻繁にアクセスされる)であり、一緒にアクセスされることを前提としています。いくつかのコールドフィールドまたはペアの重要なコードパスでのみアクセスされるフィールドがある場合があります。 cdにアクセスすることはめったにない、またはコードにabにアクセスする1つのクリティカルループと、_ [にアクセスする別のループがあるとします。 $ var] _およびc。その場合、それを2つの構造に分割すると便利です。

struct Foo1 {
    char a4; 
    char b4;
};
struct Foo2 {
    char c4;
    char d4;
};

繰り返しになりますが、コンピューターデータを「フィード」していて、コードが現時点でdフィールドとaフィールドのみに関心がある場合は、スプーン一杯のbにさらにパックできます。 のみを含むaおよびbフィールドを含む連続ブロックがある場合は、aフィールド、およびbおよびcフィールドではありません。このような場合、dフィールドとcフィールドは、コンピューターが現時点でダイジェストできないデータですが、daの間のメモリ領域に混在します。 bフィールド。コンピューターができるだけ早くデータを消費するようにしたい場合は、現時点で関心のある関連データのみをコンピューターに供給する必要があるため、これらのシナリオで構造を分割する価値があります。

シーケンシャルアクセス用のSIMDSoA

ベクトル化に移行し、シーケンシャルアクセスを想定すると、コンピューターがデータを消費できる最速の速度は、SIMDを使用して並行することがよくあります。このような場合、次のような表現になる可能性があります。

struct Foo1 {
    char* a4n;
    char* b4n;
};

... XMM/YMMレジスタへのより高速な整列移動を使用するために必要な整列とパディング(サイズ/整列はAVXの場合は16バイトまたは32バイトの倍数、さらには64バイトの倍数である必要があります)に注意してください(将来のAVX命令)。

ランダム/マルチフィールドアクセス用のAoSoA

残念ながら、abが頻繁に一緒にアクセスされる場合、特にランダムアクセスパターンでは、上記の表現は多くの潜在的な利点を失い始める可能性があります。このような場合、より最適な表現は次のようになります。

struct Foo1 {
    char a4x32[32];
    char b4x32[32];
};

...ここでこの構造を集約しています。これにより、aフィールドとbフィールドがそれほど分散しなくなり、32個のaフィールドとbフィールドのグループを単一の64個に収めることができます。 -バイトキャッシュラインと一緒にすばやくアクセスします。 128または256のaまたはb要素をXMM/YMMレジスタに収めることもできます。

プロファイリング

通常、私はパフォーマンスの質問で一般的な知恵のアドバイスを避けようとしますが、これはプロファイラーを持っている人が通常言及する詳細を避けているように見えることに気づきました。ですから、これがひいきになっているのか、プロファイラーがすでに積極的に使用されているのか、お詫びしますが、このセクションは当然のことと思います。

逸話として、私はコンピュータアーキテクチャについて私よりもはるかに優れた知識を持っている人々によって書かれたプロダクションコードを最適化することでしばしばより良い仕事をしました(私はすべきではありません!)(私はパンチカードから来た多くの人々と仕事をしました時代であり、アセンブリコードを一目で理解できます)、コードを最適化するために呼び出されることがよくありました(これは本当に奇妙に感じました)。それは1つの単純な理由です:私はプロファイラー(VTune)を「だまして」使用しました。私の仲間はしばしばそうしませんでした(彼らはそれにアレルギーがあり、プロファイラーと同様にホットスポットを理解し、プロファイリングを時間の無駄だと思っていました)。

もちろん、理想は、コンピュータアーキテクチャの専門知識とプロファイラーの両方を持っているが、どちらか一方が不足している人を見つけることです。プロファイラーは、より大きなエッジを与えることができます。最適化は、最も効果的な優先順位付けに依存する生産性の考え方に報いるものであり、最も効果的な優先順位付けは、本当に最も重要な部分を最適化することです。プロファイラーは、正確に費やされた時間と場所の詳細な内訳を、キャッシュミスや分岐予測などの有用なメトリックとともに提供します。これらは、最も高度な人間でさえ、プロファイラーが明らかにできるほど正確に予測することはできません。さらに、プロファイリングは、ホットスポットを追跡し、それらが存在する理由を調査することによって、コンピューターアーキテクチャがより速いペースでどのように機能するかを発見するための鍵となることがよくあります。私にとって、プロファイリングは、コンピューターアーキテクチャが実際にどのように機能するかをよりよく理解するための究極の入り口であり、私が想像した方法ではありませんでした。 Mysticialのようにこの点で経験を積んだ誰かの著作が、ますます意味をなすようになったのはその時だけでした。

インターフェイスデザイン

ここで明らかになり始めるかもしれないことの1つは、多くの最適化の可能性があるということです。この種の質問に対する答えは、絶対的なアプローチではなく、戦略に関するものになります。何かを試した後でも、後から考えて多くのことを発見する必要があります。それでも、必要に応じて、ますます最適なソリューションに向けて繰り返します。

複雑なコードベースでのここでの問題の1つは、インターフェイスに十分な余裕を残して、さまざまな最適化手法を実験および試行し、より高速なソリューションに向けて反復および反復することです。インターフェースがこれらの種類の最適化を求める余地を残している場合、試行錯誤の考え方でも適切に測定していれば、一日中最適化でき、多くの場合、すばらしい結果が得られます。

より高速な手法を実験して探索するために、実装に十分な余裕を残すには、多くの場合、bulkでデータを受け入れるインターフェイス設計が必要です。これは、インターフェイスが間接関数呼び出し(例:dylibまたは関数ポインターを介して)を含み、インライン化がもはや効果的な可能性ではない場合に特に当てはまります。このようなシナリオでは、インターフェイスの破損をカスケードせずに最適化する余地を残すことは、多くの場合、データのチャンク全体にポインタを渡すことを優先して、単純なスカラーパラメータを受け取るという考え方から離れて設計することを意味します(さまざまなインターリーブの可能性がある場合は、ストライドを使用する可能性があります)。したがって、これはかなり広い領域に迷い込んでいますが、ここで最適化する際の最優先事項の多くは、コードベース全体に変更をカスケードせずに実装を最適化するのに十分な余裕を残し、プロファイラーを手元に置いてガイドすることになります。正しい方法。

TL; DR

とにかく、これらの戦略のいくつかはあなたを正しい道に導くのに役立つはずです。ここには絶対的なものはなく、ガイドと試してみるものだけがあり、常にプロファイラーを手に持って行うのが最善です。しかし、この巨大なスケールのデータを処理するときは、空腹のモンスターの画像と、これらの適切なサイズでパックされたスプーン一杯の関連データを最も効果的にフィードする方法を常に覚えておく価値があります。

4
Dragon Energy

intsでコーディングします

フィールドをintsとして扱います。

blah.x宣言だけを除いて、すべてのコードで実行します。ほとんどの場合、汎整数拡張が処理します。

すべて完了したら、3つの同等のインクルードファイルを用意します。1つはintsを使用し、もう1つはcharを使用します。もう1つはビットフィールドを使用します。

そして、プロファイル。この段階では、最適化が時期尚早であり、選択したインクルードファイル以外は変更されないため、心配する必要はありません。

4

尋ねられた質問に戻る:

タイトループで使用されます。

それらの値は10億回/秒で読み取られ、それがプログラムのボトルネックです。

プログラム全体は、数十億のFoosで構成されています。

これは、実装プラットフォームごとに設計に時間がかかるプラットフォーム固有の高性能コードを作成する必要がある場合の典型的な例ですが、メリットはそのコストを上回ります。

これはプログラム全体のボトルネックであるため、一般的な解決策を探す必要はありませんが、最良の解決策はプラットフォームであるため、実際のデータに対して複数のアプローチをテストしてタイミングを合わせる必要があることを認識してください。特定

数十億のfooが多数存在するため、OPは、利用可能なものを最大限に活用するために、潜在的なソリューションとして OpenCL または OpenMP の使用を検討する必要がある可能性もあります。ランタイムハードウェア上のリソース。これは、データから何が必要かによって少し異なりますが、おそらくこのタイプの問題の最も重要な側面、つまり利用可能な並列処理を活用する方法です。

しかし、この質問に対する単一の正しい答えはありません、IMO。

3
StephenG

たとえば、少し古いメモリバスがあり、10 GB/sを配信できるとします。ここで、2.5 GHzのCPUを使用すると、メモリバスを飽和させるために、サイクルごとに少なくとも4バイトを処理する必要があることがわかります。そのため、次の定義を使用する場合

struct Foo {
    char a;
    char b;
    char c;
    char d;
}

データを通過するたびに4つの変数すべてを使用すると、コードはCPUにバインドされます。密度の高いパッキングでは速度を上げることはできません。

現在、これは、各パスが4つの値のいずれかに対して簡単な操作のみを実行する場合とは異なります。その場合は、配列の構造体を使用することをお勧めします。

struct Foo {
    size_t count;
    char* a;    //a[count]
    char* b;    //b[count]
    char* c;    //c[count]
    char* d;    //d[count]
}

あなたは一般的で曖昧なC/C++タグについて述べました。

C++を想定して、データをプライベートにし、ゲッター/セッターを追加します。いいえ、それによってパフォーマンスが低下することはありません。オプティマイザがオンになっている場合に限ります。

次に、呼び出しコードを変更せずに代替を使用するように実装を変更できます。したがって、ベンチテストの結果に基づいて実装をより簡単に調整できます。

ちなみに、@ dbushのビットフィールドを持つstructは、あなたの説明からするとおそらく最速だと思います。

これはすべて、データをキャッシュに保持することに関するものであることに注意してください。呼び出しアルゴリズムの設計がそれを支援できるかどうかも確認する必要があります。

3
Keith

必要なのがスペースの効率である場合は、structsを完全に回避することを検討する必要があります。コンパイラーは、必要に応じて構造体表現にパディングを挿入して、サイズをアライメント要件の倍数にします。これは、16バイトにもなる可能性があります(ただし、4バイトまたは8バイトになる可能性が高く、結局のところ、 1バイト)。

とにかく構造体を使用する場合、どちらを使用するかは実装によって異なります。 @dbushのビットフィールドアプローチが1バイト構造を生成する場合、それを打ち負かすことは困難です。ただし、実装で表現を少なくとも4バイトにパディングする場合は、これがおそらく使用するものです。

struct Foo {
    char a;
    char b;
    char c;
    char d;
};

または、おそらくこのバリアントを使用すると思います。

struct Foo {
    uint8_t a;
    uint8_t b;
    uint8_t c;
    uint8_t d;
};

構造体が最低4バイトを使用していると想定しているため、データをより小さなスペースにパックしても意味がありません。実際、これは逆効果になります。これは、プロセッサに値のパックとアンパックの余分な作業を行わせるためです。

大量のデータを処理する場合、CPUキャッシュを効率的に使用すると、いくつかの整数演算を回避するよりもはるかに大きなメリットが得られます。データ使用パターンが少なくともある程度体系的である場合(たとえば、以前の構造体配列の1つの要素にアクセスした後、次に近くの要素にアクセスする可能性が高い場合)、パックすることでスペース効率と速度の両方が向上する可能性があります。できるだけ厳密にデータを収集します。 Cの実装によっては(または実装の依存関係を回避したい場合)、たとえば整数の配列を使用して、別の方法でそれを実現する必要がある場合があります。それぞれ2ビットを必要とする4つのフィールドの特定の例では、各「構造体」を代わりにuint8_tとして表し、それぞれ合計1バイトにすることを検討します。

多分このようなもの:

#include <stdint.h>

#define NUMBER_OF_FOOS 1000000000
#define A 0
#define B 2
#define C 4
#define D 6

#define SET_FOO_FIELD(foos, index, field, value) \
    ((foos)[index] = (((foos)[index] & ~(3 << (field))) | (((value) & 3) << (field))))
#define GET_FOO_FIELD(foos, index, field) (((foos)[index] >> (field)) & 3)

typedef uint8_t foo;

foo all_the_foos[NUMBER_OF_FOOS];

フィールド名マクロとアクセスマクロは、配列を直接操作するよりも、個々のフィールドにアクセスするためのより読みやすく調整可能な方法を提供します(ただし、これらの特定のマクロは、引数の一部を複数回評価することに注意してください)。すべてのビットが使用され、データ構造の選択だけで達成できる限りのキャッシュ使用率が得られます。

2
John Bollinger

しばらくの間、ビデオの解凍を行いました。最速の方法は次のようなものです。

short ABCD; //use a 16 bit data type for your example

いくつかのマクロを設定します。多分:

#define GETA ((ABCD >> 12) & 0x000F)
#define GETB ((ABCD >> 8) & 0x000F)
#define GETC ((ABCD >> 4) & 0x000F)
#define GETD (ABCD  & 0x000F)  // no need to shift D

実際には、32ビット長または64ビット長を移動するようにしてください。これは、ほとんどの最新のプロセッサのネイティブのMOVEサイズだからです。

構造体を使用すると、構造体のベースアドレスからフィールドへの追加命令のコンパイル済みコードに常にオーバーヘッドが発生します。したがって、本当にループを引き締めたい場合は、それを避けてください。

編集:上記の例では、4ビット値が得られます。本当に0..3の値が必要な場合は、同じことを実行して2ビットの数値を引き出すことができるため、, GETAは次のようになります。

GETA ((ABCD >> 14) & 0x0003)

そして、あなたが本当に何十億ものものを動かしているなら、そして私はそれを疑うことはありません、ただ32ビット変数を埋めて、それを通してあなたの道をシフトしてマスクしてください。

お役に立てれば。

2
Mark Nilsen

最も効率的なパフォーマンス/実行は、プロセッサのワードサイズを使用することです。プロセッサに梱包または開梱の余分な作業を実行させないでください。

一部のプロセッサには、複数の効率的なサイズがあります。多くのARMプロセッサは8/32ビットモードで動作できます。これは、プロセッサが8ビット量または32ビット量を処理するように最適化されていることを意味します。このようなプロセッサの場合、8ビットを使用することをお勧めします。ビットデータタイプ。

あなたのアルゴリズムは効率と多くの関係があります。データを移動したり、データをコピーしたりする場合は、一度に32ビット(4つの8ビット量)でデータを移動することを検討してください。ここでの考え方は、プロセッサによるフェッチの数を減らすことです。

パフォーマンスのために、より多くのローカル変数を使用するなど、registersを使用するコードを記述します。メモリからレジスタへのフェッチは、レジスタを直接使用するよりもコストがかかります。

何よりも、コンパイラの最適化設定を確認してください。最高のパフォーマンス(速度)設定のためにコンパイルを設定します。次に、関数のアセンブリ言語リストを生成します。リストを確認して、コンパイラーがコードを生成した方法を確認してください。コードを調整して、コンパイラの最適化機能を向上させます。

2
Thomas Matthews