淺談迴圈之硬體級實現

lanzhiheng發表於2019-02-28

現代程式語言中迴圈是十分常見的功能,幾乎任何程式語言都有類似forwhile這樣的迴圈語句,不過在計算機底層就沒有那麼幸福了,許多的硬體其實並沒有提供硬體級別的迴圈。不過硬體級別的限制,似乎並沒有影響到我們日常的工作,今天就主要來看看迴圈的本質是什麼。

指令集

如今使用的程式語言,以及各類不同的軟體,其實到最後都會轉換成二進位制的形式,用以控制底層硬體的執行。這些上層軟體其實是底層功能的抽象,不管上層業務多麼複雜,在底層幾乎都是通過有限的暫存器,指令集,還有記憶體來實現相關的功能。我們所編寫的應用程式,與CPU的指令集息息相關。其實所謂的指令集,就是CPU提供的一系列用於控制硬體的指令的集合。不同的硬體廠家,所生產的CPU指令集肯能會有所不同。目前主要分成了兩大陣營,分別是CISC-複雜指令集計算機和RISC-精簡指令集計算機。AMD以及Intel這些廠家生產的CPU(x86指令集)基本上都屬於CISC,他們所包含的指令相當多也比較複雜,不過似乎不打算支援硬體級別的迴圈。而一些的手機CPU,ARM架構的開發版都屬於RISC的範疇,它們的特點是指令相對較少,也比較簡單,並且部分RISC的CPU甚至支援硬體級別的迴圈。

迴圈的“變態”

FRANKY

這裡的“變態”並沒有罵人的意思。根據生物學中的描述,變態其實指代了形態的變化。在某種意義上,迴圈也存在著變態。

在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迴圈來實現等價的功能,這裡不一一舉例。

long fact_while(long n) {
  long result = 1;
  while (n > 1) {
    result *= n;
    n -= 1;
  }
  return result;
}
複製程式碼

我們的任務就是不使用whilefor這些迴圈語句,只用goto語句來實現上述迴圈。大體上有兩種翻譯方式,分別是jump to middle以及gurade-do

1. Jump To Middle

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;這條語句去掉之後會發生什麼事情呢?

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迴圈。

2. guarded-do

另一種翻譯方式被稱為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語句來取代whilefordo-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語句模擬了底層的迴圈實現。最後還提供了一個優化等級較低的組合語言版本,能進一步體現出底層硬體的工作方式。

相關文章