現代編程語言中循環是十分常見的功能,幾乎任何編程語言都有相似for
, while
這樣的循環語句,不過在計算機底層就沒有那麼幸福了,許多的硬件其實並無提供硬件級別的循環。不過硬件級別的限制,彷佛並無影響到咱們平常的工做,今天就主要來看看循環的本質是什麼。編程
現在使用的編程語言,以及各種不一樣的軟件,其實到最後都會轉換成二進制的形式,用以控制底層硬件的運行。這些上層軟件實際上是底層功能的抽象,無論上層業務多麼複雜,在底層幾乎都是經過有限的寄存器,指令集,還有內存來實現相關的功能。咱們所編寫的應用程序,與CPU的指令集息息相關。其實所謂的指令集,就是CPU提供的一系列用於控制硬件的指令的集合。不一樣的硬件廠家,所生產的CPU指令集肯能會有所不一樣。目前主要分紅了兩大陣營,分別是CISC-複雜指令集計算機和RISC-精簡指令集計算機。AMD以及Intel這些廠家生產的CPU(x86指令集)基本上都屬於CISC,他們所包含的指令至關多也比較複雜,不過彷佛不打算支持硬件級別的循環。而一些的手機CPU,ARM架構的開發版都屬於RISC的範疇,它們的特色是指令相對較少,也比較簡單,而且部分RISC的CPU甚至支持硬件級別的循環。bash
這裏的「變態」並無罵人的意思。根據生物學中的描述,變態其實指代了形態的變化。在某種意義上,循環也存在着變態。架構
在C語言中循環廣泛有3中表達方式,分別是for
循環,while
循環以及do-while
循環編程語言
// 1. for循環
for (init-expr; test-expr; update-expr) {
....
}
// 2. while循環
init-expr;
while (test-expr) {
.....
update-expr;
}
// 3. do-while循環
init-expr;
do {
....
update-expr;
} while (test-expr)
複製代碼
不過問題是計算機底層並無那麼多表達循環的方式,爲了在底層實現循環功能,必需要以另外一種方式來表達循環。實際上循環在底層都會經過指令跳轉配合狀態更改的方式來實現。至關於用goto
這類語句來實現循環模式。goto
語句在業界是很讓人詬病的,許多的語言都不支持goto
這類語法,不過好在C語言仍是支持的。接下來來看看要如何進行這種「變態」。函數
fact_while
是一個用while
循環實現的階乘函數,固然咱們也能夠用for
循環來實現等價的功能,這裏不一一舉例。oop
long fact_while(long n) {
long result = 1;
while (n > 1) {
result *= n;
n -= 1;
}
return result;
}
複製代碼
咱們的任務就是不使用while
,for
這些循環語句,只用goto
語句來實現上述循環。大致上有兩種翻譯方式,分別是jump to middle
以及gurade-do
。測試
jump to middle直接翻譯過來就是跳轉到中間,它的原理其實就是**把條件測試寫在中間部分,在首次迭代開始以前先行跳轉並執行條件測試語句。**翻譯過來大概就是優化
long fact_jump_to_middle(long n) {
long result = 1;
goto test;
loop:
result *= n;
n --;
test:
if (n > 1) goto loop;
return result;
}
複製代碼
這種翻譯方式最爲關鍵的是goto test;
語句,在進入循環區域以前便直接跳轉到條件測試語句,測試是否符合n > 1
這個條件。若是符合條件則進入循環體並執行循環體中的邏輯,不然繼續往下執行程序,返回結果。這種翻譯方式還有個特色,當你嘗試把goto test;
這條語句去掉以後會發生什麼事情呢?spa
long fact_jump_to_middle_without_first_jump(long n) {
long result = 1;
loop:
result *= n;
n --;
test:
if (n > 1) goto loop;
return result;
}
複製代碼
從邏輯上講它其實就是一個測試條件相同的do-while
循環實現,while
語句與do-while
語句最大的不一樣就在於,while
語句是先進行條件測試,當符合條件的時候纔會進入到循環體中,而do-while
則是執行了一次循環體中的語句以後才進行循環相關的條件測試。這麼看來do-while
循環本質上就是少了初始條件檢測的while
循環。翻譯
另外一種翻譯方式被稱爲guarded-do,它的原理是在迭代以前設置一個「門衛」條件。若是不符合條件的話,則直接跳到循環邏輯以後,不然就進入循環邏輯中,此處的循環邏輯依舊用do-while
循環來實現。按照這種翻譯方式所翻譯的goto
版本以下
long fact_guarded_do(long n) {
long result = 1;
if (n <= 1) goto done;
loop:
result *= n;
n --;
if (n > 1) goto loop;
done:
return result;
}
複製代碼
可見最關鍵的地方是設置的「門衛」條件,該條件應該設置成循環條件的補集。只要知足這個「門衛」條件則跳過整個循環邏輯,不然就進入循環區域中。有些書還會把上面的過程寫成
long fact_guarded_do(long n) {
long result = 1;
if (n <= 1) goto done;
loop:
result *= n;
n --;
if (n != 1) goto loop;
done:
return result;
}
複製代碼
其實兩種方式是等價的。只要符合條件n > 1
便可以進入到循環區域中,在循環中每次迭代都會進行減一操做,那麼只要知足條件n != 1
即可持續進行迭代。
前面部分簡單地介紹了循環,以及如何對循環進行變形,用goto
語句來取代while
, for
, do-while
這類循環語句。然而正常狀況下咱們並不會去把一個C語言的循環版本,轉換成與之等價的C語言的goto
版本,這麼作其實只是爲了方便原理的解釋。真實場景下,在語言進行編譯的時候,其實會先轉換成彙編代碼。
經過命令
gcc -Og -S while.c
複製代碼
把最開始的fact_while
階乘函數編譯成彙編語言版本,生成的彙編程序會存儲在文件while.s
中,丟掉一些雜七雜八的東西以後大概結果以下
movl $1, %eax
cmpq $2, %rdi
jl LBB0_2
LBB0_1:
imulq %rdi, %rax
cmpq $2, %rdi
leaq -1(%rdi), %rdi
jg LBB0_1
LBB0_2:
retq
複製代碼
簡單起見,我把一些方法調用相關的寄存器行爲給去掉了,只保留了循環邏輯的部分。閱讀彙編代碼的關鍵點在於瞭解不一樣寄存器的做用,其中寄存器%rax
用於存放返回值,寄存器%rdi
用於存放函數第一個參數的值。把上面的彙編程序轉換成更加親民的版本,並加上註釋可得
movl $1, %eax ## 把數值1放進寄存器%eax中
cmpq $2, %rdi ## 把參數n的值與數值2進行比較
jl done ## 若是n < 2則跳到標籤done處
loop: ## 標識着即將進入循環區域
imulq %rdi, %rax ## 把%rax (就是%eax中的數值0擴展到64位)的數值與%rdi(數值n)相乘,並把結果存儲到%rax中
cmpq $2, %rdi ## 把n的值與數值2進行比較,比較結果會記錄在其餘地方 (1)
leaq -1(%rdi), %rdi ## 改變n的值,n = n - 1
jg loop ## 獲取(1)處的比較結果,若是在遞減以前n是大於2的則跳轉到循環區域開始的地方
done: ## 標識着已經離開循環區域
retq ## 函數返回,返回值存放在寄存器%rax中
複製代碼
整體上看來這裏是採用了guarded-do
的翻譯方式。不過它的具體邏輯看起來跟咱們前面用C語言的goto
語句描述的過程稍微有些不一樣,可是隻要仔細琢磨,其實它們所作的東西是等價的,爲了少執行一些指令,編譯器會進行了一些優化,不過在本例中所採用的優化等級還算是比較低的了。
這篇文章主要簡單地總結了一下在計算機底層循環的實現方式,即使是現代最流行的x86指令集都沒有硬件級循環的支持,常見的作法是利用硬件的條件跳轉指令來實現循環的相關邏輯。爲了更直觀地看到這個過程,咱們利用C語言的goto
語句模擬了底層的循環實現。最後還提供了一個優化等級較低的彙編語言版本,能進一步體現出底層硬件的工做方式。