Tại sao việc thêm các chú thích hợp ngữ nội tuyến lại gây ra sự thay đổi triệt để như vậy trong mã được tạo của GCC?


82

Vì vậy, tôi đã có mã này:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        *sum++ = *a++ + *b++;
    }
}

Tôi muốn xem mã mà GCC 4.7.2 sẽ tạo. Vì vậy, tôi đã chạy g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11Và nhận được kết quả sau:

        .file   "a.c++"
        .intel_syntax noprefix
        .text
        .p2align 4,,15
        .globl  _Z2f1PcS_S_
        .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L5
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L5
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L3:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L3
        mov     ax, 8
        mov     r9d, 992
.L2:
        sub     eax, 1
        lea     rcx, [rdx+r9]
        add     rdi, r9
        lea     r8, [rax+1]
        add     rsi, r9
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L4:
        movzx   edx, BYTE PTR [rcx+rax]
        add     dl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], dl
        add     rax, 1
        cmp     rax, r8
        jne     .L4
        rep
        ret
.L5:
        mov     eax, 1000
        xor     r9d, r9d
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
        .p2align 4,,15
        .globl  _Z2f2PcS_S_
        .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L19
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L19
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L17:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L17
        add     rdi, 992
        add     rsi, 992
        add     rdx, 992
        mov     r8d, 8
.L16:
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L18:
        movzx   ecx, BYTE PTR [rdx+rax]
        add     cl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, r8
        jne     .L18
        rep
        ret
.L19:
        mov     r8d, 1000
        jmp     .L16
        .cfi_endproc
.LFE1:
        .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
        .ident  "GCC: (GNU) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

Tôi rất chán khi đọc lắp ráp, vì vậy tôi quyết định thêm một số điểm đánh dấu để biết phần thân của các vòng lặp đã đi đâu:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        asm("# im in ur loop");
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        asm("# im in ur loop");
        *sum++ = *a++ + *b++;
    }
}

Và GCC nói ra điều này:

    .file   "a.c++"
    .intel_syntax noprefix
    .text
    .p2align 4,,15
    .globl  _Z2f1PcS_S_
    .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L2:
#APP
# 4 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L2
    rep
    ret
    .cfi_endproc
.LFE0:
    .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
    .p2align 4,,15
    .globl  _Z2f2PcS_S_
    .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L6:
#APP
# 12 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L6
    rep
    ret
    .cfi_endproc
.LFE1:
    .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Điều này ngắn hơn đáng kể và có một số khác biệt đáng kể như thiếu hướng dẫn SIMD. Tôi đã mong đợi cùng một đầu ra, với một số nhận xét ở giữa nó. Tôi có đang đưa ra một giả định sai lầm nào đó ở đây không? Trình tối ưu hóa của GCC có bị cản trở bởi các nhận xét asm không?


28
Tôi mong đợi GCC (và hầu hết các trình biên dịch) xử lý cấu trúc ASM giống như các hộp khối. Vì vậy, họ không thể lý luận về những gì xảy ra qua một chiếc hộp như vậy. Và điều đó ngăn cản nhiều tối ưu hóa, đặc biệt là những tối ưu hóa được thực hiện qua các ranh giới vòng lặp.
Ira Baxter

10
Hãy thử asmbiểu mẫu mở rộng với đầu ra trống và danh sách clobber.
Kerrek SB 19/12/12

4
@ R.MartinhoFernandes: asm("# im in ur loop" : : );(xem tài liệu )
Mike Seymour

16
Lưu ý rằng bạn có thể được trợ giúp thêm một chút khi nhìn vào assembly đã tạo bằng cách thêm -fverbose-asmcờ, cờ này sẽ thêm một số chú thích để giúp xác định mọi thứ đang di chuyển như thế nào giữa các thanh ghi.
Matthew Slattery

1
Rất thú vị. Có thể được sử dụng để tránh tối ưu hóa một cách có chọn lọc trong các vòng lặp không?
SChepurin 19/12/12

Câu trả lời:


62

Các tương tác với tối ưu hóa được giải thích về nửa chừng của trang "Hướng dẫn lắp ráp với toán hạng biểu thức C" trong tài liệu.

GCC không cố gắng hiểu bất kỳ lắp ráp thực tế nào bên trong asm; điều duy nhất nó biết về nội dung là những gì bạn (tùy chọn) cho nó biết trong đặc tả toán hạng đầu ra và đầu vào và danh sách thanh ghi tắc nghẽn.

Đặc biệt, lưu ý:

Một asmlệnh không có bất kỳ toán hạng đầu ra nào sẽ được xử lý giống hệt với một asmlệnh dễ bay hơi .

Các volatiletừ khóa chỉ ra rằng hướng dẫn có quan trọng tác dụng phụ [...]

Vì vậy, sự hiện diện của asmvòng lặp bên trong của bạn đã hạn chế tối ưu hóa vectorisation, bởi vì GCC cho rằng nó có các tác dụng phụ.


1
Lưu ý rằng các tác dụng phụ của câu lệnh Basic Asm không được bao gồm việc sửa đổi thanh ghi hoặc bất kỳ bộ nhớ nào mà mã C ++ của bạn từng đọc / ghi. Nhưng đúng vậy, asmcâu lệnh phải chạy một lần mỗi khi nó chạy trong máy trừu tượng C ++ và GCC chọn không vectơ hóa và sau đó phát ra asm 16 lần liên tiếp mỗi lần paddb. Tuy nhiên, điều đó tôi nghĩ là hợp pháp, bởi vì quyền truy cập char thì không volatile. (Không giống như với một tuyên bố asm mở rộng với một chiếc áo "memory"choàng)
Peter Cordes

1
Xem gcc.gnu.org/wiki/ConvertBasicAsmToExtended để biết lý do không sử dụng các câu lệnh GNU C Basic Asm nói chung. Mặc dù trường hợp sử dụng này (chỉ là một điểm đánh dấu nhận xét) là một trong số ít trường hợp không phải là không hợp lý để thử nó.
Peter Cordes

23

Lưu ý rằng gcc đã vectơ hóa mã, chia phần nội dung vòng lặp thành hai phần, phần đầu tiên xử lý 16 mục cùng một lúc và phần thứ hai thực hiện phần còn lại sau đó.

Như Ira nhận xét, trình biên dịch không phân tích cú pháp khối asm, vì vậy nó không biết rằng đó chỉ là một nhận xét. Ngay cả khi nó đã làm, nó không có cách nào để biết những gì bạn dự định. Các vòng optmized có phần thân tăng gấp đôi, nó có nên đặt asm của bạn vào mỗi vòng không? Bạn có muốn nó không được thực thi 1000 lần không? Nó không biết, vì vậy nó đi theo con đường an toàn và quay trở lại vòng lặp đơn giản.


3

Tôi không đồng ý với "gcc không hiểu những gì trong asm()khối". Ví dụ: gcc có thể giải quyết khá tốt với việc tối ưu hóa các tham số và thậm chí sắp xếp lại asm()các khối sao cho nó xen lẫn với mã C đã tạo. Đây là lý do tại sao, nếu bạn nhìn vào trình hợp dịch nội tuyến trong hạt nhân Linux chẳng hạn, nó gần như luôn luôn có tiền tố là__volatile__ để đảm bảo rằng trình biên dịch "không di chuyển mã xung quanh". Tôi đã cho gcc di chuyển "rdtsc" của mình xung quanh, điều này làm cho phép đo của tôi về thời gian cần thiết để thực hiện một số việc.

Theo tài liệu, gcc xử lý một số loại asm() khối là "đặc biệt" và do đó không tối ưu hóa mã ở cả hai phía của khối.

Điều đó không có nghĩa là gcc đôi khi sẽ không bị nhầm lẫn bởi các khối trình hợp dịch nội tuyến, hoặc đơn giản là quyết định từ bỏ một số tối ưu hóa cụ thể vì nó không thể tuân theo hậu quả của mã trình hợp dịch, v.v. Quan trọng hơn, nó thường có thể bị nhầm lẫn khi thiếu các thẻ clobber - vì vậy nếu bạn có một số hướng dẫn nhưcpuidlàm thay đổi giá trị của EAX-EDX, nhưng bạn đã viết mã để nó chỉ sử dụng EAX, trình biên dịch có thể lưu trữ mọi thứ trong EBX, ECX và EDX, và sau đó mã của bạn hoạt động rất lạ khi các thanh ghi này bị ghi đè ... Nếu bạn thật may mắn, nó hỏng ngay lập tức - sau đó thật dễ dàng để tìm ra điều gì đang xảy ra. Nhưng nếu bạn không may mắn, nó sẽ rơi xuống dòng ... Một điều khó khăn khác là lệnh chia cho kết quả thứ hai trong edx. Nếu bạn không quan tâm đến modulo, rất dễ quên rằng EDX đã được thay đổi.


1
gcc thực sự không hiểu những gì có trong khối asm - bạn phải nói với nó thông qua một câu lệnh asm mở rộng. nếu không có thông tin bổ sung này, gcc sẽ không di chuyển xung quanh các khối như vậy. gcc cũng không bị nhầm lẫn trong các trường hợp bạn nêu - đơn giản là bạn đã mắc lỗi lập trình bằng cách nói với gcc rằng nó có thể sử dụng các thanh ghi đó trong khi thực tế, mã của bạn chặn chúng.
Nhớ Monica

Trả lời muộn, nhưng tôi nghĩ nó đáng nói. volatile asmcho GCC biết mã có thể có 'tác dụng phụ quan trọng' và nó sẽ giải quyết nó với sự chăm sóc đặc biệt hơn. Nó vẫn có thể bị xóa như một phần của tối ưu hóa mã chết hoặc chuyển ra ngoài. Tương tác với mã C cần phải giả định trường hợp (hiếm) như vậy và áp đặt đánh giá tuần tự nghiêm ngặt (ví dụ: bằng cách tạo ra các phụ thuộc trong asm).
edmz

GNU C Asm cơ bản (không có ràng buộc toán hạng, như OP asm("")) hoàn toàn dễ bay hơi, giống như Extended asm không có toán hạng đầu ra. GCC không hiểu chuỗi mẫu asm, chỉ có các ràng buộc; đó là lý do tại sao điều cần thiết là phải mô tả chính xác và đầy đủ asm của bạn với trình biên dịch bằng cách sử dụng các ràng buộc. Việc thay thế các toán hạng vào chuỗi mẫu không hiểu gì hơn printfviệc sử dụng chuỗi định dạng. TL: DR: không sử dụng GNU C Basic asm cho bất cứ điều gì, ngoại trừ các trường hợp sử dụng như thế này với các chú thích thuần túy.
Peter Cordes

-2

Câu trả lời này hiện đã được sửa đổi: ban đầu nó được viết với tư duy coi Asm cơ bản nội tuyến là một công cụ được chỉ định khá mạnh, nhưng nó không giống như vậy trong GCC. Asm cơ bản là yếu và vì vậy câu trả lời đã được chỉnh sửa.

Mỗi nhận xét hợp ngữ hoạt động như một điểm ngắt.

CHỈNH SỬA: Nhưng một cái bị hỏng, khi bạn sử dụng Basic Asm. Inline asm(một asmcâu lệnh bên trong thân hàm) không có danh sách tắc nghẽn rõ ràng là một tính năng được chỉ định yếu trong GCC và hành vi của nó rất khó xác định. Có vẻ như nó không (tôi không hoàn toàn hiểu được các đảm bảo của nó) được gắn với bất kỳ thứ gì cụ thể, vì vậy trong khi mã hợp ngữ phải được chạy tại một số thời điểm nếu chức năng được chạy, không rõ khi nào nó được chạy cho bất kỳ mức tối ưu hóa tầm thường . Một điểm ngắt có thể được sắp xếp lại thứ tự với lệnh lân cận không phải là một "điểm ngắt" rất hữu ích. KẾT THÚC CHỈNH SỬA

Bạn có thể chạy chương trình của mình trong một trình thông dịch ngắt ở mỗi nhận xét và in ra trạng thái của mọi biến (sử dụng thông tin gỡ lỗi). Những điểm này phải tồn tại để bạn quan sát môi trường (trạng thái của thanh ghi và bộ nhớ).

Không có chú thích, không có điểm quan sát nào tồn tại và vòng lặp được biên dịch dưới dạng một hàm toán học duy nhất lấy một môi trường và tạo ra một môi trường đã sửa đổi.

Bạn muốn biết câu trả lời của một câu hỏi vô nghĩa: bạn muốn biết cách từng lệnh (hoặc có thể là khối, hoặc có thể là phạm vi lệnh) được biên dịch, nhưng không có lệnh (hoặc khối) riêng lẻ nào được biên dịch; toàn bộ nội dung được tổng hợp.

Một câu hỏi tốt hơn sẽ là:

Xin chào GCC. Tại sao bạn tin rằng đầu ra asm này đang thực hiện mã nguồn? Hãy giải thích từng bước, với mọi giả định.

Nhưng sau đó bạn sẽ không muốn đọc một bằng chứng dài hơn đầu ra asm, được viết theo thuật ngữ của biểu diễn nội bộ GCC.


1
Những điểm này phải tồn tại để bạn quan sát môi trường (trạng thái của thanh ghi và bộ nhớ). - điều này có thể đúng đối với mã chưa được tối ưu hóa. Với tối ưu hóa được bật, toàn bộ các chức năng có thể biến mất khỏi hệ nhị phân. Chúng tôi đang nói về mã được tối ưu hóa ở đây.
Bartek Banachewicz

1
Chúng ta đang nói về lắp ráp được tạo ra do kết quả của việc biên dịch có bật tính năng tối ưu hóa. Do đó, bạn đã sai khi nói rằng bất cứ thứ gì phải tồn tại.
Bartek Banachewicz

1
Vâng, IDK tại sao bất kỳ ai cũng vậy, và đồng ý rằng không ai nên như vậy. Như liên kết trong bình luận cuối cùng của tôi giải thích, không ai nên làm như vậy, và đã có cuộc tranh luận về việc tăng cường nó (ví dụ như với một chiếc áo "memory"choàng ngầm ) như một chiếc khăn che cho mã lỗi hiện có chắc chắn tồn tại. Ngay cả đối với các hướng dẫn như asm("cli")vậy chỉ ảnh hưởng đến một phần của trạng thái kiến ​​trúc mà mã do trình biên dịch tạo ra không chạm vào, bạn vẫn cần nó có thứ tự wrt. tải / lưu trữ do trình biên dịch tạo (ví dụ: nếu bạn đang tắt các ngắt xung quanh một phần quan trọng).
Peter Cordes

1
Với việc không an toàn để che giấu vùng đỏ, thậm chí lưu / khôi phục thủ công không hiệu quả các thanh ghi (với push / pop) bên trong câu lệnh asm cũng không an toàn, trừ khi bạn add rsp, -128trước. Nhưng làm điều đó rõ ràng chỉ là braindead.
Peter Cordes

1
Hiện tại, GCC xử lý Basic Asm chính xác tương đương với asm("" :::)(hoàn toàn dễ bay hơi vì nó không có đầu ra, nhưng không bị ràng buộc với phần còn lại của mã bởi các phụ thuộc đầu vào hoặc đầu ra. Và không có tắc "memory"nghẽn). Và tất nhiên nó không thực hiện %operandthay thế trên chuỗi mẫu, vì vậy chữ %không cần phải được thoát dưới dạng %%. Vì vậy, có, đã đồng ý, việc không sử dụng Basic Asm bên ngoài các __attribute__((naked))chức năng và phạm vi toàn cầu sẽ là một ý kiến ​​hay.
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.