C語言中的Const常量和優化

2016-07-31    分類:C/C++開發、程式設計開發、首頁精華0人評論發表於2016-07-31

本文由碼農網 – 唐李川原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

這裡有一個關於C語言程式設計中Const優化效果的問題。這個問題延伸的很多問題在過去的二十年都已經被回答了很多次了。從我個人角度來說,我認為都是const的錯誤。

看下面的程式碼:

void foo(const int *);

int
bar(void)
{
    int x = 0;
    int y = 0;
    for (int i = 0; i < 10; i++) {
        foo(&x);
        y += x;  // this load not optimized out
    }
    return y;
}

程式碼裡的foo函式有一個const(常量)型別的指標,該指標是foo函式定義者的一個承諾,那就是它不能修改x變數的值。根據這個程式碼資訊,編譯器會假設x變數的值一直是0,同時y變數的值也是0。

然而,在不同的編譯器裡執行程式碼後,檢查編譯器的集合輸出,我們就會發現,x變數在迴圈裡每一次都會載入。下面是gcc 4.9.2的最佳優化提示資訊:

Gcc 4.9.2介紹連結地址: https://linux.cn/lfs/LFS-BOOK-7.7-systemd/chapter06/gcc.html

-O3介紹連結地址: http://blog.chinaunix.net/uid-20662363-id-3036581.html

bar:
     push   rbp
     push   rbx
     xor    ebp, ebp              ; y = 0
     mov    ebx, 0xa              ; loop variable i
     sub    rsp, 0x18             ; allocate x
     mov    dword [rsp+0xc], 0    ; x = 0

.L0: lea    rdi, [rsp+0xc]        ; compute &x
     call   foo
     add    ebp, dword [rsp+0xc]  ; y += x  (not optmized?)
     sub    ebx, 1
     jne    .L0

     add    rsp, 0x18             ; deallocate x
     mov    eax, ebp              ; return y
     pop    rbx
     pop    rbp
     ret

Clang 3.5的輸出(用-fno-unroll-loops命令)是一樣的,除了ebp和ebx做了交換,並且&x的計算已經超出了迴圈的範圍,變成了r14。

-fno-unroll-loops命令介紹連結地址: http://www.cnblogs.com/dxz/archive/2007/06/29/gcc_option_and_env_var.html

是否這兩種編譯器都沒有用到這個有用的資訊?還是對於foo函式來說,有未定義的行為修改了x變數的值?可令人吃驚的是,問題的答案是都沒有。在這種情況下,這裡將會定義一個完整合法的foo函式。

void
foo(const int *readonly_x)
{
    int *x = (int *)readonly_x;  // cast away const
    (*x)++;
}

關鍵的事情是,記住const修飾型別的變數並不意味著不變。這裡把它作為一個用詞不當的定義。它不是一個優化的工具。它在這裡告訴程式設計者-而不是編譯器-在編譯時間內作為一個工具來catch(捕獲)一個類的具體錯誤資訊。我喜歡它的API,因為它能告訴我們一個function函式如何使用具體的引數,或者如何得到呼叫者期望處理返回的指標。通常的編譯器不足以強大到能夠改變它的行為。

儘管我剛才那麼說了,但有時編譯器也可以利用const來實現程式碼的優化。在C99說明書的§6.7.3裡,有如下的定義:

C99介紹連結地址: http://baike.sogou.com/v469198.htm?fromTitle=c99

如果有人試圖用一個const限制型別修飾通過一個不用const限制型別修飾的有效值修改一個物件的定義,這就好比沒有定義。

最初的x變數是沒有用const限制修飾的,因此這個規則無效。而沒有任何的規則可以無視const而修改一個物件,這不是物件本身的const。這也就是說,以上的foo函式的行為(不當的行為)是沒有為這次呼叫定義行為的。

Bar函式有一個微小的調整,我可以讓這條規則得到應用,同時允許優化器在它上面做一些工作。

const int x = 0;

編譯器現在也許假設foo函式修改了x變數的值,這種行為是沒有定義的,因此這種行為永遠不會發生。不管怎樣,這是一個C語言優化器優化你程式碼的原因的一個重要部分。編譯器不受約束地假設x變數從來沒有改變過,並且允許它優化每個迭代的載入和y變數。

bar:
     push   rbx
     mov    ebx, 0xa            ; loop variable i
     sub    rsp, 0x10           ; allocate x
     mov    dword [rsp+0xc], 0  ; x = 0

.L0: lea    rdi, [rsp+0xc]      ; compute &x
     call   foo
     sub    ebx, 1
     jne    .L0

     add    rsp, 0x10           ; deallocate x
     xor    eax, eax            ; return 0
     pop    rbx
     ret

本次載入的結果消失了,y變數不見了,而且函式的返回值一直是0。

可令人好奇的是,規格說明書上允許編譯器繼續允許。它允許不在棧中的某個位置來分配x變數值,甚至是在read-only(只讀儲存器裡)也可以。例如,它可能像下面這樣執行一次轉換:

static int __x = 0;

int
bar(void)
{
    for (int i = 0; i < 10; i++)
        foo(&__x);
    return 0;
}

或者在x86-64(-fPIC引數,小型的程式碼模型)上,我們可以看到更多的指令消失了:

X86-64介紹連結地址: http://baike.sogou.com/v617198.htm?fromTitle=X86-64

-fPIC,small code model的連結地址: http://eli.thegreenplace.net/2012/01/03/understanding-the-x64-code-models

-fPIC介紹連結地址: http://blog.sina.com.cn/s/blog_54f82cc201011op1.html

section .rodata
x:   dd     0

section .text
bar:
     push   rbx
     mov    ebx, 0xa        ; loop variable i

.L0: lea    rdi, [rel x]    ; compute &x
     call   foo
     sub    ebx, 1
     jne    .L0

     xor    eax, eax        ; return 0
     pop    rbx
     ret

無論是clang還是gcc都做不到,也許是因為它對混亂不好的程式碼來說更具破壞性。

即使有明確具體的const規則,但只有你自己用const,並且給和你一樣是程式設計師的同伴用,你才明白什麼是const。讓優化器自己就什麼是常量什麼不是常量給自己個解釋理由。

譯文連結:http://www.codeceo.com/article/c-const-optimization.html
英文原文:Const and Optimization in C
翻譯作者:碼農網 – 唐李川
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章