C のビットフィールド実装を使用する価値はありますか? もしそうなら、それはいつ使われますか?
エミュレータ コードを調べたところ、ビット フィールドを使用してチップのレジスタが実装されていないようです。
これは、パフォーマンス上の理由 (またはその他の理由) で回避されるものですか?
ビットフィールドが使用されることはまだありますか? (つまり、実際のチップに搭載するファームウェアなど)
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 倍悪い (メモリの読み取り/書き込みは上記のボラティリティによって強調されており、ループ オーバーヘッドなどがあるため、結果では違いが控えめに表現されています)。
主にビットフィールドまたは主に個別の整数である数百メガバイトの構造体を扱っている場合、キャッシュの問題が支配的になる可能性があるため、システムのベンチマークになります。
AMD Ryzen 9 3900X および -O2 -march=native を使用した 2021 年からの更新:
bitfields: 0.0224893
1449991808
separate ints: 0.0288447
1449991808
explicit and/or/shift: 0.0190325
1449991808
ここでは、すべてが大幅に変更されていることがわかります。主な意味は、関心のあるシステムのベンチマークです。
更新: 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 つは、組み込みコードを記述するときにハードウェア レジスタをミラーリングすることでした。ただし、ビットの順序はプラットフォームに依存するため、ハードウェアのビットの順序がプロセッサと異なる場合は機能しません。とはいえ、ビットフィールドの用途はもう考えられません。プラットフォーム間で移植できるビット操作ライブラリを実装することをお勧めします。
昔は、プログラムのメモリを節約するためにビットフィールドが使用されていました。
レジスタはそれらを処理できないため、パフォーマンスが低下します。そのため、レジスタを処理するには整数に変換する必要があります。それらは、移植性がなく、理解するのが難しい、より複雑なコードにつながる傾向があります(実際に値を使用するには、常に物事をマスクおよびマスク解除する必要があるため)。
http://www.nethack.org/のソースをチェックして、ビットフィールドの栄光のすべてを事前に確認してください。
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 */
};
:1
IMO にビットフィールドを使用しない理由は基本的にありませんbool
。最新のコンパイラは非常に効率的なコードを生成するからです。ただし、C では、bool
typedef が C99 であるか、 unsigned_Bool
int に失敗していることを確認してください。これは、符号付き 1 ビット フィールドは値のみを保持できるためです( 2 の補数ではないマシンを使用している場合を除く)。0
-1
列挙型では、非効率的なコード生成 (通常は読み取り-変更-書き込みサイクルの繰り返し) を避けるために、プリミティブ整数型 (通常の CPU では 8/16/32/64 ビット) のいずれかのサイズに対応するサイズを常に使用してください。 .
ビットフィールドを使用して、外部で定義されたデータ形式 (パケット ヘッダー、メモリ マップ I/O レジスタ) と構造体を並べることが一般的に提案されていますが、C ではエンディアンを十分に制御できないため、実際にはそれは悪い習慣だと考えています。 、パディング、および (I/O reg の場合) 正確にどのアセンブリ シーケンスが出力されるか。この領域で C がどれだけ欠けているかを確認したい場合は、Ada の表現句を参照してください。
Boost.Thread はshared_mutex
、少なくとも Windowsではビットフィールドを使用します。
struct state_data
{
unsigned shared_count:11,
shared_waiting:11,
exclusive:1,
upgrade:1,
exclusive_waiting:7,
exclusive_waiting_blocked:1;
};
考慮すべき別の方法は、各バイトがビットを表すダミー構造 (インスタンス化されていない) を使用してビット フィールド構造を指定することです。
struct Bf_format
{
char field1[5];
char field2[9];
char field3[18];
};
このアプローチでは、 sizeofはビット フィールドの幅を示し、offsetofはビット フィールドのオフセットを示します。少なくとも GNU gcc の場合、(定数シフトとマスクを使用した) ビット単位の操作のコンパイラの最適化は、(ベース言語の) ビット フィールドと大まかに同等になったようです。
ビットフィールドの構造を定義して、パフォーマンスが高く、より移植性が高く、より柔軟な方法で使用できるようにする C++ ヘッダーファイルを (このアプローチを使用して) 作成しました: https://github.com/wkaras/C-plus- plus-library-bit-fields . したがって、C の使用に行き詰まっていない限り、ビット フィールドにベース言語機能を使用する正当な理由はほとんどないと思います。