型はその値に関係なく常に特定のサイズであるのはなぜですか?


149

実装は型の実際のサイズ間で異なる場合がありますが、ほとんどの場合、unsigned intやfloatなどの型は常に4バイトです。しかし、なぜ型はその値に関係なく常に特定の量のメモリを占有するのでしょうか。たとえば、255の値で次の整数を作成した場合

int myInt = 255;

次にmyInt、私のコンパイラで4バイトを占有します。しかし、実際の値は2551バイトだけで表すことができるので、なぜmyInt1バイトのメモリを占有しないのでしょうか。または、より一般的な質問の方法:値を表すために必要なスペースがそのサイズよりも小さい可能性があるのに、型に関連付けられているサイズが1つしかないのはなぜですか?


15
1)「しかし、実際の値256は1バイトでしか表現できません1バイトunsingedで表現できる最大の値は間違っています255。2)値の変化に応じて、変数の最適なストレージサイズを計算し、ストレージ領域を縮小/拡張するオーバーヘッドを考慮します。
AlgirdasPreidžius18年

99
さて、メモリから値を読み取る時が来たら、どのくらいのバイトを読み取るかをマシンが決定することをどのように提案しますか?値の読み取りを停止する場所をマシンはどのようにして知るのでしょうか?これには追加の設備が必要になります。また、一般に、これらの追加機能のメモリとパフォーマンスのオーバーヘッドは、unsigned int値に固定4バイトを単に使用する場合よりもはるかに高くなります。
AnT

74
私はこの質問が本当に好きです。答えは簡単に見えるかもしれませんが、正確に説明するには、コンピュータとコンピュータアーキテクチャが実際にどのように機能するかをよく理解する必要があると思います。ほとんどの人は、それについて包括的な説明がなくても、おそらくそれを当たり前のことだと思うでしょう。
andreee

37
変数の値に1を加えて256にした場合、何が起こるか考えてみてください。そうすると、変数を拡張する必要があります。それはどこに拡張されますか?残りのメモリを移動してスペースを確保していますか?変数自体は動きますか?その場合、どこに移動し、更新する必要があるポインタをどのようにして見つけますか?
molbdnilo

13
@someidiotいいえ、あなたは間違っています。std::vector<X>常に同じサイズ、つまりsizeof(std::vector<X>)コンパイル時の定数です。
SergeyA 2018年

回答:


131

コンパイラーは、あるマシン用のアセンブラー(および最終的にはマシンコード)を生成することになっています。一般に、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

これの意味は:

  1. int numパラメータは、それはIntelがネイティブレジスタのために期待して正確にサイズとレイアウトだ意味、レジスタEDIで可決されました。関数は何も変換する必要はありません
  2. 乗算は単一の命令(imul)であり、非常に高速です。
  3. 結果を返すことは、単にそれを別のレジスタにコピーすることです(呼び出し元は結果が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 ++は、プラットフォームのネイティブで自然なデータ型のセットへの暗黙的なアクセスを提供し、他の(非ネイティブ)表現を自分でコーディングできるようにします。


私はそれらすべてを理解しようとしながら、すべての素晴らしい答えを見ています。したがって、あなたの答えに関しては、動的サイズではありません。たとえば、整数の場合は32ビット未満であり、レジスタ内の変数を増やすだけではありません。 ?エンディアンが同じである場合、なぜこれが最適ではないのですか?
Nichlas Uden

7
@asdが、レジスタに現在格納されている変数の数を把握するコードで、レジスタをいくつ使用しますか?
user253751

1
FWIW一般に、スペースの節約がそれらのパックとアンパックの速度コストよりも重要であると判断した場合、複数の値を使用可能な最小スペースにパックすることが一般的です。プロセッサーは組み込みレジスター以外の演算を正しく行う方法を知らないため、通常、それらをパックされた形式で自然に操作することはできません。プロセッササポートのある部分的な例外についてBCDを
役に立たない

3
ある値に実際に32ビットすべて必要な場合でも、長さを格納する場所が必要なので、場合によって 32ビット以上が必要になります。
役に立たない

1
+1。「シンプルで自然なフォーマットしてから圧縮」は、典型的に良くあることについて注意:これは間違いなく、一般的に真のしかし:VLQ-各値-当時圧縮-全体の事を行い、特に、より良いだけよりも、いくつかのデータの圧縮、 -全体、および一部のアプリケーションでは、データが異なる(のメタデータのように)か、実際にメモリに保持しているため、データを一緒に圧縮できない場合があります。git値(HTML + CSSレンダリングエンジンの場合と同様)。したがって、インプレースのVLQなどを使用しないと回避できません。
mtraceur 2018年

139

タイプは基本的にストレージを表し、現在の値ではなく、それらが保持できる最大値に関して定義されるためです。

非常に単純な例えは家です-家は何人が住んでいるかに関係なく固定されたサイズであり、特定のサイズの家に住むことができる人々の最大数を規定する建築基準もあります。

ただし、10人が収容できる家に一人で住んでいても、現在の居住者数が家の大きさに影響を与えることはありません。


31
私は類推が好きです。少し拡張すると、型に固定メモリサイズを使用しないプログラミング言語を使用することを想像できます。これは、家の部屋が使用されていないときはいつでも破壊し、必要に応じて再構築するのと同じです。 (つまり、家をたくさん建てて、必要なときに備えておくことができる場合は、膨大なオーバーヘッドが発生します)。
ahouse101

5
「タイプは基本的にストレージを表すため」これはすべての言語に当てはまるわけではありません(たとえば、typescriptなど)
corvus_192

56
@ corvus_192タグには意味があります。この質問には「typescript」ではなくC ++がタグ付けされています
SergeyA

4
@ ahouse101実際、無制限の精度の整数を持つ言語は数多くあり、必要に応じて拡張されます。これらの言語では、変数に固定メモリを割り当てる必要はなく、内部的にオブジェクト参照として実装されています。例:Lisp、Python。
Barmar

2
@jamesqf MP演算がLispで最初に採用されたのは、おそらく自動同調ではなく、Lispは自動メモリ管理も行いました。設計者は、パフォーマンスへの影響はプログラミングのしやすさに次ぐものであると感じていました。また、影響を最小限に抑えるために最適化手法が開発されました。
Barmar

44

これは最適化と簡素化です。

固定サイズのオブジェクトを使用できます。したがって、値を格納します。
または、可変サイズのオブジェクトを使用できます。しかし、値とサイズを保存します。

固定サイズのオブジェクト

数値を操作するコードは、サイズを気にする必要はありません。常に4バイトを使用し、コードを非常に単純にすることを想定しています。

動的サイズのオブジェクト

数値を操作するコードは、変数を読み取るときに、値とサイズを読み取る必要があることを理解する必要があります。サイズを使用して、レジスタのすべての上位ビットがゼロになるようにします。

値が現在のサイズを超えていない場合、値をメモリに戻すときは、単に値をメモリに戻します。ただし、値が縮小または拡大した場合は、オブジェクトの格納場所をメモリ内の別の場所に移動して、オーバーフローしないようにする必要があります。次に、その番号の位置を追跡する必要があります(サイズに対して大きくなりすぎると移動する可能性があるため)。また、未使用の変数の場所をすべて追跡して、それらを再利用できるようにする必要もあります。

概要

固定サイズのオブジェクト用に生成されたコードは、はるかに単純です。

注意

圧縮は、255が1バイトに収まるという事実を使用します。さまざまな数値にさまざまなサイズの値を積極的に使用する大きなデータセットを格納するための圧縮スキームがあります。ただし、これはライブデータではないため、上記のような複雑さはありません。データを格納するために使用するスペースが少なくなりますが、データを圧縮して圧縮解除して保存します。


4
これは私にとって最良の答えです。どのようにしてサイズを追跡しますか?より多くのメモリ?
オンライントーマス

@ThomasMoorsはい、まさに:より多くのメモリを備えています。たとえば動的配列がある場合、一部intはその配列の要素の数を格納します。それint自体は再び固定サイズになります。
Alfe

1
@ThomasMoors一般的に使用される2つのオプションがあり、どちらも追加のメモリを必要とします-どちらも(固定サイズ)フィールドがあり、そこにデータの量を示します(たとえば、配列サイズのint、または最初の要素には文字数が含まれます)、または要素が最後の文字であるかどうかを何らかの方法で示すチェーン(またはより複雑な構造)を持つことができます(例:ゼロで終了する文字列、またはリンクされたリストのほとんどの形式)。
Peteris

27

C ++のような言語では、設計目標は単純な演算を単純な機械語命令にコンパイルすることです。

すべての主流のCPU命令セットは固定幅型で機能し、可変幅型を実行する場合は、それらを処理するために複数の機械語命令を実行する必要があります。

基盤となるコンピューターハードウェアがこのようになっている理由については、多くの場合(すべてではありませんが)単純で効率的だからです。

コンピュータをテープの一部として想像してください。

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

単にテープの最初のバイトを見るようにコンピュータに指示した場合xx、タイプがそこで停止するかどうか、または次のバイトに進むかどうかはどのようにしてわかりますか?あなたのような数がある場合は255(16進数FF)等の番号65535(16進数FFFF)最初のバイトは常にありますFF

どうやって知っていますか?ロジックを追加し、少なくとも1つのビットまたはバイト値の意味を「オーバーロード」して、値が次のバイトに続くことを示す必要があります。そのロジックは、ソフトウェアでエミュレートするか、CPUに多数のトランジスタを追加して実行することで、「フリー」になることはありません。

CやC ++などの言語の固定幅タイプは、それを反映しています。

この方法である必要はありません。最大限に効率的なコードへのマッピングにあまり関係のないより抽象的な言語は、数値型に対して可変幅エンコーディング(「可変長数量」またはVLQとも呼ばれます)を自由に使用できます。

参考資料:「可変長の数量」を検索すると、その種のエンコード実際に効率的であり、追加のロジックに値する例がいくつか見つかります。通常は、大きな範囲内のどこかにある可能性がある大量の値を格納する必要がある場合ですが、ほとんどの値はいくつかの小さなサブ範囲に向かう傾向があります。


コンパイラが、コードを壊すことなく、より少ないスペースに値を格納できることを証明できる場合(たとえば、単一の翻訳単位内でのみ内部的に見える変数である場合)、その最適化の経験則から、ターゲットハードウェアでより効率的になります。コードの残りの部分が標準的な動作をしているように機能する限り、それに応じ最適化し、より少ないスペースに格納することが完全に許可されます。

ただし、コードが個別にコンパイルされる可能性のある他のコードと相互運用する必要がある場合は、サイズを一貫させるか、すべてのコードが同じ規則に従うようにする必要があります。

一貫性がない場合は、この複雑な問題int x = 255;があるためx = yです。コードの後半にある場合はどうなりますか?int可変幅の可能性がある場合、コンパイラーは、必要なスペースの最大量を事前に割り当てるために、事前に知る必要があります。y個別にコンパイルされた別のコードから引数が渡された場合はどうなるでしょうか。


26

Javaは、「BigInteger」および「BigDecimal」と呼ばれるクラスを使用して、C ++のGMP C ++クラスインターフェイスが明らかにそうであるように、これを正確に実行します(Digital Traumaに感謝)。必要に応じて、ほとんどすべての言語で自分で簡単に行うことができます。

CPUは常に、任意の長さの操作をサポートするように設計されたBCD(2進化10進数)を使用する機能を備えていました(ただし、今日のGPU標準ではSLOWになる1バイトずつ手動で操作する傾向があります)。

これらまたは他の同様のソリューションを使用しない理由は?パフォーマンス。最もパフォーマンスの高い言語では、タイトなループ操作の途中で変数を拡張する余裕がありません。非常に非決定的です。

大容量ストレージおよびトランスポートの状況では、パックされた値は多くの場合、使用する唯一のタイプの値です。たとえば、コンピューターにストリーミングされる音楽/ビデオパケットは、サイズの最適化として、次の値が2バイトか4バイトかを指定するのに少し時間がかかる場合があります。

それが使用できるコンピューターに配置されれば、メモリは安価ですが、サイズ変更可能な変数の速度と複雑さはそうではありません。それが本当に唯一の理由です。


4
誰かがBigIntegerについて言及してくれてうれしいです。それがばかげた考えであるということではありません、それはそれが非常に大きな数のためにそれを行うことが意味があるというだけです。
Max Barraclough

1
徹底的に言うと、実際には非常に正確な数値を意味します:)少なくともBigDecimalの場合は...
Bill K

2
これはc ++とタグ付けされているので、JavaのBig *と同じ考え方であるGMP C ++クラスインターフェースについて言及する価値があるかもしれません。
デジタルトラウマ

20

動的なサイズの単純な型を持つことは非常に複雑で計算が重いためです。これが可能かどうかはわかりません。
コンピュータは、値が変わるたびに、そのビットが何ビットかかるかをチェックする必要があります。それはかなり多くの追加操作になります。また、コンパイル中に変数のサイズがわからない場合は、計算を実行するのがはるかに難しくなります。

変数の動的サイズをサポートするために、コンピューターは実際に変数が現在何バイトあるかを覚えておく必要があります。その情報を格納するために追加のメモリが必要になります。そして、この情報は、適切なプロセッサ命令を選択するために、変数のすべての操作の前に分析する必要があります。

コンピューターの仕組みと変数のサイズが一定である理由をよりよく理解するには、アセンブラー言語の基本を学びます。

とはいえ、constexprの値でそのようなことを達成することは可能だと思います。ただし、これにより、プログラマはコードを予測できなくなります。一部のコンパイラの最適化はそのようなことをするかもしれないと思いますが、それらは物事を単純に保つためにプログラマからそれを隠します。

ここでは、プログラムのパフォーマンスに関連する問題のみを説明しました。変数のサイズを小さくしてメモリを節約するために解決する必要があるすべての問題を省略しました。正直言って、それさえ可能だとは思いません。


結論として、宣言されたよりも小さい変数を使用しても意味があるのは、それらの値がコンパイル中にわかっている場合だけです。最近のコンパイラがそうすることはかなりありそうです。他の場合では、それは非常に多くの困難な、あるいは解決不可能な問題さえ引き起こすでしょう。


そのようなことがコンパイル時に行われることを私は強く疑います。このようにコンパイラのメモリを節約しても意味がありません。それが唯一の利点です。
Bartek Banachewicz

1
constexpr変数に通常の変数を乗算するような演算については、むしろ考えていました。たとえば、(理論的には)値56を持つ8バイトのconstexpr変数があり、それに2バイトの変数を掛けます。一部のアーキテクチャでは、64ビット演算の方が計算量が多いため、コンパイラーは16ビット乗算のみを実行するように最適化できます。
NO_NAME

一部のAPL実装およびSNOBOLファミリーの一部の言語(私はSPITBOLだと思いますか?多分アイコン)は、これを正確に(粒度で)行いました。実際の値に応じて動的に表現形式を変更しました。APLはブール型から整数型、浮動小数点型、浮動小数点型に戻ります。SPITBOLは、ブール(バイト配列に格納された8つの個別のブール配列)の列表現から整数(IIRC)に移行します。
davidbak

16

次にmyInt、私のコンパイラで4バイトを占有します。しかし、実際の値は2551バイトだけで表すことができるので、なぜmyInt1バイトのメモリを占有しないのでしょうか。

これは可変長エンコーディングと呼ばれ、VLQなどのさまざまなエンコーディングが定義されています。ただし、最も有名なものの1つはおそらくUTF-8です。UTF-8は、コードポイントを1から4までの可変バイト数でエンコードします。

または、より一般的な質問の方法:値を表すために必要なスペースがそのサイズよりも小さい可能性があるのに、型に関連付けられているサイズが1つしかないのはなぜですか?

いつもエンジニアリングのように、それはトレードオフについてすべてです。メリットしかないソリューションはないため、ソリューションを設計する際には、メリットとトレードオフのバランスをとる必要があります。

解決された設計は、固定サイズの基本型を使用することであり、ハードウェア/言語はそこから飛んでいっただけです。

では、可変エンコーディングの根本的な弱点は何ですか?それにより、メモリを大量に消費するスキームのために拒否されました。ランダムアドレス指定なし

UTF-8文字列で4番目のコードポイントが始まるバイトのインデックスは何ですか?

それは前のコードポイントの値に依存し、線形スキャンが必要です。

ランダムアドレス指定の方が優れている可変長エンコーディングスキームは確かにありますか?

はい、しかし、それらはさらに複雑です。理想的なものがあれば、まだ見たことがない。

とにかく、ランダムアドレス指定は本当に重要ですか?

ああそう!

問題は、あらゆる種類の集約/配列が固定サイズの型に依存していることです。

  • struct?の3番目のフィールドにアクセスする ランダムアドレス指定!
  • 配列の3番目の要素にアクセスしますか?ランダムアドレス指定!

つまり、基本的に次のトレードオフがあります。

固定サイズタイプまたは線形メモリスキャン


これは、音を立てるほど問題ではありません。常にベクターテーブルを使用できます。メモリのオーバーヘッドと追加のフェッチがありますが、線形スキャンは必要ありません。
Artelius

2
@Artelius:整数の幅が可変の場合、ベクターテーブルをどのようにエンコードしますか?また、メモリで1〜4バイトを使用する整数に1をエンコードする場合、ベクターテーブルのメモリオーバーヘッドはどのくらいですか。
Matthieu M.

見て、あなたが正しい、OPが与えた特定の例では、ベクトルテーブルを使用しても利点はありません。ベクターテーブルを作成する代わりに、固定サイズの要素の配列にデータを配置することもできます。ただし、 OPはより一般的な回答も要求しました。Pythonでは、整数の配列がある可変サイズの整数のベクタテーブル!これは、この問題を解決するためではありませんが、Pythonではコンパイル時に、リストの要素が整数、浮動小数点数、辞書、文字列、リストのどれであるかがわからないためです。
Artelius

@Artelius:Pythonでは、配列には要素への固定サイズのポインタが含まていることに注意してください。これは、間接参照を犠牲にして、要素に到達することをO(1)にします。
Matthieu M.

16

コンピューターのメモリは、特定のサイズ(多くの場合8ビットで、バイトと呼ばれる)の連続してアドレス指定されるチャンクに分割され、ほとんどのコンピューターは、連続するアドレスを持つ一連のバイトに効率的にアクセスするように設計されています。

オブジェクトのアドレスがオブジェクトの有効期間内に変更されない場合、そのアドレスを指定されたコードは問題のオブジェクトにすばやくアクセスできます。ただし、このアプローチの本質的な制限は、アドレスがアドレスXに割り当てられ、次に別のアドレスがNバイト離れているアドレスYに割り当てられた場合、存続期間内にXがNバイトより大きくなることができないことです。 XまたはYのいずれかが移動されない限り、Yの Xが移動するためには、Xのアドレスを保持するユニバース内のすべてが新しいアドレスを反映するように更新され、同様にYが移動する必要があります。このような更新を容易にするシステムを設計することは可能ですが(Javaと.NETの両方でかなりうまく管理されます)、ライフタイムを通じて同じ場所に留まるオブジェクトを操作する方がはるかに効率的です。


「XまたはYのいずれかが移動されない限り、XはYの存続期間内にNバイトより大きくなることはできません。Xを移動するには、Xのアドレスを保持するユニバース内のすべてを更新して反映する必要があります。新しいもの、そして同様にYが動くために」彼らの現在の値のニーズが追加する必要があるだろうと多くのサイズとしてのみ使用オブジェクト:これはIMO顕著なポイントがあるトン 1つの熟考どのように可能性が今までの作品のサイズ/番兵、メモリ移動、参照グラフなど、非常に明白なため、オーバーヘッドのを...しかし、それでも、特に他の人がそうでなかったように、非常に明確に述べる価値は非常にあります。
underscore_d

@underscore_d:可変サイズのオブジェクトを処理するためにゼロから設計されたJavascriptのような言語は、驚くほど効率的です。一方、可変サイズのオブジェクトシステムを単純化して高速化することは可能ですが、単純な実装は遅く、高速な実装は非常に複雑です。
スーパーキャット2018年

13

短い答えは:C ++標準がそう言っているからです。

長い答えは、次のとおりです。コンピュータで実行できることは、最終的にハードウェアによって制限されます。もちろん、格納用に整数を可変バイト数にエンコードすることは可能ですが、それを読み取るには、特別なCPU命令を実行する必要があるか、ソフトウェアで実装することもできますが、非常に遅くなります。固定サイズの操作は、事前定義された幅の値をロードするためにCPUで使用できます。可変幅にはありません。

考慮すべきもう1つのポイントは、コンピューターのメモリのしくみです。整数型が1〜4バイトのストレージを占めるとしましょう。値42を整数に格納するとします。1バイトを使用し、メモリアドレスXに配置します。次に、次の変数を位置X + 1に格納します(この時点では位置合わせは考慮していません)。 。後で、値を6424に変更することにしました。

しかし、これは1バイトに収まりません!それで、あなたは何をしますか?残りはどこに置きますか?X + 1にはすでに何かがあるので、そこに配置できません。何処か別の場所?後でどこでどうやって知るのですか?コンピューターのメモリは挿入のセマンティクスをサポートしていません。場所に何かを置いて、余白を空けて横に置いてすべてをプッシュすることはできません。

余談:あなたが話しているのは、実際にはデータ圧縮の領域です。圧縮アルゴリズムはすべてをより密にパックするために存在するため、少なくとも一部のアルゴリズムでは、整数に必要以上のスペースを使用しないことを検討します。ただし、圧縮データは(可能な場合は)変更するのが容易ではなく、変更を加えるたびに再圧縮されるだけです。


11

これを実行することにより、かなりの実行時パフォーマンスの利点があります。可変サイズタイプを操作する場合は、操作を実行する前に各数値をデコードし(マシンコードの命令は通常固定幅です)、操作を実行し、結果を保持するのに十分な大きさのメモリ内のスペースを見つける必要があります。これらは非常に難しい操作です。すべてのデータをわずかに非効率的に保存する方がはるかに簡単です。

これは常にそれが行われる方法ではありません。GoogleのProtobufプロトコルを検討してください。Protobufは、データを非常に効率的に送信するように設計されています。送信するバイト数を減らすことは、データを操作するときに追加の命令を実行するコストに見合う価値があります。したがって、protobufは整数を1、2、3、4、または5バイトにエンコードするエンコーディングを使用し、整数が小さいほどバイトが少なくなります。ただし、メッセージが受信されると、操作が簡単な従来の固定サイズの整数形式に解凍されます。このようなスペース効率の良い可変長整数を使用するのは、ネットワーク伝送中だけです。


11

好き セルゲイさん家のアナロジーが、私は車のアナロジーが良いだろうと思います。

変数の型を車の型として、人をデータとして想像してください。新しい車を探すときは、目的に最も合った車を選びます。1人か2人しか乗れない小型のスマートカーが必要ですか?またはより多くの人々を運ぶリムジン?どちらにも、速度や燃費などの利点と欠点があります(速度とメモリ使用量を考えてください)。

あなたがリムジンを持っていて、あなたが一人で運転しているなら、それはあなただけに合うように縮小するつもりはありません。そのためには、車を販売し(読み取り:割り当て解除)、自分用に新しい小さい車を購入する必要があります。

類推を続けると、メモリは車でいっぱいの巨大な駐車場と考えることができます。読書に行くと、あなたの車のタイプ専用に訓練された専門の運転手があなたのためにそれをフェッチしに行きます。車内の人に応じて車の種類を変えることができる場合、車がどのような種類の車であるかわからないため、車を手に入れようとするたびに運転手全員を連れて行く必要があります。

言い換えると、実行時に読み取る必要のあるメモリの量を決定しようとすることは、非常に非効率的であり、駐車場にさらに数台の車を駐車できるという事実を上回ります。


10

いくつかの理由があります。1つは、任意のサイズの数値を処理するための複雑さが増し、すべてのintが正確にXバイトであるという仮定に基づいてコンパイラーが最適化できなくなるため、パフォーマンスに影響を与えます。

2つ目は、単純な型をこのように格納すると、長さを保持するために追加のバイトが必要になることです。したがって、この新しいシステムでは、255以下の値では実際には1バイトではなく2バイトが必要であり、最悪の場合、4バイトではなく5バイトが必要になります。考えて、いくつかのエッジケースでは実際には純損失になるかもしれません。

3番目の理由は、コンピュータのメモリは一般的に言葉でアドレス指定できることです、バイトではなくで。(ただし脚注を参照)。ワードはバイトの倍数で、通常32ビットシステムでは4バイト、64ビットシステムでは8バイトです。通常、個々のバイトを読み取ることはできません。ワードを読み取り、そのワードからn番目のバイトを抽出します。これは、ワードから個々のバイトを抽出することは、ワード全体を読み取るよりも少し手間がかかることと、メモリ全体がワードサイズ(つまり、4バイトサイズ)のチャンクに均等に分割されている場合に非常に効率的であることを意味します。なぜなら、任意のサイズの整数が浮かんでいると、整数の一部が1つの単語になり、別の部分が次の単語になるため、完全な整数を取得するために2回の読み取りが必要になるからです。

脚注:より正確には、バイト単位でアドレスを指定しましたが、ほとんどのシステムは「不均一」バイトを無視しました。つまり、アドレス0、1、2、3はすべて同じワードを読み取り、4、5、6、7は次のワードを読み取るというように続きます。

関係のないメモでは、32ビットシステムに最大4 GBのメモリが搭載されていたのもこのためです。メモリ内の場所をアドレス指定するために使用されるレジスターは、通常、ワードを保持するのに十分な大きさ、つまり(2 ^ 32)-1 = 4294967295の最大値を持つ4バイトです。4294967296バイトは4 GBです。


8

のようなC ++標準ライブラリには、ある意味で可変サイズのオブジェクトがありますstd::vector。ただし、これらはすべて、必要な追加のメモリを動的に割り当てます。を取るsizeof(std::vector<int>)と、オブジェクトが管理するメモリとは関係のない定数が得られ、を含む配列または構造体を割り当てるstd::vector<int>と、同じ配列または構造体に追加のストレージを配置するのではなく、この基本サイズが予約されます。このようなものをサポートするC構文には、特に可変長の配列と構造がいくつかありますが、C ++はそれらをサポートすることを選択しませんでした。

言語標準は、コンパイラが効率的なコードを生成できるように、オブジェクトサイズをそのように定義します。たとえば、int一部の実装でたまたま4バイトの長さでありaint値へのポインタまたは配列として宣言した場合a[i]、「アドレスを逆参照a + 4×i」という疑似コードに変換します。これは一定の時間で実行でき、x86やCが最初に開発されたDEC PDPマシンを含む多くの命令セットアーキテクチャが単一のマシン命令で実行できるほど一般的で重要な操作です。

可変長単位として連続して格納されるデータの一般的な実例の1つは、UTF-8としてエンコードされた文字列です。(ただし、コンパイラにUTF-8文字列の基になる型がまだchar1幅と持っているこれは、ASCII文字列が有効なUTF-8として解釈することができ、およびなどのライブラリコードの多くstrlen()strncpy()仕事を継続すること。) UTF-8コードポイントのエンコードは1〜4バイトの長さにできるため、文字列内の5番目のUTF-8コードポイントが必要な場合は、データの5番目のバイトから17番目のバイトまでどこでも開始できます。それを見つける唯一の方法は、文字列の最初からスキャンして、各コードポイントのサイズを確認することです。5番目の書記素を見つけたい場合、文字クラスも確認する必要があります。文字列で100万番目のUTF-8文字を見つけたい場合は、このループを100万回実行する必要があります。インデックスを頻繁に使用する必要があることがわかっている場合は、文字列を1回トラバースしてインデックスを作成するか、UCS-4などの固定幅エンコーディングに変換できます。文字列内で100万番目のUCS-4文字を見つけることは、配列のアドレスに400万を追加するだけです。

可変長データのもう1つの厄介な問題は、データを割り当てるときに、使用するメモリをできるだけ多く割り当てるか、必要に応じて動的に再割り当てする必要があることです。最悪の場合に割り当てることは非常に無駄です。連続したメモリブロックが必要な場合、再割り当てを行うと、すべてのデータを別の場所にコピーしなければならなくなりますが、メモリを連続していないチャンクに格納すると、プログラムロジックが複雑になります。

だから、それは可変長bignumsの代わりに、固定幅を持つことが可能だshort intintlong intそしてlong long int、割り当て、それらを使用するのは非効率的だろう。さらに、すべての主流のCPUは固定幅レジスターで演算を行うように設計されており、ある種の可変長bignumを直接操作する命令はありません。これらはソフトウェアで実装する必要がありますが、はるかにゆっくりです。

現実の世界では、ほとんど(すべてではない)のプログラマーが、UTF-8エンコーディングの利点、特に互換性が重要であり、文字列を前から後ろにスキャンしたり、可変幅の欠点は許容できることを思い出してください。他の目的には、UTF-8に似たパックされた可変幅の要素を使用できます。しかし、私たちが行うことはほとんどなく、それらは標準ライブラリにありません。


7

値を表すために必要なスペースがそのサイズよりも小さい場合に、タイプに関連付けられているサイズが1つしかないのはなぜですか?

主にアライメント要件のため。

あたりとしてbasic.align / 1

オブジェクトタイプには、そのタイプのオブジェクトを割り当てることができるアドレスを制限する配置要件があります。

多くのフロアがあり、各フロアに多くの部屋がある建物を考えてみてください。
各部屋は、N人またはオブジェクトを保持できるサイズ(固定スペース)です。
部屋のサイズが事前にわかっているため、建物の構造コンポーネントが適切に構造化されています。

部屋が整列していない場合、建物の骨格は適切に構成されません。


7

少なくなる可能性があります。関数を考えてみましょう:

int foo()
{
    int bar = 1;
    int baz = 42;
    return bar+baz;
}

コンパイルしてアセンブリコードを生成します(g ++、x64、詳細は削除されます)

$43, %eax
ret

ここでは、barbaz表現するためにゼロバイトを使用して終了。


5

では、なぜmyIntが1バイトのメモリを占有しないのでしょうか。

そんなに使うように言ったから。を使用する場合unsigned int、一部の規格では4バイトが使用され、その使用可能な範囲は0〜4,294,967,295であると規定されています。unsigned char代わりに使用する場合は、おそらく、探している1バイトのみを使用しているでしょう(標準に依存し、C ++は通常これらの標準を使用します)。

これらの標準に当てはまらない場合は、このことを覚えておく必要があります。コンパイラまたはCPUは、4ではなく1バイトのみを使用するようになっているはずです。プログラムの後半で、その値を追加または乗算する可能性があり、より多くのスペースが必要になります。メモリを割り当てるときはいつでも、OSはそのスペースを見つけてマッピングし、提供する必要があります(メモリを仮想RAMにスワップする可能性もあります)。これには時間がかかる場合があります。事前にメモリを割り当てる場合、別の割り当てが完了するのを待つ必要はありません。

バイトあたり8ビットを使用する理由については、これを見てください: バイトが8ビットである理由の歴史は何ですか?

余談ですが、整数をオーバーフローさせることができます。しかし、符号付き整数を使用する場合、C \ C ++標準では、整数オーバーフローは未定義の動作を引き起こすと述べています。 整数オーバーフロー


5

ほとんどの回答が見逃しているように見える単純なもの:

C ++の設計目標に適合しているためです。

コンパイル時に型のサイズを計算できることにより、コンパイラーとプログラマーが膨大な数の単純化の仮定を行うことが可能になり、特にパフォーマンスに関して多くの利点がもたらされます。もちろん、固定サイズの型には、整数オーバーフローなどの付随する落とし穴があります。これが、異なる言語が異なる設計決定を行う理由です。(たとえば、Python整数は基本的に可変サイズです。)

おそらく、C ++が固定サイズ型に非常に強く傾く主な理由は、C互換性の目標です。ただし、C ++は静的に型付けされた言語であり、非常に効率的なコードを生成しようとするため、プログラマーが明示的に指定していないものを追加する必要がないため、固定サイズの型は依然として多くの意味があります。

では、なぜCが最初に固定サイズの型を選択したのでしょうか。シンプル。70年代のオペレーティングシステム、サーバーソフトウェア、ユーティリティを作成するために設計されました。他のソフトウェアのインフラストラクチャ(メモリ管理など)を提供するもの。このように低いレベルでは、パフォーマンスが重要であり、コンパイラーもユーザーの指示どおりに動作します。


5

変数のサイズを変更するには再割り当てが必要であり、これは通常、数バイトのメモリを浪費する場合と比較して、追加のCPUサイクルに値するものではありません。

ローカル変数は、それらの変数のサイズが変化しない場合に操作が非常に速いスタックに配置されます。変数のサイズを1バイトから2バイトに拡張する場合は、スタック上のすべてのものを1バイトずつ移動して、そのスペースを確保する必要があります。移動する必要のあるものの数によっては、多くのCPUサイクルがかかる可能性があります。

これを行うもう1つの方法は、すべての変数をヒープの場所へのポインターにすることですが、実際には、この方法でさらに多くのCPUサイクルとメモリを浪費することになります。ポインターは4バイト(32ビットアドレス指定)または8バイト(64ビットアドレス指定)であるため、ポインターには既に4または8を使用しており、ヒープ上のデータの実際のサイズを使用しています。この場合でも、再割り当てにはコストがかかります。ヒープデータを再割り当てする必要がある場合、幸運にもインラインで拡張できる余地がありますが、必要なサイズの連続したメモリブロックを確保するために、ヒープ上の別の場所に移動する必要がある場合があります。

使用するメモリ量を事前に決定する方が常に高速です。動的なサイズ変更を回避できる場合は、パフォーマンスが向上します。メモリを浪費することは、通常、パフォーマンスの向上に値します。そのため、コンピュータには大量のメモリが搭載されています。:)


3

コンパイラーは、物事が機能する限り(「現状のまま」のルール)、コードに多くの変更を加えることができます。

fullを移動するのに必要な長い(32/64ビット)の代わりに、8ビットのリテラル移動命令を使用することが可能ですint。ただし、ロードを実行する前にレジスタをゼロに設定する必要があるため、ロードを完了するには2つの命令が必要になります。

値を32ビットとして処理する方が(少なくともメインコンパイラによれば)より効率的です。実際、インラインアセンブリなしで8ビットロードを実行するx86 / x86_64コンパイラはまだ見たことがありません。

ただし、64ビットに関しては状況が異なります。プロセッサーの以前の拡張(16ビットから32ビット)を設計するときに、Intelは誤りを犯しました。ここにそれらがどのように見えるかの良い表現があります。ここでの主なポイントは、ALまたはAHに書き込むときに、もう一方は影響を受けないということです(十分に公正で、それがポイントであり、当時は理にかなっていた)。しかし、32ビットに拡張すると興味深いものになります。最下位ビット(AL、AHまたはAX)を書き込んだ場合、EAXの上位16ビットには何も起こりません。つまり、a charをに昇格させるint場合は、最初にそのメモリをクリアする必要がありますが、その方法はありません。実際にはこれらの上位16ビットのみを使用しているため、この「機能」は何よりも面倒です。

AMDは64ビットで、はるかに優れた仕事をしました。下位32ビットの何かに触れると、上位32ビットは単に0に設定されます。これにより、このgodboltで確認できる実際の最適化がいくつか行われます。8ビットまたは32ビットの読み込みは同じ方法で行われますが、64ビット変数を使用する場合、コンパイラーはリテラルの実際のサイズに応じて異なる命令を使用します。

ご覧のとおり、同じ結果が得られる場合、コンパイラーはCPU内の変数の実際のサイズを完全に変更できますが、小さい型の場合は変更しても意味がありません。


訂正:as-if。また、より短いロード/ストアを使用できる場合、他のバイトを解放して使用する方法はわかりません。これは、OPが不思議に思っているようです。現在の値で不要なメモリへの接触を回避するだけではなく、しかし、読み取るバイト数を指示したり、実行時にすべてのRAMを魔法のようにシフトしたりして、スペース効率の奇妙な哲学的アイデア(巨大なパフォーマンスコストを気にしないでください)を満たすことができます。 「解決しない」。CPU / OSが実行する必要があることは非常に複雑であるため、IMOの質問に最も明確に答えます。
underscore_d

1
ただし、レジスタに実際に「メモリを保存」することはできません。AHとALを悪用して変なことをしようとしない限り、同じ汎用レジスタにいくつかの異なる値を設定することはできません。ローカル変数は多くの場合、レジスタにとどまり、必要がない場合はRAMに移動しません。
meneldal
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.