1. ホーム
  2. c++

[解決済み] なぜ、型はその値に関係なく、常に一定の大きさなのですか?

2022-04-29 10:01:43

質問

実装によって型の大きさは異なりますが、unsigned intやfloatのような型は常に4バイトです。しかし、なぜある型が常に ある は、どのような値であってもメモリ量に影響を与えるのでしょうか?例えば、次のような255という値を持つ整数を作ったとします。

int myInt = 255;

次に myInt は、私のコンパイラでは4バイトを占有することになります。しかし、実際の値では 255 は1バイトで表現できるのに、なぜ myInt は、1バイトのメモリを占有するだけではないのでしょうか?あるいは、もっと一般化した聞き方。値を表現するのに必要な領域はそのサイズよりも小さいかもしれないのに、なぜ型には1つのサイズしか関連付けられていないのか?

どうすれば解決するの?

コンパイラは、あるマシン用のアセンブラ(最終的にはマシンコード)を生成することになっており、一般にC++はそのマシンに同調しようとする。

基礎となるマシンに共感するということは、大まかに言って、マシンが高速に実行できる操作に効率的にマッピングするC++コードを書きやすくするということです。つまり、私たちは、私たちのハードウェアプラットフォームで高速かつ自然なデータ型と操作へのアクセスを提供したいのです。

具体的に、あるマシン・アーキテクチャを考えてみましょう。現在のIntel x86ファミリーを例に挙げてみましょう。

インテル® 64およびIA-32アーキテクチャー・ソフトウェア開発者向けマニュアル vol1 ( リンク )の3.4.1項にはこう書かれています。

32ビット汎用レジスタEAX, EBX, ECX, EDX, ESI、EDI、EBP、およびESPは、以下の情報を保持するために用意されています。 以下の項目があります。

- 論理演算、算術演算のオペランド

- アドレス計算のためのオペランド

- メモリポインタ

そこで、コンパイラがC++の単純な整数演算をコンパイルする際に、これらのEAX、EBXなどのレジスタを使用するようにしたいのです。これはつまり、私が int これらのレジスタを効率的に使用できるように、これらのレジスタと互換性のあるものである必要があります。

レジスタは常に同じサイズ(ここでは32ビット)であるため、私の int 変数も常に32ビットになります。変数の値をレジスタにロードしたり、レジスタを変数にストアしたりするたびに変換する必要がないように、同じレイアウト(リトルエンディアン)を使用することにします。

使用方法 ゴッドボルト を使えば、コンパイラがどのような処理をしているかがわかります。

int square(int num) {
    return num * num;
}

はコンパイルします(GCC 8.1および -fomit-frame-pointer -O3 を簡略化しています)。

square(int):
  imul edi, edi
  mov eax, edi
  ret

という意味です。

  1. その int num パラメータはレジスタ EDI で渡され、Intel がネイティブ・レジスタに期待するサイズとレイアウトを正確に表しています。この関数は何も変換する必要がありません。
  2. 乗算は1つの命令( imul )であり、非常に高速です。
  3. 結果を返すのは、単に他のレジスタにコピーする問題です(呼び出し側は結果がEAXに置かれることを期待しています)。

編集:非ネイティブレイアウトを使用した場合の違いを示すために、関連する比較を追加することができます。最も単純なケースは、ネイティブ幅以外のもので値を保存することです。

使用方法 ゴッドボルト 再び、単純なネイティブの乗算を比較します

unsigned mult (unsigned x, unsigned y)
{
    return x*y;
}

mult(unsigned int, unsigned int):
  mov eax, edi
  imul eax, esi
  ret

を、非標準の幅に対応する同等のコードに置き換えたものです。

struct pair {
    unsigned x : 31;
    unsigned y : 31;
};

unsigned mult (pair p)
{
    return p.x*p.y;
}

mult(pair):
  mov eax, edi
  shr rdi, 32
  and eax, 2147483647
  and edi, 2147483647
  imul eax, edi
  ret

余分な命令はすべて、入力フォーマット(2つの31ビット符号なし整数)を、プロセッサがネイティブに扱えるフォーマットに変換することに関係しています。もし結果を31ビットの値に戻したいなら、そのための命令がもう1つか2つ必要になります。

このように余計に複雑になるということは、スペースの節約が非常に重要な場合にのみ、これを気にすることになります。この場合、ネイティブの unsigned または uint32_t という型があれば、もっとシンプルなコードが生成されるはずです。


ダイナミックサイズについての注意点。

上の例は、可変幅ではなく固定幅の値のままですが、幅(とアライメント)がネイティブレジスタと一致しなくなりました。

x86プラットフォームには、メインの32ビット以外に8ビットや16ビットなど、いくつかのネイティブサイズがあります(わかりやすくするため、64ビットモードやその他いろいろなことは割愛します)。

これらの型(char、int8_t、uint8_t、int16_tなど)は また これは、古い 8086/286/386/ 等の命令セットとの後方互換性を確保するためでもあります。

確かに、最小の 自然な固定サイズ シングル命令でロードとストアをすばやく実行でき、フルスピードのネイティブ演算が可能で、キャッシュミスを減らすことによってパフォーマンスを向上させることもできます。

これは可変長エンコーディングとは全く異なるもので、私もいくつか扱ったことがありますが、ひどいものでした。ロードするたびに1つの命令ではなく、ループになります。すべてのストアはループになります。すべての構造体が可変長であるため、当然ながら配列は使えません。


効率化に関するさらなる注意点

その後のコメントで、ストレージサイズに関して、私の知る限りでは「quot;efficient"」という言葉を使っていますね。非常に多くの値をファイルに保存したり、ネットワーク経由で送信したりする場合には、ストレージサイズを最小化することが重要になることがあります。そのトレードオフとして、これらの値をレジスタにロードして する 変換は無料ではありません。

効率について議論するときは、何を最適化しているのか、トレードオフの関係はどうなっているのかを知る必要があります。非ネイティブのストレージタイプを使用することは、処理速度とスペースを交換する方法の1つであり、時には理にかなっています。可変長ストレージの使用は(少なくとも算術型については)、以下のようなトレードオフをもたらします。 より 処理速度(およびコードの複雑さと開発者の時間)は、多くの場合、さらに最小限のスペースの節約になります。

このために支払うスピードのペナルティは、帯域幅や長期保存を絶対に最小化する必要がある場合にのみ価値があるということです。そのような場合には、通常はシンプルで自然なフォーマットを使用して、汎用システム(zip、gzip、bzip2、xyなど)で圧縮する方が簡単です。


tl;dr

各プラットフォームには1つのアーキテクチャがありますが、データを表現する方法は基本的に無限に思いつくことができます。どの言語でも、組み込みのデータ型を無制限に提供するのは合理的ではありません。そこでC++は、プラットフォームが本来持っている自然なデータ型の集合に暗黙的にアクセスできるようにし、それ以外の(ネイティブではない)表現を自分でコーディングできるようにしています。