Gì register
từ khóa làm bằng ngôn ngữ C? Tôi đã đọc rằng nó được sử dụng để tối ưu hóa nhưng không được xác định rõ ràng trong bất kỳ tiêu chuẩn nào. Nó vẫn còn liên quan và nếu vậy, khi nào bạn sẽ sử dụng nó?
register
biến.
Gì register
từ khóa làm bằng ngôn ngữ C? Tôi đã đọc rằng nó được sử dụng để tối ưu hóa nhưng không được xác định rõ ràng trong bất kỳ tiêu chuẩn nào. Nó vẫn còn liên quan và nếu vậy, khi nào bạn sẽ sử dụng nó?
register
biến.
Câu trả lời:
Đó là một gợi ý cho trình biên dịch rằng biến sẽ được sử dụng nhiều và bạn khuyên bạn nên giữ nó trong một thanh ghi bộ xử lý nếu có thể.
Hầu hết các trình biên dịch hiện đại làm điều đó một cách tự động, và tốt hơn trong việc chọn chúng hơn con người chúng ta.
register
biến được lấy; đây là hiệu ứng bắt buộc duy nhất của register
từ khóa. Thậm chí điều này là đủ để cải thiện tối ưu hóa, bởi vì nó trở nên tầm thường để nói rằng biến chỉ có thể được sửa đổi trong hàm này.
Tôi ngạc nhiên khi không ai đề cập rằng bạn không thể lấy địa chỉ của biến đăng ký, ngay cả khi trình biên dịch quyết định giữ biến trong bộ nhớ thay vì trong thanh ghi.
Vì vậy, sử dụng register
bạn sẽ không có gì (dù sao trình biên dịch sẽ tự quyết định nơi đặt biến) và mất &
toán tử - không có lý do để sử dụng nó.
register
hữu ích cho việc này ngay cả khi trình biên dịch không đưa nó vào một thanh ghi.
const
này cũng vô dụng vì nó chẳng giúp gì cho bạn, bạn chỉ mất khả năng thay đổi một biến.register
có thể được sử dụng để đảm bảo không ai lấy địa chỉ của một biến trong tương lai mà không cần suy nghĩ. Tôi chưa bao giờ có lý do để sử dụng register
mặc dù.
Nó báo cho trình biên dịch thử sử dụng một thanh ghi CPU, thay vì RAM, để lưu trữ biến. Các thanh ghi nằm trong CPU và truy cập nhanh hơn nhiều so với RAM. Nhưng đó chỉ là một gợi ý cho trình biên dịch, và nó có thể không tuân theo.
Tôi biết câu hỏi này là về C, nhưng câu hỏi tương tự cho C ++ đã được đóng lại như là một bản sao chính xác của câu hỏi này. Câu trả lời này do đó có thể không áp dụng cho C.
Bản nháp mới nhất của tiêu chuẩn C ++ 11, N3485 , cho biết điều này trong 7.1.1 / 3:
Một
register
specifier là một gợi ý cho việc thực hiện rằng biến được khai báo sẽ được sử dụng nhiều. [ lưu ý: Gợi ý có thể bị bỏ qua và trong hầu hết các triển khai, nó sẽ bị bỏ qua nếu lấy địa chỉ của biến. Sử dụng này không được chấp ... -end note ]
Trong C ++ (nhưng không phải trong C), tiêu chuẩn không nêu rõ rằng bạn không thể lấy địa chỉ của một biến được khai báo register
; tuy nhiên, vì một biến được lưu trữ trong một thanh ghi CPU trong suốt vòng đời của nó không có vị trí bộ nhớ được liên kết với nó, việc cố lấy địa chỉ của nó sẽ không hợp lệ và trình biên dịch sẽ bỏ qua register
từ khóa để cho phép lấy địa chỉ.
Nó đã không liên quan trong ít nhất 15 năm vì các trình tối ưu hóa đưa ra quyết định tốt hơn về điều này hơn bạn có thể. Ngay cả khi nó có liên quan, nó có ý nghĩa hơn đối với kiến trúc CPU với rất nhiều thanh ghi, như SPARC hoặc M68000 so với Intel với các thanh ghi nhỏ, hầu hết được trình biên dịch dành riêng cho mục đích riêng của nó.
Trên thực tế, thanh ghi cho trình biên dịch biết rằng biến không bí danh với bất kỳ thứ gì khác trong chương trình (thậm chí không phải là char).
Điều đó có thể được khai thác bởi các trình biên dịch hiện đại trong nhiều tình huống khác nhau và có thể giúp trình biên dịch khá nhiều mã phức tạp - trong mã đơn giản, trình biên dịch có thể tự mình tìm ra điều này.
Mặt khác, nó không phục vụ mục đích và không được sử dụng để phân bổ đăng ký. Nó thường không phát sinh sự suy giảm hiệu suất để chỉ định nó, miễn là trình biên dịch của bạn đủ hiện đại.
register
cấm lấy địa chỉ, vì nếu không thì có thể hữu ích cho trình biên dịch biết các trường hợp trình biên dịch có thể áp dụng tối ưu hóa đăng ký mặc dù địa chỉ của biến được truyền cho hàm bên ngoài (biến sẽ phải được xóa vào bộ nhớ cho cuộc gọi cụ thể đó , nhưng một khi hàm trả về, trình biên dịch lại có thể coi nó như một biến có địa chỉ chưa bao giờ được thực hiện).
bar
là một register
biến, trình biên dịch có thể thay thế foo(&bar);
bằng int temp=bar; foo(&temp); bar=temp;
cách sử dụng, nhưng việc lấy địa chỉ bar
sẽ bị cấm trong hầu hết các bối cảnh khác có vẻ không phải là một quy tắc quá phức tạp. Nếu biến có thể được giữ trong một thanh ghi, sự thay thế sẽ làm cho mã nhỏ hơn. Nếu biến số cần phải được giữ trong RAM, thì sự thay thế sẽ làm cho mã lớn hơn. Để lại câu hỏi liệu có nên thay thế trình biên dịch sẽ dẫn đến mã tốt hơn trong cả hai trường hợp.
register
độ về các biến toàn cục, cho dù trình biên dịch có cho phép lấy địa chỉ hay không, sẽ cho phép một số tối ưu hóa tốt trong trường hợp hàm nội tuyến sử dụng biến toàn cục được gọi lặp lại trong một vòng lặp. Tôi không thể nghĩ ra bất kỳ cách nào khác để cho biến đó được giữ trong một thanh ghi giữa các lần lặp lặp - bạn có thể không?
Tôi đã đọc rằng nó được sử dụng để tối ưu hóa nhưng không được xác định rõ ràng trong bất kỳ tiêu chuẩn nào.
Trong thực tế, nó được xác định rõ ràng bởi tiêu chuẩn C. Trích dẫn dự thảo N1570 phần 6.7.1 đoạn 6 (các phiên bản khác có cùng từ ngữ):
Một tuyên bố về một định danh cho một đối tượng với trình xác định lớp lưu trữ
register
cho thấy rằng việc truy cập vào đối tượng đó càng nhanh càng tốt. Mức độ mà các đề xuất đó có hiệu quả được xác định theo thực hiện.
&
Toán tử đơn nguyên có thể không được áp dụng cho một đối tượng được xác định với register
vàregister
không được sử dụng trong khai báo bên ngoài.
Có một vài quy tắc khác (khá tối nghĩa) dành riêng cho register
các đối tượng đủ tiêu chuẩn:
register
có hành vi không xác định. register
, nhưng bạn không thể làm bất cứ điều gì hữu ích với một đối tượng như vậy (lập chỉ mục vào một mảng yêu cầu lấy địa chỉ của phần tử ban đầu)._Alignas
xác định (mới trong C11) có thể không được áp dụng cho một đối tượng như vậy.va_start
macro được định mức register
, hành vi không được xác định.Có thể có một vài người khác; tải về bản nháp của tiêu chuẩn và tìm kiếm "đăng ký" nếu bạn quan tâm.
Như tên của nó, ý nghĩa ban đầu của nó register
là yêu cầu một đối tượng được lưu trữ trong một thanh ghi CPU. Nhưng với những cải tiến trong việc tối ưu hóa trình biên dịch, điều này đã trở nên ít hữu ích hơn. Các phiên bản hiện đại của tiêu chuẩn C không đề cập đến các thanh ghi CPU, vì chúng không còn (cần phải) cho rằng có một thứ như vậy (có những kiến trúc không sử dụng các thanh ghi). Sự khôn ngoan phổ biến là việc áp dụng register
cho một khai báo đối tượng có nhiều khả năng làm xấu đi mã được tạo, bởi vì nó can thiệp vào phân bổ đăng ký riêng của nhà biên dịch. Vẫn có thể có một vài trường hợp hữu ích (giả sử, nếu bạn thực sự biết mức độ thường xuyên truy cập của một biến và kiến thức của bạn tốt hơn những gì trình biên dịch tối ưu hóa hiện đại có thể tìm ra).
Tác dụng hữu hình chính của register
nó là nó ngăn chặn mọi nỗ lực lấy địa chỉ của một đối tượng. Điều này không đặc biệt hữu ích như một gợi ý tối ưu hóa, vì nó chỉ có thể được áp dụng cho các biến cục bộ và trình biên dịch tối ưu hóa có thể tự thấy rằng địa chỉ của một đối tượng như vậy không được sử dụng.
register
đối tượng mảng đủ tiêu chuẩn, nếu đó là những gì bạn đang nghĩ.
register
các đối tượng mảng; xem điểm đầu tiên được cập nhật trong câu trả lời của tôi. Việc xác định một đối tượng như vậy là hợp pháp, nhưng bạn không thể làm gì với nó. Nếu bạn thêm register
vào định nghĩa s
trong ví dụ của mình , chương trình là bất hợp pháp (vi phạm ràng buộc) trong C. C ++ không đặt ra các hạn chế tương tự register
, vì vậy chương trình sẽ là C ++ hợp lệ (nhưng việc sử dụng register
sẽ là vô nghĩa).
register
Từ khóa có thể phục vụ mục đích hữu ích nếu việc lấy địa chỉ của biến đó là hợp pháp nhưng chỉ trong trường hợp ngữ nghĩa sẽ không bị ảnh hưởng bằng cách sao chép biến thành tạm thời khi lấy địa chỉ tạm thời và tải lại từ tạm thời tại điểm tiếp theo. Điều đó sẽ cho phép các trình biên dịch giả định rằng biến có thể được lưu giữ an toàn trong một thanh ghi trên tất cả các truy cập con trỏ với điều kiện là nó bị xóa ở bất kỳ vị trí nào mà địa chỉ của nó được lấy.
Giờ kể chuyện!
C, như một ngôn ngữ, là một sự trừu tượng của một máy tính. Nó cho phép bạn làm mọi thứ, về mặt máy tính làm gì, đó là thao tác bộ nhớ, làm toán, in mọi thứ, v.v.
Nhưng C chỉ là một sự trừu tượng. Và cuối cùng, những gì nó trích ra từ bạn là ngôn ngữ hội. Hội là ngôn ngữ mà CPU đọc và nếu bạn sử dụng nó, bạn sẽ làm mọi thứ theo CPU. CPU làm gì? Về cơ bản, nó đọc từ bộ nhớ, làm toán và ghi vào bộ nhớ. CPU không chỉ làm toán trên các số trong bộ nhớ. Đầu tiên, bạn phải di chuyển một số từ bộ nhớ sang bộ nhớ bên trong CPU được gọi là thanh ghi. Khi bạn đã hoàn thành bất cứ điều gì bạn cần làm với con số này, bạn có thể chuyển nó trở lại bộ nhớ hệ thống bình thường. Tại sao lại sử dụng bộ nhớ hệ thống? Đăng ký bị giới hạn về số lượng. Bạn chỉ nhận được khoảng một trăm byte trong các bộ xử lý hiện đại và các bộ xử lý phổ biến cũ thậm chí còn bị giới hạn tuyệt vời hơn (6502 có 3 thanh ghi 8 bit để bạn sử dụng miễn phí). Vì vậy, phép toán trung bình của bạn trông như sau:
load first number from memory
load second number from memory
add the two
store answer into memory
Rất nhiều trong số đó là ... không phải toán học. Những hoạt động tải và lưu trữ có thể mất đến một nửa thời gian xử lý của bạn. C, là một sự trừu tượng của máy tính, giải phóng cho lập trình viên nỗi lo về việc sử dụng và tung hứng các thanh ghi, và vì số lượng và loại khác nhau giữa các máy tính, C đặt trách nhiệm phân bổ đăng ký chỉ trên trình biên dịch. Với một ngoại lệ.
Khi bạn khai báo một biến register
, bạn đang nói với trình biên dịch "Yo, tôi dự định biến này sẽ được sử dụng rất nhiều và / hoặc tồn tại trong thời gian ngắn. Nếu tôi là bạn, tôi sẽ cố gắng giữ nó trong sổ đăng ký." Khi tiêu chuẩn C nói rằng trình biên dịch không thực sự phải làm bất cứ điều gì, đó là vì tiêu chuẩn C không biết bạn đang biên dịch máy tính nào và nó có thể giống như 6502 ở trên, nơi cần cả 3 thanh ghi chỉ để hoạt động và không có đăng ký dự phòng để giữ số của bạn. Tuy nhiên, khi nó nói rằng bạn không thể lấy địa chỉ, đó là vì các thanh ghi không có địa chỉ. Chúng là tay của bộ xử lý. Vì trình biên dịch không phải cung cấp cho bạn một địa chỉ và vì nó không bao giờ có địa chỉ, nên một số tối ưu hóa hiện đang mở cho trình biên dịch. Nó có thể, nói, giữ số trong một đăng ký luôn. Nó không' Không phải lo lắng về nơi nó được lưu trữ trong bộ nhớ máy tính (ngoài việc cần phải lấy lại nó). Nó thậm chí có thể trừng phạt nó thành một biến khác, đưa nó cho một bộ xử lý khác, cho nó một vị trí thay đổi, v.v.
tl; dr: Các biến có thời gian ngắn làm nhiều phép toán. Đừng khai báo quá nhiều cùng một lúc.
Bạn đang rối tung với thuật toán tô màu đồ thị tinh vi của nhà biên dịch. Điều này được sử dụng để phân bổ đăng ký. Vâng, chủ yếu. Nó hoạt động như một gợi ý cho trình biên dịch - đó là sự thật. Nhưng không được bỏ qua toàn bộ vì bạn không được phép lấy địa chỉ của một biến đăng ký (hãy nhớ trình biên dịch, bây giờ vì lòng thương xót của bạn, sẽ cố gắng hành động khác nhau). Mà theo một cách nào đó là nói với bạn không sử dụng nó.
Các từ khóa đã được sử dụng lâu dài, trở lại dài. Khi chỉ có rất ít thanh ghi có thể đếm tất cả bằng ngón tay trỏ của bạn.
Nhưng, như tôi đã nói, không dùng nữa không có nghĩa là bạn không thể sử dụng nó.
Chỉ là một bản demo nhỏ (không có mục đích trong thế giới thực) để so sánh: khi xóa register
từ khóa trước mỗi biến, đoạn mã này mất 3,41 giây trên i7 (GCC) của tôi, với register
cùng một mã hoàn thành trong 0,7 giây.
#include <stdio.h>
int main(int argc, char** argv) {
register int numIterations = 20000;
register int i=0;
unsigned long val=0;
for (i; i<numIterations+1; i++)
{
register int j=0;
for (j;j<i;j++)
{
val=j+i;
}
}
printf("%d", val);
return 0;
}
Tôi đã kiểm tra từ khóa đăng ký theo QNX 6.5.0 bằng mã sau:
#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <sys/neutrino.h>
#include <sys/syspage.h>
int main(int argc, char *argv[]) {
uint64_t cps, cycle1, cycle2, ncycles;
double sec;
register int a=0, b = 1, c = 3, i;
cycle1 = ClockCycles();
for(i = 0; i < 100000000; i++)
a = ((a + b + c) * c) / 2;
cycle2 = ClockCycles();
ncycles = cycle2 - cycle1;
printf("%lld cycles elapsed\n", ncycles);
cps = SYSPAGE_ENTRY(qtime) -> cycles_per_sec;
printf("This system has %lld cycles per second\n", cps);
sec = (double)ncycles/cps;
printf("The cycles in seconds is %f\n", sec);
return EXIT_SUCCESS;
}
Tôi đã nhận được kết quả như sau:
-> 807679611 đã trôi qua
-> Hệ thống này có 3300830000 chu kỳ mỗi giây
-> Chu kỳ tính bằng giây là ~ 0,244600
Và bây giờ không có đăng ký int:
int a=0, b = 1, c = 3, i;
Tôi đã nhận:
-> 1421694077 chu kỳ trôi qua
-> Hệ thống này có 3300830000 chu kỳ mỗi giây
-> Chu kỳ tính bằng giây là ~ 0,430700
Đăng ký sẽ thông báo cho trình biên dịch rằng bộ mã hóa tin rằng biến này sẽ được ghi / đọc đủ để chứng minh lưu trữ của nó trong một trong số ít các thanh ghi có sẵn để sử dụng biến. Đọc / ghi từ các thanh ghi thường nhanh hơn và có thể yêu cầu một bộ mã op nhỏ hơn.
Ngày nay, điều này không hữu ích lắm, vì hầu hết các trình tối ưu hóa của trình biên dịch tốt hơn bạn trong việc xác định liệu một thanh ghi có nên được sử dụng cho biến đó hay không và trong bao lâu.
Trong những năm bảy mươi, vào đầu ngôn ngữ C, từ khóa đăng ký đã được giới thiệu để cho phép lập trình viên đưa ra gợi ý cho trình biên dịch, nói với nó rằng biến đó sẽ được sử dụng rất thường xuyên và nên khôn ngoan giữ giá trị của nó trong một trong các thanh ghi nội bộ của bộ xử lý.
Ngày nay, trình tối ưu hóa hiệu quả hơn nhiều so với lập trình viên để xác định các biến có nhiều khả năng được lưu giữ trong các thanh ghi và trình tối ưu hóa không phải lúc nào cũng đưa ra gợi ý của lập trình viên.
Vì vậy, nhiều người sai khuyến cáo không sử dụng từ khóa đăng ký.
Hãy xem tại sao!
Từ khóa đăng ký có tác dụng phụ liên quan: bạn không thể tham chiếu (lấy địa chỉ của) một biến loại đăng ký.
Mọi người khuyên người khác không nên sử dụng các thanh ghi nhận sai điều này như là một đối số bổ sung.
Tuy nhiên, thực tế đơn giản là bạn biết rằng bạn không thể lấy địa chỉ của biến đăng ký, cho phép trình biên dịch (và trình tối ưu hóa của nó) biết rằng giá trị của biến này không thể được sửa đổi gián tiếp thông qua một con trỏ.
Khi tại một điểm nhất định của luồng lệnh, biến đăng ký có giá trị được gán trong thanh ghi của bộ xử lý và thanh ghi không được sử dụng kể từ khi lấy giá trị của biến khác, trình biên dịch biết rằng nó không cần tải lại giá trị của biến trong thanh ghi đó. Điều này cho phép tránh truy cập bộ nhớ vô dụng đắt tiền.
Làm các bài kiểm tra của riêng bạn và bạn sẽ nhận được những cải tiến hiệu suất đáng kể trong các vòng lặp bên trong nhất của bạn.
Trình biên dịch Visual C ++ của Microsoft bỏ qua register
từ khóa khi tối ưu hóa phân bổ đăng ký toàn cầu (cờ trình biên dịch / Oe) được bật.
Xem đăng ký từ khóa trên MSDN.
Đăng ký từ khóa báo cho trình biên dịch lưu trữ biến cụ thể trong các thanh ghi CPU để có thể truy cập nhanh. Từ quan điểm của một lập trình viên, từ khóa đăng ký được sử dụng cho các biến được sử dụng nhiều trong một chương trình, để trình biên dịch có thể tăng tốc mã. Mặc dù nó phụ thuộc vào trình biên dịch nên giữ biến trong các thanh ghi CPU hoặc bộ nhớ chính.
Đăng ký chỉ ra trình biên dịch để tối ưu hóa mã này bằng cách lưu trữ biến cụ thể đó trong các thanh ghi sau đó trong bộ nhớ. nó là một yêu cầu cho trình biên dịch, trình biên dịch có thể hoặc không thể xem xét yêu cầu này. Bạn có thể sử dụng cơ sở này trong trường hợp một số biến của bạn đang được truy cập rất thường xuyên. Ví dụ: Một vòng lặp.
Một điều nữa là nếu bạn khai báo một biến là đăng ký thì bạn không thể lấy địa chỉ của nó vì nó không được lưu trong bộ nhớ. nó được phân bổ trong thanh ghi CPU.
gcc 9.3 đầu ra asm, không sử dụng cờ tối ưu hóa (mọi thứ trong câu trả lời này đều đề cập đến việc biên dịch tiêu chuẩn mà không cần cờ tối ưu hóa):
#include <stdio.h>
int main(void) {
int i = 3;
i++;
printf("%d", i);
return 0;
}
.LC0:
.string "%d"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 3
add DWORD PTR [rbp-4], 1
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
#include <stdio.h>
int main(void) {
register int i = 3;
i++;
printf("%d", i);
return 0;
}
.LC0:
.string "%d"
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 8
mov ebx, 3
add ebx, 1
mov esi, ebx
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
add rsp, 8
pop rbx
pop rbp
ret
Lực lượng này ebx
được sử dụng để tính toán, có nghĩa là nó cần được đẩy lên ngăn xếp và khôi phục ở cuối hàm vì nó được lưu lại.register
tạo ra nhiều dòng mã hơn và 1 bộ nhớ ghi và 1 bộ nhớ đọc (mặc dù thực tế, điều này có thể đã được tối ưu hóa thành 0 R / W nếu việc tính toán được thực hiện esi
, đó là những gì xảy ra khi sử dụng C ++ const register
). Không sử dụng register
nguyên nhân 2 ghi và 1 đọc (mặc dù lưu trữ để tải chuyển tiếp sẽ xảy ra trên đọc). Điều này là do giá trị phải được trình bày và cập nhật trực tiếp trên ngăn xếp để có thể đọc giá trị chính xác theo địa chỉ (con trỏ). register
không có yêu cầu này và không thể được chỉ ra. const
và register
về cơ bản là trái ngược vớivolatile
và sử dụngvolatile
sẽ ghi đè tối ưu hóa const ở tập tin và phạm vi khối và register
tối ưu hóa ở phạm vi khối. const register
và register
sẽ tạo ra các đầu ra giống hệt nhau vì const không làm gì trên C ở phạm vi khối, do đó chỉ register
áp dụng tối ưu hóa.
Trên clang, register
được bỏ qua nhưng const
tối ưu hóa vẫn xảy ra.