컴파일러는 일부 기계에 대해 어셈블러 (및 궁극적으로 기계 코드)를 생성해야하며 일반적으로 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 ++은 플랫폼의 고유 한 자연 데이터 유형 세트에 대한 암시 적 액세스를 제공하며 다른 (네이티브가 아닌) 표현을 직접 코딩 할 수 있습니다.
unsinged로 표현 될 수있는 가장 큰 값 은 잘못되었습니다255. 2) 값이 변함에 따라 변수의 최적 저장 크기 계산 및 저장 영역 축소 / 확장의 오버 헤드를 고려하십시오.