Trình biên dịch được cho là tạo ra trình biên dịch chương trình (và cuối cùng là mã máy) cho một số máy và nói chung C ++ cố gắng thông cảm với máy đó.
Thông cảm với máy bên dưới có nghĩa là đại khái: giúp dễ dàng viết mã C ++, nó sẽ ánh xạ hiệu quả vào các hoạt động mà máy có thể thực hiện nhanh chóng. Vì vậy, chúng tôi muốn cung cấp quyền truy cập vào các loại dữ liệu và hoạt động nhanh và "tự nhiên" trên nền tảng phần cứng của chúng tôi.
Cụ thể, xem xét một kiến trúc máy cụ thể. Hãy lấy gia đình Intel x86 hiện tại.
Hướng dẫn sử dụng dành cho nhà phát triển phần mềm Intel® 64 và IA-32 Architectures vol 1 ( liên kết ), phần 3.4.1 nói:
Các thanh ghi mục đích chung 32 bit EAX, EBX, ECX, EDX, ESI, EDI, EBP và ESP được cung cấp để giữ các mục sau:
• Toán tử cho các phép toán logic và số học
• Toán tử để tính toán địa chỉ
• Con trỏ bộ nhớ
Vì vậy, chúng tôi muốn trình biên dịch sử dụng các thanh ghi EAX, EBX, vv khi nó biên dịch số học số nguyên C ++ đơn giản. Điều này có nghĩa là khi tôi khai báo int
, nó phải tương thích với các thanh ghi này, để tôi có thể sử dụng chúng một cách hiệu quả.
Các thanh ghi luôn có cùng kích thước (ở đây, 32 bit), vì vậy int
các biến của tôi sẽ luôn là 32 bit. Tôi sẽ sử dụng cùng một bố cục (endianian nhỏ) để tôi không phải thực hiện chuyển đổi mỗi khi tôi tải một giá trị biến vào một thanh ghi hoặc lưu lại một thanh ghi vào một biến.
Sử dụng godbolt chúng ta có thể thấy chính xác những gì trình biên dịch làm cho một số mã tầm thường:
int square(int num) {
return num * num;
}
biên dịch (với GCC 8.1 và -fomit-frame-pointer -O3
để đơn giản) thành:
square(int):
imul edi, edi
mov eax, edi
ret
điều này có nghĩa là:
- các
int num
tham số được thông qua năm đăng ký EDI, có nghĩa là nó là chính xác kích thước và bố trí Intel mong đợi cho một thanh ghi nguồn gốc. Hàm không phải chuyển đổi bất cứ thứ gì
- phép nhân là một lệnh đơn (
imul
), rất nhanh
- trả về kết quả chỉ đơn giản là vấn đề sao chép nó vào một thanh ghi khác (người gọi hy vọng kết quả sẽ được đưa vào EAX)
Chỉnh sửa: chúng ta có thể thêm một so sánh có liên quan để hiển thị sự khác biệt bằng cách sử dụng bố cục không phải là bản địa. Trường hợp đơn giản nhất là lưu trữ các giá trị trong một cái gì đó ngoài chiều rộng riêng.
Sử dụng lại Godbolt , chúng ta có thể so sánh một phép nhân bản địa đơn giản
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
với mã tương đương cho chiều rộng không chuẩn
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
Tất cả các hướng dẫn bổ sung đều liên quan đến việc chuyển đổi định dạng đầu vào (hai số nguyên không dấu 31 bit) thành định dạng mà bộ xử lý có thể xử lý nguyên bản. Nếu chúng tôi muốn lưu trữ kết quả trở lại thành giá trị 31 bit, sẽ có một hoặc hai hướng dẫn khác để thực hiện việc này.
Sự phức tạp thêm này có nghĩa là bạn chỉ bận tâm với điều này khi việc tiết kiệm không gian là rất quan trọng. Trong trường hợp này, chúng tôi chỉ tiết kiệm hai bit so với sử dụng nguồn gốc unsigned
hoặc uint32_t
loại, điều này sẽ tạo ra mã đơn giản hơn nhiều.
Một lưu ý về kích thước động:
Ví dụ trên vẫn là các giá trị độ rộng cố định thay vì chiều rộng thay đổi, nhưng chiều rộng (và căn chỉnh) không còn phù hợp với các thanh ghi riêng.
Nền tảng x86 có một số kích thước gốc, bao gồm 8 bit và 16 bit ngoài 32 bit chính (Tôi đang sử dụng chế độ 64 bit và nhiều thứ khác để đơn giản).
Các loại này (char, int8_t, uint8_t, int16_t, v.v.) cũng được kiến trúc hỗ trợ trực tiếp - một phần để tương thích ngược với 8086/286/386 / v.v. vv bộ hướng dẫn.
Chắc chắn là trường hợp chọn loại kích thước cố định tự nhiên nhỏ nhất sẽ đủ, có thể là một cách thực hành tốt - chúng vẫn nhanh chóng, tải và hướng dẫn đơn lẻ, bạn vẫn có được số học bản địa tốc độ đầy đủ và thậm chí bạn có thể cải thiện hiệu suất bằng cách giảm nhớ cache.
Điều này rất khác với mã hóa có độ dài thay đổi - Tôi đã làm việc với một số trong số này và chúng thật kinh khủng. Mỗi tải trở thành một vòng lặp thay vì một lệnh đơn. Mỗi cửa hàng cũng là một vòng lặp. Mọi cấu trúc đều có độ dài thay đổi, vì vậy bạn không thể sử dụng mảng một cách tự nhiên.
Một lưu ý thêm về hiệu quả
Trong các bình luận tiếp theo, bạn đã sử dụng từ "hiệu quả", theo như tôi có thể nói về kích thước lưu trữ. Đôi khi chúng tôi chọn giảm thiểu kích thước lưu trữ - điều này có thể quan trọng khi chúng tôi lưu số lượng giá trị rất lớn vào tệp hoặc gửi chúng qua mạng. Sự đánh đổi là chúng ta cần tải các giá trị đó vào các thanh ghi để làm bất cứ điều gì với chúng và thực hiện chuyển đổi không miễn phí.
Khi chúng ta thảo luận về hiệu quả, chúng ta cần biết những gì chúng ta tối ưu hóa, và sự đánh đổi là gì. Sử dụng các loại lưu trữ không phải là bản địa là một cách để trao đổi tốc độ xử lý cho không gian và đôi khi có ý nghĩa. Sử dụng lưu trữ có độ dài thay đổi (ít nhất là đối với các loại số học), giao dịch nhiều tốc độ xử lý hơn (và độ phức tạp của mã và thời gian của nhà phát triển) để tiết kiệm không gian hơn nữa.
Hình phạt tốc độ bạn phải trả cho điều này có nghĩa là nó chỉ đáng giá khi bạn cần giảm thiểu tối đa băng thông hoặc lưu trữ dài hạn và đối với những trường hợp đó, việc sử dụng định dạng đơn giản và tự nhiên thường dễ dàng hơn - và sau đó chỉ cần nén nó bằng hệ thống đa năng (như zip, gzip, bzip2, xy hoặc bất cứ thứ gì).
tl; dr
Mỗi nền tảng có một kiến trúc, nhưng bạn có thể đưa ra một số lượng lớn các cách khác nhau để thể hiện dữ liệu. Bất kỳ ngôn ngữ nào cũng không hợp lý để cung cấp số lượng dữ liệu tích hợp không giới hạn. Vì vậy, C ++ cung cấp quyền truy cập ngầm định bộ dữ liệu tự nhiên, tự nhiên của nền tảng và cho phép bạn tự viết mã cho bất kỳ đại diện nào khác (không phải bản địa).
unsinged
giá trị lớn nhất , có thể được biểu thị bằng 1 byte là255
. 2) Xem xét chi phí tính toán kích thước lưu trữ tối ưu và thu nhỏ / mở rộng vùng lưu trữ của một biến khi giá trị thay đổi.