register
使用修飾符register
聲明的變量屬于寄存器存儲類型。該類型與自動存儲類型相似善炫,具有自動存儲時期撩幽、代碼塊作用域和內(nèi)連接。聲明為register
僅僅是一個請求,因此該變量仍然可能是普通的自動變量窜醉。無論哪種情況宪萄,用register
修飾的變量都無法獲取地址。如果沒有被初始化榨惰,它的值是未定的拜英。
#include <stdio.h>
int main(int argc, char** argv) {
register int a = 1;
printf("%lld", &a);
return 0;
}
如上代碼編譯使用GCC
不通過gcc main.c -o main
:
我們換一種方式g++ main.c -o main
:
編譯通過且輸出了數(shù)值,看起來像是內(nèi)存地址读串。原來
C++
中寄存器變量在內(nèi)存中存在副本聊记,這里打印副本的地址。
volatile
volatile
告訴編譯器該被變量除了可被程序修改外恢暖,還可能被其他代理排监、線程修改。因此杰捂,當(dāng)使用volatile
聲明的變量的值的時候舆床,系統(tǒng)總是重新從它所在的內(nèi)存讀取數(shù)據(jù),而不使用寄存器中的緩存的值嫁佳。
#include <stdio.h>
int main(int argc, char** argv) {
volatile int a = 1;
printf("%llu", &a);
return 0;
}
結(jié)果輸出地址:
volatile
總是與優(yōu)化有關(guān)挨队,編譯器有一種技術(shù)叫做數(shù)據(jù)流分析,分析程序中的變量在哪里賦值蒿往、在哪里使用盛垦、在哪里失效,分析結(jié)果可以用于常量合并瓤漏,常量傳播等優(yōu)化腾夯,進一步可以死代碼消除。但有時這些優(yōu)化不是程序所需要的蔬充,這時可以用volatile
關(guān)鍵字禁止做這些優(yōu)化蝶俱,volatile的字面含義是易變,作用在三個方面:
- 不會在兩個操作之間把
volatile
變量緩存在寄存器中饥漫。在多任務(wù)榨呆、中斷、甚至setjmp
環(huán)境下庸队,變量可能被其他的程序改變积蜻,編譯器自己無法知道,volatile
就是告訴編譯器這種情況 - 不做常量合并彻消、常量傳播等優(yōu)化
- 對
volatile
變量的讀寫不會被優(yōu)化掉浅侨。如果你對一個變量賦值但后面沒用到,編譯器常持づ颍可以省略那個賦值操作如输,然而對Memory Mapped IO
的處理是不能這樣優(yōu)化
應(yīng)該使用volatile的地房
- 中斷服務(wù)程序中修改的供其它程序檢測的變量需要加volatile
- 多任務(wù)環(huán)境下各任務(wù)間共享的標志應(yīng)該加volatile
- 存儲器映射的硬件寄存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義
volatile的配合使用
- 可以和
const
配合使用,例如只讀的狀態(tài)寄存器不见,它是volatile因為它可能被意想不到地改變澳化,它是const因為程序不應(yīng)該試圖去修改它 - 指針也可以是volatile,當(dāng)一個服務(wù)中子程序修該一個指向一個buffer的指針時
restrict
restrict
是C99
引入的稳吮,它只可以用于限定指針缎谷,并表明指針是訪問一個數(shù)據(jù)對象的唯一且初始的方式。這個關(guān)鍵字據(jù)說來源于古老的FORTRAN
灶似,主要用來修飾指針指向的內(nèi)存不能被別的指針引用列林。所有修改該指針所指向內(nèi)存中內(nèi)容的操作都必須通過該指針來修改,而不能通過其它途徑(其它變量或指針)來修改。這樣做的好處是酪惭,能幫助編譯器進行更好的優(yōu)化代碼,生成更有效率的匯編代碼希痴。如 int *restrict ptr, ptr
指向的內(nèi)存單元只能被ptr
訪問到,任何同樣指向這個內(nèi)存單元的其他指針都是未定義的春感,直白點就是無效指針砌创。restrict
的出現(xiàn)是因為C
語言本身固有的缺陷,C
程序員應(yīng)當(dāng)主動地規(guī)避這個缺陷鲫懒,而編譯器也會很配合地優(yōu)化你的代碼嫩实。如下代碼示例:
#include <stdio.h>
int main(int argc, char** argv) {
int a[5];
int * restrict ra = (int*) malloc(5 * sizeof(int));
int * pa = a;
for(int n = 0; n < 10; n++) {
pa[n] += 5;
ra[n] += 5;
a[n] *= 2;
pa[n] += 3;
ra[n] += 3;
}
}
分析面的代碼,因為ra
是訪問分配的內(nèi)存的唯一且初始的方式窥岩,那么編譯器可以將上述對ra
的操作進行優(yōu)化:
ra[n] += 8;
而pa
并不是訪問數(shù)組a
的唯一方式甲献,因此并不能進行下面的優(yōu)化:
pa[n] +=8;
因為在pa[n] += 3;
前,a[n]* = 2;
進行了改變颂翼。使用了關(guān)鍵字restrict
晃洒,編譯器就可以放心地進行優(yōu)化。
內(nèi)存拷貝函數(shù)
C
庫中有兩個函數(shù)可以從一個位置把字節(jié)復(fù)制到另一個位置疚鲤。在C99
標準下锥累,它們的原型如下:
void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
void * memove(void * s1, const void * s2, size_t n);
這兩個函數(shù)均從s2
指向的位置復(fù)制n
字節(jié)數(shù)據(jù)到s1
指向的位置缘挑,且均返回s1
的值集歇。兩者之間的差別由關(guān)鍵字restrict
造成,即memcpy()
可以假定兩個內(nèi)存區(qū)域沒有重疊语淘,memmove()
函數(shù)則不做這個假诲宇。因此,復(fù)制過程類似于首先將所有字節(jié)復(fù)制到一個臨時緩沖區(qū)惶翻,然后再復(fù)制到最終目的地姑蓝。如果兩個區(qū)域存在重疊時使用 memcpy()
會怎樣?其行為是不可預(yù)知的吕粗,既可以正常工作纺荧,也可能失敗。在不應(yīng)該使用memcpy()
時,編譯器不會禁止使用memcpy()
宙暇。因此输枯,使用memcpy()
時,您必須確保沒有重疊區(qū)域占贫。這是程序員的任務(wù)的一部分桃熄,驗證結(jié)果如下代碼:
編譯優(yōu)化
硬件優(yōu)化
內(nèi)存訪問速度遠不及CPU
處理速度,為提高機器整體性能型奥,在硬件上引入硬件高速緩存Cache
瞳收,加速對內(nèi)存的訪問。另外在現(xiàn)代CPU
中指令的執(zhí)行并不一定嚴格按照順序執(zhí)行厢汹,沒有相關(guān)性的指令可以亂序執(zhí)行螟深,以充分利用CPU
的指令流水線,提高執(zhí)行速度坑匠。以上是硬件級別的優(yōu)化血崭。
軟件優(yōu)化
一種是在編寫代碼時由程序員優(yōu)化,另一種是由編譯器進行優(yōu)化厘灼。編譯器優(yōu)化常用的方法有:將內(nèi)存變量緩存到寄存器夹纫;調(diào)整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令设凹。對常規(guī)內(nèi)存進行優(yōu)化的時候舰讹,這些優(yōu)化是透明的,而且效率很好闪朱。由編譯器優(yōu)化或者硬件重新排序引起的問題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序執(zhí)行的操作之間設(shè)置內(nèi)存屏障(memory barrier)
月匣,linux
提供了一個宏解決編譯器的執(zhí)行順序問題。
void Barrier(void)
這個函數(shù)通知編譯器插入一個內(nèi)存屏障奋姿,但對硬件無效锄开,編譯后的代碼會把當(dāng)前CPU
寄存器中的所有修改過的數(shù)值存入內(nèi)存,需要這些數(shù)據(jù)的時候再重新從內(nèi)存中讀出称诗。
linux中的編譯優(yōu)化
上面我們已經(jīng)看了一點編譯優(yōu)化的??萍悴。我們先看如下的代碼:
#include <stdio.h>
int thread_func(LPVOID signal) {
int* intSignal = reinterpret_cast<int*>(signal);
*intSignal = 2;
while(*intSignal != 1)
return 0;
}
int main(int argc, char** argv) {
thread_func(singal)
return 0;
}
該線程啟動時將intSignal
置為2,然后循環(huán)等待直到intSignal
為1 時退出寓免。顯然intSignal
的值必須在外部被改變癣诱,否則該線程不會退出。但是實際運行的時候該線程卻不會退出袜香,即使在外部將它的值改為1撕予。這是一個線程優(yōu)化的例子,下面看一個簡單的賦值優(yōu)化:
#include <stdio.h>
int main (void) {
int i = 5;
int a = i; //優(yōu)化
return 0;
}
int main(int argc, char** argv) {
volatile int i = 5;
int a = i; //優(yōu)化
return 0;
}
兩段代碼寫在一起蜈首,自行分開∈德眨現(xiàn)在分別編譯他們:
- 有編譯的優(yōu)化欠母,沒有volatile關(guān)鍵字
.section __TEXT,__text_startup,regular,pure_instructions
.align 4
.globl _main
_main:
LFB1:
xorl %eax, %eax
ret
LFE1:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
.set L$set$0,LECIE1-LSCIE1
.long L$set$0
LSCIE1:
.long 0
.byte 0x1
.ascii "zR\0"
.byte 0x1
.byte 0x78
.byte 0x10
.byte 0x1
.byte 0x10
.byte 0xc
.byte 0x7
.byte 0x8
.byte 0x90
.byte 0x1
.align 3
LECIE1:
LSFDE1:
.set L$set$1,LEFDE1-LASFDE1
.long L$set$1
LASFDE1:
.long LASFDE1-EH_frame1
.quad LFB1-.
.set L$set$2,LFE1-LFB1
.quad L$set$2
.byte 0
.align 3
LEFDE1:
.subsections_via_symbols
- 無編譯優(yōu)化,有volatile關(guān)鍵字
.section __TEXT,__text_startup,regular,pure_instructions
.align 4
.globl _main
_main:
LFB1:
movl $5, -4(%rsp)
movl -4(%rsp), %eax
xorl %eax, %eax
ret
LFE1:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
.set L$set$0,LECIE1-LSCIE1
.long L$set$0
LSCIE1:
.long 0
.byte 0x1
.ascii "zR\0"
.byte 0x1
.byte 0x78
.byte 0x10
.byte 0x1
.byte 0x10
.byte 0xc
.byte 0x7
.byte 0x8
.byte 0x90
.byte 0x1
.align 3
LECIE1:
LSFDE1:
.set L$set$1,LEFDE1-LASFDE1
.long L$set$1
LASFDE1:
.long LASFDE1-EH_frame1
.quad LFB1-.
.set L$set$2,LFE1-LFB1
.quad L$set$2
.byte 0
.align 3
LEFDE1:
.subsections_via_symbols
觀察發(fā)現(xiàn)在LFB1
段是不同的吆寨,使用volatile
的代碼編譯未優(yōu)化艺蝴,還在訪問內(nèi)存,而優(yōu)化過的代碼鸟废,沒有訪問內(nèi)存猜敢,只有寄存器。
優(yōu)化的編譯參數(shù)-O
-O設(shè)置一共有五種:-O0盒延、-O1缩擂、-O2、-O3和-Os添寺,只能設(shè)置一種胯盯。除了-O0以外,每一個-O設(shè)置都會多啟用幾個選項计露,請查閱gcc手冊的優(yōu)化選項章節(jié)博脑,以便了解每個-O等級啟用了哪些選項及它們有何作
- -O0: 這個等級(字母“O”后面跟個零)關(guān)閉所有優(yōu)化選項,也是
CFLAGS
或CXXFLAGS
中沒有設(shè)置-O等級時的默認等級票罐。這樣就不會優(yōu)化代碼叉趣,這通常不是我們想要的 - -O1: 這是最基本的優(yōu)化等級。編譯器會在不花費太多編譯時間的同時試圖生成更快更小的代碼该押。這些優(yōu)化是非沉粕迹基礎(chǔ)的,但一般這些任務(wù)肯定能順利完成
- -O2: -O1的進階蚕礼。這是推薦的優(yōu)化等級烟具,除非你有特殊的需求。-O2會比-O1啟用多一些標記奠蹬。設(shè)置了-O2后朝聋,編譯器會試圖提高代碼性能而不會增大體積和大量占用的編譯時間
- -O3: 這是最高最危險的優(yōu)化等級。用這個選項會延長編譯代碼的時間囤躁,并且在使用gcc4.x的系統(tǒng)里不應(yīng)全局啟用冀痕。自從3.x版本以來gcc的行為已經(jīng)有了極大地改變。在3.x割以,-O3生成的代碼也只是比-O2快一點點而已金度,而gcc4.x中還未必更快应媚。用-O3來編譯所有的軟件包將產(chǎn)生更大體積更耗內(nèi)存的二進制文件严沥,大大增加編譯失敗的機會或不可預(yù)知的程序行為(包括錯誤)。這樣做將得不償失中姜,記住過猶不及消玄。在gcc 4.x.中使用-O3是不推薦的
- -Os: 這個等級用來優(yōu)化代碼尺寸跟伏。其中啟用了-O2中不會增加磁盤空間占用的代碼生成選項。這對于磁盤空間極其緊張或者CPU緩存較小的機器非常有用翩瓜。但也可能產(chǎn)生些許問題受扳,因此軟件樹中的大部分
ebuild
都過濾掉這個等級的優(yōu)化。使用-Os是不推薦的
下面是一個示例兔跌,編譯gcc -O0 -S main.c
#include <stdio.h>
int main (void) {
int i = 5;
int a = i; //優(yōu)化
return 0;
}
編譯出來結(jié)果:
.text
.globl _main
_main:
LFB1:
pushq %rbp
LCFI0:
movq %rsp, %rbp
LCFI1:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $5, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, -8(%rbp)
movl $0, %eax
popq %rbp
LCFI2:
ret
LFE1:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
.set L$set$0,LECIE1-LSCIE1
.long L$set$0
LSCIE1:
.long 0
.byte 0x1
.ascii "zR\0"
.byte 0x1
.byte 0x78
.byte 0x10
.byte 0x1
.byte 0x10
.byte 0xc
.byte 0x7
.byte 0x8
.byte 0x90
.byte 0x1
.align 3
LECIE1:
LSFDE1:
.set L$set$1,LEFDE1-LASFDE1
.long L$set$1
LASFDE1:
.long LASFDE1-EH_frame1
.quad LFB1-.
.set L$set$2,LFE1-LFB1
.quad L$set$2
.byte 0
.byte 0x4
.set L$set$3,LCFI0-LFB1
.long L$set$3
.byte 0xe
.byte 0x10
.byte 0x86
.byte 0x2
.byte 0x4
.set L$set$4,LCFI1-LCFI0
.long L$set$4
.byte 0xd
.byte 0x6
.byte 0x4
.set L$set$5,LCFI2-LCFI1
.long L$set$5
.byte 0xc
.byte 0x7
.byte 0x8
.align 3
LEFDE1:
.subsections_via_symbols
觀察LCFI1
段勘高,發(fā)現(xiàn)仍然在訪問內(nèi)存,不像加了編譯優(yōu)化級別-O2
時只是訪問寄存器坟桅。這只是編譯優(yōu)化的一個小例子华望,實際中還有許多其他的可以優(yōu)化的內(nèi)容。