2012-01-12 18 views
63

tôi đã viết này chương trình C đơn giản:Làm thế nào để GCC tối ưu hóa một biến không sử dụng được tăng lên bên trong một vòng lặp?

int main() { 
    int i; 
    int count = 0; 
    for(i = 0; i < 2000000000; i++){ 
     count = count + 1; 
    } 
} 

tôi muốn tìm hiểu xem trình biên dịch gcc tối ưu hóa vòng lặp này (thêm rõ ràng 2000000000 lần nên "thêm một thời gian"). Vì vậy:

gcc test.c và sau đó time trên a.out cho:

real 0m7.717s 
user 0m7.710s 
sys 0m0.000s 

$ gcc -O2 test.c và sau đó time on a.out` cho:

real 0m0.003s 
user 0m0.000s 
sys 0m0.000s 

Sau đó, tôi đã tháo rời cả hai với gcc -S. Người đầu tiên có vẻ khá rõ ràng:

.file "test.c" 
    .text 
.globl main 
    .type main, @function 
main: 
.LFB0: 
    .cfi_startproc 
    pushq %rbp 
    .cfi_def_cfa_offset 16 
    movq %rsp, %rbp 
    .cfi_offset 6, -16 
    .cfi_def_cfa_register 6 
    movl $0, -8(%rbp) 
    movl $0, -4(%rbp) 
    jmp .L2 
.L3: 
    addl $1, -8(%rbp) 
    addl $1, -4(%rbp) 
.L2: 
    cmpl $1999999999, -4(%rbp) 
    jle .L3 
    leave 
    .cfi_def_cfa 7, 8 
    ret 
    .cfi_endproc 
.LFE0: 
    .size main, .-main 
    .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 
    .section .note.GNU-stack,"",@progbits 

L3 thêm, L2 so sánh -4(%rbp) với 1999999999 và vòng để L3 nếu i < 2000000000.

Bây giờ tối ưu hóa một:

.file "test.c" 
    .text 
    .p2align 4,,15 
.globl main 
    .type main, @function 
main: 
.LFB0: 
    .cfi_startproc 
    rep 
    ret 
    .cfi_endproc 
.LFE0: 
    .size main, .-main 
    .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 
    .section .note.GNU-stack,"",@progbits 

Tôi không thể hiểu tại tất cả những gì đang diễn ra ở đó! Tôi đã có chút kiến ​​thức về lắp ráp, nhưng tôi mong đợi một cái gì đó giống như

addl $2000000000, -8(%rbp) 

Tôi thậm chí đã cố gắng với gcc -c -g -Wa, -a, -ad -O2 test.c để xem mã C cùng với việc lắp ráp nó đã được chuyển đổi thành, nhưng kết quả là không rõ ràng hơn rằng trước đó.

Ai đó có thể giải thích ngắn gọn:

  1. Các gcc -S -O2 đầu ra.
  2. Nếu vòng lặp được tối ưu hóa như tôi mong đợi (một tổng thay vì nhiều khoản tiền)?
+19

Câu hỏi hay về btw và chào mừng bạn đến với Stackoverflow! Đây là một ví dụ tốt về câu hỏi đầu tiên tuyệt vời để hỏi. :) – Mysticial

Trả lời

72

Trình biên dịch thậm chí còn thông minh hơn thế. :)

Thực tế, nó nhận ra rằng bạn không sử dụng kết quả của vòng lặp. Vì vậy, nó đã diễn ra toàn bộ vòng lặp hoàn toàn!

Điều này được gọi là Dead Code Elimination.

Một thử nghiệm tốt hơn là để in kết quả:

#include <stdio.h> 
int main(void) { 
    int i; int count = 0; 
    for(i = 0; i < 2000000000; i++){ 
     count = count + 1; 
    } 

    // Print result to prevent Dead Code Elimination 
    printf("%d\n", count); 
} 

EDIT: Tôi đã thêm các yêu cầu #include <stdio.h>; danh sách hội đồng MSVC tương ứng với phiên bản không có #include, nhưng nó phải giống nhau.


Tôi không có GCC trước mặt tôi vào lúc này, vì tôi được khởi động vào Windows. Nhưng đây là việc tháo gỡ phiên bản với printf() trên MSVC:

CHỈNH SỬA: Tôi đã có đầu ra lắp ráp sai. Đây là cái đúng.

; 57 : int main(){ 

$LN8: 
    sub rsp, 40     ; 00000028H 

; 58 : 
; 59 : 
; 60 :  int i; int count = 0; 
; 61 :  for(i = 0; i < 2000000000; i++){ 
; 62 :   count = count + 1; 
; 63 :  } 
; 64 : 
; 65 :  // Print result to prevent Dead Code Elimination 
; 66 :  printf("%d\n",count); 

    lea rcx, OFFSET FLAT:[email protected][email protected][email protected] 
    mov edx, 2000000000    ; 77359400H 
    call QWORD PTR __imp_printf 

; 67 : 
; 68 : 
; 69 : 
; 70 : 
; 71 :  return 0; 

    xor eax, eax 

; 72 : } 

    add rsp, 40     ; 00000028H 
    ret 0 

Vì vậy, Visual Studio thực hiện tối ưu hóa này. Tôi cho rằng GCC có lẽ cũng vậy.

Và có, GCC thực hiện tối ưu hóa tương tự. Dưới đây là một danh sách lắp ráp cho các chương trình tương tự với gcc -S -O2 test.c (gcc 4.5.2, Ubuntu 11.10, x86):

 .file "test.c" 
     .section  .rodata.str1.1,"aMS",@progbits,1 
.LC0: 
     .string "%d\n" 
     .text 
     .p2align 4,,15 
.globl main 
     .type main, @function 
main: 
     pushl %ebp 
     movl %esp, %ebp 
     andl $-16, %esp 
     subl $16, %esp 
     movl $2000000000, 8(%esp) 
     movl $.LC0, 4(%esp) 
     movl $1, (%esp) 
     call __printf_chk 
     leave 
     ret 
     .size main, .-main 
     .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 
     .section  .note.GNU-stack,"",@progbits 
+2

Vâng, tôi cảm thấy thực sự câm ngay bây giờ. Không nghĩ (eew .. không biết) về việc loại bỏ mã chết. Tôi đã thử với printf() và gcc, và nó tạo ra cùng một mã được tối ưu hóa tương tự. Cảm ơn bạn đã trả lời! – Haile

+10

Đừng cảm thấy câm. Loại điều này không rõ ràng chút nào nếu bạn chỉ tham gia vào việc đo điểm chuẩn micro. Nó chỉ là một phần của quá trình học tập. – Mysticial

+0

Thật thú vị khi biết trình biên dịch đưa ra quyết định như thế nào. Điều gì sẽ xảy ra nếu vòng lặp đó thực sự cần thiết vì một lý do nào đó? – kechapito

0

Trình biên dịch có một vài công cụ theo ý của họ để làm cho mã hiệu quả hơn hoặc nhiều "hiệu quả":

  1. Nếu kết quả tính toán không bao giờ được sử dụng, có thể bỏ qua mã (nếu tính toán theo giá trị volatile, các giá trị này vẫn phải đọc nhưng kết quả đọc có thể bị bỏ qua). Nếu kết quả của các tính toán cho ăn nó không được sử dụng, mã thực hiện chúng cũng có thể được bỏ qua. Nếu thiếu sót như vậy làm cho mã cho cả hai đường dẫn trên một nhánh có điều kiện giống nhau, điều kiện có thể được coi là không sử dụng và bị bỏ qua. Điều này sẽ không ảnh hưởng đến các hành vi (ngoài thời gian thực hiện) của bất kỳ chương trình nào không thực hiện truy cập bộ nhớ ngoài hoặc gọi những gì mà Phụ lục L gọi là "Hành vi không xác định quan trọng". Nếu trình biên dịch xác định rằng mã máy tính giá trị chỉ có thể tạo ra kết quả trong một phạm vi nhất định, nó có thể bỏ qua bất kỳ thử nghiệm có điều kiện nào mà kết quả có thể được dự đoán trên cơ sở đó. Như trên, điều này sẽ không ảnh hưởng đến các hành vi khác ngoài thời gian thực thi trừ khi mã gọi "Hành vi không xác định quan trọng". Nếu trình biên dịch xác định rằng một số đầu vào nhất định sẽ gọi bất kỳ hình thức nào của Hành vi không xác định với mã như được viết, Chuẩn sẽ cho phép trình biên dịch bỏ qua bất kỳ mã nào chỉ có liên quan khi nhận được các đầu vào đó, ngay cả khi hành vi của các nền tảng thực hiện cho đầu vào như vậy sẽ có được lành tính và viết lại của trình biên dịch sẽ làm cho nó nguy hiểm.

Trình biên dịch tốt làm # 1 và # 2. Tuy nhiên, vì lý do nào đó, # 3 đã trở nên thời trang.