1. 程式人生 > >處理器的亂序和併發執行

處理器的亂序和併發執行

目前的高階處理器,為了提高內部邏輯元件的利用率以提高執行速度,通常會採用多指令發射、亂序執行等各種措施。現在普遍使用的一些超標量處理器通常
能夠在一個指令週期內併發執行多條指令。處理器從L1
I-Cache預取了一批指令後,就會分析找出那些互相沒有關聯可以併發執行的指令,然後送到幾個獨立的執行單元進行併發執行。比如下面這樣的程式碼(假定
編譯器不做優化): 
z = x + y;
p = m + n;
CPU就有可能將這兩行無關程式碼分別送到兩個算術單元去同時執行。像Freescale的MPC8541這種嵌入式處理器一個指令週期能夠載入4條指令、發射2條指令到流水線、用5個獨立的執行單元來併發執行。 
通常來說訪存指令(由LSU單元執行)所需要的指令週期可能很多(可能要幾十甚至上百個週期),而一般的算術指令通常在一個指令週期就搞

定。所以有 可能程式碼中的訪存指令耗費了多個週期完成執行後,其他幾個執行單元可能已經把後面有多條邏輯上無關的算術指令都執行完了,這就產生了亂序。
另外訪存指令之間也存在亂序的問題。高階的CPU可以根據自己Cache的組織特性,將訪存指令重新排序執行。訪問一些連續地址的可能會
先執行,因
為這時候Cache命中率高。有的還允許訪存的Non-blocking,即如果前面一條訪存指令因為Cache不命中,造成長延時的儲存訪問時,後面的
訪存指令可以先執行以便從Cache取數。對寫指令的訪存亂序有可能造成的錯誤後果,所以處理器通常有專門的機制(通常是做了個緩衝)保證在出現異常或者
錯誤的時候,可以丟棄異常點後面的寫指令的結果不做寫入。 

處理器的分支預測功能也能引起併發執行。處理器的分支預測單元有可能直接把兩條分支的指令都預取來一塊併發執行掉。等到分支判斷的結果出來以後,再丟棄錯誤分支的計算結果。這樣在很多情況下可以實現0週期跳轉。比如這樣的程式碼(假定編譯器不做優化): 
z = x + y; 
if (z > 0) then
    p = m + n;
else
    p = m - n;
看上去如果z不計算出來是無法繼續的。但是實際上CPU有可能先把三個加法都同時進行計算,然後根據z=x+y的結果直接挑選正確的p值。 
因此,即使是從彙編上看順序正確的指令,其執行的順序也是不可預知的。處理器能夠保證併發和亂序執行不會得到錯誤結果,但是如果是對一些硬

件暫存器
的操作不能允許亂序的話,程式設計師就必須把這個情況告訴CPU。告訴的方法就是通過CPU提供的一組同步指令實現,通常在CPU的文件裡面有對同步指令的使
用說明。系統函式庫裡面的記憶體屏障(rmb/wmb/mb)實際上也是通過這些同步指令實現的。因此在C編碼的時候,只要設定好記憶體屏障,就能告訴CPU
哪些程式碼是不能亂序的。 
編譯器的亂序優化
受到處理器預取單元的能力限制,處理器每次只能分析一小塊指令的併發性,如果指令相隔比較遠就無能為力了。但是從編譯器的角度來看,編譯器能夠對很
大一個範圍的程式碼進行分析,能夠從更大的範圍內分辨出可以併發的指令,並將其儘量靠近排列讓處理器更容易預取和併發執行,充分利用處理器的亂序併發功能。
所以現代的高效能編譯器在目標碼優化上都具備對指令進行亂序優化的能力。並且可以對訪存的指令進行進一步的亂序,減少邏輯上不必要的訪存,以及儘量提高
Cache命中率和CPU的LSU(load/store
unit)的工作效率。所以在開啟編譯器優化以後,看到生成的彙編碼並不嚴格按照程式碼的邏輯順序是正常的。和處理器一樣,如果想要告訴編譯器不要去對某些
指令亂序優化,也要通過一些方式來告訴編譯器。通常可以通過volatile關鍵字來抑制(注意,不是禁止)編譯器對相關變數的訪問優化。舉個例子: 
int *p, *q; 
......; 
*p = 1; 
*p = 2; 
*q = *p;
這樣,編譯器通常會優化掉前面一個對*p的寫入(邏輯上冗餘),僅對*p寫入2。而對*q賦值的時候,編譯器認為此時*q的結果就應該是上次*p的值,會優化掉從*p取數的過程,直接把在暫存器中儲存的*p的值給*q(PowrPC彙編): 
(假設r3=p,r4=q) 
li   r5, 2      // r5賦值2 
stw  r5, 0(r3)  // 把r5寫到*p 
stw  r5, 0(r4)  // 把r5寫到*q
但是如果為p指標加上了volatile關鍵字,情況就不同了: 
volatile int *p; 
int *q; 
......; 
*p = 1; 
*p = 2; 
*q = *p;
在這種情況下,編譯器看見*p是volatile的時候,就會: 

不對*p操作生成亂序指令(通常如此,具體請看後面的解釋) 

每次從*p取資料的時候,一定會進行一次訪存操作,哪怕前面不久才取過*p的值放在暫存器裡。 

不合並對*p的寫操作(也只是通常如此,解釋見後)
所以這回的結果如下(PowrPC彙編): 
(假設r3=p,r4=q) 
li   r5, 1      // r5賦值1 
stw  r5, 0(r3)  // 把r5寫到*p 
li   r5, 2      // r5賦值2 
stw  r5, 0(r3)  // 把r5寫到*p 
lwz  r5, 0(r3)  // 從*p取值到r5 
stw  r5, 0(r4)  // 把r5寫到*q
這樣編譯器會在彙編碼級別保證指令有序和不優化掉訪存操作。通常簡單地使用volatile關鍵字就可以解決編譯器的亂序問題,但是這些指令到了處理器執行的時候,仍然可能被亂序。對於處理器亂序執行的避免就需要用到一組記憶體屏障函式(barrier)了。 
重要 
絕大多數的編譯器,通常不會優化掉對volatile物件的訪問,並且通常保持同一個volatile物件的一系列讀寫操作是有序的(但是不能保證不同的volatile物件之間有序)。 
但是,這不是絕對的。因為ANSI
C99標準關於對volatile物件訪問時編譯器是否要絕對保證禁止亂序(reorder)和禁止訪問合併(combine
access)並沒有做任何規定!僅僅是鼓勵編譯器最好不要去優化對volatile物件的訪問,而唯一的強制要求僅僅是要求編譯器保證對
volatile物件的訪問優化不會跨越“sequence point”即可(所謂sequence
point是指一些諸如外部函式呼叫、條件或迴圈跳轉等關鍵點,具體定義請查閱C99標準內的詳細說明)。 
這就是說,如果一個編譯器在兩個sequence point之間像對待普通變數一樣去優化volatile變數,也是完全符合C99標準的!比如:
volatile int a;
if (...) { ... }  // sequence point
a = 1;
a = 2;
a = 3;
printk("...");    // sequence point
在兩個sequence
point之間,要是有編譯器對a的賦值操作合併(即僅寫入3)或者亂序(如寫1和寫2對調),都是完全符合C99標準的。所以,我們在使用的時候,不能
指望用了volatile以後絕對能生成有序的完整的彙編碼,即不要指望volatile來保證訪存有序。實質上
volatile最大的作用主要還是在保證每次使用從記憶體中取值,而並不能保證編譯器不做其他任何優化(畢竟volatile從字面上看意思是“易變”而
不是“有序”。編譯器只保證對volatile物件即時更新但不保證訪問有序也不是說不過去的)。
從另一個角度看,即使是編譯器生成的彙編碼有序,處理器也不一定能保證有序。就算編譯器生成了有序的彙編碼,到了處理器那裡也拿不準是不
是會按照程式碼順序執行。所以就算編譯器保證有序了,程式設計師也還是要往程式碼裡面加記憶體屏障才能保證絕對訪存有序,這倒不如編譯器乾脆不管算了,因為記憶體屏障
本身就是一個sequence point,加入後已經能夠保證編譯器也有序。 
因此,對於切實是需要保障訪存順序的程式碼,就算當前使用的編譯器能夠編譯出有序的目標碼來,我們也還是必須通過設定記憶體屏障的方式來保證有序,否則都是不嚴謹,有隱患的。
Barrier屏障函式
Barrier函式可以在程式碼中設定屏障,這個屏障可以阻擋編譯器的優化,也可以阻擋處理器的優化。 
對於編譯器來說,設定任何一個屏障都可以保證: 

編譯器的亂序優化不會跨越屏障,即屏障前後的程式碼不會亂序; 

在屏障後所有對變數或者地址的操作,都會重新從記憶體中取值(相當於重新整理暫存器中的變數副本)。
而對於處理器來說,根據不同的屏障有不同的表現(以下僅僅列舉3種最簡單的屏障): 

讀屏障rmb() 
處理器對讀屏障前後的取數指令(LOAD)能保證有序,但是不一定能保證其他算術指令或者是寫指令的有序。對於讀指令的執行完成時間也不能保證,即它不能保證在屏障之前的讀指令一定都執行完成,只能保證屏障之前的讀指令一定能在屏障之後的讀指令之前完成。 

寫屏障wmb() 
處理器對屏障前後的寫指令(STORE)能保證有序,但是不一定能保證其他算術指令或者是讀指令的有序。對於寫指令的執行完成時間也不能保證,即它不能保證在屏障之前的寫指令一定都執行完成,只能保證屏障之前的寫指令一定能在屏障之後的寫指令之前完成。 

通用記憶體屏障mb() 
處理器保障只有屏障之前的訪存操作(包括讀寫)都完
成以後才會執行屏障之後的訪存操作。即可以保障讀寫之間的有序(但是同樣無法保證指令完成的時
間)。這種屏障對處理器的執行單元效率產生的負面影響要比單純用讀屏障或者寫屏障來的大。比如對於PowerPC來說這種通用屏障通常是使用sync指令
實現的,在這種情況下處理器會丟棄所有預取的指令並清空流水線。所以頻繁使用記憶體屏障會降低處理器執行單元的效率。 
對於驅動開發者來說,一些對裝置暫存器的操作,通常是必須保證有序的。在絕大部分情況下,一般都是寫操作。對於有序的寫操作,必須設定寫屏障(wmb): 
例:在驅動中使用寫屏障 
/* Mask out everything */ 
im_intctl->ic_simrh = 0x00000000; 
im_intctl->ic_simrl = 0x00000000;wmb();  /* Ack everything */ 
im_intctl->ic_sipnrh = 0xffffffff;
im_intctl->ic_sipnrl = 0xffffffff;
這是一個對中斷控制器操作的例子。在設定兩個mask暫存器的值的時候,這兩個寫操作沒有順序要求,因此可以不加屏障。但是對ack暫存器的設定必須在mask暫存器完成設定以後,所以在中間要加入寫屏障wmb()以保證對兩組暫存器的寫有序。 
同樣的,對於一系列的只讀操作,也可以簡單使用rmb()來保證有序。 
注意 
任何一個rmb()或者wmb()都是可以被替換成mb()的。但是因為上面提到過的mb()的效率問題,所以應該只有在同時需要讀屏障和寫屏障的
時候,才建議使用mb()。否則應該根據實際情況來選擇合適的屏障。當然,在裝置初始化的時候,即使是使用mb()也不會對效能帶來什麼影響,因為裝置一
般只會初始化一次。但是在發生很頻繁的裝置操作(比如網口的收發幀中斷等)時,應該考慮到mb()對效能的影響。 
如果驅動不僅僅需要在單純的讀指令或者寫指令之間有序,還需要保證讀寫指令之間有序的時候,就需要設定mb()屏障了。下面將演示一個這樣的例子: 
例:使用mb()屏障保證讀寫有序 
我們假設有一個裝置,在讀取裝置資訊時需要依次對REG1~3這三個暫存器進行寫入操作(寫入裝置讀取命令),然後才能依次讀取REG4和REG5取得裝置返回的資訊。 
REG1 = a; 
wmb();  // 保證REG1和REG2的寫有序
REG2 = b; 
wmb();  // 保證REG2和REG3的寫有序
REG3 = c;
mb();   // 保證在對裝置讀之前,前面的配置操作都完成(讀寫之間有序)
*d = REG4; 
rmb();  // 保證REG4和REG5的讀有序
*e = REG5;
mb();   // 保證與未來對裝置的操作有序 
return;

  • 對於REG1~3的寫入,可以通過設定寫屏障來保證有序; 

  • 在進行REG4和5的讀取之前,因為得保證前面的暫存器寫操作都執行完才能讀,所以需要設定一個記憶體屏障mb()來保證前面對暫存器的寫都完成,以保障讀寫指令之間的有序; 

  • 後面兩個讀操作之間就可以通過設定讀屏障來保證有序了; 

  • 最後通常在從裝置操作函式返回之前,我們一般需要保證對裝置的操作都執行完畢了。這樣下次對裝置進行操作的時候我們可以保證裝置已經完成了上次操作,避免反覆呼叫裝置操作函式帶來的函式間的亂序問題。所以在最後設定一個記憶體屏障mb(),保障和未來對裝置的其他訪問有序。

相關推薦

處理器併發執行

目前的高階處理器,為了提高內部邏輯元件的利用率以提高執行速度,通常會採用多指令發射、亂序執行等各種措施。現在普遍使用的一些超標量處理器通常能夠在一個指令週期內併發執行多條指令。處理器從L1I-Cache預取了一批指令後,就會分析找出那些互相沒有關聯可以併發執行的指令,然

TCP的丟包判斷(附Reordering更新演算法)-理論

又到了週末,生物鐘準時在午夜讓我恍驚起而長嗟,一想到TCP,恍如昨日,也不知怎麼就千里迢迢之後心依舊茫然,算是拾起來的東西吧,就坐下來再寫點關於TCP的東西。由於最近在追《龍珠超》,也是很想寫點關於龍珠的隨筆,也只能等到明天我被我的偶像弗利薩(目標明確,乾淨利索

深入探索併發程式設計系列(五)-將記憶體逮個正著

當用C/C++編寫無鎖程式碼時,一定要小心謹慎,以保證正確的記憶體順序。不然的話,會發生一些詭異的事情。 Intel在x86/x64體系結構手冊的Volume 3, §8.2.3 中列出了一些可能會發生的詭異的事情。這裡介紹其中一個最簡單的例子。假設在記憶體中有兩個整型變數x和y,都初始化

JAVA多執行併發面試問題

1. 程序和執行緒之間有什麼不同? 一個程序是一個獨立(self contained)的執行環境,它可以被看作一個程式或者一個應用。而執行緒是在程序中執行的一個任務。Java執行環境是一個包含了不同的類和程式的單一程序。執行緒可以被稱為輕量級程序。執行緒需要較少的資源來建立和駐留在程

【TestNG】TestNG併發執行用例詳解範例

前言 TestNG有多種併發方式支援,方法的併發,class級的併發,test級的併發等; 根據實際應用可以靈活的配置和使用,下面分別對幾種併發方法進行說明: 一、方法級併發 方法級併發即method級併發,此種併發方式需要將xml中的suite標籤的parallel屬性設定為m

java併發執行

volatile—保證可見性、禁止指令重排序,不保證原子性 出於執行速率的考慮,java編譯器會把經常訪問的變數存放在快取,直接從快取中讀取變數,多執行緒下記憶體與快取不一樣 volatile不會被快取到暫存器,多執行緒下可見 使用條件: 只有單個執行緒更新變數的值 該變數不與

Python框架下django 的併發執行

django 的併發能力真的是令人擔憂,django本身框架下只有一個執行緒在處理請求,任何一個請求阻塞,就會影響另一個情感求的響應,尤其是涉及到IO操作時,基於框架下開發的檢視的響應並沒有對應的開啟多執行緒,再者Python的多執行緒對於多核CPU有效利用率非常低,參照 這裡就使用 nginx

(WCF) 多執行緒 (Multi-threading) 併發性 (Concurency)

問題:WCF 有個Server端,還有個Client端,他們之間是如何進行併發,多執行緒通訊的呢?多個Client端同時訪問Server,如何保證Server端的操作執行緒安全呢?   在理解WCF Concurency之前,首先需要理解 WCF instance management &nb

網路程式設計併發之多執行緒程式設計

多執行緒threading 執行緒與程序的區別可以歸納為以下4點:   1)地址空間和其它資源(如開啟檔案):程序間相互獨立,同一程序的各執行緒間共享。某程序內的執行緒在其它程序不可見。   2)通訊: 程序間通訊 IPC,執行緒間可以直接讀寫程序資料段(如全域性變數)來進行通訊——

Java面試:投行的15個多執行併發面試題

多執行緒和併發問題已成為各種 Java 面試中必不可少的一部分。如果你準備參加投行的 Java 開發崗位面試,比如巴克萊銀行(Barclays)、花旗銀行(Citibank)、摩根史坦利投資公司(Morgan Stanley),你會遇到很多有關多執行緒的面試題。多執行緒和併發

C++程式設計思想 第2卷 第11章 併發 執行緒間協作 等待訊號

在ZThread庫中 使用互斥鎖並允許任務掛起的基類是Condition 可以在條件Condition上呼叫wait()掛起一個任務 WaxOMatic.cpp有兩個程序 一個程序給Car上蠟 另一個程序給Car拋光 拋光程序在上臘程序完成前不能進行工作 並且上臘程序在汽

python-同步非同步、阻塞非阻塞、序列並行、並行併發、密集型、執行程序的相關概念

1. 同步和非同步   關注的是訊息的通訊機制,描述的是一種行為方式,是多個任務之間的關係。 ① 同步: 呼叫者主動等待被呼叫方返回結果,在沒有返回結果之前,就一直專職等待。 千萬不要把計算機中“同步”理解成“同時執行”。 ② 非同步:呼叫者傳送請求請求,不會專職等待

投行的 15 個多執行併發面試題——你都會嗎?

多執行緒和併發問題已成為各種 Java 面試中必不可少的一部分。如果你準備參加投行的 Java 開發崗位面試,比如巴克萊銀行(Barclays)、花旗銀行(Citibank)、摩根史坦利投資公司(Morgan Stanley),你會遇到很多有關多執行緒的面試題。多執行緒和併發

網路程式設計實驗四——利用多程序執行緒實現伺服器端的併發處理

一、實驗目的 1.在TCP檔案傳輸程式碼的基礎上,利用多程序實現伺服器端的併發處理。  2.利用多執行緒實現伺服器端的併發處理。 二、實驗原理 併發的面向連線伺服器演算法: 主1、建立套接字並將其繫結到所提供服務的熟知地址上。讓該套接字保持為無連線的。 主2、將

網路程式設計——4.利用多程序執行緒實現伺服器端的併發處理

一、實驗要求     在TCP檔案傳輸程式碼的基礎上,利用單執行緒程序併發模型和多執行緒併發模型實現伺服器端的併發處理。 二、實驗分析     多執行緒與多程序相比,使用多執行緒相比多程序有以下兩個優點:更高的效率和共享儲存器,效率的提高源於上下文切換次數的減少。

《Java併發執行緒介紹》-Java TheadLocal

原文連結 作者:Jakob Jenkov   檢視全部文章 Java中的ThreadLocal類可以讓你建立的變數只被同一個執行緒進行讀和寫操作。因此,儘管有兩個執行緒同時執行一段相同的程式碼,而且這段程式碼又有一個指向同一個ThreadLocal變數的引用,但是這兩個執行緒依然不能看到彼此的

Java併發執行緒介紹目錄

ThreadPoolExecutor.addIfUnderCorePoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; ma

Java併發執行緒介紹

作者:Jakob Jenkov 譯者:Simon-SZ  校對:方騰飛 在過去單CPU時代,單任務在一個時間點只能執行單一程式。之後發展到多工階段,計算機能在同一時間點並行執行多工或多程序。雖然並不是真正意義上的“同一時間點”,而是多個任務或程序共享一個CPU,並交由作業系統來完成多工間對C

JAVA多執行併發基礎面試問答

原文連結 譯文連線 作者:Pankaj  譯者:鄭旭東  校對:方騰飛 多執行緒和併發問題是Java技術面試中面試官比較喜歡問的問題之一。在這裡,從面試的角度列出了大部分重要的問題,但是你仍然應該牢固的掌握Java多執行緒基礎知識來對應日後碰到的問題。(校對注:非常贊同這個觀點) Java多執

(十)java併發程式設計--建立啟動執行緒(java.lang.Thread 、java.lang.Runnable)

執行緒建立的幾種方式. 建立和啟動一個執行緒 建立一個執行緒. Thread thread = new Thread(); 啟動java執行緒. thread.start(); 這兩個例子並沒有執行執行緒執行體,執行緒將會啟動後然後