Tại sao GCC tổng hợp khởi tạo một mảng lại lấp đầy toàn bộ mọi thứ bằng các số 0 trước, bao gồm các phần tử khác không?


21

Tại sao gcc lấp đầy toàn bộ mảng bằng số 0 thay vì chỉ 96 số nguyên còn lại? Các khởi tạo khác không là tất cả ở đầu mảng.

void *sink;
void bar() {
    int a[100]{1,2,3,4};
    sink = a;             // a escapes the function
    asm("":::"memory");   // and compiler memory barrier
    // forces the compiler to materialize a[] in memory instead of optimizing away
}

Cả MinGW8.1 và gcc9.2 đều tạo ra asm như thế này ( trình thám hiểm trình biên dịch Godbolt ).

# gcc9.2 -O3 -m32 -mno-sse
bar():
    push    edi                       # save call-preserved EDI which rep stos uses
    xor     eax, eax                  # eax=0
    mov     ecx, 100                  # repeat-count = 100
    sub     esp, 400                  # reserve 400 bytes on the stack
    mov     edi, esp                  # dst for rep stos
        mov     DWORD PTR sink, esp       # sink = a
    rep stosd                         # memset(a, 0, 400) 

    mov     DWORD PTR [esp], 1        # then store the non-zero initializers
    mov     DWORD PTR [esp+4], 2      # over the zeroed part of the array
    mov     DWORD PTR [esp+8], 3
    mov     DWORD PTR [esp+12], 4
 # memory barrier empty asm statement is here.

    add     esp, 400                  # cleanup the stack
    pop     edi                       # and restore caller's EDI
    ret

(với SSE được bật, nó sẽ sao chép cả 4 trình khởi tạo với tải / lưu trữ Movdqa)

Tại sao GCC không làm lea edi, [esp+16]và ghi nhớ (với rep stosd) chỉ 96 yếu tố cuối cùng, giống như Clang không? Đây có phải là một tối ưu hóa bị bỏ lỡ, hoặc bằng cách nào đó hiệu quả hơn để làm theo cách này? (Clang thực sự gọi memsetthay vì nội tuyến rep stos)


Lưu ý của biên tập viên: câu hỏi ban đầu có đầu ra trình biên dịch không được tối ưu hóa hoạt động theo cùng một cách, nhưng mã không hiệu quả tại -O0không chứng minh được điều gì. Nhưng hóa ra việc tối ưu hóa này bị GCC bỏ lỡ ngay cả tại -O3.

Truyền con trỏ đến amột hàm không nội tuyến sẽ là một cách khác để buộc trình biên dịch thực hiện a[], nhưng trong mã 32 bit dẫn đến sự lộn xộn đáng kể của mã asm. (Stack args dẫn đến kết quả đẩy, được trộn lẫn với các cửa hàng vào ngăn xếp để khởi tạo mảng.)

Sử dụng volatile a[100]{1,2,3,4}được GCC để tạo và sau đó sao chép mảng, đó là điên rồ. Thông thường volatilelà tốt để xem cách trình biên dịch khởi tạo các biến cục bộ hoặc đặt chúng ra trên ngăn xếp.


1
@Damien Bạn hiểu nhầm câu hỏi của tôi. Tôi hỏi tại sao ví dụ [0] được gán giá trị hai lần như a[0] = 0;sau và sau đó a[0] = 1;.
Lassie

1
Tôi không thể đọc được phần lắp ráp, nhưng nó cho thấy mảng được điền hoàn toàn bằng số không?
smac89

3
Một sự thật thú vị khác: đối với nhiều mục được khởi tạo, cả gcc và clang đều hoàn nguyên để sao chép toàn bộ mảng từ .rodata... Tôi không thể tin rằng sao chép 400 byte nhanh hơn zeroing và đặt 8 mục.
Jester

2
Bạn đã vô hiệu hóa tối ưu hóa; mã không hiệu quả không đáng ngạc nhiên cho đến khi bạn xác minh rằng điều tương tự xảy ra tại -O3( điều đó xảy ra ). godbolt.org/z/rh_TNF
Peter Cordes

12
Bạn muốn biết thêm gì nữa? Đó là một tối ưu hóa bị bỏ lỡ, hãy báo cáo nó trên bugzilla của GCC với missed-optimizationtừ khóa.
Peter Cordes

Câu trả lời:


2

Về lý thuyết, việc khởi tạo của bạn có thể trông như thế:

int a[100] = {
  [3] = 1,
  [5] = 42,
  [88] = 1,
};

do đó, nó có thể hiệu quả hơn về ý nghĩa của bộ đệm và tối ưu hóa trước hết bằng toàn bộ khối bộ nhớ và sau đó đặt các giá trị riêng lẻ.

Có thể thay đổi hành vi tùy thuộc vào:

  • kiến trúc mục tiêu
  • HĐH mục tiêu
  • chiều dài mảng
  • tỷ lệ khởi tạo (giá trị / chiều dài khởi tạo rõ ràng)
  • vị trí của các giá trị khởi tạo

Tất nhiên, trong trường hợp của bạn, việc khởi tạo được nén khi bắt đầu mảng và việc tối ưu hóa sẽ không đáng kể.

Vì vậy, có vẻ như gcc đang làm cách tiếp cận chung nhất ở đây. Trông giống như một sự tối ưu hóa còn thiếu.


Đúng, một chiến lược tối ưu cho mã này có lẽ sẽ là bằng không mọi thứ, hoặc có thể chỉ là mọi thứ bắt đầu từ khi a[6]trở đi với những khoảng trống ban đầu chứa đầy các cửa hàng đơn lẻ hoặc số không. Đặc biệt nếu nhắm mục tiêu x86-64 để bạn có thể sử dụng các cửa hàng qword để thực hiện 2 yếu tố cùng một lúc, với yếu tố khác không. ví dụ: mov QWORD PTR [rsp+3*4], 1để thực hiện các phần tử 3 và 4 với một cửa hàng qword bị sai lệch.
Peter Cordes

Về mặt lý thuyết, hành vi có thể phụ thuộc vào HĐH đích, nhưng trong GCC thực tế thì nó sẽ không và không có lý do gì. Chỉ kiến ​​trúc mục tiêu (và trong đó, các tùy chọn điều chỉnh cho các cấu trúc vi mô khác nhau, như -march=skylakeso với so -march=k8với -march=knltất cả sẽ rất khác nhau và có thể về mặt chiến lược phù hợp cho việc này.)
Peter Cordes

Điều này thậm chí có được phép trong C ++ không? Tôi nghĩ đó chỉ là C.
Lassie

@Lassie bạn đúng trong c ++, điều này không được phép, nhưng câu hỏi liên quan nhiều hơn đến phụ trợ trình biên dịch, vì vậy nó không quan trọng lắm. mã hiển thị cũng có thể là cả hai
vlad_tepesch

Bạn thậm chí có thể dễ dàng xây dựng các ví dụ hoạt động tương tự trong C ++ bằng cách khai báo một số struct Bar{ int i; int a[100]; int j;} và khởi tạo Bar a{1,{2,3,4},4};gcc thực hiện điều tương tự: zero all out, sau đó đặt 5 giá trị
vlad_tepesch
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.