實操體驗 CPU 的流水線/多發射

前言

前文 <一行機器指令感覺下內存操做到底有多慢> 中,咱們體驗到了 CPU 流水線阻塞帶來的數量級性能差別。當時只是根據機器碼,分析推斷出來的,此次咱們作一些更小的實驗來分析驗證。segmentfault

動手以前,咱們先了解一些背景。在 \<CPU 提供了什麼> 一文中介紹過,CPU 對外提供了運行機器指令的能力。那 CPU 又是如何執行機器指令的呢?緩存

CPU 是如何執行機器指令的

一條機器指令,在 CPU 內部也分爲好多個細分步驟,邏輯上來講能夠劃分爲這麼五個階段:性能優化

  1. 獲取指令
  2. 解析指令
  3. 執行執行
  4. 訪問內存
  5. 結果寫回

流水線做業

例如連續的 ABCD 四條指令,CPU 並非先完整的執行完 A,纔會開始執行 B;而是 A 取指令完成,則開始解析指令 A,同時繼續取指令 B,依次類推,造成了流水線做業。併發

理想狀況下,充分利用 CPU 硬件資源,也就是讓流水線上的每一個器件,一直保持工做。然而實際上,由於各類緣由,CPU 無法完整的跑滿流水線。分佈式

好比:函數

  1. 跳轉指令,可能跳轉執行另外的指令,並非固定的順序執行。
    例如這樣的跳轉指令,可能接下來就須要跳轉到 400553 的指令。
je    400553

對於這種分支指令,CPU 有分支預測技術,基於以前的結果預測本次分支的走向,儘可能減小流水線阻塞。性能

  1. 數據依賴,後面的指令,依賴前面的指令。
    例以下面的兩條指令,第二條指令的操做數 r8 依賴於第一條指令的結果。
mov    r8,QWORD PTR [rdi]
add    r8,0x1

這種時候,CPU 會利用操做數前推技術,儘可能減小阻塞等待。學習

多發射

現代複雜的 CPU 硬件,其實也不僅有一條 CPU 流水線。簡單從邏輯上來理解,能夠假設是有多條流水線,能夠同時執行指令,可是也並非簡單的重複整個流水線上的全部硬件。測試

多發射能夠理解爲 CPU 硬件層面的併發,若是兩條指令沒有先後的順序依賴,那麼是徹底能夠併發執行的。CPU 只須要保證執行的最終結果是符合指望的就能夠,其實不少的性能優化,都是這一個原則,經過優化執行過程,可是保持最終結果一致。優化

實踐體驗

理論須要結合實踐,有實際的體驗,才能更清晰的理解原理。

此次咱們用 C 內聯彙編來構建了幾個用例來體會這其中的差別。

基準用例

#include <stdio.h>

void test(long *a, long *b, long *c, long *d) {
    __asm__ (
        "mov r8, 0x0;"
        "mov r9, 0x0;"
        "mov r10, 0x0;"
        "mov r11, 0x0;"
    );

    for (long i = 0; i <= 0xffffffff; i++) {
    }

    __asm__ (
        "mov [rdi], r8;"
        "mov [rsi], r9;"
        "mov [rdx], r10;"
        "mov [rcx], r11;"
    );
}

int main(void) {
    long a = 0;
    long b = 0;
    long c = 0;
    long d = 0;

    test(&a, &b, &c, &d);

    printf("a = %ldn", a);
    printf("b = %ldn", b);
    printf("c = %ldn", c);
    printf("d = %ldn", d);

    return 0;
}

咱們用以下命令才執行,只須要 1.38 秒。
注意,須要使用 -O1 編譯,由於 -O0 下,基準代碼自己的開銷也會很大。

$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c
$ time ./asm
a = 0
b = 0
c = 0
d = 0

real    0m1.380s
user    0m1.379s
sys     0m0.001s

以上的代碼,咱們主要是構建了一個空的 for 循環,能夠看下彙編代碼來確認下。
一下是 test 函數對應的彙編,確認空的 for 循環代碼沒有被編譯器優化掉。

000000000040052d <test>:
  40052d:       49 c7 c0 00 00 00 00    mov    r8,0x0
  400534:       49 c7 c1 00 00 00 00    mov    r9,0x0
  40053b:       49 c7 c2 00 00 00 00    mov    r10,0x0
  400542:       49 c7 c3 00 00 00 00    mov    r11,0x0
  400549:       48 b8 00 00 00 00 01    movabs rax,0x100000000
  400550:       00 00 00
  400553:       48 83 e8 01             sub    rax,0x1  // 在 -O1 的優化下,變成了 -1 操做
  400557:       75 fa                   jne    400553 <test+0x26>
  400559:       4c 89 07                mov    QWORD PTR [rdi],r8
  40055c:       4c 89 0e                mov    QWORD PTR [rsi],r9
  40055f:       4c 89 12                mov    QWORD PTR [rdx],r10
  400562:       4c 89 19                mov    QWORD PTR [rcx],r11
  400565:       c3                      ret

加入兩條簡單指令

此次咱們在 for 循環中,加入了 「加一」 和 「寫內存」 的兩條指令。

for (long i = 0; i <= 0xffffffff; i++) {
        __asm__ (
            "add r8, 0x1;"
            "mov [rdi], r8;"
        );
    }

本次執行時間,跟基礎測試基本無差異。
說明新加入的兩條指令,和基準測試用的空 for 循環,被「併發」 執行了,因此並無增長執行時間。

$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c
$ time ./asm
a = 4294967296
b = 0
c = 0
d = 0

real    0m1.381s
user    0m1.381s
sys     0m0.000s

再加入內存讀

這個例子,也就是上一篇中優化 LuaJIT 時碰到的狀況。
新加入的內存讀,跟原有的內存寫,構成了數據依賴。

for (long i = 0; i <= 0xffffffff; i++) {
        __asm__ (
            "mov r8, [rdi];"
            "add r8, 0x1;"
            "mov [rdi], r8;"
        );
    }

再來看執行時間,此次明顯慢了很是多,是的,流水線阻塞的效果就是這麼感人😅

$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c
$ time ./asm
a = 4294967296
b = 0
c = 0
d = 0

real    0m8.723s
user    0m8.720s
sys     0m0.003s

更多的相似指令

此次咱們加入另外三組相似的指令,每一組都構成同樣的數據依賴。

for (long i = 0; i <= 0xffffffff; i++) {
        __asm__ (
            "mov r8, [rdi];"
            "add r8, 0x1;"
            "mov [rdi], r8;"
            "mov r9, [rsi];"
            "add r9, 0x1;"
            "mov [rsi], r9;"
            "mov r10, [rdx];"
            "add r10, 0x1;"
            "mov [rdx], r10;"
            "mov r11, [rcx];"
            "add r11, 0x1;"
            "mov [rcx], r11;"
        );
    }

咱們再看執行時間,跟上一組幾乎無差異。由於 CPU 的亂序執行,並不會只是在那裏傻等。
反過來講,其實流水線阻塞也不是那麼可怕,有指令阻塞的時候,CPU 仍是能夠乾點別的。

$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c
$ time ./asm
a = 4294967296
b = 4294967296
c = 4294967296
d = 4294967296

real    0m8.675s
user    0m8.674s
sys     0m0.001s

總結

現代 CPU 幾十億個的晶體管,不是用來擺看的,內部其實有很是複雜的電路。
不少軟件層面常見的優化技術,在 CPU 硬件裏也是有大量使用的。

流水線,多發射,這兩個在我我的看來,是屬於很重要的概念,對於軟件工程師來講,也是須要能深刻理解的。不只僅是理解這個機器指令,在 CPU 上是如何執行,也是典型的系統構建思路。
最近有學習一些分佈式事務的知識,其實原理上跟 CPU 硬件系統也是很是的相似。

多動手實踐,仍是頗有好處的。

其餘知識點:

  1. CPU 的 L1 cache,指令和數據是分開緩存的,這樣不容易緩存失敗。

參考資料:

https://techdecoded.intel.io/...

相關文章
相關標籤/搜索