計算機組成原理筆記(三)


我的博客 : https://www.luozhiyun.com/

超線程

超線程的CPU,其實是把一個物理層面CPU核心,“偽裝”成兩個邏輯層面的CPU核心。這個CPU,會在硬件層面增加很多電路,使得我們可以在一個CPU核心內部,維護兩個不同線程的指令的狀態信息。

比如,在一個物理CPU核心內部,會有雙份的PC寄存器、指令寄存器乃至條件碼寄存器。這樣,這個CPU核心就可以維護兩條並行的指令的狀態。

超線程並不是真的去同時運行兩個指令,超線程的目的,是在一個線程A的指令,在流水線里停頓的時候,讓另外一個線程去執行指令。因為這個時候,CPU的譯碼器和ALU就空出來了,那么另外一個線程B,就可以拿來干自己需要的事情。這個線程B可沒有對於線程A里面指令的關聯和依賴。

所以超線程只在特定的應用場景下效果比較好。一般是在那些各個線程“等待”時間比較長的應用場景下。比如,我們需要應對很多請求的數據庫應用,就很適合使用超線程。各個指令都要等待訪問內存數據,但是並不需要做太多計算。

SIMD加速矩陣乘法

SIMD,中文叫作單指令多數據流(Single Instruction Multiple Data)。

下面是兩段示例程序,一段呢,是通過循環的方式,給一個list里面的每一個數加1。另一段呢,是實現相同的功能,但是直接調用NumPy這個庫的add方法。

$ python
>>> import numpy as np
>>> import timeit
>>> a = list(range(1000))
>>> b = np.array(range(1000))
>>> timeit.timeit("[i + 1 for i in a]", setup="from __main__ import a", number=1000000)
32.82800309999993
>>> timeit.timeit("np.add(1, b)", setup="from __main__ import np, b", number=1000000)
0.9787889999997788
>>>

兩個功能相同的代碼性能有着巨大的差異,足足差出了30多倍。原因就是,NumPy直接用到了SIMD指令,能夠並行進行向量的操作。

使用循環來一步一步計算的算法呢,一般被稱為SISD,也就是單指令單數據(Single Instruction Single Data)的處理方式。如果你手頭的是一個多核CPU呢,那么它同時處理多個指令的方式可以叫作MIMD,也就是多指令多數據(Multiple Instruction Multiple Dataa)。

Intel在引入SSE指令集的時候,在CPU里面添上了8個 128 Bits的寄存器。128 Bits也就是 16 Bytes ,也就是說,一個寄存器一次性可以加載 4 個整數。比起循環分別讀取4次對應的數據,時間就省下來了。

在數據讀取到了之后,在指令的執行層面,SIMD也是可以並行進行的。4個整數各自加1,互相之前完全沒有依賴,也就沒有冒險問題需要處理。只要CPU里有足夠多的功能單元,能夠同時進行這些計算,這個加法就是4路同時並行的,自然也省下了時間。

所以,對於那些在計算層面存在大量“數據並行”(Data Parallelism)的計算中,使用SIMD是一個很划算的辦法。

異常和中斷

異常

關於異常,它其實是一個硬件和軟件組合到一起的處理過程。異常的前半生,也就是異常的發生和捕捉,是在硬件層面完成的。但是異常的后半生,也就是說,異常的處理,其實是由軟件來完成的。

計算機會為每一種可能會發生的異常,分配一個異常代碼(Exception Number)。有些教科書會把異常代碼叫作中斷向量(Interrupt Vector)。

異常發生的時候,通常是CPU檢測到了一個特殊的信號。這些信號呢,在組成原理里面,我們一般叫作發生了一個事件(Event)。CPU在檢測到事件的時候,其實也就拿到了對應的異常代碼。

這些異常代碼里,I/O發出的信號的異常代碼,是由操作系統來分配的,也就是由軟件來設定的。而像加法溢出這樣的異常代碼,則是由CPU預先分配好的,也就是由硬件來分配的。

拿到異常代碼之后,CPU就會觸發異常處理的流程。計算機在內存里,會保留一個異常表(Exception Table)。我們的CPU在拿到了異常碼之后,會先把當前的程序執行的現場,保存到程序棧里面,然后根據異常碼查詢,找到對應的異常處理程序,最后把后續指令執行的指揮權,交給這個異常處理程序。

異常的分類:中斷、陷阱、故障和中止

第一種異常叫中斷(Interrupt)。顧名思義,自然就是程序在執行到一半的時候,被打斷了。
第二種異常叫陷阱(Trap)。陷阱,其實是我們程序員“故意“主動觸發的異常。就好像你在程序里面打了一個斷點,這個斷點就是設下的一個"陷阱"。
第三種異常叫故障(Fault)。比如,我們在程序執行的過程中,進行加法計算發生了溢出,其實就是故障類型的異常。
最后一種異常叫中止(Abort)。與其說這是一種異常類型,不如說這是故障的一種特殊情況。當CPU遇到了故障,但是恢復不過來的時候,程序就不得不中止了。

異常的處理:上下文切換

在實際的異常處理程序執行之前,CPU需要去做一次“保存現場”的操作。有了這個操作,我們才能在異常處理完成之后,重新回到之前執行的指令序列里面來。

因為異常情況往往發生在程序正常執行的預期之外,比如中斷、故障發生的時候。所以,除了本來程序壓棧要做的事情之外,我們還需要把CPU內當前運行程序用到的所有寄存器,都放到棧里面。最典型的就是條件碼寄存器里面的內容。

像陷阱這樣的異常,涉及程序指令在用戶態和內核態之間的切換。對應壓棧的時候,對應的數據是壓到內核棧里,而不是程序棧里。

虛擬機技術

解釋型虛擬機

我們把原先的操作系統叫作宿主機(Host),把能夠有能力去模擬指令執行的軟件,叫作模擬器(Emulator),而實際運行在模擬器上被“虛擬”出來的系統呢,我們叫客戶機(Guest VM)。

例如在windows上跑的Android模擬器,或者能在Windows下運行的游戲機模擬器。

這種解釋執行方式的最大的優勢就是,模擬的系統可以跨硬件。比如,Android手機用的CPU是ARM的,而我們的開發機用的是Intel X86的,兩邊的CPU指令集都不一樣,但是一樣可以正常運行。

缺陷:
第一個是,我們做不到精確的“模擬”。很多的老舊的硬件的程序運行,要依賴特定的電路乃至電路特有的時鍾頻率,想要通過軟件達到100%模擬是很難做到的。
第二個是這種解釋執行的方式,性能實在太差了。因為我們並不是直接把指令交給CPU去執行的,而是要經過各種解釋和翻譯工作。

Type-1和Type-2虛擬機

如果我們需要一個“全虛擬化”的技術,可以在現有的物理服務器的硬件和操作系統上,去跑一個完整的、不需要做任何修改的客戶機操作系統(Guest OS),有一個很常用的一個解決方案,就是加入一個中間層。在虛擬機技術里面,這個中間層就叫作虛擬機監視器,英文叫VMM(Virtual Machine Manager)或者Hypervisor。

Type-2虛擬機

在Type-2虛擬機里,我們上面說的虛擬機監視器好像一個運行在操作系統上的軟件。你的客戶機的操作系統呢,把最終到硬件的所有指令,都發送給虛擬機監視器。而虛擬機監視器,又會把這些指令再交給宿主機的操作系統去執行。

Type-1虛擬機

在數據中心里面用的虛擬機,我們通常叫作Type-1型的虛擬機。客戶機的指令交給虛擬機監視器之后呢,不再需要通過宿主機的操作系統,才能調用硬件,而是可以直接由虛擬機監視器去調用硬件。

在Type-1型的虛擬機里,我們的虛擬機監視器其實並不是一個操作系統之上的應用層程序,而是一個嵌入在操作系統內核里面的一部分。

Docker

在我們實際的物理機上,我們可能同時運行了多個的虛擬機,而這每一個虛擬機,都運行了一個屬於自己的單獨的操作系統。多運行一個操作系統,意味着我們要多消耗一些資源在CPU、內存乃至磁盤空間上。

在服務器領域,我們開發的程序都是跑在Linux上的。其實我們並不需要一個獨立的操作系統,只要一個能夠進行資源和環境隔離的“獨立空間”就好了。

通過Docker,我們不再需要在操作系統上再跑一個操作系統,而只需要通過容器編排工具,比如Kubernetes或者Docker Swarm,能夠進行各個應用之間的環境和資源隔離就好了。

存儲器

SRAM

SRAM(Static Random-Access Memory,靜態隨機存取存儲器),被用在CPU Cache中。

SRAM之所以被稱為“靜態”存儲器,是因為只要處在通電狀態,里面的數據就可以保持存在。而一旦斷電,里面的數據就會丟失了。在SRAM里面,一個比特的數據,需要6~8個晶體管。所以SRAM的存儲密度不高。同樣的物理空間下,能夠存儲的數據有限。不過,因為SRAM的電路簡單,所以訪問速度非常快。

在CPU里,通常會有L1、L2、L3這樣三層高速緩存。每個CPU核心都有一塊屬於自己的L1高速緩存。

L2的Cache同樣是每個CPU核心都有的,不過它往往不在CPU核心的內部。所以,L2 Cache的訪問速度會比L1稍微慢一些。

L3 Cache,則通常是多個CPU核心共用的,尺寸會更大一些,訪問速度自然也就更慢一些。

DRAM

內存用的芯片是一種叫作DRAM(Dynamic Random Access Memory,動態隨機存取存儲器)的芯片,比起SRAM來說,它的密度更高,有更大的容量,而且它也比SRAM芯片便宜不少。

DRAM被稱為“動態”存儲器,是因為DRAM需要靠不斷地“刷新”,才能保持數據被存儲起來。DRAM的一個比特,只需要一個晶體管和一個電容就能存儲。所以,DRAM在同樣的物理空間下,能夠存儲的數據也就更多,也就是存儲的“密度”更大。

CPU Cache

目前看來,一次內存的訪問,大約需要120個CPU Cycle,這也意味着,在今天,CPU和內存的訪問速度已經有了120倍的差距。

為了彌補兩者之間的性能差異,我們能真實地把CPU的性能提升用起來,而不是讓它在那兒空轉,我們在現代CPU中引入了高速緩存。

CPU從內存中讀取數據到CPU Cache的過程中,是一小塊一小塊來讀取數據的,而不是按照單個數組元素來讀取數據的。這樣一小塊一小塊的數據,在CPU Cache里面,我們把它叫作Cache Line(緩存塊)。

在我們日常使用的Intel服務器或者PC里,Cache Line的大小通常是64字節。

直接映射Cache(Direct Mapped Cache)

對於讀取內存中的數據,我們首先拿到的是數據所在的內存塊(Block)的地址。而直接映射Cache采用的策略,就是確保任何一個內存塊的地址,始終映射到一個固定的CPU Cache地址(Cache Line)。而這個映射關系,通常用mod運算(求余運算)來實現。

比如說,我們的主內存被分成0~31號這樣32個塊。我們一共有8個緩存塊。用戶想要訪問第21號內存塊。如果21號內存塊內容在緩存塊中的話,它一定在5號緩存塊(21 mod 8 = 5)中。

在對應的緩存塊中,我們會存儲一個組標記(Tag)。這個組標記會記錄,當前緩存塊內存儲的數據對應的內存塊,而緩存塊本身的地址表示訪問地址的低N位。

除了組標記信息之外,緩存塊中還有兩個數據。一個自然是從主內存中加載來的實際存放的數據,另一個是有效位(valid bit)。啥是有效位呢?它其實就是用來標記,對應的緩存塊中的數據是否是有效的,確保不是機器剛剛啟動時候的空數據。如果有效位是0,無論其中的組標記和Cache Line里的數據內容是什么,CPU都不會管這些數據,而要直接訪問內存,重新加載數據。

CPU在讀取數據的時候,並不是要讀取一整個Block,而是讀取一個他需要的整數。這樣的數據,我們叫作CPU里的一個字(Word)。具體是哪個字,就用這個字在整個Block里面的位置來決定。這個位置,我們叫作偏移量(Offset)。

一個內存的訪問地址,最終包括高位代表的組標記、低位代表的索引,以及在對應的Data Block中定位對應字的位置偏移量。

如果內存中的數據已經在CPU Cache里了,那一個內存地址的訪問,就會經歷這樣4個步驟:

  1. 根據內存地址的低位,計算在Cache中的索引;
  2. 判斷有效位,確認Cache中的數據是有效的;
  3. 對比內存訪問地址的高位,和Cache中的組標記,確認Cache中的數據就是我們要訪問的內存數據,從Cache Line中讀取到對應的數據塊(Data Block);
  4. 根據內存地址的Offset位,從Data Block中,讀取希望讀取到的字。

CPU高速緩存的寫入

每一個CPU核里面,都有獨立屬於自己的L1、L2的Cache,然后再有多個CPU核共用的L3的Cache、主內存。

寫直達(Write-Through)

最簡單的一種寫入策略,叫作寫直達(Write-Through)。在這個策略里,每一次數據都要寫入到主內存里面。在寫直達的策略里面,寫入前,我們會先去判斷數據是否已經在Cache里面了。如果數據已經在Cache里面了,我們先把數據寫入更新到Cache里面,再寫入到主內存里面;如果數據不在Cache里,我們就只更新主內存。

這個策略很慢。無論數據是不是在Cache里面,我們都需要把數據寫到主內存里面。

寫回(Write-Back)

如果發現我們要寫入的數據,就在CPU Cache里面,那么我們就只是更新CPU Cache里面的數據。同時,我們會標記CPU Cache里的這個Block是臟(Dirty)的。所謂臟的,就是指這個時候,我們的CPU Cache里面的這個Block的數據,和主內存是不一致的。

如果我們發現,我們要寫入的數據所對應的Cache Block里,放的是別的內存地址的數據,那么我們就要看一看,那個Cache Block里面的數據有沒有被標記成臟的。如果是臟的話,我們要先把這個Cache Block里面的數據,寫入到主內存里面。

然后,再把當前要寫入的數據,寫入到Cache里,同時把Cache Block標記成臟的。如果Block里面的數據沒有被標記成臟的,那么我們直接把數據寫入到Cache里面,然后再把Cache Block標記成臟的就好了。

MESI協議:讓多核CPU的高速緩存保持一致

MESI協議,是一種叫作寫失效(Write Invalidate)的協議。在寫失效協議里,只有一個CPU核心負責寫入數據,其他的核心,只是同步讀取到這個寫入。在這個CPU核心寫入Cache之后,它會去廣播一個“失效”請求告訴所有其他的CPU核心。其他的CPU核心,只是去判斷自己是否也有一個“失效”版本的Cache Block,然后把這個也標記成失效的就好了。

MESI協議對Cache Line的四個不同的標記,分別是:

  • M:代表已修改(Modified)
  • E:代表獨占(Exclusive)
  • S:代表共享(Shared)
  • I:代表已失效(Invalidated)

所謂的“已修改”,就是我們上一講所說的“臟”的Cache Block。Cache Block里面的內容我們已經更新過了,但是還沒有寫回到主內存里面。

所謂的“已失效“,自然是這個Cache Block里面的數據已經失效了,我們不可以相信這個Cache Block里面的數據。

在獨占狀態下,對應的Cache Line只加載到了當前CPU核所擁有的Cache里。其他的CPU核,並沒有加載對應的數據到自己的Cache里。這個時候,如果要向獨占的Cache Block寫入數據,我們可以自由地寫入數據,而不需要告知其他CPU核。

在獨占狀態下的數據,如果收到了一個來自於總線的讀取對應緩存的請求,它就會變成共享狀態。這個共享狀態是因為,這個時候,另外一個CPU核心,也把對應的Cache Block,從內存里面加載到了自己的Cache里來。

在共享狀態下,因為同樣的數據在多個CPU核心的Cache里都有。所以,當我們想要更新Cache里面的數據的時候,不能直接修改,而是要先向所有的其他CPU核心廣播一個請求,要求先把其他CPU核心里面的Cache,都變成無效的狀態,然后再更新當前Cache里面的數據。這個廣播操作,一般叫作RFO(Request For Ownership),也就是獲取當前對應Cache Block數據的所有權。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM