コンパイラーは、あるマシン用のアセンブラー(および最終的にはマシンコード)を生成することになっています。一般に、C ++はそのマシンに同情しようとします。
基盤となるマシンに同情するということは、おおよそのことです。マシンがすばやく実行できる操作に効率的にマップするC ++コードを簡単に記述できるようにすることです。したがって、ハードウェアプラットフォーム上で高速かつ「自然」なデータタイプと操作へのアクセスを提供したいと考えています。
具体的には、特定のマシンアーキテクチャを検討します。現在のIntel x86ファミリーを見てみましょう。
インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアルvol 1(リンク)、セクション3.4.1は次のように述べています。
32ビットの汎用レジスターEAX、EBX、ECX、EDX、ESI、EDI、EBP、およびESPは、以下の項目を保持するために提供されています。
•論理演算および算術演算のオペランド
•アドレス計算のオペランド
•メモリポインタ
したがって、単純なC ++整数演算をコンパイルするときに、コンパイラーがこれらのEAX、EBXなどのレジスターを使用するようにします。つまり、を宣言するとint
、これらのレジスタと互換性があり、効率的に使用できるようになります。
レジスターは常に同じサイズ(ここでは32ビット)であるため、int
変数も常に32ビットになります。同じレイアウト(リトルエンディアン)を使用するので、変数の値をレジスターにロードしたり、レジスターを変数に格納したりするたびに変換を行う必要はありません。
godboltを使用すると、いくつかの簡単なコードに対してコンパイラーが何をするかを正確に確認できます。
int square(int num) {
return num * num;
}
コンパイルして(GCC 8.1 -fomit-frame-pointer -O3
で簡単にするため)、次のようにします。
square(int):
imul edi, edi
mov eax, edi
ret
これの意味は:
int num
パラメータは、それはIntelがネイティブレジスタのために期待して正確にサイズとレイアウトだ意味、レジスタEDIで可決されました。関数は何も変換する必要はありません
- 乗算は単一の命令(
imul
)であり、非常に高速です。
- 結果を返すことは、単にそれを別のレジスタにコピーすることです(呼び出し元は結果がEAXに入れられることを期待しています)
編集:非ネイティブレイアウトを使用して違いを示すために、関連する比較を追加できます。最も単純なケースは、ネイティブの幅以外の値で値を格納することです。
もう一度godboltを使用して、単純なネイティブ乗算を比較できます
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
追加の命令はすべて、入力形式(31ビットの符号なし整数2つ)を、プロセッサがネイティブに処理できる形式に変換することに関するものです。結果を31ビット値に保存したい場合、これを行うための別の1つまたは2つの命令があります。
この余分な複雑さは、スペースの節約が非常に重要な場合にのみこれに悩まされることを意味します。この場合、ネイティブunsigned
またはuint32_t
タイプを使用する場合に比べて2ビットしか節約できません。これにより、はるかに単純なコードが生成されます。
動的サイズに関する注意:
上記の例は可変幅ではなく固定幅の値ですが、幅(および配置)はネイティブレジスタと一致しません。
x86プラットフォームには、メインの32ビットに加えて、8ビットや16ビットなど、いくつかのネイティブサイズがあります(簡単にするために、64ビットモードやその他のさまざまなものを強調しています)。
これらのタイプ(char、int8_t、uint8_t、int16_tなど)も、アーキテクチャによって直接サポートされています。一部は、古い8086/286/386 / etcとの下位互換性のためです。などの命令セット。
確かにそうですが、十分な最小の自然な固定サイズタイプを選択することは良い習慣になる可能性があります。これらは依然として高速で、単一の命令をロードおよび格納し、フルスピードのネイティブ演算を実行し、さらにパフォーマンスを向上させることができます。キャッシュミスを減らします。
これは可変長エンコーディングとは大きく異なります-私はこれらのいくつかを扱ってきましたが、恐ろしいです。すべてのロードは、単一の命令ではなくループになります。すべての店舗もループです。すべての構造は可変長であるため、配列を自然に使用することはできません。
効率に関する補足
以降のコメントでは、ストレージサイズに関して言えば、「効率的」という言葉を使用しています。ストレージサイズを最小化することもあります。非常に多くの値をファイルに保存する場合や、ネットワーク経由で送信する場合に重要になることがあります。トレードオフは、これらの値をレジスタにロードして何かを実行する必要があり、変換を実行することは自由ではないということです。
効率性について議論するとき、最適化しているものとトレードオフが何であるかを知る必要があります。非ネイティブストレージタイプを使用することは、処理速度とスペースのトレードオフの1つの方法であり、場合によっては理にかなっています。(少なくとも算術タイプの)可変長記憶装置を使用して、取引より空間のしばしば最小さらに保存するための処理速度(およびコードの複雑さと現像時間)。
これに対して支払う速度のペナルティは、帯域幅または長期保存を完全に最小限に抑える必要がある場合にのみ価値があることを意味します。これらの場合、通常は単純で自然な形式を使用し、汎用システムで圧縮するだけです。 (zip、gzip、bzip2、xyなど)。
tl; dr
各プラットフォームには1つのアーキテクチャがありますが、基本的に無制限の数のさまざまな方法でデータを表現できます。どの言語でも無制限の数の組み込みデータ型を提供するのは合理的ではありません。したがって、C ++は、プラットフォームのネイティブで自然なデータ型のセットへの暗黙的なアクセスを提供し、他の(非ネイティブ)表現を自分でコーディングできるようにします。
unsinged
で表現できる最大の値は間違っています255
。2)値の変化に応じて、変数の最適なストレージサイズを計算し、ストレージ領域を縮小/拡張するオーバーヘッドを考慮します。