구현은 실제 크기 유형에 따라 다를 수 있지만 대부분 부호없는 int 및 float와 같은 유형은 항상 4 바이트입니다. 그러나 왜 유형에 관계없이 값이 항상 특정 양의 메모리를 차지 합니까? 예를 들어 값이 255 인 다음 정수를 만든 경우
int myInt = 255;
그런 다음 myInt
컴파일러에서 4 바이트를 차지합니다. 그러나 실제 값 255
은 1 바이트로만 표현할 수 있습니다. 왜 myInt
1 바이트의 메모리 만 차지하지 않습니까? 또는보다 일반적인 방법은 다음과 같습니다. 값을 나타내는 데 필요한 공간이 해당 크기보다 작을 때 왜 유형에 하나의 크기 만 연결되어 있습니까?
답변
컴파일러는 일부 기계에 대해 어셈블러 (및 궁극적으로 기계 코드)를 생성해야하며 일반적으로 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
이것은 다음을 의미합니다.
- 이
int num
매개 변수는 레지스터 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 비트 부호없는 정수)을 프로세서가 기본적으로 처리 할 수있는 형식으로 변환하는 것과 관련이 있습니다. 결과를 다시 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 ++ 클래스 인터페이스 (Digital Trauma 덕분에)와 마찬가지로이를 정확하게 수행합니다. 원한다면 거의 모든 언어로 쉽게 할 수 있습니다.
CPU는 항상 모든 길이의 작업을 지원하도록 설계된 BCD (Binary Coded Decimal) 를 사용할 수있었습니다. 그러나 오늘날의 GPU 표준에 따라 한 번에 한 바이트 씩 수동으로 작동하는 경향이 있습니다.
이러한 솔루션이나 다른 유사한 솔루션을 사용하지 않는 이유는 무엇입니까? 공연. 성능이 가장 우수한 언어는 엄격한 루프 작업 중에 변수를 확장 할 여유가 없으므로 결정적이지 않습니다.
대량 저장 및 운송 상황에서 압축 값은 종종 사용하는 유일한 유형의 값입니다. 예를 들어, 컴퓨터로 스트리밍되는 음악 / 비디오 패킷은 다음 값이 크기 최적화로 2 바이트인지 4 바이트인지를 지정하는 데 약간의 시간을 소비 할 수 있습니다.
컴퓨터에 메모리를 사용할 수 있으면 메모리는 저렴하지만 크기 조정이 가능한 변수의 속도와 복잡성은 .. 그것이 유일한 이유입니다.
답변
동적 크기를 가진 간단한 유형을 갖는 것은 매우 복잡하고 계산이 무거 우므로. 이것이 가능 할지도 모르겠습니다.
컴퓨터는 값을 변경할 때마다 숫자가 몇 비트인지 확인해야합니다. 추가 작업이 상당히 많을 것입니다. 그리고 컴파일하는 동안 변수의 크기를 모르면 계산을 수행하기가 훨씬 어려울 것입니다.
동적 크기의 변수를 지원하려면 컴퓨터는 실제로 변수에 몇 바이트가 있는지 기억해야합니다.이 정보를 저장하려면 추가 메모리가 필요합니다. 그리고이 정보는 변수에 대한 모든 작업 전에 올바른 프로세서 명령을 선택하기 전에 분석해야합니다.
컴퓨터의 작동 방식과 변수의 크기가 일정한 이유를 더 잘 이해하려면 어셈블러 언어의 기초를 배우십시오.
그러나 constexpr 값을 사용하여 이와 같은 것을 달성 할 수 있다고 생각합니다. 그러나 이것은 프로그래머가 코드를 예측하기 어렵게 만듭니다. 일부 컴파일러 최적화는 이와 같은 작업을 수행 할 수 있지만 프로그래머에게 숨기고 간단하게 유지합니다.
여기에서는 프로그램 성능과 관련된 문제에 대해서만 설명했습니다. 변수의 크기를 줄임으로써 메모리를 절약하기 위해 해결해야 할 모든 문제를 생략했습니다. 솔직히, 나는 그것이 가능하다고 생각하지 않습니다.
결론적으로 선언 된 것보다 작은 변수를 사용하는 것은 컴파일 중에 값이 알려진 경우에만 의미가 있습니다. 현대 컴파일러가 그렇게 할 가능성이 높습니다. 다른 경우에는 너무 많거나 해결 불가능한 문제가 발생할 수 있습니다.
답변
그런 다음
myInt
컴파일러에서 4 바이트를 차지합니다. 그러나 실제 값255
은 1 바이트로만 표현할 수 있습니다. 왜myInt
1 바이트의 메모리 만 차지하지 않습니까?
이를 가변 길이 인코딩 이라고하며 VLQ 와 같은 다양한 인코딩이 정의 되어 있습니다. 그러나 가장 유명한 것 중 하나는 아마도 UTF-8 일 것입니다 . UTF-8은 가변 바이트 수의 코드 포인트를 1에서 4까지 인코딩합니다.
또는보다 일반적인 방법은 다음과 같습니다. 값을 나타내는 데 필요한 공간이 해당 크기보다 작을 때 왜 유형에 하나의 크기 만 연결되어 있습니까?
엔지니어링에서 항상 그렇듯이 트레이드 오프에 관한 것입니다. 장점 만있는 솔루션은 없으므로 솔루션을 설계 할 때 장점과 절충점의 균형을 유지해야합니다.
정착 된 디자인은 고정 된 크기의 기본 유형을 사용하는 것이었고 하드웨어 / 언어는 거기서 내려 왔습니다.
그렇다면 가변 인코딩 의 근본적인 약점은 무엇입니까? 임의의 주소 지정 없음 .
UTF-8 문자열에서 4 번째 코드 포인트가 시작되는 바이트의 인덱스는 무엇입니까?
이전 코드 포인트의 값에 따라 선형 스캔이 필요합니다.
무작위 주소 지정에서 더 나은 가변 길이 인코딩 체계가 있습니까?
예, 그러나 더 복잡합니다. 이상적인 것이 있다면 아직 본 적이 없습니다.
임의 주소 지정이 실제로 중요합니까?
어 그래!
문제는 모든 종류의 집계 / 배열이 고정 크기 유형에 의존한다는 것입니다.
struct
? 의 3 번째 필드에 액세스 랜덤 어드레싱!- 배열의 세 번째 요소에 액세스하고 있습니까? 랜덤 어드레싱!
이는 본질적으로 다음과 같은 상충 관계가 있음을 의미합니다.
고정 크기 유형 또는 선형 메모리 스캔