Programing

값에 관계없이 유형이 항상 특정 크기 인 이유는 무엇입니까?

crosscheck 2020. 6. 12. 00:04
반응형

값에 관계없이 유형이 항상 특정 크기 인 이유는 무엇입니까?


구현은 실제 크기 유형에 따라 다를 수 있지만 대부분 부호없는 int 및 float와 같은 유형은 항상 4 바이트입니다. 그러나 왜 유형에 관계없이 값이 항상 특정 양의 메모리를 차지 합니까? 예를 들어 255 값으로 다음 정수를 만든 경우

int myInt = 255;

그런 다음 myInt컴파일러에서 4 바이트를 차지합니다. 그러나 실제 값 255은 1 바이트로만 표현할 수 있습니다. 왜 myInt1 바이트의 메모리 만 차지하지 않습니까? 또는보다 일반적인 방법은 다음과 같습니다. 값을 나타내는 데 필요한 공간이 해당 크기보다 작을 때 왜 유형에 하나의 크기 만 연결되어 있습니까?


컴파일러는 일부 기계에 대해 어셈블러 (및 궁극적으로 기계 코드)를 생성해야하며 일반적으로 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 비트입니다. 변수 값을 레지스터에로드하거나 레지스터를 변수에 다시 저장할 때마다 변환 할 필요가 없도록 동일한 레이아웃 (little-endian)을 사용합니다.

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매개 변수는 레지스터 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 비트 부호없는 정수)을 프로세서가 기본적으로 처리 할 수있는 형식으로 변환하는 것과 관련이 있습니다. 결과를 31 비트 값으로 다시 저장하려면이 작업을 수행하는 또 하나의 지침이있을 것입니다.

이러한 추가 복잡성은 공간 절약이 매우 중요한 경우에만 귀찮게 할 수 있음을 의미합니다. 이 경우 네이티브 unsigned또는 uint32_t유형 을 사용하는 것과 비교하여 훨씬 간단한 코드를 생성하는 것 보다 두 비트 만 절약 합니다.


동적 크기에 대한 참고 사항 :

위의 예는 여전히 가변 너비가 아닌 고정 너비 값이지만 너비 (및 정렬)는 더 이상 기본 레지스터와 일치하지 않습니다.

x86 플랫폼에는 기본 32 비트 외에도 8 비트 및 16 비트를 포함한 여러 가지 기본 크기가 있습니다 (64 비트 모드 및 기타 여러 가지 단순함을 염두에두고 있습니다).

이러한 유형 (char, int8_t, uint8_t, int16_t 등) 아키텍처에서 직접 지원되며 부분적으로 이전 8086 / 286 / 386 / etc와의 하위 호환성을 위해 사용됩니다. 명령 세트 등.

충분하고, 실용적이 될 수있는 가장 작은 자연적인 고정 크기 유형 을 선택하는 것은 분명한 경우입니다. 여전히 빠르고 단일 명령어로드 및 저장이 가능하며 여전히 전속 네이티브 산술을 얻거나 성능을 향상시킬 수도 있습니다. 캐시 미스 감소.

이것은 가변 길이 인코딩과는 매우 다릅니다.이 중 일부와 함께 작업했으며 끔찍합니다. 모든로드는 단일 명령어 대신 루프가됩니다. 모든 상점은 또한 루프입니다. 모든 구조는 가변 길이이므로 배열을 자연스럽게 사용할 수 없습니다.


효율성에 대한 추가 참고 사항

이후의 의견에서는 스토리지 크기와 관련하여 "효율적"이라는 단어를 사용했습니다. 스토리지 크기를 최소화하기로 선택하는 경우도 있습니다. 파일에 많은 수의 값을 저장하거나 네트워크를 통해 전송할 때 중요 할 수 있습니다. 트레이드 오프는 그 값을 레지스터에로드하여 값을 처리 해야 하며 변환을 수행하는 것이 자유롭지 않다는 것입니다.

효율성에 대해 논의 할 때, 우리가 최적화하고있는 것이 무엇인지, 그리고 트레이드 오프가 무엇인지 알아야합니다. 비원시 스토리지 유형을 사용하는 것은 공간 처리 속도를 교환하는 한 가지 방법이며 때로는 의미가 있습니다. 가변 길이 저장소 (산술 유형 의 경우)를 사용하면 공간을 최소로 절약하기 위해 더 많은 처리 속도 (및 코드 복잡성 및 개발자 시간)를 제공합니다.

이 비용을 지불하면 대역폭이나 장기 저장소를 절대적으로 최소화해야 할 때만 가치가 있으며, 이러한 경우 일반적으로 단순하고 자연스러운 형식을 사용하는 것이 더 쉽고 범용 시스템으로 압축하는 것이 더 쉽습니다. (zip, gzip, bzip2, xy 등)


tl; dr

각 플랫폼에는 하나의 아키텍처가 있지만 데이터를 표현하는 다양한 방법을 기본적으로 무제한으로 만들 수 있습니다. 모든 언어가 무제한의 내장 데이터 유형을 제공하는 것은 합리적이지 않습니다. 따라서 C ++은 플랫폼의 고유 한 자연 데이터 유형 세트에 대한 암시 적 액세스를 제공하며 다른 (네이티브가 아닌) 표현을 직접 코딩 할 수 있습니다.


유형은 기본적으로 스토리지를 나타내며 현재 값이 아니라 보유 할 수있는 최대 값으로 정의됩니다 .

아주 간단한 비유는 집이 될 것입니다-집은 얼마나 많은 사람들이 살고 있는지에 관계없이 크기가 고정되어 있으며 특정 크기의 집에 살 수있는 사람들의 최대 수를 규정하는 건물 코드도 있습니다.

그러나 한 사람이 10 명을 수용 할 수있는 집에 살고 있더라도 집의 크기는 현재 거주자 수에 영향을받지 않습니다.


최적화 및 단순화입니다.

고정 된 크기의 객체를 가질 수 있습니다. 따라서 값을 저장합니다.
또는 다양한 크기의 오브제를 사용할 수 있습니다. 그러나 가치와 크기를 저장합니다.

고정 크기의 객체

숫자를 조작하는 코드는 크기에 대해 걱정할 필요가 없습니다. 항상 4 바이트를 사용하고 코드를 매우 간단하게 만든다고 가정합니다.

동적 크기의 객체

숫자를 조작하는 코드는 변수를 읽을 때 값과 크기를 읽어야한다는 것을 이해해야합니다. 크기를 사용하여 레지스터에서 모든 높은 비트가 0으로 설정되도록합니다.

값이 현재 크기를 초과하지 않은 경우 값을 메모리에 다시 배치 할 경우 간단히 값을 메모리에 다시 배치하십시오. 그러나 값이 줄어들거나 커지면 개체의 저장 위치를 ​​메모리의 다른 위치로 이동하여 오버플로되지 않도록해야합니다. 이제 숫자의 위치를 ​​추적해야합니다 (크기에 비해 너무 커지면 움직일 수 있음). 또한 사용하지 않는 모든 변수 위치를 추적하여 잠재적으로 재사용 할 수 있도록해야합니다.

요약

고정 크기 객체에 대해 생성 된 코드는 훨씬 간단합니다.

노트

압축은 255가 1 바이트에 적합하다는 사실을 사용합니다. 다른 숫자에 대해 다른 크기 값을 능동적으로 사용하는 큰 데이터 세트를 저장하기위한 압축 체계가 있습니다. 그러나 이것은 실제 데이터가 아니기 때문에 위에서 설명한 복잡성이 없습니다. 저장을 위해 데이터를 압축 / 압축 해제하는 비용으로 더 적은 공간을 사용하여 데이터를 저장합니다.


C ++과 같은 언어에서 디자인 목표는 간단한 작업이 간단한 기계 명령어로 컴파일되는 것입니다.

모든 주류 CPU 명령 세트는 고정 너비 유형에서 작동하며 가변 너비 유형을 수행하려면 여러 시스템 명령을 수행하여 처리해야합니다.

에 관해서는 이유 는 간단하고, 더 효율적 때문입니다 : 기본 컴퓨터 하드웨어가 그 방법 많은 경우 (전부는 아니지만).

컴퓨터를 테이프로 상상해보십시오.

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

컴퓨터에서 테이프의 첫 번째 바이트를 보도록 지시 xx하면 유형이 거기서 멈추거나 다음 바이트로 진행되는지 여부를 어떻게 알 수 있습니까? 당신은 같은 수있는 경우 255(16 진수 FF) 또는 같은 숫자 65535(16 진수 FFFF) 첫 번째 바이트는 항상 FF.

그래서 어떻게 알지? 추가 논리를 추가하고 값이 다음 바이트로 계속됨을 나타 내기 위해 하나 이상의 비트 또는 바이트 값의 의미를 "오버로드"해야합니다. 이 논리는 결코 "무료"가 아닙니다. 소프트웨어에서 논리를 에뮬레이션하거나이를 위해 추가 트랜지스터를 CPU에 추가합니다.

C 및 C ++와 같은 고정 너비 유형의 언어가이를 반영합니다.

이러한 방식 일 필요 는 없으며 , 최대 효율적인 코드로의 매핑에 관심이없는보다 추상적 인 언어는 숫자 유형에 대해 가변 폭 인코딩 ( "가변 길이 수량"또는 VLQ라고도 함)을 자유롭게 사용할 수 있습니다.

추가 정보 : "가변 길이 수량"을 검색하면 해당 종류의 인코딩 실제로 효율적이고 추가 논리의 가치가있는 몇 가지 예를 찾을 수 있습니다 . 일반적으로 넓은 범위 내 어느 곳에 나있을 수있는 막대한 양의 값을 저장해야하지만 대부분의 값은 작은 하위 범위로 경향이 있습니다.


컴파일러가 코드를 손상시키지 않고 더 작은 공간에 값을 저장하여 도망 갈 있음증명할 수 있다면 (예 : 단일 번역 단위 내에서만 내부적으로 볼 수있는 변수) 최적화 휴리스틱은 다음 같이 제안합니다. 대상 하드웨어에서 더 효율적일 것 입니다. 코드의 나머지 부분이 표준 작업을 수행하는 것처럼 "있는 것처럼"작동하는 한 그에 따라 하드웨어 최적화하여 더 적은 공간에 저장할 수 있습니다.

그러나 코드가 개별적으로 컴파일 될 수있는 다른 코드와 상호 운용 되어야하는 경우 크기는 일관성을 유지하거나 모든 코드가 동일한 규칙을 따르도록해야합니다.

일관성이 없으면 다음과 같은 합병증이 있습니다. int x = 255;코드가 있으면 나중에 어떻게해야 x = y합니까? int가변 폭일 수있는 경우 컴파일러는 필요한 최대 공간을 사전 할당하기 위해 미리 알고 있어야합니다. y별도의 컴파일 된 다른 코드에서 인수가 전달 되면 어떻게됩니까?


Java는 "BigInteger"및 "BigDecimal"이라는 클래스를 사용하여 C ++의 GMP C ++ 클래스 인터페이스 (디지털 트라우마 덕분에)와 마찬가지로이 작업을 정확하게 수행합니다. 원한다면 거의 모든 언어로 쉽게 할 수 있습니다.

CPU는 항상 모든 길이의 작업을 지원하도록 설계된 BCD (Binary Coded Decimal) 를 사용할 수있었습니다. 그러나 오늘날의 GPU 표준에 따라 한 번에 한 바이트 씩 수동으로 작동하는 경향이 있습니다.

이러한 솔루션이나 다른 유사한 솔루션을 사용하지 않는 이유는 무엇입니까? 공연. 가장 성능이 우수한 언어는 엄격한 루프 작업 중에 변수를 확장 할 여유가 없으므로 결정적이지 않습니다.

대량 저장 및 운송 상황에서 압축 값은 종종 사용하는 유일한 유형의 값입니다. 예를 들어, 컴퓨터로 스트리밍되는 음악 / 비디오 패킷은 다음 값이 크기 최적화로 2 바이트인지 4 바이트인지를 지정하는 데 약간의 시간을 소비 할 수 있습니다.

컴퓨터에 메모리를 사용할 수 있으면 메모리는 저렴하지만 크기 조정이 가능한 변수의 속도와 복잡성은 .. 그것이 유일한 이유입니다.


동적 크기를 가진 간단한 유형을 갖는 것은 매우 복잡하고 계산이 무겁기 때문입니다. 이것이 가능 할지도 모르겠습니다.
컴퓨터는 값을 변경할 때마다 숫자가 몇 비트인지 확인해야합니다. 추가 작업이 상당히 많을 것입니다. 그리고 컴파일하는 동안 변수의 크기를 모르면 계산을 수행하기가 훨씬 어려울 것입니다.

동적 크기의 변수를 지원하려면 컴퓨터는 실제로 변수에 몇 바이트가 있는지 기억해야합니다.이 정보를 저장하려면 추가 메모리가 필요합니다. 그리고이 정보는 변수에 대한 모든 작업 전에 올바른 프로세서 명령을 선택하기 전에 분석해야합니다.

컴퓨터의 작동 방식과 변수의 크기가 일정한 이유를 더 잘 이해하려면 어셈블러 언어의 기초를 배우십시오.

그러나 constexpr 값을 사용하여 이와 같은 것을 달성 할 수 있다고 생각합니다. 그러나 이것은 프로그래머가 코드를 예측하기 어렵게 만듭니다. 일부 컴파일러 최적화는 이와 같은 작업을 수행 할 수 있지만 프로그래머에게 숨기고 간단하게 유지합니다.

여기에서는 프로그램 성능과 관련된 문제에 대해서만 설명했습니다. 변수의 크기를 줄임으로써 메모리를 절약하기 위해 해결해야 할 모든 문제를 생략했습니다. 솔직히, 나는 그것이 가능하다고 생각하지 않습니다.


결론적으로 선언 된 것보다 작은 변수를 사용하는 것은 컴파일 중에 값이 알려진 경우에만 의미가 있습니다. 현대 컴파일러가 그렇게 할 가능성이 높습니다. 다른 경우에는 너무 많거나 해결 불가능한 문제가 발생할 수 있습니다.


그런 다음 myInt컴파일러에서 4 바이트를 차지합니다. 그러나 실제 값 255은 1 바이트로만 표현할 수 있습니다. 왜 myInt1 바이트의 메모리 만 차지하지 않습니까?

이를 가변 길이 인코딩 이라고하며 VLQ 와 같은 다양한 인코딩이 정의 되어 있습니다. 그러나 가장 유명한 것 중 하나는 아마도 UTF-8 일 것입니다 . UTF-8은 가변 바이트 수의 코드 포인트를 1에서 4까지 인코딩합니다.

또는보다 일반적인 방법은 다음과 같습니다. 값을 나타내는 데 필요한 공간이 해당 크기보다 작을 때 왜 유형에 하나의 크기 만 연결되어 있습니까?

엔지니어링에서 항상 그렇듯이 트레이드 오프에 관한 것입니다. 장점 만있는 솔루션은 없으므로 솔루션을 설계 할 때 장점과 절충점의 균형을 유지해야합니다.

정착 된 디자인은 고정 된 크기의 기본 유형을 사용하는 것이었고 하드웨어 / 언어는 거기서 내려 왔습니다.

그렇다면 가변 인코딩근본적인 약점은 무엇입니까? 더 많은 메모리 배고픈 구성표를 선호하여 거부되었습니다. 임의의 주소 지정 없음 .

UTF-8 문자열에서 4 번째 코드 포인트가 시작되는 바이트의 인덱스는 무엇입니까?

이전 코드 포인트의 값에 따라 선형 스캔이 필요합니다.

무작위 주소 지정에서 더 나은 가변 길이 인코딩 체계가 있습니까?

예, 그러나 더 복잡합니다. 이상적인 것이 있다면 아직 본 적이 없습니다.

임의 주소 지정이 실제로 중요합니까?

아, 네!

문제는 모든 종류의 집계 / 배열이 고정 크기 유형에 의존한다는 것입니다.

  • struct? 의 3 번째 필드에 액세스 랜덤 어드레싱!
  • 배열의 세 번째 요소에 액세스하고 있습니까? 랜덤 어드레싱!

이는 본질적으로 다음과 같은 상충 관계가 있음을 의미합니다.

고정 크기 유형 또는 선형 메모리 스캔


컴퓨터 메모리는 일정한 크기 (종종 8 비트, 바이트라고 함)의 연속적으로 주소가 지정된 청크로 세분화되며 대부분의 컴퓨터는 연속적인 주소를 갖는 바이트 시퀀스에 효율적으로 액세스하도록 설계되었습니다.

객체의 주소가 객체의 수명 내에서 변경되지 않으면 해당 주소가 지정된 코드가 해당 객체에 빠르게 액세스 할 수 있습니다. 그러나이 접근 방식의 필수 제한 사항은 주소 X에 주소가 할당되고 N 바이트 떨어져있는 주소 Y에 다른 주소가 할당되면 X가 수명 내에서 N 바이트보다 커질 수 없다는 것입니다. X 또는 Y가 이동하지 않는 한 Y X가 이동하려면 X의 주소를 보유하는 유니버스의 모든 내용이 새 주소를 반영하도록 업데이트되고 Y도 이동해야합니다. 그러한 업데이트를 용이하게하는 시스템을 설계 할 수 있지만 (Java 및 .NET 모두 꽤 잘 관리함) 일생 동안 동일한 위치에 머무를 객체를 다루는 것이 훨씬 효율적입니다.


짧은 대답은 다음과 같습니다. C ++ 표준이 그렇게 말했기 때문입니다.

긴 대답은 : 컴퓨터에서 할 수있는 일은 궁극적으로 하드웨어에 의해 제한됩니다. 물론 저장을 위해 정수를 가변 바이트 수로 인코딩하는 것이 가능하지만이를 읽으려면 특별한 CPU 명령이 필요하거나 소프트웨어로 구현할 수 있지만 너무 느릴 것입니다. 미리 정의 된 너비의 값을로드하기 위해 CPU에서 고정 크기 작업을 사용할 수 있으며 가변 너비에 대한 값은 없습니다.

고려해야 할 또 다른 사항은 컴퓨터 메모리의 작동 방식입니다. 정수 유형이 1-4 바이트의 저장 영역을 차지할 수 있다고 가정하십시오. 값 42를 정수에 저장한다고 가정합니다. 1 바이트를 차지하고 메모리 주소 X에 배치합니다. 그런 다음 X + 1 위치에 다음 변수를 저장합니다 (이 시점에서는 정렬을 고려하지 않습니다). . 나중에 값을 6424로 변경하기로 결정했습니다.

그러나 이것은 단일 바이트에 맞지 않습니다! 그래서 당신은 무엇을합니까? 나머지는 어디에 두나요? X + 1에 이미 무언가가 있으므로 배치 할 수 없습니다. 다른 곳? 나중에 어디에서 어떻게 알 수 있습니까? 컴퓨터 메모리는 삽입 의미론을 지원하지 않습니다. 위치에 무언가를 놓고 공간을 확보하기 위해 모든 것을 옆으로 밀어 넣을 수는 없습니다!

따로 : 당신이 말하는 것은 실제로 데이터 압축 영역입니다. 압축 알고리즘은 모든 것을 더 단단히 압축하기 위해 존재하므로 적어도 일부는 필요한 것보다 정수에 더 많은 공간을 사용하지 않는 것을 고려할 것입니다. 그러나 압축 된 데이터는 수정하기 쉽지 않으며 (가능한 경우) 수정하기 전마다 다시 압축됩니다.


이렇게하면 런타임 성능이 상당히 향상됩니다. 가변 크기 유형으로 작업하려면 작업을 수행하기 전에 각 숫자를 디코딩해야하며 (기계 코드 명령어는 일반적으로 고정 너비) 작업을 수행 한 다음 결과를 보유 할만큼 충분한 공간을 메모리에서 찾으십시오. 그것들은 매우 어려운 작업입니다. 모든 데이터를 약간 비효율적으로 저장하는 것이 훨씬 쉽습니다.

이것이 항상 수행되는 방법은 아닙니다. Google의 Protobuf 프로토콜을 고려하십시오. 프로토 타입은 데이터를 매우 효율적으로 전송하도록 설계되었습니다. 전송 된 바이트 수를 줄이면 데이터를 조작 할 때 추가 명령 비용이들 수 있습니다. 따라서 프로토 타입은 1, 2, 3, 4 또는 5 바이트의 정수를 인코딩하는 인코딩을 사용하며 작은 정수는 더 적은 바이트를 사용합니다. 그러나 메시지가 수신되면보다 전통적인 고정 크기 정수 형식으로 압축 해제되어 작동하기가 더 쉽습니다. 네트워크 전송 중에 만 공간 효율적인 가변 길이 정수를 사용합니다.


나는 Sergey의 집 비유를 좋아 하지만 자동차 비유가 더 좋을 것이라고 생각합니다.

변수 유형을 자동차 유형으로, 사람을 데이터로 상상해보십시오. 우리는 새 차를 찾을 때 목적에 가장 적합한 차를 선택합니다. 한 두 사람 만 들어갈 수있는 작은 스마트 자동차를 원하십니까? 아니면 더 많은 사람들을 태울 리무진? 둘 다 속도와 주행 거리 (속도와 메모리 사용량을 고려)와 같은 장점과 단점이 있습니다.

리무진이 있고 혼자 운전하는 경우 자신에게만 맞게 줄지 않습니다. 그러기 위해서는 차를 팔고 (읽기 : 할당 해제) 새로운 작은 차를 사야합니다.

비유를 계속하면서, 당신은 기억을 자동차로 가득 찬 거대한 주차장이라고 생각할 수 있습니다. 그리고 당신이 읽을 때, 당신의 차 유형만을 위해 훈련 된 전문 운전사가 그것을 가져옵니다. 차 안에있는 사람들에 따라 차가 종류를 바꿀 수 있다면, 어떤 종류의 차가 그 자리에 앉을 지 알 수 없으므로 차를 타기를 원할 때마다 모든 운전 기사를 가져와야합니다.

다시 말해, 런타임시 읽어야 할 메모리 양을 결정하는 것은 매우 비효율적이며 주차장에 몇 대의 자동차를 더 장착 할 수 있다는 사실보다 중요합니다.


몇 가지 이유가 있습니다. 하나는 임의의 크기의 숫자를 처리하기위한 복잡성이 증가하고 컴파일러가 더 이상 모든 int의 길이가 정확히 X 바이트라는 가정을 기반으로 최적화 할 수 없기 때문에 성능에 미치는 영향입니다.

두 번째 방법은 간단한 형식을 이런 식으로 저장하면 길이를 유지하기 위해 추가 바이트가 필요하다는 것입니다. 따라서이 새로운 시스템에서는 255 이하의 값이 실제로는 2 바이트가 필요하지만 최악의 경우 이제 4 대신 5 바이트가 필요합니다. 이는 사용 된 메모리 측면에서 성능 승리가 예상보다 적음을 의미합니다. 생각하고 어떤 경우에는 실제로 순 손실이 될 수 있습니다.

A third reason is that computer memory is generally addressable in words, not bytes. (But see footnote). Words are a multiple of bytes, usually 4 on 32-bit systems and 8 on 64 bit systems. You usually can't read an individual byte, you read a word and extract the nth byte from that word. This means both that extracting individual bytes from a word takes a bit more effort than just reading the entire word and that it is very efficient if the entire memory is evenly divided in word-sized (ie, 4-byte sized) chunks. Because, if you have arbitrary sized integers floating around, you might end up with one part of the integer being in one word, and another in the next word, necessitating two reads to get the full integer.

Footnote: To be more precise, while you addressed in bytes, most systems ignored the 'uneven' bytes. Ie, address 0, 1, 2 and 3 all read the same word, 4, 5, 6 and 7 read the next word, and so on.

On an unreleated note, this is also why 32-bit systems had a max of 4 GB memory. The registers used to address locations in memory are usually large enough to hold a word, ie 4 bytes, which has a max value of (2^32)-1 = 4294967295. 4294967296 bytes is 4 GB.


There are objects that in some sense have variable size, in the C++ standard library, such as std::vector. However, these all dynamically allocate the extra memory they will need. If you take sizeof(std::vector<int>), you will get a constant that has nothing to do with the memory managed by the object, and if you allocate an array or structure containing std::vector<int>, it will reserve this base size rather than putting the extra storage in the same array or structure. There are a few pieces of C syntax that support something like this, notably variable-length arrays and structures, but C++ did not choose to support them.

The language standard defines object size that way so that compilers can generate efficient code. For example, if int happens to be 4 bytes long on some implementation, and you declare a as a pointer to or array of int values, then a[i] translates into the pseudocode, “dereference the address a + 4×i.” This can be done in constant time, and is such a common and important operation that many instruction-set architectures, including x86 and the DEC PDP machines on which C was originally developed, can do it in a single machine instruction.

One common real-world example of data stored consecutively as variable-length units is strings encoded as UTF-8. (However, the underlying type of a UTF-8 string to the compiler is still char and has width 1. This allows ASCII strings to be interpreted as valid UTF-8, and a lot of library code such as strlen() and strncpy() to continue to work.) The encoding of any UTF-8 codepoint can be one to four bytes long, and therefore, if you want the fifth UTF-8 codepoint in a string, it could begin anywhere from the fifth byte to the seventeenth byte of the data. The only way to find it is to scan from the beginning of the string and check the size of each codepoint. If you want to find the fifth grapheme, you also need to check the character classes. If you wanted to find the millionth UTF-8 character in a string, you’d need to run this loop a million times! If you know you will need to work with indices often, you can traverse the string once and build an index of it—or you can convert to a fixed-width encoding, such as UCS-4. Finding the millionth UCS-4 character in a string is just a matter of adding four million to the address of the array.

Another complication with variable-length data is that, when you allocate it, you either need to allocate as much memory as it could ever possibly use, or else dynamically reallocate as needed. Allocating for the worst case could be extremely wasteful. If you need a consecutive block of memory, reallocating could force you to copy all the data over to a different location, but allowing the memory to be stored in non-consecutive chunks complicates the program logic.

So, it’s possible to have variable-length bignums instead of fixed-width short int, int, long int and long long int, but it would be inefficient to allocate and use them. Additionally, all mainstream CPUs are designed to do arithmetic on fixed-width registers, and none have instructions that directly operate on some kind of variable-length bignum. Those would need to be implemented in software, much more slowly.

In the real world, most (but not all) programmers have decided that the benefits of UTF-8 encoding, especially compatibility, are important, and that we so rarely care about anything other than scanning a string from front to back or copying blocks of memory that the draw­backs of variable width are acceptable. We could use packed, variable-width elements similar to UTF-8 for other things. But we very rarely do, and they aren’t in the standard library.


Why does a type have only one size associated with it when the space required to represent the value might be smaller than that size?

Primarily because of alignment requirements.

As per basic.align/1:

Object types have alignment requirements which place restrictions on the addresses at which an object of that type may be allocated.

Think of a building that has many floors and each floor has many rooms.
Each room is your size (a fixed space) capable of holding N amount of people or objects.
With the room size known beforehand, it makes the structural component of the building well-structured.

If the rooms are not aligned, then the building skeleton won't be well-structured.


It can be less. Consider the function:

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

it compiles to assembly code (g++, x64, details stripped)

$43, %eax
ret

Here, bar and baz end up using zero bytes to represent.


so why would myInt not just occupy 1 byte of memory?

Because you told it to use that much. When using an unsigned int, some standards dictate that 4 bytes will be used and that the available range for it will be from 0 to 4,294,967,295. If you were to use an unsigned char instead, you would probably only be using the 1 byte that you're looking for, (depending on the standard and C++ normally uses these standards).

If it weren't for these standards you'd have to keep this in mind: how is the compiler or CPU supposed to know to only use 1 byte instead of 4? Later on in your program you might add or multiply that value, which would require more space. Whenever you make a memory allocation, the OS has to find, map, and give you that space, (potentially swapping memory to virtual RAM as well); this can take a long time. If you allocate the memory before hand, you won't have to wait for another allocation to be completed.

As for the reason why we use 8 bits per byte, you can take a look at this: What is the history of why bytes are eight bits?

On a side note, you could allow the integer to overflow; but should you use a signed integer, the C\C++ standards state that integer overflows result in undefined behavior. Integer overflow


Something simple which most answers seem to miss:

because it suits the design goals of C++.

Being able to work out a type's size at compile time allows a huge number of simplifying assumptions to be made by the compiler and the programmer, which bring a lot of benefits, particularly with regards to performance. Of course, fixed-size types have concomitant pitfalls like integer overflow. This is why different languages make different design decisions. (For instance, Python integers are essentially variable-size.)

Probably the main reason C++ leans so strongly to fixed-size types is its goal of C compatibility. However, since C++ is a statically-typed language which tries to generate very efficient code, and avoids adding things not explicitly specified by the programmer, fixed-size types still make a lot of sense.

So why did C opt for fixed-size types in the first place? Simple. It was designed to write '70s-era operating systems, server software, and utilities; things which provided infrastructure (such as memory management) for other software. At such a low level, performance is critical, and so is the compiler doing precisely what you tell it to.


To change the size of a variable would require reallocation and this is usually not worth the additional CPU cycles compared to wasting a few more bytes of memory.

Local variables go on a stack which is very fast to manipulate when those variables do not change in size. If you decided you want to expand the size of a variable from 1 byte to 2 bytes then you have to move everything on the stack by one byte to make that space for it. That can potentially cost a lot of CPU cycles depending on how many things need to be moved.

Another way you could do it is by making every variable a pointer to a heap location, but you would waste even more CPU cycles and memory this way, actually. Pointers are 4 bytes (32 bit addressing) or 8 bytes (64 bit addressing), so you are already using 4 or 8 for the pointer, then the actual size of the data on the heap. There is still a cost to reallocation in this case. If you need to reallocate heap data, you could get lucky and have room to expand it inline, but sometimes you have to move it somewhere else on the heap to have the contiguous block of memory of the size you want.

It's always faster to decide how much memory to use beforehand. If you can avoid dynamic sizing you gain performance. Wasting memory is usually worth the performance gain. That's why computers have tons of memory. :)


The compiler is allowed to make a lot of changes to your code, as long as things still work (the "as-is" rule).

It would be possible to use a 8-bit literal move instruction instead of the longer (32/64 bit) required to move a full int. However, you would need two instructions to complete the load, since you would have to set the register to zero first before doing the load.

It is simply more efficient (at least according to the main compilers) to handle the value as 32 bit. Actually, I've yet to see a x86/x86_64 compiler that would do 8-bit load without inline assembly.

However, things are different when it comes to 64 bit. When designing the previous extensions (from 16 to 32 bit) of their processors, Intel made a mistake. Here is a good representation of what they look like. The main takeaway here is that when you write to AL or AH, the other is not affected (fair enough, that was the point and it made sense back then). But it gets interesting when they expanded it to 32 bits. If you write the bottom bits (AL, AH or AX), nothing happens to the upper 16 bits of EAX, which means that if you want to promote a char into a int, you need to clear that memory first, but you have no way of actually using only these top 16 bits, making this "feature" more a pain than anything.

Now with 64 bits, AMD did a much better job. If you touch anything in the lower 32 bits, the upper 32 bits are simply set to 0. This leads to some actual optimizations that you can see in this godbolt. You can see that loading something of 8 bits or 32 bits is done the same way, but when you use 64 bits variables, the compiler uses a different instruction depending on the actual size of your literal.

So you can see here, compilers can totally change the actual size of your variable inside the CPU if it would produce the same result, but it makes no sense to do so for smaller types.

참고URL : https://stackoverflow.com/questions/50819413/why-are-types-always-a-certain-size-no-matter-its-value

반응형