Prefix Scan
prefix scan 是計算一個數值 vector 所有的部份和,每個結果 element 是原始 vector 對應位置之前數值的和,包含自己的稱為 inclusive prefix scan,不包含稱為 exclusive prefix scan。例如 [3, 5, 4, 10, 8] 的 inclusive prefix scan 是 [3, 8, 12, 22, 30],exclusive 是 [0, 3, 8, 12, 22]。
prefix scan 是計算一個數值 vector 所有的部份和,每個結果 element 是原始 vector 對應位置之前數值的和,包含自己的稱為 inclusive prefix scan,不包含稱為 exclusive prefix scan。例如 [3, 5, 4, 10, 8] 的 inclusive prefix scan 是 [3, 8, 12, 22, 30],exclusive 是 [0, 3, 8, 12, 22]。
Bob 大叔的書之一《The Clean Coder》,算是《Clean Code》的續集吧。
《Clean Code》講怎麼寫 code,《The Clean Coder》講怎麼當 clean coder。記錄些心有戚戚焉的片段跟感想。
設計「易於測試的程式碼」(p.48)
我想設計良好的程式應該也會好測試?從設計還是從測試出發,最後目標是一樣的,偏好哪種我覺得沒什麼差。
到目前為止,我大多從設計出發。先想概略設計、class 的責任、class 之間的關係等等,接著實作會邊寫功能邊寫測試,但不是 TDD。通常是實作一小塊、寫那一小塊的測試,而不是先寫測試才實作。即使從設計出發還是需要測試的,因為可以確保程式是對的,之後也不怕修改。
沒有實際用 TDD 寫 code,只在 dojo 小小玩過,所以說不上從測試出發是不是會導向好設計,能想像的只有因為要測試所以功能切分上應該不會太糟。
用 concurrency 就是為了更快,為了比 sequential 程式快,不然搞得那麼複雜幹嘛呢。
如何評估 concurrent 程式的效能提升?
speedup = sequential 程式執行時間 / concurrent 程式執行時間
speedup 可能隨著使用的 core 數改變,標示程式在不同 core 數上的 speedup,可以由不同 core 數的 speedup 變化知道程式的 scalability。speedup 的提升隨著使用 core 數增加而增加甚至比例接近 core 增加的數量,表示有好的 scalability。理想狀況是 core 數加倍 speedup 也加倍。
有時會發生 speedup 的提升超過 core 的增加數量,稱為 superlinear speedup。這通常是有問題,要再確認 concurrent 程式執行結果是否正確、是否使用一般情境下的 dataset 而非只是測試資料。superlinear speedup 常見的原因是資料量太小,sequential 程式因為不斷需要新資料而被清掉的 cache 反而在 concurrent 程式中因為各 core 處理的資料量太小而 cache 住,自然 concurrent 程式在資料讀取上會比較快,造成效能變超好的錯覺。(假的!)
如果已有 sequential 程式、要決定是否 concurrent 化,能事先估算效能提升比較好,畢竟 concurrent 程式複雜也需要投入成本開發,如果效能提升不好就白費工夫了。
接下來兩個定理皆假設一個 core 上只執行一個 thread,如果一個 core 上執行多個 thread,就要把下面公式中的 core 數換成 thread 的數量。
設計 concurrent algorithm 後當然要確認正確性。在《Principles of Concurrent and Distributed Programming》中 Ben-Ari 定義了為驗證 concurrent algorithm 各種特性的抽象結構(concurrency abstraction),可分為以下四個部份:
concurrency 是如何拆分程式成多個獨立的工作,讓這些工作可以一起「正在進行(in progress)」但不一定要「同時執行」。「正在進行」是多個工作可以輪流到 CPU(或者 core)上執行,雖然不是同時執行但這些工作都是正在進行中的。而 parallelism 則是多個工作「同時執行」,也就是實際上必須要有多個 core 能同時執行工作。
打個比方,有一個廚師要煮一頓飯,他「同時」(實際上是輪流,廚師不會分身術)洗菜、切菜、炒菜跟煮湯,這是 concurrency,把煮一頓飯分成四個工作。現在廚師會分身術喔不是,是有四個廚師,一個洗菜、一個切菜、一個煮湯、一個炒菜,所有人一起動作就是 parallelism(如果同時間只有一個廚師能動作,不算 parallelism)。煮一頓飯也可以拆成炒青菜、煮飯、煮湯三件事,每件事各自從洗到切到煮或炒,這樣也是一個 concurrency solution,而如果有多個廚師一起做,便成了 parallelism。
concurrency 關乎程式結構,parallelism 關乎程式執行。concurrency 是程式或系統怎麼切分成多個工作,而 parallelism 則得要同時「執行」。
concurrency 的中文翻譯是「並行」,parallelism 是「平行」。但「平行」很容易出現在描述裡,常常搞不清楚到底「平行」是指 parallelism 還是只是一個形容?我比較喜歡在需要精準指出是 concurrency 或 parallelism 的時候用原文,單純的形容或者描述用「平行」,除非英文用在中文句子有點怪才會用中文「並行」跟「平行」並且加註英文。
看這篇 paper。
Reactor pattern 處理一個 application 收到從一個或多個 client 同時(concurrently)送 request 的情況。
分散式環境的 server 必須處理多個送 request 過來的 client。為了處理 request,server 必須 demultiplex 以及 dispatch request 給對應的 service。設計 demultiplexing 及 dispatching 需考量:
常常聽到看到 reentrancy 但老是跟 thread-safety 搞得不是太清楚。
reentrancy 是 function 可以在執行過程中被中斷,在上一次執行還沒完成前可以再次進入這個 function 執行而不會有問題,也就是第二次進入 function 執行結束後,第一次的執行仍能正確完成。中斷可以是 jump 或 call,也可以是系統的 interrupt 或 signal。
reentrancy 是 single thread 環境下就有的概念。像是被系統 interrupt 中斷時 interrupt handler 是不是能夠再 call 剛剛執行到一半的 function?反過來說,interrupt handler 能 call 的只有 reentrant function。另外,recursive function 一般會是 reentrancy,但如果它使用了 global 變數就不一定。
reentrancy function 可以是 thread-safe 但不一定是 thread-safe。thread-safe function 可以是 reentrancy 也可以不是(繞口令時間)。function 是 thread-safe 但不是 reentrancy 的例子:
1 | function foo() |
在 single thread 的環境下,如果 foo() 執行到一半被 interrupt 打斷,interrupt handler 又 call foo() 會永遠等不到 mutex unlock,整個卡在那。
達成 reentrancy 的基本原則:
thread-safety 是在 multiple thread 的環境下,thread 間的 shared data 可以在任意執行順序下依然保持正確。
達到 thread-safety 的方式分兩大類,一是避免 race condition,二是 synchronization。
不同的 library、系統或語言可能自己定義 thread-safe 跟 reentrant。
例如 Qt 將 reentrant 以及 thread-safe 都定義在 multiple thread 的環境下。針對 class 來說,Qt 的 thread-safe class 是 multiple thread 可以 call 同一個 class instance 的 member function,而 reentrant class 則是 multiple thread 只要使用不同的 class instance 就能同時 call member function。
這篇是設計 concurrent algorithem 的理論概念,沒什麼例子。
兩種設計 concurrent algorithem 的方法:
task decomposition 的目標是找出完全獨立的計算,才能 concurrent 執行多個 task。但是計算常常互相有關係,這樣的關係稱為 dependency。程式平行化前必須先消除 dependency。
將 sequential 程式轉成 concurrent 程式要確保原本有順序性的步驟也要依序完成,也就是確保 sequential consistency,使得 concurrent 程式在相同的 input 可以得到跟 sequential 程式一樣的結果。
concurrent 程式的基本架構:main thread 定義以及準備 task,產生 thread 執行 task,最後等待 thread 們完成工作。
一種分解 task 的方式是在要進行 concurrent 計算的外層進行分解,接著將 task 分給 thread 執行。另一種是 task 會在計算過程中動態產生,這需要額外程式碼封裝新產生的 task。
Task Decomposition 要考慮:
基本上,要識別出能獨立執行的 task,得依靠對程式以及其計算的理解。要對可能平行化執行的程式模擬兩個 thread 一起執行的狀況(真心覺得這靠練習跟經驗,做多了自然會有莫名的直覺(?)),以此決定 task 是否彼此獨立或有可接受的 dependency。
並行化(concurrent)程式執行最頻繁或者計算最繁重的地方可以有比較好的成效。因為並行化需要修改程式碼、驗證以及debug 等等,也會讓程式變得複雜,當然要選擇最有成效的地方做。除此之外,concurrent 程式會比 sequential 程式多出為了並行化的程式碼,例如管理及分派 task,所以會有 overhead。
找到可以獨立執行的 task 後,拆分 task 需注意:
這兩個條件看起來有點矛盾,因為在總 task 固定的情況下 task 愈大數量愈少,反之亦然,因此 要在 task 計算量及數量間取得平衡。並行化(concurrent)程式最主要的目的是減少執行時間,如果使用所有 core/thread 卻需要較長執行時間,寧可調整為使用較少 core/thread 但執行時間較短的 task decomposition。
拆分 task 可能需要多次嘗試,一個切法的效果不太好,就應該嘗試其他分解方式。
task 間的 dependency 有兩種:
消除 dependency 的方法很多,不見得都得用 mutually exclusive access,如果可以以消除 dependency 的作法來達到 concurrency,就可以減少使用 synchoronization 物件帶來的 overhead。
thread 執行時希望各 thread 有差不多的計算量,平衡 thread 們的 loading。
兩種分派 task 的方式:static scheduling 以及 dynamic scheduling。
static scheduling 會要並行化(concurrent)的計算外面決定 task 的切分,而且計算過程中不會改變。實作 static scheduling 會讓 N 個 thread 有 0 到 N-1 的唯一識別碼,可以透過識別碼分派 task 給 thread。static scheduling 適用於每個 task 的運算量差不多,或是能預先知道計算量的情況。反之,各 task 計算量變動大或無法預先知道的狀況比較適合 dynamic scheduling。
dynamic scheduling 在計算過程中才將 task 指派給 thread,會儘量平均各 thread 的計算量,理想上會有較好的 load balance。分派 task 給 thread 以及 thread 找新 task 的邏輯是原先 sequential 程式沒有的,這些額外的邏輯當然有 overhead。
最簡單的 scheduling 是為 task 加上 index,用 counter 記錄下一個要分派的 task,當 thread 要新 task 時就從 counter 得知拿哪個 task。這個 counter 是 thread 間共用的,因此 access 它需要 mutually exclusive。另一種 dynamic scheduling 使用 shared container 保存 task,通常會用 queue,thread 從 container 拿新 task 執行,例如 producer-consumer 就是這種方式的變形。因為放在 container 裡,task 必須被封裝成可以放進 container 的結構,例如可能是一包描述 task input 的 structure。
計算是一連串更新一個或多個巨大 data structure 中的 element 時,如果這些更新是彼此獨立的,則可以切割 data structure 並將不同部份分給不同 thread 處理,進而達到並行化(concurrent)。切割 data structure 並交給 thread 處理的方法稱為 data decomposition。
如何將 data structure 切割成多個連續區域(稱為 chunk)取決於 data structure 的 type,例如常見的 array 會依照 dimension 來切割。
將資料分割成 chunk 也代表將整個計算分解成針對單一 chunk 的 task,這些 task 會分配給 thread 執行。由於資料是已經分解過的,所以 thread 可以安心使用這些 data,不需要擔心其他 thread 也會使用到。但如果計算需要 chunk 鄰近的資料,task 間就得共享這些資料,thread 也需要彼此協調資料存取。
data decomposition 要考慮:
每個 chunk 都跟 task 有關,要考慮上述 task 的幾個條件,除此之外還需要注意:
考慮 chunk 的 shape 時,granularity 愈大表示 chunk 擁有的 element 數量愈多,也代表有愈多需要資料交換的 element,因此資料交換的 overhead 愈大,又需要在 shape 與 granularity 之間取得平衡。針對這個問題,一個解法是最大化 volume-to-surface ratio,也就是 chunk 的 element 數量與需要交換資料的邊界比例,也就是 chunk 的 element 數 / chunk 中在邊界的 element 數。例如一個 4 x 8 的資料(想像一個二維陣列),分割成兩個 2 x 8 的 chunk,volume-to-surface ratio 是 16 / 8 = 2,分割兩個 4 x 4 的 chunk 則是 16 / 4 = 4。
如果 chunk 內包含計算與更新所需的所有資料,task 間就不需要協調資料交換(這不是廢話)。反之,如果計算需要鄰近 chunk 的資料,就得找出較有效率的資料交換機制。一般有兩種方法:將資料從鄰近 chunk 複製到 task/thread 的 local structure,或者直接 access 鄰近 chunk 的資料。
如果鄰近 chunk 的資料不會隨著計算改變,複製資料是比較容易的方法,因為不需要處理同步問題。缺點是需要額外的儲存空間存放資料副本。這些額外配置的儲存空間一般稱為 ghost cells。複製資料也需要考慮複製次數,如果次數太多或每次複製太多資料,比較適合直接 access 鄰近 chunk 的資料。
直接 access 鄰近 chunk 的資料可以利用 thread 間以 shared memory 為基礎的溝通機制,在真正需要資料前也可以不用協調。缺點是必須確保拿到正確的資料,尤其是鄰近 chunk 也會更新其資料的時候。要確保拿到正確的資料,得找出計算過程中資料交換與更新資料間的關係或互動,才能知道如何協調。有些 sequential 程式計算 element 時需要鄰近 element 的舊資料而非更新後的新資料,此時 sequential 程式與 concurrent 程式遇到的問題是一樣的,concurrent 程式有時能借用 sequential 程式的解法。
類似 task decomposition,與 chunk 關聯的 task 可以用 static 或 dynamic scheduling 指派給 thread。static scheduling 所有跟資料交換有關的協調可以在計算外面、也就是計算開始前處理好,適合 task 的計算是一致或可預期的情況。如果 task 處理各 chunk 的計算量不一致,dynamic scheduling 可能有比較好的 load balance,但也讓資料更新、交換以及與其他 task 協調變得複雜。
data decomposition 中 task 是由資料的分解方式定義的,應該以 task 間的關係找出所需的互動,不管這個 task 是由哪個 thread 執行。
在做 task 或 data decomposition 時,會考量以下四個因素以建立最有效率的 concurrent solution:
重要性:scalability > efficiency > simplicity ~ portability
有時候 concurrent 程式的 scalability 會在某個階段達到高峰後趨於平緩,這可能是因為 algorithem 的需求或是 threading model 提供的環境。遇到這種狀況要評估是否值得花更多力氣嘗試提高 scalability 或者使用不同的 threading model 重新寫過。
記錄開放街圖 Open Street Map(OSM) API 的基本使用跟文件。
RESTful API,data 格式為 XML,目前版本為 v0.6。
文件:
程式化編輯資料要注意的事情:
OSM 有提供測試用 API,測試站跟正式網站的帳號是分開的。
編輯修改 element 必須 reference 到一個 changeset,可以想成修改 log。changeset 跟 element 一樣有 tag,文件建議包含 comment=* 以及 created_by=* tag,簡述改了什麼以及由誰修改。
changeset 如果沒有關掉,OSM 有規則會自動關閉它。
editing API 需要 authentication,可以用 HTTP basic authentication,即在 http header 加上 Authorization: Basic xxx,其中 xxx 是 user:password 的 base64 字串。nodejs 下可以用 new Buffer("Hello World").toString('base64'); 得到 base64 字串。
以 curl 送 request:
1 | $ curl https://master.apis.dev.openstreetmap.org/api/capabilities |
除了 HTTP basic authentication 外也可以使用 OAuth,這我沒研究。
剩下 API 使用參考文件定義囉。
read only API
Overpass API 有幾種 query 語法,大致分成 XML query format 跟 Overpass QL,相關文件:
我只用 XAPI Compatibility Layer 做簡單的 map 讀取跟 tag query,沒使用上面比較複雜的 query 功能。
XAPI Compatibility Layer 基本 HTTP GET:
1 | http://www.overpass-api.de/api/xapi?map?bbox=left,bottom,right,top |
tag 可以串很多,會被 and 起來,其中 node 也可以改成 way 跟 relation。
ELF 將依賴的 shared library 存在 .dynamic section 的 DT_NEED 欄位。
1 | $ readelf -d main |
如果是絕對路徑, dynamic linker 直接去該路徑存取 shared library。如果是相對路徑,則會依照以下順序到不同路徑下找 library:
LD_LIBRARY_PATH 環境變數指定的路徑.dynamic section 中 DT_RUNPATH 欄位記錄的路徑/etc/ld.so.conf 設定的路徑(實際上是讀取 /etc/ld.so.cache)/lib/usr/lib因為一個個搜尋路徑很慢,所以指令 ldconfig 除了更新系統中 library 的 SO-NAME soft link 外,還會將這些 SO-NAME 以特殊的形式存在 /etc/ld.so.cache 作為 cache 以加快搜尋。所以增加、刪除或更新 shared library 以及修改 /etc/ld.so.conf 後,都要執行 ldconfig 來更新 SO-NAME。
幾個跟 dynamic linking 有關的常用環境變數。
臨時改變 shared library 的搜尋路徑,開發跟 debug 的時候很好用。
1 | $ LD_LIBRARY_PATH=/home/foo ls |
可以指定預先 load 的 shared library 或 object file,無論執行檔或使用的 shared library 有沒有依賴它。因為會使用先 load 的 shared library 的 symbol,所以可以用來改掉原本執行檔使用的 shared library,例如 standard C library。
1 | $ LD_PRELOAD=./libfoo.so ./main |
可以看 dynamic linker 的 debug 訊息,設成 help 可以看可設定的值。
build 出 shared library:
1 | $ gcc -shared -fPIC -Wl,-soname,mysoname -o library_name sources |
-shared 表示輸出 shared library,-fPIC 表示使用 PIC。-Wl 可以將參數傳給 linker。-soname 是指定 SO-NAME,如果不指定,shared library 預設沒有 SO-NAME,即無法用 ldconfig 建 soft link。shared library 的 SO-NAME 可以用 readelf -d 查看。
去除 symbol 資訊可以讓 shared library 檔案變小,常用在 release 時:
1 | $ strip libfoo.so |
安裝 shared library 通常是放到系統相關的路徑下,例如 /lib、/usr/lib 等,然後執行 ldconfig 更新 soft link。不過通常安裝會用 make install 之類的指令,不需要手動 copy 跟 ldconfig。