Một biến thành viên không sử dụng có chiếm bộ nhớ không?


91

Việc khởi tạo một biến thành viên và không tham chiếu / sử dụng nó có chiếm thêm RAM trong thời gian chạy hay trình biên dịch chỉ đơn giản là bỏ qua biến đó?

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

Trong ví dụ trên, thành viên 'var1' nhận một giá trị sau đó được hiển thị trong bảng điều khiển. Tuy nhiên, 'Var2' hoàn toàn không được sử dụng. Do đó ghi nó vào bộ nhớ trong thời gian chạy sẽ rất lãng phí tài nguyên. Trình biên dịch có đưa các loại tình huống này vào tài khoản và đơn giản là bỏ qua các biến không sử dụng hay đối tượng Foo luôn có cùng kích thước, bất kể các thành viên của nó có được sử dụng hay không?


25
Điều này phụ thuộc vào trình biên dịch, kiến ​​trúc, hệ điều hành và sự tối ưu hóa được sử dụng.
Owl

16
Có rất nhiều chỉ số mã trình điều khiển cấp thấp có thể thêm các thành viên cấu trúc không làm gì để đệm để khớp với kích thước khung dữ liệu phần cứng và như một thủ thuật để có được căn chỉnh bộ nhớ mong muốn. Nếu một trình biên dịch bắt đầu tối ưu hóa những thứ này thì sẽ có nhiều sự cố.
Andy Brown,

2
@Andy họ không thực sự không làm gì cả vì địa chỉ của các thành viên dữ liệu sau được đánh giá. Điều này có nghĩa là sự tồn tại của các thành viên đệm đó có một hành vi quan sát được trên chương trình. Đây, var2không.
YSC

4
Tôi sẽ ngạc nhiên nếu trình biên dịch có thể tối ưu hóa nó vì bất kỳ đơn vị biên dịch nào giải quyết một cấu trúc như vậy có thể được liên kết với một đơn vị biên dịch khác sử dụng cùng một cấu trúc và trình biên dịch không thể biết liệu đơn vị biên dịch riêng biệt có địa chỉ thành viên hay không.
Galik

2
@geza sizeof(Foo)không thể giảm theo định nghĩa - nếu bạn in, sizeof(Foo)nó phải mang lại lợi nhuận 8(trên các nền tảng chung). Các trình biên dịch có thể tối ưu hóa không gian được sử dụng bởi var2(bất kể thông qua newhoặc trên ngăn xếp hoặc trong các lệnh gọi hàm ...) trong bất kỳ ngữ cảnh nào mà họ thấy hợp lý, ngay cả khi không có LTO hoặc tối ưu hóa toàn bộ chương trình. Ở những nơi không thể thực hiện được, họ sẽ không làm điều đó, cũng như đối với bất kỳ tối ưu hóa nào khác. Tôi tin rằng việc chỉnh sửa câu trả lời được chấp nhận khiến khả năng bị đánh lừa ít hơn đáng kể.
Max Langhof

Câu trả lời:


106

Quy tắc vàng "as-if" 1 của C ++ nói rằng, nếu hành vi quan sát được của một chương trình không phụ thuộc vào sự tồn tại của thành viên dữ liệu không được sử dụng, thì trình biên dịch được phép tối ưu hóa nó .

Một biến thành viên không sử dụng có chiếm bộ nhớ không?

Không (nếu nó "thực sự" không được sử dụng).


Bây giờ có hai câu hỏi trong đầu:

  1. Khi nào thì hành vi có thể quan sát được sẽ không phụ thuộc vào sự tồn tại của một thành viên?
  2. Những tình huống đó có xảy ra trong các chương trình đời thực không?

Hãy bắt đầu với một ví dụ.

Thí dụ

#include <iostream>

struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };

struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };

void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

Nếu chúng tôi yêu cầu gcc biên dịch đơn vị dịch này , nó sẽ xuất ra:

f1():
        mov     esi, 5
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
        jmp     f1()

f2giống như f1, và không có bộ nhớ nào được sử dụng để chứa một thực tế Foo2::var2. ( Clang làm điều gì đó tương tự ).

Thảo luận

Một số người có thể nói rằng điều này là khác nhau vì hai lý do:

  1. đây là một ví dụ quá tầm thường,
  2. cấu trúc được tối ưu hóa hoàn toàn, nó không được tính.

Chà, một chương trình tốt là một tổ hợp thông minh và phức tạp của những thứ đơn giản chứ không phải là sự xếp chồng đơn giản của những thứ phức tạp. Trong cuộc sống thực, bạn viết rất nhiều hàm đơn giản bằng các cấu trúc đơn giản hơn là trình biên dịch tối ưu hóa. Ví dụ:

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}

Đây là một ví dụ chính xác về một data-member (ở đây, std::pair<std::set<int>::iterator, bool>::first) không được sử dụng. Đoán xem? Nó được tối ưu hóa đi ( ví dụ đơn giản hơn với một bộ giả nếu lắp ráp đó khiến bạn khóc).

Bây giờ sẽ là thời điểm hoàn hảo để đọc câu trả lời xuất sắc của Max Langhof (vui lòng ủng hộ nó cho tôi). Nó giải thích tại sao, cuối cùng, khái niệm cấu trúc không có ý nghĩa ở cấp độ lắp ráp mà trình biên dịch xuất ra.

"Nhưng, nếu tôi làm X, thực tế là thành viên không sử dụng được tối ưu hóa đi là một vấn đề!"

Đã có một số bình luận cho rằng câu trả lời này phải sai vì một số thao tác (như assert(sizeof(Foo2) == 2*sizeof(int))) sẽ phá vỡ một cái gì đó.

Nếu X là một phần của hành vi có thể quan sát được của chương trình 2 , trình biên dịch không được phép tối ưu hóa mọi thứ. Có rất nhiều hoạt động trên một đối tượng có chứa thành phần dữ liệu "không được sử dụng" sẽ có tác động có thể quan sát được đối với chương trình. Nếu một hoạt động như vậy được thực hiện hoặc nếu trình biên dịch không thể chứng minh không có hoạt động nào được thực hiện, thì phần tử dữ liệu "không được sử dụng" đó là một phần của hành vi quan sát được của chương trình và không thể được tối ưu hóa đi .

Các hoạt động ảnh hưởng đến hành vi có thể quan sát được bao gồm, nhưng không giới hạn ở:

  • lấy kích thước của một loại đối tượng ( sizeof(Foo)),
  • lấy địa chỉ của thành viên dữ liệu được khai báo sau thành viên "chưa sử dụng",
  • sao chép đối tượng với một chức năng như memcpy,
  • thao tác biểu diễn của đối tượng (như với memcmp),
  • xác định một đối tượng là dễ bay hơi ,
  • vân vân .

1)

[intro.abstract]/1

Các mô tả ngữ nghĩa trong tài liệu này xác định một máy trừu tượng không xác định được tham số hóa. Tài liệu này không yêu cầu về cấu trúc của việc triển khai tuân thủ. Đặc biệt, họ không cần sao chép hoặc mô phỏng cấu trúc của máy trừu tượng. Thay vào đó, các triển khai tuân thủ được yêu cầu để mô phỏng (chỉ) hành vi có thể quan sát được của máy trừu tượng như được giải thích bên dưới.

2) Giống như một sự khẳng định là đậu hay trượt.


Nhận xét đề xuất cải tiến câu trả lời đã được lưu trữ trong trò chuyện .
Cody Grey

1
Ngay cả khi assert(sizeof(…)…)không thực sự ràng buộc trình biên dịch — nó phải cung cấp một sizeofmã cho phép mã sử dụng những thứ giống như memcpyhoạt động, nhưng điều đó không có nghĩa là bằng cách nào đó, trình biên dịch được yêu cầu sử dụng nhiều byte đó trừ khi chúng có thể được tiếp xúc với một mã memcpymà nó có thể Không viết lại để tạo ra giá trị chính xác.
Davis Herring

@Davis Hoàn toàn có thể.
YSC

63

Điều quan trọng là phải nhận ra rằng mã mà trình biên dịch tạo ra không có kiến ​​thức thực tế về cấu trúc dữ liệu của bạn (vì thứ như vậy không tồn tại ở cấp độ lắp ráp) và trình tối ưu hóa cũng vậy. Trình biên dịch chỉ tạo cho mỗi chức năng , không tạo cấu trúc dữ liệu .

Ok, nó cũng ghi các phần dữ liệu không đổi và như vậy.

Dựa trên điều đó, chúng ta đã có thể nói rằng trình tối ưu hóa sẽ không "loại bỏ" hoặc "loại bỏ" các thành viên, bởi vì nó không xuất ra cấu trúc dữ liệu. Nó xuất ra , có thể sử dụng hoặc không sử dụng các thành viên, và trong số các mục tiêu của nó là tiết kiệm bộ nhớ hoặc chu kỳ bằng cách loại bỏ việc sử dụng vô nghĩa (tức là ghi / đọc) của các thành viên.


Ý chính của nó là "nếu trình biên dịch có thể chứng minh trong phạm vi của một hàm (bao gồm cả các hàm được đưa vào trong nó) rằng phần tử không sử dụng không tạo ra sự khác biệt nào cho cách hàm hoạt động (và những gì nó trả về) thì rất có thể là sự hiện diện của các thành viên không gây ra chi phí ".

Khi bạn làm cho các tương tác của một hàm với thế giới bên ngoài phức tạp hơn / không rõ ràng hơn đối với trình biên dịch (lấy / trả về cấu trúc dữ liệu phức tạp hơn, ví dụ: a std::vector<Foo>, ẩn định nghĩa của một hàm trong một đơn vị biên dịch khác, cấm / không phân biệt nội tuyến, v.v.) , ngày càng có nhiều khả năng trình biên dịch không thể chứng minh rằng thành viên không sử dụng không có tác dụng.

Không có quy tắc cứng nào ở đây vì tất cả phụ thuộc vào sự tối ưu hóa mà trình biên dịch thực hiện, nhưng miễn là bạn làm những việc nhỏ nhặt (chẳng hạn như được hiển thị trong câu trả lời của YSC) thì rất có thể sẽ không có chi phí nào, trong khi làm những việc phức tạp (ví dụ: quay lại a std::vector<Foo>từ một hàm quá lớn đối với nội tuyến) có thể sẽ phải chịu chi phí.


Để minh họa quan điểm, hãy xem xét ví dụ sau:

struct Foo {
    int var1 = 3;
    int var2 = 4;
    int var3 = 5;
};

int test()
{
    Foo foo;
    std::array<char, sizeof(Foo)> arr;
    std::memcpy(&arr, &foo, sizeof(Foo));
    return arr[0] + arr[4];
}

Chúng tôi làm những việc không nhỏ ở đây (lấy địa chỉ, kiểm tra và thêm byte từ biểu diễn byte ) nhưng trình tối ưu hóa có thể phát hiện ra rằng kết quả luôn giống nhau trên nền tảng này:

test(): # @test()
  mov eax, 7
  ret

Các thành viên của Fookhông chỉ không chiếm bất kỳ bộ nhớ nào, Foothậm chí còn không tồn tại! Nếu có những cách sử dụng khác không thể được tối ưu hóa thì ví dụ sizeof(Foo)có thể có vấn đề - nhưng chỉ dành cho đoạn mã đó! Nếu tất cả các cách sử dụng có thể được tối ưu hóa như vậy thì sự tồn tại của ví dụ var3không ảnh hưởng đến mã được tạo. Nhưng ngay cả khi nó được sử dụng ở một nơi khác, test()sẽ vẫn được tối ưu hóa!

Tóm lại: Mỗi cách sử dụng đều Foođược tối ưu hóa một cách độc lập. Một số có thể sử dụng nhiều bộ nhớ hơn vì một thành viên không cần thiết, một số có thể không. Tham khảo hướng dẫn sử dụng trình biên dịch của bạn để biết thêm chi tiết.


6
Thả micrô "Tham khảo hướng dẫn sử dụng trình biên dịch của bạn để biết thêm chi tiết." : D
YSC

22

Trình biên dịch sẽ chỉ tối ưu hóa biến thành viên không sử dụng (đặc biệt là biến công khai) nếu nó có thể chứng minh rằng việc loại bỏ biến không có tác dụng phụ và không có phần nào của chương trình phụ thuộc vào kích thước của Foonó.

Tôi không nghĩ rằng bất kỳ trình biên dịch hiện tại nào thực hiện tối ưu hóa như vậy trừ khi cấu trúc không thực sự được sử dụng. Một số trình biên dịch ít nhất có thể cảnh báo về các biến riêng tư không được sử dụng nhưng thường không áp dụng cho các biến công khai.


1
Tuy nhiên, nó thực hiện: godbolt.org/z/UJKguS + không có trình biên dịch nào sẽ cảnh báo cho thành viên dữ liệu không sử dụng.
YSC

@YSC clang ++ cảnh báo về các thành viên và biến dữ liệu không được sử dụng.
Maxim Egorushkin

3
@YSC Tôi nghĩ đó là một tình huống hơi khác nhau, tối ưu hóa của nó cấu trúc đi hoàn toàn và chỉ in 5 trực tiếp
Alan Birtles

4
@AlanBirtles Tôi không thấy nó khác biệt như thế nào. Trình biên dịch đã tối ưu hóa mọi thứ từ đối tượng mà không ảnh hưởng đến hành vi có thể quan sát được của chương trình. Vì vậy, câu đầu tiên của bạn "trình biên dịch rất khó tối ưu hóa awau một biến thành viên không sử dụng" là sai.
YSC

2
@YSC trong mã thực nơi cấu trúc thực sự được sử dụng thay vì chỉ xây dựng cho các tác dụng phụ của nó có lẽ nó khó hơn nó sẽ được tối ưu hóa đi
Alan Birtles

7

Nói chung, bạn phải giả định rằng bạn nhận được những gì bạn đã yêu cầu, ví dụ, các biến thành viên "không được sử dụng" ở đó.

Vì trong ví dụ của bạn là cả hai thành viên public, trình biên dịch không thể biết liệu một số mã (đặc biệt là từ các đơn vị dịch khác = các tệp * .cpp khác, được biên dịch riêng và sau đó được liên kết) có truy cập thành viên "không sử dụng" hay không.

Câu trả lời của YSC đưa ra một ví dụ rất đơn giản, trong đó kiểu lớp chỉ được sử dụng như một biến thời lượng lưu trữ tự động và không có con trỏ đến biến đó được sử dụng. Ở đó, trình biên dịch có thể nội dòng tất cả các mã và sau đó có thể loại bỏ tất cả các mã chết.

Nếu bạn có giao diện giữa các chức năng được xác định trong các đơn vị dịch khác nhau, thông thường trình biên dịch không biết gì cả. Các giao diện thường tuân theo một số ABI được xác định trước (như vậy ) để các tệp đối tượng khác nhau có thể được liên kết với nhau mà không gặp bất kỳ sự cố nào. Thông thường, ABI không tạo ra sự khác biệt nếu một thành viên được sử dụng hay không. Vì vậy, trong những trường hợp như vậy, thành viên thứ hai phải ở trong bộ nhớ (trừ khi bị trình liên kết loại bỏ sau đó).

Và chừng nào bạn còn ở trong ranh giới của ngôn ngữ, bạn không thể quan sát rằng bất kỳ sự loại bỏ nào xảy ra. Nếu bạn gọi sizeof(Foo), bạn sẽ nhận được 2*sizeof(int). Nếu bạn tạo một mảng Foos, khoảng cách giữa các phần đầu của hai đối tượng liên tiếp của Fooluôn là sizeof(Foo)byte.

Kiểu của bạn là kiểu bố cục tiêu chuẩn , có nghĩa là bạn cũng có thể truy cập vào các thành viên dựa trên hiệu số thời gian biên dịch được tính toán (xem offsetofmacro). Hơn nữa, bạn có thể kiểm tra biểu diễn từng byte của đối tượng bằng cách sao chép vào một mảng charsử dụng std::memcpy. Trong tất cả các trường hợp này, thành viên thứ hai có thể được quan sát thấy ở đó.


Nhận xét không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Cody Grey

2
+1: chỉ tối ưu hóa toàn bộ chương trình tích cực mới có thể điều chỉnh bố cục dữ liệu (bao gồm cả kích thước thời gian biên dịch và hiệu suất) cho các trường hợp đối tượng cấu trúc cục bộ không được tối ưu hóa hoàn toàn,. gcc -fwhole-program -O3 *.cVề lý thuyết thì có thể làm được, nhưng trong thực tế thì có thể không. (ví dụ như trong trường hợp chương trình làm cho một số giả định về những gì giá trị chính xác sizeof()có đúng mục tiêu này, và bởi vì đó là một tối ưu hóa thực sự phức tạp mà các lập trình viên nên làm bằng tay nếu họ muốn nó.)
Peter Cordes

6

Các ví dụ được cung cấp bởi các câu trả lời khác cho câu hỏi var2này dựa trên một kỹ thuật tối ưu hóa duy nhất: sự lan truyền không đổi và sự tách rời sau đó của toàn bộ cấu trúc (không phải sự tách rời của chỉ var2). Đây là trường hợp đơn giản và việc tối ưu hóa trình biên dịch sẽ thực hiện nó.

Đối với mã C / C ++ không được quản lý, câu trả lời là trình biên dịch nói chung sẽ không giải quyết var2. Theo như tôi biết thì không có hỗ trợ nào cho việc chuyển đổi cấu trúc C / C ++ như vậy trong thông tin gỡ lỗi và nếu cấu trúc có thể truy cập được dưới dạng một biến trong trình gỡ lỗi thì var2không thể giải thích được. Theo như tôi biết không có trình biên dịch C / C ++ hiện tại nào có thể chuyên biệt hóa các hàm theo cách giải thích var2, vì vậy nếu cấu trúc được chuyển tới hoặc trả về từ một hàm không nội tuyến thì var2không thể giải thích được.

Đối với các ngôn ngữ được quản lý như C # / Java với trình biên dịch JIT, trình biên dịch có thể xử lý an toàn var2vì nó có thể theo dõi chính xác xem nó có đang được sử dụng hay không và liệu nó có thoát sang mã không được quản lý hay không. Kích thước vật lý của cấu trúc trong các ngôn ngữ được quản lý có thể khác với kích thước được báo cáo cho lập trình viên.

Năm 2019 các trình biên dịch C / C ++ không thể var2thoát khỏi cấu trúc trừ khi toàn bộ biến cấu trúc được giải thích. Đối với những trường hợp thú vị về việc loại bỏ var2từ struct, câu trả lời là: Không.

Một số trình biên dịch C / C ++ trong tương lai sẽ có thể var2tách ra khỏi cấu trúc và hệ sinh thái được xây dựng xung quanh trình biên dịch sẽ cần phải thích ứng với thông tin xử lý do trình biên dịch tạo ra.


1
Đoạn văn của bạn về thông tin gỡ lỗi tóm tắt thành "chúng tôi không thể tối ưu hóa nó nếu điều đó sẽ khiến việc gỡ lỗi khó khăn hơn", điều này hoàn toàn sai. Hoặc tôi đang đọc sai. Bạn có thể làm rõ?
Max Langhof

Nếu trình biên dịch phát ra thông tin gỡ lỗi về cấu trúc thì nó không thể loại bỏ var2. Tùy chọn là: (1) Không phát ra các thông tin gỡ lỗi nếu không tương ứng với các đại diện vật lý của các cấu trúc, (2) Hỗ trợ thành viên struct sự bỏ bớt các thông tin gỡ lỗi và phát ra những thông tin debug
atomsymbol

Có lẽ tổng quát hơn là đề cập đến Sự thay thế vô hướng của các uẩn (và sau đó là loại bỏ các kho chết, v.v. ).
Davis Herring

4

Nó phụ thuộc vào trình biên dịch của bạn và mức độ tối ưu hóa của nó.

Trong gcc, nếu bạn chỉ định -O, nó sẽ bật các cờ tối ưu hóa sau :

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdcelà viết tắt của Dead Code Elimination .

Bạn có thể sử dụng __attribute__((used))để ngăn gcc loại bỏ một biến không sử dụng với lưu trữ tĩnh:

Thuộc tính này, được gắn với một biến có lưu trữ tĩnh, có nghĩa là biến đó phải được phát ra ngay cả khi có vẻ như biến đó không được tham chiếu.

Khi được áp dụng cho thành viên dữ liệu tĩnh của mẫu lớp C ++, thuộc tính cũng có nghĩa là thành viên đó được khởi tạo nếu bản thân lớp đó được khởi tạo.


Đó là dành cho các thành viên dữ liệu tĩnh , không phải các thành viên không sử dụng cho mỗi trường hợp (không được tối ưu hóa trừ khi toàn bộ đối tượng có). Nhưng có, tôi đoán điều đó có tính. BTW, loại bỏ các biến tĩnh không sử dụng không phải là loại bỏ chết , trừ khi GCC bẻ cong thuật ngữ.
Peter Cordes
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.