Cのビットフィールド実装を使用する価値はありますか?その場合、いつ使用されますか?
エミュレータコードを調べていましたが、チップのレジスタがビットフィールドを使用して実装されていないようです。
これは、パフォーマンス上の理由(またはその他の理由)のために避けられるものですか?
ビットフィールドが使用される時期はまだありますか? (つまり、実際のチップに搭載するファームウェアなど)
ビットフィールドは、通常、構造体フィールドを特定のビットスライスにマップする必要がある場合にのみ使用されます。ハードウェアによっては、生のビットを解釈します。例として、IPパケットヘッダーの組み立てがあります。エミュレーターが実際のハードウェアに触れることはないので、エミュレーターがビットフィールドを使用してレジスターをモデル化する説得力のある理由はわかりません!
ビットフィールドはきちんとした構文につながる可能性がありますが、かなりプラットフォームに依存しているため、移植できません。より移植性がありますが、より冗長なアプローチは、シフトとビットマスクを使用して、直接ビットごとの操作を使用することです。
一部の物理インターフェイスで構造体の組み立て(または分解)以外の目的でビットフィールドを使用すると、パフォーマンスが低下する可能性があります。これは、ビットフィールドから読み取りまたは書き込みを行うたびに、コンパイラがマスキングとシフトを実行するコードを生成する必要があるためです。
まだ言及されていないビットフィールドの1つの用途は、unsigned
ビットフィールドが「無料で」2のべき乗を法とする算術を提供することです。たとえば、次のような場合:
struct { unsigned x:10; } foo;
foo.x
の演算は2を法として実行されます10 = 1024。
(もちろん、ビット単位の&
演算を使用して同じことを直接実現することもできますが、コンパイラーに実行させるコードがより明確になる場合もあります)。
FWIW、そして相対的なパフォーマンスの質問のみを見る-だらしないベンチマーク:
#include <time.h>
#include <iostream>
struct A
{
void a(unsigned n) { a_ = n; }
void b(unsigned n) { b_ = n; }
void c(unsigned n) { c_ = n; }
void d(unsigned n) { d_ = n; }
unsigned a() { return a_; }
unsigned b() { return b_; }
unsigned c() { return c_; }
unsigned d() { return d_; }
volatile unsigned a_:1,
b_:5,
c_:2,
d_:8;
};
struct B
{
void a(unsigned n) { a_ = n; }
void b(unsigned n) { b_ = n; }
void c(unsigned n) { c_ = n; }
void d(unsigned n) { d_ = n; }
unsigned a() { return a_; }
unsigned b() { return b_; }
unsigned c() { return c_; }
unsigned d() { return d_; }
volatile unsigned a_, b_, c_, d_;
};
struct C
{
void a(unsigned n) { x_ &= ~0x01; x_ |= n; }
void b(unsigned n) { x_ &= ~0x3E; x_ |= n << 1; }
void c(unsigned n) { x_ &= ~0xC0; x_ |= n << 6; }
void d(unsigned n) { x_ &= ~0xFF00; x_ |= n << 8; }
unsigned a() const { return x_ & 0x01; }
unsigned b() const { return (x_ & 0x3E) >> 1; }
unsigned c() const { return (x_ & 0xC0) >> 6; }
unsigned d() const { return (x_ & 0xFF00) >> 8; }
volatile unsigned x_;
};
struct Timer
{
Timer() { get(&start_tp); }
double elapsed() const {
struct timespec end_tp;
get(&end_tp);
return (end_tp.tv_sec - start_tp.tv_sec) +
(1E-9 * end_tp.tv_nsec - 1E-9 * start_tp.tv_nsec);
}
private:
static void get(struct timespec* p_tp) {
if (clock_gettime(CLOCK_REALTIME, p_tp) != 0)
{
std::cerr << "clock_gettime() error\n";
exit(EXIT_FAILURE);
}
}
struct timespec start_tp;
};
template <typename T>
unsigned f()
{
int n = 0;
Timer timer;
T t;
for (int i = 0; i < 10000000; ++i)
{
t.a(i & 0x01);
t.b(i & 0x1F);
t.c(i & 0x03);
t.d(i & 0xFF);
n += t.a() + t.b() + t.c() + t.d();
}
std::cout << timer.elapsed() << '\n';
return n;
}
int main()
{
std::cout << "bitfields: " << f<A>() << '\n';
std::cout << "separate ints: " << f<B>() << '\n';
std::cout << "explicit and/or/shift: " << f<C>() << '\n';
}
テストマシンでの出力(数値は実行ごとに約20%異なります):
bitfields: 0.140586
1449991808
separate ints: 0.039374
1449991808
explicit and/or/shift: 0.252723
1449991808
最近のAthlonでg ++ -O3を使用すると、ビットフィールドは個別のintよりも数倍遅く、この特定の実装やビットシフト実装は、少なくとも2倍悪い(メモリ読み取りなどの他の操作と比べて「悪い」)書き込みは上記のボラティリティによって強調され、ループオーバーヘッドなどがあるため、結果の違いは控えめです。
主にビットフィールドまたは主に個別のintである可能性がある数百メガバイトの構造体を扱っている場合、キャッシュの問題が支配的になる可能性があるため、システムのベンチマークを確認します。
更新:user2188211は編集を試みましたが、拒否されましたが、データ量が増えるにつれてビットフィールドがどのように速くなるかを示しています:「上記のコードの[変更されたバージョン]で数百万の要素のベクトルを反復すると、変数はキャッシュやレジスターに常駐していないため、ビットフィールドコードが最も高速な場合があります。」
template <typename T>
unsigned f()
{
int n = 0;
Timer timer;
std::vector<T> ts(1024 * 1024 * 16);
for (size_t i = 0, idx = 0; i < 10000000; ++i)
{
T& t = ts[idx];
t.a(i & 0x01);
t.b(i & 0x1F);
t.c(i & 0x03);
t.d(i & 0xFF);
n += t.a() + t.b() + t.c() + t.d();
idx++;
if (idx >= ts.size()) {
idx = 0;
}
}
std::cout << timer.elapsed() << '\n';
return n;
}
実行例の結果(g ++ -03、Core2Duo):
0.19016
bitfields: 1449991808
0.342756
separate ints: 1449991808
0.215243
explicit and/or/shift: 1449991808
もちろん、タイミングはすべて相対的であり、これらのフィールドを実装する方法は、システムのコンテキストではまったく問題ではない場合があります。
コンピューターゲームとハードウェアインターフェイスの2つの状況でビットフィールドを確認/使用しました。ハードウェアの使用は非常に簡単です。ハードウェアは、手動または事前定義されたライブラリ構造を介して定義できる特定のビット形式のデータを期待します。ビットフィールドを使用するか、ビット操作のみを使用するかは、特定のライブラリに依存します。
「昔」のコンピュータゲームでは、ビットフィールドを頻繁に使用して、コンピュータ/ディスクメモリを最大限に活用していました。たとえば、RPGのNPC定義の場合、次のような例があります):
struct charinfo_t
{
unsigned int Strength : 7; // 0-100
unsigned int Agility : 7;
unsigned int Endurance: 7;
unsigned int Speed : 7;
unsigned int Charisma : 7;
unsigned int HitPoints : 10; //0-1000
unsigned int MaxHitPoints : 10;
//etc...
};
コンピュータがより多くのメモリを取得するにつれてスペースの節約が比例して悪化しているため、より現代的なゲーム/ソフトウェアではそれほど多くは見られません。コンピュータに16MBしかない場合に1MBのメモリを節約することは重要ですが、4GBの場合はそれほどではありません。
ビットフィールドの主な目的は、データをより厳密にパッキングすることにより、大量にインスタンス化された集約データ構造でメモリを節約する方法を提供することです。
全体のアイデアは、いくつかの標準データ型の幅全体(および範囲)を必要としない構造体型にいくつかのフィールドがある状況を利用することです。これにより、そのようなフィールドのいくつかを1つのアロケーションユニットにパックする機会が得られるため、構造体タイプの全体的なサイズを削減できます。そして、極端な例は、個々のビットで表現できるブールフィールドです(たとえば、32ビットは単一のunsigned int
割り当てユニットにパック可能です)。
明らかに、これは、メモリ消費の削減の長所が、ビットフィールドに格納された値へのアクセスが遅いという短所を上回る状況でのみ意味をなします。ただし、このような状況は頻繁に発生するため、ビットフィールドは絶対に不可欠な言語機能になります。これは、ビットフィールドの最新の使用に関するあなたの質問に答えるはずです:それらは使用されるだけでなく、大量の同種データ(たとえば、大きなグラフのような)を処理することに向けられた実用的に意味のあるコードでは本質的に必須です。 -節約効果は、個々のアクセスのパフォーマンスのペナルティを大幅に上回ります。
ある意味で、ビットフィールドは、その目的で「小さい」算術型(signed/unsigned char
、short
、float
)などに非常に似ています。実際のデータ処理コードでは、通常、int
またはdouble
より小さいタイプは使用しません(いくつかの例外はあります)。 signed/unsigned char
、short
、float
のような算術型は、単に「ストレージ」型として機能するために存在します。範囲(または精度)が異なる状況では、構造体型のメモリ節約コンパクトメンバーとして十分であることが知られています。ビットフィールドは、同じ方向のもう1つのステップにすぎません。これは、パフォーマンスを犠牲にして、メモリを大幅に節約できるという利点があります。
したがって、ビットフィールドを使用する価値のあるかなり明確な一連の条件が得られます。
条件が満たされた場合、すべてのビットパック可能フィールドを連続して(通常は構造体型の最後に)宣言し、適切なビット幅を割り当てます(通常、ビット幅が適切であることを確認するためにいくつかの手順を実行します)。 。ほとんどの場合、これらのフィールドの順序をいじって、最適なパッキングやパフォーマンスを実現することは理にかなっています。
また、ビットフィールドの奇妙な2次的な使用方法もあります。それらを使用して、ハードウェアレジスタ、浮動小数点形式、ファイル形式など、外部で指定されたさまざまな表現でビットグループをマッピングします。これは、ビットフィールドの適切な使用を意図したものではありません。 、理由は不明ですが、この種のビットフィールドの乱用は、実際のコードでポップアップし続けています。これをしないでください。
ビットフィールドは、プログラムメモリを節約するために昔で使用されました。
レジスターはレジスターを処理できないため、パフォーマンスを低下させます。レジスターを処理するには整数に変換する必要があります。それらは、移植性がなく理解しにくいより複雑なコードにつながる傾向があります(実際に値を使用するには、常に物事をマスキングおよびマスキング解除する必要があるため)。
http://www.nethack.org/ のソースをチェックして、ビットフィールドの栄光の中でpre ansi cを確認してください!
70年代には、ビットフィールドを使用してtrs80のハードウェアを制御しました。ディスプレイ/キーボード/カセット/ディスクはすべてメモリマップされたデバイスでした。個々のビットはさまざまなものを制御しました。
私が覚えているように、ディスクドライブコントロールにはいくつかありました。合計4バイトありました。 2ビットのドライブ選択があったと思います。しかし、それはずっと前のことです。それは、プラントフォーム用に少なくとも2つの異なるcコンパイラがあったという点で、当時は一種の印象的でした。
他の観察は、ビットフィールドは実際にはプラットフォーム固有であるということです。ビットフィールドを含むプログラムを別のプラットフォームに移植することは期待されていません。
埋め込みコードを書き込むときにハードウェアレジスタをミラーリングするために使用されていたビットフィールドの1つの用途。ただし、ビット順序はプラットフォームに依存しているため、ハードウェアがプロセッサとは異なるビットを順序付けすると機能しません。とは言っても、ビットフィールドの使用はもう考えられません。プラットフォーム間で移植できるビット操作ライブラリを実装したほうがよいでしょう。
最近のコードでは、ビットフィールドを使用する理由は1つだけです。構造体/クラス内でbool
またはenum
型のスペース要件を制御するためです。たとえば(C++):
enum token_code { TK_a, TK_b, TK_c, ... /* less than 255 codes */ };
struct token {
token_code code : 8;
bool number_unsigned : 1;
bool is_keyword : 1;
/* etc */
};
IMO bool
に:1
ビットフィールドを使用しない理由は基本的にありません。最新のコンパイラーは非常に効率的なコードを生成するからです。ただし、Cでは、bool
typedefがC99 _Bool
であること、またはnsigned intに失敗していることを確認してください。 0
と-1
(2の補数でないマシンがある場合を除いて)。
列挙型では、非効率的なコード生成(通常、読み取り/変更/書き込みの繰り返し)を回避するために、常にプリミティブ整数型(通常のCPUでは8/16/32/64ビット)の1つに対応するサイズを使用します。 。
外部で定義されたデータ形式(パケットヘッダー、メモリマップI/Oレジスター)で構造体を整列させるためにビットフィールドを使用することは一般的に推奨されますが、Cではエンディアンネスを十分に制御できないため、実際には悪い習慣と見なしています、パディング、および(I/O regの場合は)アセンブリシーケンスが発行される正確な内容。この領域でどれだけのCが欠落しているのかを確認したい場合は、Adaの表現節を参照してください。
Boost.Threadは、少なくともWindowsではshared_mutex
でビットフィールドを使用します。
struct state_data
{
unsigned shared_count:11,
shared_waiting:11,
exclusive:1,
upgrade:1,
exclusive_waiting:7,
exclusive_waiting_blocked:1;
};