Tại sao các loại luôn luôn có kích thước nhất định cho dù giá trị của nó là gì?


149

Việc triển khai có thể khác nhau giữa các kích thước thực tế của các loại, nhưng trên hầu hết, các loại như int unsign và float luôn là 4 byte. Nhưng tại sao một loại luôn chiếm một lượng bộ nhớ nhất định bất kể giá trị của nó là gì? Ví dụ: nếu tôi tạo số nguyên sau với giá trị 255

int myInt = 255;

Sau đó myIntsẽ chiếm 4 byte với trình biên dịch của tôi. Tuy nhiên, giá trị thực tế, 255có thể được biểu diễn chỉ với 1 byte, vậy tại sao myIntkhông chỉ chiếm 1 byte bộ nhớ? Hoặc cách hỏi tổng quát hơn: Tại sao một loại chỉ có một kích thước được liên kết với nó khi không gian cần thiết để biểu thị giá trị có thể nhỏ hơn kích thước đó?


15
1) " Tuy nhiên, giá trị thực tế, 256 có thể được biểu diễn chỉ với 1 byte " Sai, unsingedgiá 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.
Algirdas Preidžius

99
Chà, khi đến lúc đọc giá trị từ bộ nhớ, làm thế nào để bạn đề xuất máy sẽ xác định có bao nhiêu byte để đọc? Làm thế nào máy sẽ biết nơi dừng đọc giá trị? Điều này sẽ yêu cầu các cơ sở bổ sung. Và trong trường hợp chung, bộ nhớ và hiệu năng hoạt động cho các phương tiện bổ sung này sẽ cao hơn nhiều so với trường hợp chỉ sử dụng 4 byte cố định cho unsigned intgiá trị.
AnT

74
Tôi thực sự thích câu hỏi này. Mặc dù có vẻ đơn giản để trả lời nó, tôi nghĩ rằng việc đưa ra một lời giải thích chính xác đòi hỏi phải hiểu rõ về cách thức hoạt động của máy tính và kiến ​​trúc máy tính. Hầu hết mọi người có thể sẽ chỉ coi nó là đương nhiên, mà không có một lời giải thích toàn diện cho nó.
andreee

37
Xem xét những gì sẽ xảy ra nếu bạn thêm 1 vào giá trị của biến, biến nó thành 256, vì vậy nó sẽ cần mở rộng. Nó mở rộng đến đâu? Bạn có di chuyển phần còn lại của bộ nhớ để tạo không gian? Liệu các biến tự di chuyển? Nếu có, nó di chuyển đến đâu và làm thế nào để bạn tìm thấy các con trỏ mà bạn cần cập nhật?
molbdnilo

13
@someidiot không, bạn sai rồi. std::vector<X>luôn có cùng kích thước, tức sizeof(std::vector<X>)là hằng số thời gian biên dịch.
SergeyA

Câu trả lời:


131

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 intcá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à:

  1. các int numtham 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ì
  2. phép nhân là một lệnh đơn ( imul), rất nhanh
  3. 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 unsignedhoặc uint32_tloạ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).


Tôi đang xem xét tất cả các câu trả lời hay trong khi cố gắng hiểu tất cả chúng .. Vì vậy, liên quan đến câu trả lời của bạn, sẽ không có kích thước động, nói ít hơn 32 bit cho một số nguyên, không chỉ cho phép nhiều biến hơn trong một thanh ghi ? Nếu endianess là như nhau, tại sao điều này sẽ không tối ưu?
Nichlas Uden

7
@asd nhưng bạn sẽ sử dụng bao nhiêu thanh ghi trong mã để chỉ ra có bao nhiêu biến hiện được lưu trữ trong một thanh ghi?
dùng253751

1
FWIW thường đóng gói nhiều giá trị vào không gian nhỏ nhất có sẵn nơi bạn quyết định tiết kiệm không gian quan trọng hơn chi phí tốc độ của việc đóng gói và giải nén chúng. Nói chung, bạn không thể vận hành chúng một cách tự nhiên ở dạng đóng gói của chúng vì bộ xử lý không biết cách thực hiện số học một cách chính xác trên bất kỳ thứ gì khác ngoài các thanh ghi tích hợp. Tra cứu BCD cho một ngoại lệ một phần với hỗ trợ bộ xử lý
Vô dụng

3
Nếu tôi thực sự làm cần tất cả 32 bit cho một số giá trị, tôi vẫn cần một nơi để lưu trữ các chiều dài, vì vậy bây giờ tôi cần nhiều hơn 32 bit trong một số trường hợp.
Vô dụng

1
+1. Một lưu ý về "định dạng đơn giản và tự nhiên rồi nén" thường tốt hơn: Điều này hoàn toàn đúng , nhưng : đối với một số dữ liệu, VLQ-mỗi-giá trị-sau đó-nén-toàn bộ mọi thứ hoạt động tốt hơn đáng kể so với chỉ nén -khi-thing, và đối với một số ứng dụng, dữ liệu của bạn không thể được nén cùng nhau , vì nó khác nhau (như trong gitsiêu dữ liệu) hoặc bạn thực sự giữ nó trong bộ nhớ đôi khi cần truy cập ngẫu nhiên hoặc sửa đổi một vài nhưng không phải hầu hết các giá trị (như trong các công cụ kết xuất HTML + CSS) và do đó chỉ có thể được xử lý bằng cách sử dụng một cái gì đó giống như VLQ tại chỗ.
mtraceur

139

Bởi vì các loại về cơ bản đại diện cho lưu trữ và chúng được xác định theo giá trị tối đa mà chúng có thể giữ, không phải giá trị hiện tại.

Sự tương tự rất đơn giản sẽ là một ngôi nhà - một ngôi nhà có kích thước cố định, bất kể có bao nhiêu người sống trong đó, và cũng có một mã xây dựng quy định số người tối đa có thể sống trong một ngôi nhà có kích thước nhất định.

Tuy nhiên, ngay cả khi một người sống trong một ngôi nhà có thể chứa 10 người, kích thước của ngôi nhà sẽ không bị ảnh hưởng bởi số lượng người cư ngụ hiện tại.


31
Tôi thích sự tương tự. Nếu chúng ta mở rộng nó ra một chút, chúng ta có thể tưởng tượng sử dụng ngôn ngữ lập trình không sử dụng kích thước bộ nhớ cố định cho các loại và điều đó sẽ giống như đánh sập các phòng trong nhà của chúng ta bất cứ khi nào chúng không được sử dụng và xây dựng lại chúng khi cần thiết (tức là hàng tấn chi phí khi chúng ta có thể xây dựng một loạt các ngôi nhà và để chúng ở lại khi chúng ta cần).
ahouse101

5
"Bởi vì các loại cơ bản đại diện cho lưu trữ", điều này không đúng với tất cả các ngôn ngữ (ví dụ như bản in)
corvus_192

56
Thẻ @ corvus_192 có ý nghĩa. Câu hỏi này được gắn thẻ với C ++, không phải 'bản thảo'
SergeyA

4
@ ahouse101 Thật vậy, có một số ngôn ngữ có số nguyên chính xác không giới hạn, chúng phát triển khi cần thiết. Các ngôn ngữ này không yêu cầu bạn phân bổ bộ nhớ cố định cho các biến, chúng được triển khai bên trong dưới dạng tham chiếu đối tượng. Ví dụ: Lisp, Python.
Barmar

2
@jamesqf Có lẽ không phải ngẫu nhiên mà số học MP được chấp nhận lần đầu tiên ở Lisp, nơi cũng quản lý bộ nhớ tự động. Các nhà thiết kế cảm thấy rằng các tác động hiệu suất là thứ yếu để dễ lập trình. Và các kỹ thuật tối ưu hóa đã được phát triển để giảm thiểu tác động.
Barmar

44

Nó là một tối ưu hóa và đơn giản hóa.

Bạn có thể có các đối tượng có kích thước cố định. Do đó lưu trữ giá trị.
Hoặc bạn có thể có các chướng ngại vật có kích thước thay đổi. Nhưng lưu trữ giá trị và kích thước.

vật có kích thước cố định

Mã thao tác số không cần phải lo lắng về kích thước. Bạn giả sử rằng bạn luôn sử dụng 4 byte và làm cho mã rất đơn giản.

Đối tượng kích thước động

Mã số thao tác phải hiểu khi đọc một biến mà nó phải đọc giá trị và kích thước. Sử dụng kích thước để đảm bảo tất cả các bit cao bằng 0 trong thanh ghi.

Khi đặt giá trị trở lại trong bộ nhớ nếu giá trị không vượt quá kích thước hiện tại thì chỉ cần đặt lại giá trị trong bộ nhớ. Nhưng nếu giá trị bị thu hẹp hoặc tăng lên, bạn cần di chuyển vị trí lưu trữ của đối tượng sang vị trí khác trong bộ nhớ để đảm bảo nó không bị tràn. Bây giờ bạn phải theo dõi vị trí của số đó (vì nó có thể di chuyển nếu nó phát triển quá lớn so với kích thước của nó). Bạn cũng cần theo dõi tất cả các vị trí biến không sử dụng để chúng có thể được sử dụng lại.

Tóm lược

Mã được tạo cho các đối tượng kích thước cố định đơn giản hơn rất nhiều.

Ghi chú

Nén sử dụng thực tế là 255 sẽ phù hợp với một byte. Có các sơ đồ nén để lưu trữ các tập dữ liệu lớn sẽ chủ động sử dụng các giá trị kích thước khác nhau cho các số khác nhau. Nhưng vì đây không phải là dữ liệu trực tiếp nên bạn không có sự phức tạp được mô tả ở trên. Bạn sử dụng ít không gian hơn để lưu trữ dữ liệu với chi phí nén / khử dữ liệu để lưu trữ.


4
Đây là câu trả lời tốt nhất cho tôi: Làm thế nào để bạn theo dõi kích thước? Với bộ nhớ nhiều hơn ?
trực tuyến Thomas

@ThomasMoors Có, chính xác: có nhiều bộ nhớ hơn . Nếu bạn, ví dụ có một mảng động, thì một số intsẽ lưu trữ số lượng phần tử trong mảng đó. Điều đó inttự nó sẽ có một kích thước cố định một lần nữa.
Alfe

1
@ThomasMoors có hai tùy chọn thường được sử dụng, cả hai đều cần thêm bộ nhớ - hoặc bạn có trường (kích thước cố định) cho bạn biết có bao nhiêu dữ liệu (ví dụ: int cho kích thước mảng hoặc chuỗi "kiểu pascal" trong đó đầu tiên phần tử chứa bao nhiêu ký tự), hoặc thay vào đó, bạn có thể có một chuỗi (hoặc cấu trúc phức tạp hơn) trong đó mỗi phần tử bằng cách nào đó lưu ý nếu đó là phần cuối cùng - ví dụ: chuỗi kết thúc bằng không, hoặc hầu hết các dạng danh sách được liên kết.
Peteris

27

Bởi vì trong một ngôn ngữ như C ++, mục tiêu thiết kế là các thao tác đơn giản biên dịch theo các hướng dẫn máy đơn giản.

Tất cả các bộ hướng dẫn CPU chính hoạt động với các loại chiều rộng cố định và nếu bạn muốn thực hiện các loại chiều rộng thay đổi , bạn phải thực hiện nhiều hướng dẫn máy để xử lý chúng.

Về lý do tại sao phần cứng máy tính cơ bản là như vậy: Đó là vì nó đơn giản hơn và hiệu quả hơn cho nhiều trường hợp (nhưng không phải tất cả).

Hãy tưởng tượng máy tính như một miếng băng keo:

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

Nếu bạn chỉ đơn giản yêu cầu máy tính nhìn vào byte đầu tiên trên băng xx, làm thế nào để biết loại đó có dừng ở đó hay không hoặc tiếp tục với byte tiếp theo? Nếu bạn có một số như 255(thập lục phân FF) hoặc một số như 65535(thập lục phân FFFF) thì byte đầu tiên luôn luôn là FF.

Vậy làm sao bạn biết? Bạn phải thêm logic bổ sung và "quá tải" ý nghĩa của ít nhất một giá trị bit hoặc byte để chỉ ra rằng giá trị tiếp tục đến byte tiếp theo. Logic đó không bao giờ là "miễn phí", hoặc bạn mô phỏng nó trong phần mềm hoặc bạn thêm một loạt các bóng bán dẫn bổ sung vào CPU để làm điều đó.

Các loại ngôn ngữ có độ rộng cố định như C và C ++ phản ánh điều đó.

Nó không phải là theo cách này, và ngôn ngữ trừu tượng hơn được ít quan tâm đến việc lập bản đồ mã tối đa hiệu quả có thể tự do sử dụng mã hóa biến-width (hay còn gọi là "Số lượng Variable Length" hoặc VLQ) với nhiều loại số.

Đọc thêm: Nếu bạn tìm kiếm "số lượng chiều dài thay đổi" bạn có thể tìm thấy một số ví dụ về nơi mà loại mã hóa thực sự hiệu quả và giá trị logic bổ sung. Thông thường khi bạn cần lưu trữ một lượng lớn giá trị có thể ở bất kỳ đâu trong phạm vi lớn, nhưng hầu hết các giá trị đều hướng đến một phạm vi phụ nhỏ.


Lưu ý rằng nếu trình biên dịch có thể chứng minh rằng nó có thể thoát khỏi việc lưu trữ giá trị trong một không gian nhỏ hơn mà không phá vỡ bất kỳ mã nào (ví dụ: đó là một biến chỉ hiển thị bên trong một đơn vị dịch) các phương pháp tối ưu hóa của nó cho thấy rằng ' sẽ hiệu quả hơn trên phần cứng đích, nó hoàn toàn được phép tối ưu hóa nó cho phù hợp và lưu trữ nó trong một không gian nhỏ hơn, miễn là phần còn lại của mã hoạt động "như thể" nó đã làm điều tiêu chuẩn.

Nhưng , khi mã phải hoạt động tương tác với các mã khác có thể được biên dịch riêng, các kích thước phải nhất quán hoặc đảm bảo rằng mọi đoạn mã đều tuân theo cùng một quy ước.

Bởi vì nếu nó không nhất quán, có sự phức tạp này: Nếu tôi có int x = 255;nhưng sau đó trong mã tôi sẽ làm gì x = y? Nếu intcó thể là chiều rộng thay đổi, trình biên dịch sẽ phải biết trước để phân bổ trước dung lượng tối đa mà nó sẽ cần. Điều đó không phải lúc nào cũng có thể, bởi vì nếu ymột đối số được truyền từ một đoạn mã khác được biên dịch riêng thì sao?


26

Java sử dụng các lớp được gọi là "BigInteger" và "BigDecimal" để thực hiện chính xác điều này, cũng như giao diện lớp GMP C ++ của C ++ rõ ràng (cảm ơn Digital Trauma). Bạn có thể dễ dàng tự làm điều đó bằng bất kỳ ngôn ngữ nào nếu bạn muốn.

CPU luôn có khả năng sử dụng BCD (Binary Coded Decimal) được thiết kế để hỗ trợ các hoạt động ở bất kỳ độ dài nào (nhưng bạn có xu hướng hoạt động thủ công trên một byte tại thời điểm SLOW theo tiêu chuẩn GPU ngày nay.)

Lý do chúng tôi không sử dụng những giải pháp này hoặc tương tự khác? Hiệu suất. Các ngôn ngữ có hiệu suất cao nhất của bạn không thể đủ khả năng để mở rộng một biến ở giữa một số thao tác vòng lặp chặt chẽ - nó sẽ không mang tính quyết định.

Trong các tình huống lưu trữ và vận chuyển hàng loạt, các giá trị được đóng gói thường là loại giá trị DUY NHẤT bạn sẽ sử dụng. Ví dụ: gói nhạc / video được truyền đến máy tính của bạn có thể tốn một chút để chỉ định xem giá trị tiếp theo là 2 byte hay 4 byte dưới dạng tối ưu hóa kích thước.

Khi nó ở trên máy tính của bạn, nơi nó có thể được sử dụng, bộ nhớ thì rẻ nhưng tốc độ và sự phức tạp của các biến có thể thay đổi kích thước thì không .. đó thực sự là lý do duy nhất.


4
Vui mừng khi thấy ai đó đề cập đến BigInteger. Đó không phải là một ý tưởng ngớ ngẩn, chỉ là nó chỉ có ý nghĩa để thực hiện nó với số lượng cực lớn.
Max Barraclough

1
Để trở thành người phạm tội, bạn thực sự có nghĩa là những con số cực kỳ chính xác :) Ít nhất là trong trường hợp của BigDecimal ...
Bill K

2
Và vì điều này được gắn thẻ c ++ , nên có lẽ đáng nói đến giao diện lớp GMP C ++ , đây là ý tưởng tương tự như Big * của Java.
Chấn thương kỹ thuật số

20

Bởi vì sẽ rất phức tạp và tính toán nặng nề khi có các loại đơn giản với kích thước động. Tôi không chắc điều này thậm chí có thể xảy ra.
Máy tính sẽ phải kiểm tra số lượng bit mất sau mỗi lần thay đổi giá trị của nó. Nó sẽ là khá nhiều hoạt động bổ sung. Và sẽ khó hơn nhiều khi thực hiện các phép tính khi bạn không biết kích thước của các biến trong quá trình biên dịch.

Để hỗ trợ kích thước động của các biến, máy tính thực sự sẽ phải nhớ có bao nhiêu byte một biến ngay bây giờ mà ... sẽ cần thêm bộ nhớ để lưu trữ thông tin đó. Và thông tin này sẽ phải được phân tích trước mỗi thao tác trên biến để chọn hướng dẫn bộ xử lý phù hợp.

Để hiểu rõ hơn về cách thức máy tính hoạt động và tại sao các biến có kích thước không đổi, hãy tìm hiểu cơ bản về ngôn ngữ trình biên dịch.

Mặc dù, tôi cho rằng có thể đạt được điều gì đó tương tự với các giá trị constexpr. Tuy nhiên, điều này sẽ làm cho mã ít dự đoán hơn đối với một lập trình viên. Tôi cho rằng một số tối ưu hóa trình biên dịch có thể làm một cái gì đó như thế nhưng họ che giấu nó khỏi một lập trình viên để giữ cho mọi thứ đơn giản.

Tôi chỉ mô tả ở đây những vấn đề liên quan đến hiệu suất của một chương trình. Tôi đã bỏ qua tất cả các vấn đề sẽ phải giải quyết để tiết kiệm bộ nhớ bằng cách giảm kích thước của các biến. Thành thật mà nói, tôi không nghĩ rằng nó thậm chí có thể.


Tóm lại, sử dụng các biến nhỏ hơn khai báo chỉ có ý nghĩa nếu giá trị của chúng được biết trong quá trình biên dịch. Rất có khả năng các trình biên dịch hiện đại làm điều đó. Trong các trường hợp khác, nó sẽ gây ra quá nhiều vấn đề khó khăn hoặc thậm chí không thể giải quyết.


Tôi rất nghi ngờ rằng một điều như vậy được thực hiện trong thời gian biên dịch. Có rất ít điểm trong việc bảo tồn bộ nhớ trình biên dịch như thế và đó là lợi ích duy nhất.
Bartek Banachewicz

1
Tôi đã suy nghĩ về các hoạt động như nhân biến constexpr với biến thông thường. Ví dụ, về mặt lý thuyết, chúng ta có biến constexpr 8 byte với giá trị 56và chúng ta nhân nó với một số biến 2 byte. Trên một số kiến ​​trúc, hoạt động 64 bit sẽ nặng hơn tính toán để trình biên dịch có thể tối ưu hóa điều đó để chỉ thực hiện phép nhân 16 bit.
NO_NAME

Một số triển khai APL và một số ngôn ngữ trong họ SNOBOL (SPITBOL tôi nghĩ? Có lẽ Biểu tượng) đã làm chính xác điều này (với độ chi tiết): thay đổi định dạng biểu diễn động tùy thuộc vào giá trị thực. APL sẽ chuyển từ Boolean sang số nguyên để nổi và quay lại. SPITBOL sẽ đi từ biểu diễn cột của Booleans (8 mảng Boolean riêng biệt được lưu trữ trong một mảng byte) đến các số nguyên (IIRC).
davidbak

16

Sau đó myIntsẽ chiếm 4 byte với trình biên dịch của tôi. Tuy nhiên, giá trị thực tế, 255có thể được biểu diễn chỉ với 1 byte, vậy tại sao myIntkhông chỉ chiếm 1 byte bộ nhớ?

Điều này được gọi là mã hóa độ dài thay đổi , có nhiều mã hóa được định nghĩa, ví dụ như VLQ . Tuy nhiên, một trong những nổi tiếng nhất có lẽ là UTF-8 : UTF-8 mã hóa các điểm mã trên một số byte khác nhau, từ 1 đến 4.

Hoặc cách hỏi tổng quát hơn: Tại sao một loại chỉ có một kích thước được liên kết với nó khi không gian cần thiết để biểu thị giá trị có thể nhỏ hơn kích thước đó?

Như mọi khi trong kỹ thuật, đó là tất cả về sự đánh đổi. Không có giải pháp nào chỉ có lợi thế, vì vậy bạn phải cân bằng lợi thế và đánh đổi khi thiết kế giải pháp của mình.

Thiết kế đã được giải quyết là sử dụng các loại cơ bản có kích thước cố định và phần cứng / ngôn ngữ đã bay xuống từ đó.

Vì vậy, điểm yếu cơ bản của mã hóa biến là gì , khiến nó bị từ chối để ủng hộ các chương trình đói bộ nhớ hơn? Không có địa chỉ ngẫu nhiên .

Chỉ số của byte mà điểm mã thứ 4 bắt đầu trong chuỗi UTF-8 là gì?

Nó phụ thuộc vào các giá trị của các điểm mã trước đó, cần phải quét tuyến tính.

Chắc chắn có các sơ đồ mã hóa có độ dài thay đổi tốt hơn trong việc đánh địa chỉ ngẫu nhiên?

Có, nhưng chúng cũng phức tạp hơn. Nếu có một lý tưởng, tôi chưa bao giờ nhìn thấy nó.

Địa chỉ ngẫu nhiên có thực sự quan trọng không?

Ồ CÓ!

Vấn đề là, bất kỳ loại tổng hợp / mảng nào đều phụ thuộc vào các loại kích thước cố định:

  • Truy cập vào trường thứ 3 của a struct? Địa chỉ ngẫu nhiên!
  • Truy cập phần tử thứ 3 của một mảng? Địa chỉ ngẫu nhiên!

Điều đó có nghĩa là về cơ bản bạn có sự đánh đổi sau:

Các loại kích thước cố định HOẶC Quét bộ nhớ tuyến tính


Đây không phải là một vấn đề như bạn làm cho nó âm thanh. Bạn luôn có thể sử dụng bảng vector. Có một chi phí bộ nhớ và tìm nạp thêm nhưng quét tuyến tính là không cần thiết.
Artelius

2
@Artelius: Làm thế nào để bạn mã hóa bảng vectơ khi số nguyên có chiều rộng thay đổi? Ngoài ra, chi phí bộ nhớ của bảng vectơ là gì khi mã hóa một cho các số nguyên sử dụng 1 đến 4 byte trong bộ nhớ?
Matthieu M.

Hãy nhìn xem, bạn đã đúng, trong ví dụ cụ thể mà OP đưa ra, sử dụng bảng vectơ có lợi thế bằng không. Thay vì xây dựng một bảng vectơ, bạn cũng có thể đặt dữ liệu vào một mảng các phần tử có kích thước cố định. Tuy nhiên, OP cũng yêu cầu một câu trả lời chung chung hơn. Trong Python, một mảng các số nguyên một bảng vectơ gồm các số nguyên có kích thước thay đổi! Đó không phải là vì nó giải quyết được vấn đề này , mà bởi vì Python không biết tại thời điểm biên dịch liệu các thành phần danh sách sẽ là Số nguyên, Nổi, Dicts, Chuỗi hoặc Danh sách, tất cả đều có kích thước khác nhau.
Artelius

@Artelius: Lưu ý rằng trong Python, mảng chứa các con trỏ có kích thước cố định cho các phần tử; điều này làm cho O (1) trở thành một phần tử, với chi phí của một sự gián tiếp.
Matthieu M.

16

Bộ nhớ máy tính được chia thành các khối có địa chỉ liên tiếp có kích thước nhất định (thường là 8 bit và được gọi là byte) và hầu hết các máy tính được thiết kế để truy cập hiệu quả các chuỗi byte có địa chỉ liên tiếp.

Nếu địa chỉ của đối tượng không bao giờ thay đổi trong vòng đời của đối tượng, thì mã được cung cấp địa chỉ của đối tượng có thể nhanh chóng truy cập vào đối tượng được đề cập. Tuy nhiên, một hạn chế thiết yếu với cách tiếp cận này là nếu một địa chỉ được gán cho địa chỉ X, và sau đó một địa chỉ khác được gán cho địa chỉ Y cách N byte, thì X sẽ không thể phát triển lớn hơn N byte trong vòng đời của Y, trừ khi X hoặc Y được di chuyển. Để X di chuyển, cần phải cập nhật mọi thứ trong vũ trụ chứa địa chỉ của X để phản ánh địa chỉ mới và tương tự cho Y di chuyển. Mặc dù có thể thiết kế một hệ thống để tạo điều kiện cho các bản cập nhật như vậy (cả Java và .NET quản lý nó khá tốt) hoạt động hiệu quả hơn với các đối tượng sẽ ở cùng một vị trí trong suốt cuộc đời của chúng,


"X sẽ không thể phát triển lớn hơn N byte trong vòng đời của Y, trừ khi X hoặc Y bị di chuyển. Để X di chuyển, cần phải cập nhật mọi thứ trong vũ trụ chứa địa chỉ của X để phản ánh cái mới và tương tự cho Y di chuyển. " Đây là điểm nổi bật IMO: các đối tượng chỉ sử dụng kích thước lớn như nhu cầu giá trị hiện tại của chúng sẽ cần thêm hàng tấn chi phí cho kích thước / trọng tâm, di chuyển bộ nhớ, biểu đồ tham chiếu, v.v. Và khá rõ ràng khi một người suy nghĩ về cách nó có thể hoạt động ... nhưng vẫn, rất đáng để nói rõ ràng, đặc biệt là rất ít người khác đã làm.
gạch dưới

@underscore_d: Các ngôn ngữ như Javascript được thiết kế từ đầu để đối phó với các đối tượng có kích thước thay đổi có thể mang lại hiệu quả đáng kinh ngạc cho nó. Mặt khác, trong khi có thể làm cho các hệ thống đối tượng có kích thước thay đổi trở nên đơn giản và có thể làm cho chúng nhanh, thì việc triển khai đơn giản lại chậm và việc triển khai nhanh là vô cùng phức tạp.
supercat

13

Câu trả lời ngắn gọn là: Bởi vì tiêu chuẩn C ++ nói như vậy.

Câu trả lời dài là: Những gì bạn có thể làm trên máy tính cuối cùng bị giới hạn bởi phần cứng. Tất nhiên, có thể mã hóa một số nguyên thành một số byte khác nhau để lưu trữ, nhưng sau đó đọc nó sẽ yêu cầu các hướng dẫn CPU đặc biệt để thực hiện hoặc bạn có thể thực hiện nó trong phần mềm, nhưng sau đó sẽ rất chậm. Các hoạt động có kích thước cố định có sẵn trong CPU để tải các giá trị về độ rộng được xác định trước, không có giá trị nào cho chiều rộng thay đổi.

Một điểm khác để xem xét là làm thế nào bộ nhớ máy tính hoạt động. Giả sử loại số nguyên của bạn có thể chiếm từ 1 đến 4 byte dung lượng lưu trữ. Giả sử bạn lưu trữ giá trị 42 vào số nguyên của mình: nó chiếm 1 byte và bạn đặt nó tại địa chỉ bộ nhớ X. Sau đó, bạn lưu trữ biến tiếp theo của mình tại vị trí X + 1 (Tôi không xem xét căn chỉnh tại điểm này), v.v. . Sau đó, bạn quyết định thay đổi giá trị của bạn thành 6424.

Nhưng điều này không phù hợp với một byte đơn! Vậy bạn làm gì? Nơi nào bạn đặt phần còn lại? Bạn đã có một cái gì đó ở X + 1, vì vậy không thể đặt nó ở đó. Ở đâu đó khác? Làm thế nào bạn sẽ biết sau này ở đâu? Bộ nhớ máy tính không hỗ trợ ngữ nghĩa chèn: bạn không thể đặt thứ gì đó tại một vị trí và đẩy mọi thứ sau đó sang một bên để nhường chỗ!

Ngoài ra: Những gì bạn đang nói thực sự là lĩnh vực nén dữ liệu. Các thuật toán nén tồn tại để đóng gói mọi thứ chặt chẽ hơn, vì vậy ít nhất một số trong số chúng sẽ xem xét không sử dụng nhiều không gian hơn cho số nguyên của bạn hơn mức cần thiết. Tuy nhiên, dữ liệu nén không dễ sửa đổi (nếu có thể) và cuối cùng chỉ được nén lại mỗi khi bạn thực hiện bất kỳ thay đổi nào đối với dữ liệu đó.


11

Có những lợi ích hiệu suất thời gian chạy khá đáng kể từ việc này. Nếu bạn đã thao tác trên các loại kích thước thay đổi, bạn sẽ phải giải mã từng số trước khi thực hiện thao tác (hướng dẫn mã máy thường có chiều rộng cố định), thực hiện thao tác, sau đó tìm khoảng trống trong bộ nhớ đủ lớn để giữ kết quả. Đó là những hoạt động rất khó khăn. Việc lưu trữ tất cả các dữ liệu hơi kém hiệu quả sẽ dễ dàng hơn nhiều.

Đây không phải là luôn luôn làm thế nào nó được thực hiện. Hãy xem xét giao thức Protobuf của Google. Protobuf được thiết kế để truyền dữ liệu rất hiệu quả. Giảm số lượng byte được truyền có giá trị chi phí của các hướng dẫn bổ sung khi vận hành trên dữ liệu. Theo đó, protobuf sử dụng mã hóa mã hóa các số nguyên theo 1, 2, 3, 4 hoặc 5 byte và các số nguyên nhỏ hơn chiếm ít byte hơn. Tuy nhiên, khi nhận được tin nhắn, nó sẽ được giải nén thành định dạng số nguyên có kích thước cố định truyền thống, dễ thao tác hơn. Chỉ trong quá trình truyền mạng, họ sử dụng số nguyên có độ dài biến không gian hiệu quả như vậy.


11

Tôi thích sự tương tự nhà của Sergey , nhưng tôi nghĩ rằng một sự tương tự xe hơi sẽ tốt hơn.

Hãy tưởng tượng các loại biến là loại xe và người như dữ liệu. Khi chúng tôi tìm kiếm một chiếc xe mới, chúng tôi chọn chiếc xe phù hợp nhất với mục đích của chúng tôi. Chúng ta có muốn một chiếc xe thông minh nhỏ chỉ có thể chứa một hoặc hai người không? Hoặc một chiếc limousine để chở nhiều người hơn? Cả hai đều có những lợi ích và hạn chế như tốc độ và tiết kiệm xăng (nghĩ tốc độ và sử dụng bộ nhớ).

Nếu bạn có một chiếc xe limousine và bạn đang lái xe một mình, nó sẽ không co lại để chỉ phù hợp với bạn. Để làm điều đó, bạn sẽ phải bán chiếc xe (đọc: thỏa thuận) và mua một chiếc mới nhỏ hơn cho chính mình.

Tiếp tục sự tương tự, bạn có thể nghĩ về bộ nhớ như một bãi đậu xe khổng lồ chứa đầy ô tô và khi bạn đi đọc, một tài xế chuyên biệt được đào tạo chỉ dành cho loại xe của bạn sẽ lấy nó cho bạn. Nếu chiếc xe của bạn có thể thay đổi loại tùy thuộc vào những người bên trong nó, bạn sẽ cần phải mang theo cả đống tài xế mỗi khi bạn muốn lấy xe vì họ sẽ không bao giờ biết loại xe nào sẽ ngồi tại chỗ.

Nói cách khác, cố gắng xác định lượng bộ nhớ bạn cần đọc trong thời gian chạy sẽ cực kỳ kém hiệu quả và vượt xa thực tế là bạn có thể lắp thêm một vài chiếc xe trong bãi đậu xe của mình.


10

Có một vài lý do. Một là độ phức tạp được thêm vào để xử lý các số có kích thước tùy ý và hiệu năng đạt được do trình biên dịch không còn có thể tối ưu hóa dựa trên giả định rằng mọi int đều chính xác là X byte.

Cách thứ hai là lưu trữ các kiểu đơn giản theo cách này có nghĩa là chúng cần một byte bổ sung để giữ độ dài. Vì vậy, giá trị 255 hoặc ít hơn thực sự cần hai byte trong hệ thống mới này chứ không phải một và trong trường hợp xấu nhất bạn cần 5 byte thay vì 4. Điều này có nghĩa là hiệu suất giành được về bộ nhớ được sử dụng ít hơn bạn có thể nghĩ và trong một số trường hợp cạnh thực sự có thể là một mất mát ròng.

Một lý do thứ ba là bộ nhớ máy tính thường có thể định địa chỉ bằng từ , không phải byte. (Nhưng xem chú thích). Các từ là bội số của byte, thường là 4 trên hệ thống 32 bit và 8 trên hệ thống 64 bit. Bạn thường không thể đọc một byte riêng lẻ, bạn đọc một từ và trích xuất byte thứ n từ từ đó. Điều này có nghĩa là cả việc trích xuất từng byte riêng lẻ từ một từ tốn nhiều công sức hơn so với việc chỉ đọc toàn bộ từ và sẽ rất hiệu quả nếu toàn bộ bộ nhớ được chia đều thành các đoạn có kích thước từ (nghĩa là cỡ 4 byte). Bởi vì, nếu bạn có các số nguyên có kích thước tùy ý trôi nổi xung quanh, bạn có thể kết thúc với một phần của số nguyên nằm trong một từ và phần khác trong từ tiếp theo, yêu cầu hai lần đọc để có được số nguyên đầy đủ.

Lưu ý: Để chính xác hơn, trong khi bạn giải quyết bằng byte, hầu hết các hệ thống đều bỏ qua các byte 'không đồng đều'. Tức là, địa chỉ 0, 1, 2 và 3 đều đọc cùng một từ, 4, 5, 6 và 7 đọc từ tiếp theo, v.v.

Trên một lưu ý không liên quan, đây cũng là lý do tại sao các hệ thống 32 bit có bộ nhớ tối đa 4 GB. Các thanh ghi được sử dụng để định vị các vị trí trong bộ nhớ thường đủ lớn để chứa một từ, tức là 4 byte, có giá trị tối đa là (2 ^ 32) -1 = 4294967295. 4294967296 byte là 4 GB.


8

Có những đối tượng theo một nghĩa nào đó có kích thước thay đổi, trong thư viện chuẩn C ++, chẳng hạn như std::vector. Tuy nhiên, tất cả đều tự động phân bổ bộ nhớ bổ sung mà họ sẽ cần. Nếu bạn lấy sizeof(std::vector<int>), bạn sẽ nhận được một hằng số không liên quan gì đến bộ nhớ do đối tượng quản lý và nếu bạn phân bổ một mảng hoặc cấu trúc có chứa std::vector<int>, nó sẽ dự trữ kích thước cơ sở này thay vì đặt bộ nhớ bổ sung vào cùng một mảng hoặc cấu trúc . Có một vài cú pháp C hỗ trợ một cái gì đó như thế này, đáng chú ý là các mảng và cấu trúc có độ dài thay đổi, nhưng C ++ đã không chọn hỗ trợ chúng.

Tiêu chuẩn ngôn ngữ xác định kích thước đối tượng theo cách đó để trình biên dịch có thể tạo mã hiệu quả. Ví dụ: nếu inttình cờ dài 4 byte trong một số triển khai và bạn khai báo alà con trỏ tới hoặc mảng các intgiá trị, sau đó a[i]chuyển thành mã giả, Giả định địa chỉ a + 4 × i. Điều này có thể được thực hiện trong thời gian liên tục và là một hoạt động phổ biến và quan trọng đến mức nhiều kiến ​​trúc tập lệnh, bao gồm x86 và các máy PDP DEC mà C được phát triển ban đầu, có thể thực hiện trong một lệnh máy duy nhất.

Một ví dụ phổ biến trong thế giới thực của dữ liệu được lưu trữ liên tiếp dưới dạng các đơn vị có độ dài thay đổi là các chuỗi được mã hóa dưới dạng UTF-8. (Tuy nhiên, loại cơ bản của chuỗi UTF-8 cho trình biên dịch vẫn còn charvà có chiều rộng 1. Điều này cho phép các chuỗi ASCII được hiểu là UTF-8 hợp lệ và rất nhiều mã thư viện như strlen()strncpy()tiếp tục hoạt động.) Mã hóa của bất kỳ mã hóa UTF-8 nào có thể dài từ một đến bốn byte, và do đó, nếu bạn muốn mã hóa UTF-8 thứ năm trong một chuỗi, nó có thể bắt đầu ở bất cứ đâu từ byte thứ năm đến byte thứ mười bảy của dữ liệu. Cách duy nhất để tìm thấy nó là quét từ đầu chuỗi và kiểm tra kích thước của mỗi điểm mã. Nếu bạn muốn tìm đồ thị thứ năm, bạn cũng cần kiểm tra các lớp nhân vật. Nếu bạn muốn tìm ký tự UTF-8 thứ một triệu trong chuỗi, bạn cần chạy vòng lặp này một triệu lần! Nếu bạn biết bạn sẽ cần phải làm việc với các chỉ số thường xuyên, bạn có thể duyệt qua chuỗi một lần và xây dựng một chỉ mục của nó hoặc bạn có thể chuyển đổi sang mã hóa có chiều rộng cố định, chẳng hạn như UCS-4. Tìm ký tự UCS-4 thứ một triệu trong chuỗi chỉ là vấn đề thêm bốn triệu vào địa chỉ của mảng.

Một điều phức tạp khác với dữ liệu có độ dài thay đổi là, khi bạn phân bổ nó, bạn cần phân bổ càng nhiều bộ nhớ càng tốt, hoặc nếu không thì sẽ tự động phân bổ lại khi cần. Phân bổ cho trường hợp xấu nhất có thể là vô cùng lãng phí. Nếu bạn cần một khối bộ nhớ liên tiếp, việc tái phân bổ có thể buộc bạn sao chép tất cả dữ liệu sang một vị trí khác, nhưng cho phép bộ nhớ được lưu trữ trong các khối không liên tiếp làm phức tạp logic chương trình.

Vì vậy, nó có thể có bignums biến có độ dài thay vì chiều rộng cố định short int, int, long intlong long int, nhưng nó sẽ không hiệu quả phân bổ và sử dụng chúng. Ngoài ra, tất cả các CPU chính được thiết kế để thực hiện số học trên các thanh ghi có chiều rộng cố định và không có hướng dẫn nào hoạt động trực tiếp trên một số loại bignum có chiều dài thay đổi. Những người sẽ cần phải được thực hiện trong phần mềm, chậm hơn nhiều.

Trong thế giới thực, hầu hết (nhưng không phải tất cả) các lập trình viên đã quyết định rằng lợi ích của mã hóa UTF-8, đặc biệt là tính tương thích là rất quan trọng và chúng tôi hiếm khi quan tâm đến bất cứ điều gì ngoài việc quét một chuỗi từ trước ra sau hoặc sao chép các khối bộ nhớ mà nhược điểm của chiều rộng thay đổi được chấp nhận. Chúng ta có thể sử dụng các phần tử đóng gói, có chiều rộng thay đổi tương tự như UTF-8 cho những thứ khác. Nhưng chúng tôi rất hiếm khi làm, và họ không có trong thư viện tiêu chuẩn.


7

Tại sao một loại chỉ có một kích thước được liên kết với nó khi không gian cần thiết để biểu thị giá trị có thể nhỏ hơn kích thước đó?

Chủ yếu vì yêu cầu căn chỉnh.

Theo basic.align / 1 :

Các loại đối tượng có các yêu cầu căn chỉnh đặt ra các hạn chế đối với các địa chỉ mà tại đó một đối tượng của loại đó có thể được phân bổ.

Hãy nghĩ về một tòa nhà có nhiều tầng và mỗi tầng có nhiều phòng.
Mỗi phòng là kích thước của bạn (một không gian cố định) có khả năng chứa N lượng người hoặc đồ vật.
Với kích thước phòng được biết trước, nó làm cho thành phần cấu trúc của tòa nhà có cấu trúc tốt .

Nếu các phòng không được căn chỉnh, thì bộ xương tòa nhà sẽ không được cấu trúc tốt.


7

Nó có thể ít hơn. Hãy xem xét chức năng:

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

nó biên dịch thành mã lắp ráp (g ++, x64, chi tiết bị tước)

$43, %eax
ret

Ở đây, barbazkết thúc bằng cách sử dụng byte không để biểu diễn.


5

vậy tại sao myInt không chỉ chiếm 1 byte bộ nhớ?

Bởi vì bạn nói với nó để sử dụng nhiều. Khi sử dụng một unsigned int, một số tiêu chuẩn cho rằng 4 byte sẽ được sử dụng và phạm vi khả dụng cho nó sẽ là từ 0 đến 4.294.967.295. Nếu bạn sử dụng unsigned charthay thế, có lẽ bạn sẽ chỉ sử dụng 1 byte mà bạn đang tìm kiếm (tùy thuộc vào tiêu chuẩn và C ++ thường sử dụng các tiêu chuẩn này).

Nếu không có các tiêu chuẩn này, bạn phải ghi nhớ điều này: làm thế nào trình biên dịch hoặc CPU phải biết chỉ sử dụng 1 byte thay vì 4? Sau này trong chương trình của bạn, bạn có thể thêm hoặc nhân giá trị đó, sẽ cần nhiều không gian hơn. Bất cứ khi nào bạn thực hiện cấp phát bộ nhớ, HĐH phải tìm, ánh xạ và cung cấp cho bạn không gian đó, (cũng có khả năng trao đổi bộ nhớ với RAM ảo); điều này có thể mất một thời gian dài. Nếu bạn phân bổ bộ nhớ trước khi sử dụng, bạn sẽ không phải chờ phân bổ khác hoàn thành.

Về lý do tại sao chúng tôi sử dụng 8 bit cho mỗi byte, bạn có thể xem điều này: lịch sử tại sao byte là tám bit?

Trên một lưu ý phụ, bạn có thể cho phép số nguyên tràn; nhưng bạn nên sử dụng một số nguyên đã ký, các tiêu chuẩn C \ C ++ nói rằng số nguyên tràn ra dẫn đến hành vi không xác định. Tràn số nguyên


5

Một cái gì đó đơn giản mà hầu hết các câu trả lời dường như bỏ lỡ:

bởi vì nó phù hợp với mục tiêu thiết kế của C ++.

Việc có thể tính ra kích thước của một kiểu trong thời gian biên dịch cho phép một số lượng lớn các giả định đơn giản hóa được đưa ra bởi trình biên dịch và lập trình viên, mang lại rất nhiều lợi ích, đặc biệt là liên quan đến hiệu suất. Tất nhiên, các loại kích thước cố định có cạm bẫy đồng thời như tràn số nguyên. Đây là lý do tại sao các ngôn ngữ khác nhau đưa ra quyết định thiết kế khác nhau. (Ví dụ: số nguyên Python về cơ bản có kích thước thay đổi.)

Có lẽ lý do chính khiến C ++ dựa rất nhiều vào các loại kích thước cố định là mục tiêu tương thích của nó. Tuy nhiên, vì C ++ là ngôn ngữ được nhập tĩnh, cố gắng tạo mã rất hiệu quả và tránh thêm những thứ không được chỉ định rõ ràng bởi lập trình viên, các loại kích thước cố định vẫn có ý nghĩa rất lớn.

Vậy tại sao C lại chọn loại kích thước cố định ở vị trí đầu tiên? Đơn giản. Nó được thiết kế để viết các hệ điều hành, phần mềm máy chủ và tiện ích của những năm 70; những thứ cung cấp cơ sở hạ tầng (như quản lý bộ nhớ) cho các phần mềm khác. Ở mức độ thấp như vậy, hiệu suất là rất quan trọng và trình biên dịch cũng đang thực hiện chính xác những gì bạn nói với nó.


5

Để thay đổi kích thước của một biến sẽ yêu cầu phân bổ lại và điều này thường không đáng với các chu kỳ CPU bổ sung so với việc lãng phí thêm một vài byte bộ nhớ.

Các biến cục bộ đi trên một ngăn xếp rất nhanh để thao tác khi các biến đó không thay đổi kích thước. Nếu bạn quyết định muốn mở rộng kích thước của biến từ 1 byte thành 2 byte thì bạn phải di chuyển mọi thứ trên ngăn xếp thêm một byte để tạo khoảng trống cho nó. Điều đó có khả năng có thể tiêu tốn rất nhiều chu kỳ CPU tùy thuộc vào số lượng cần phải di chuyển.

Một cách khác bạn có thể làm là bằng cách biến mỗi biến thành một con trỏ đến một vị trí heap, nhưng thực tế bạn sẽ lãng phí nhiều chu kỳ CPU và bộ nhớ hơn theo cách này. Con trỏ là 4 byte (địa chỉ 32 bit) hoặc 8 byte (địa chỉ 64 bit), vì vậy bạn đã sử dụng 4 hoặc 8 cho con trỏ, sau đó là kích thước thực của dữ liệu trên heap. Vẫn còn chi phí để tái phân bổ trong trường hợp này. Nếu bạn cần phân bổ lại dữ liệu heap, bạn có thể gặp may mắn và có chỗ để mở rộng dữ liệu nội tuyến, nhưng đôi khi bạn phải di chuyển nó đến một nơi khác trên heap để có khối bộ nhớ liền kề có kích thước bạn muốn.

Luôn luôn nhanh hơn để quyết định sử dụng bao nhiêu bộ nhớ trước đó. Nếu bạn có thể tránh kích thước năng động, bạn đạt được hiệu suất. Bộ nhớ lãng phí thường có giá trị đạt được hiệu suất. Đó là lý do tại sao máy tính có hàng tấn bộ nhớ. :)


3

Trình biên dịch được phép thực hiện nhiều thay đổi đối với mã của bạn, miễn là mọi thứ vẫn hoạt động (quy tắc "nguyên trạng").

Có thể sử dụng hướng dẫn di chuyển bằng chữ 8 bit thay vì dài hơn (32/64 bit) để di chuyển đầy đủ int. Tuy nhiên, bạn sẽ cần hai hướng dẫn để hoàn thành tải, vì bạn sẽ phải đặt thanh ghi về 0 trước khi thực hiện tải.

Nó đơn giản là hiệu quả hơn (ít nhất là theo trình biên dịch chính) để xử lý giá trị là 32 bit. Trên thực tế, tôi vẫn chưa thấy trình biên dịch x86 / x86_64 sẽ tải 8 bit mà không cần lắp ráp nội tuyến.

Tuy nhiên, mọi thứ khác nhau khi nói đến 64 bit. Khi thiết kế các phần mở rộng trước đó (từ 16 đến 32 bit) cho bộ xử lý của họ, Intel đã mắc lỗi. Đây là một đại diện tốt của những gì họ trông như thế nào. Điểm nổi bật chính ở đây là khi bạn viết thư cho AL hoặc AH, cái khác không bị ảnh hưởng (đủ công bằng, đó là điểm chính và nó có ý nghĩa hồi đó). Nhưng nó trở nên thú vị khi họ mở rộng nó lên 32 bit. Nếu bạn viết các bit dưới cùng (AL, AH hoặc AX), không có gì xảy ra với 16 bit trên của EAX, điều đó có nghĩa là nếu bạn muốn quảng bá a charthành a int, trước tiên bạn cần xóa bộ nhớ đó, nhưng bạn không có cách nào để thực sự chỉ sử dụng 16 bit hàng đầu này, làm cho "tính năng" này trở nên khó khăn hơn bất cứ điều gì.

Bây giờ với 64 bit, AMD đã làm tốt hơn nhiều. Nếu bạn chạm vào bất cứ thứ gì trong 32 bit thấp hơn, 32 bit trên chỉ đơn giản được đặt thành 0. Điều này dẫn đến một số tối ưu hóa thực tế mà bạn có thể thấy trong Godbolt này . Bạn có thể thấy rằng việc tải một cái gì đó 8 bit hoặc 32 bit được thực hiện theo cùng một cách, nhưng khi bạn sử dụng các biến 64 bit, trình biên dịch sẽ sử dụng một lệnh khác tùy thuộc vào kích thước thực của chữ của bạn.

Vì vậy, bạn có thể thấy ở đây, trình biên dịch hoàn toàn có thể thay đổi kích thước thực của biến của bạn bên trong CPU nếu nó sẽ tạo ra kết quả tương tự, nhưng sẽ không có ý nghĩa gì khi làm như vậy đối với các loại nhỏ hơn.


sửa: như thể nếu . Ngoài ra, tôi không thấy làm thế nào, nếu có thể sử dụng tải / lưu trữ ngắn hơn, nó sẽ giải phóng các byte khác để sử dụng - đó dường như là điều OP tự hỏi: không chỉ tránh chạm vào bộ nhớ không cần thiết bởi giá trị hiện tại, nhưng có thể cho biết có bao nhiêu byte để đọc và chuyển đổi một cách kỳ diệu tất cả RAM trong thời gian chạy để một số ý tưởng triết học kỳ lạ về hiệu quả không gian (không bao giờ bận tâm đến chi phí hiệu suất khổng lồ!) "Giải quyết" điều đó. Những gì CPU / HĐH sẽ cần phải làm điều đó phức tạp đến mức nó trả lời câu hỏi rõ ràng nhất IMO.
gạch dưới

1
Bạn thực sự không thể "tiết kiệm bộ nhớ" trong sổ đăng ký. Trừ khi bạn đang cố gắng làm điều gì đó kỳ lạ bằng cách lạm dụng AH và AL, bạn không thể có một vài giá trị khác nhau trong cùng một thanh ghi mục đích chung. Các biến cục bộ thường ở trong các thanh ghi và không bao giờ vào RAM nếu không có nhu cầu về nó.
meneldal
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.