windows系統的記憶體用量指標

最近看到同事在抓漏,但是觀察Memory的指標是Working Set,這是有些問題的,於是花了點時間整理Windows的記憶體指標。Memory大概是Windows系統上最模糊的用詞之一,整理這篇文章順便釐清自己一些疑惑。

Page files

大家都知道Windows上當實體記憶體不足時,會把一些比較不常用的記憶體內容置換到硬碟上先暫存起來,稱之為虛擬記憶體(virtual memory也是個很多義的詞),這個過程稱之為Pageout,存放的檔案叫做Page files。

注意到我用了複數(page files),Windows系統上的Page files可以有0~16個。Page files的功能是儲存那些因為系統記憶體需求的壓力而需要被放到硬碟上的記憶體分頁。

Page files的所在位置可以通過註冊表查詢,或是透過(Win10) 本機=>內容=>進階系統設定=>進階=>效能=>設定=>進階=>虛擬記憶體,進行設定。

根據系統不同,Page files有不同的大小限制,x86最大可以到4095MB,x64或PAE後的系統可以到16TB,IA64可以到32TB。Page files一旦被打開使用後,就沒辦法刪除。因此在系統啟動的狀態下,Page files是無法進行磁碟重組的。Page file可以在系統關機後繼續以硬碟的形式存在在檔案中,但可能會造成安全性上的問題。

Page files的大小一般來說min~max大概在1~3倍RAM之間,也可能會因為系統設定而不在該範圍內。

Commit Charge

我們知道系統有兩種記憶體資源,一種是RAM,另一種是page files。Commit Charge指的是系統承諾一定會在RAM或page files保留空間的記憶體數量。

這裡有點抽象,我解釋一下我對Commit Charge的理解。

某些記憶體的內容,系統並不需要保證「這些內容一定要留在RAM上或page files上」。像是mapping到dll檔的記憶體區塊。當然實際執行時,還是要把這些dll的內容找一塊實體記憶體load上去供程式載入,但拋棄掉這塊記憶體上的內容,之後需要時再從檔案載入回來也行。我們可以說,這種記憶體區塊的內容,他們有associated files作為backing store。Commit Charge指的是那些一定要留在RAM或paging files上的內容,因為這些東西一定得在系統上找個地方放。

順帶一題,Commit Charge的值和實際的記憶體使用量是兩回事。系統承諾會替你保留這段空間,但是你現在並不見得會用到這空間,Lazy是好事,因此實際記憶體的用量會小於所承諾的空間。

系統能承諾的Commit Charge,其大小限制就是RAM + page files的大小。如果Page files可以調大,那麼該限制也會跟著被放寬。如果process持續無限制的索求記憶體,導致系統沒辦法承諾給予,當然就會fail。

以下情況會導致Commit Charge增加,另外可以注意一個指標叫做page file quota(其實就是Private Bytes),每個process都有這個指標,大部分情況下,process page file quota會和commit charge一同增加。

  • 當Process透過VisualAlloc call with COMMIT option的時候,代表Process要系統承諾記憶體。這個值也會加入process page quota。這是最常見的情況了。
  • 注意Commit和Reserved不一樣,VisualAlloc一塊Reserved的記憶體,只是代表我想預訂這一段範圍的virtual memory,但是系統不用承諾我要保留這些空間。系統只會建立紀錄該virtual memory範圍的page table,而這些page table占用的空間會被加入commit charge。
  • 當系統透過MapViewOfFile建立memory mapping,可是背後沒有對應到實際的檔案時,系統會使用paging files作為該塊記憶體的backing store。這種狀況下也會增加commit charge,但並不會計入process page quota。因為這個記憶體用量和process較無關(non-persistent memory-mapped file,通常用於IPC)
  • Copy On Write時,背後要拉一塊新的RAM或paging files去對應Write的內容,因此也必須要取得Commit Charge,這類也不會計入process page file quota。
  • Nonpaged and paged pool and other allocation in system space not backed by explicitly associated files. 系統上會有些可以被分頁的記憶體,也有一些不能被分頁的記憶體區塊,這些都會造成commit charge. 也都不會計入Process page file quota
  • Kernel stacks.
  • Page Table、Page Table的保留空間也會占用Commit Charge。
  • Allocations of physical memory made via the Address Windowing Extension (AWE) APIs

常見的記憶體指標

理解Commit Charge和Page Files後,我們來解釋Windows系統常見的記憶體指標。
我自己在查資料、查書,理解這些指標的過程中,覺得相當混亂,有些指標是其他指標的別名,有些在書上有提到,但網路上查不太到資料,讓人相當困擾。

MSDN的官方文件是我目前覺得最準確的溝通方式,因為其包含了該指標可以在哪個Windows API上取得。而且還附上了工作管理員的對照表。

官方的指標分為兩大類,System層級和Process層級,我想下列幾個指標是一般開發者比較常看到,比較重要的。

文件網址: https://msdn.microsoft.com/en-us/library/windows/desktop/aa965225(v=vs.85).aspx

System Memory Performance Information

  • Committed Bytes
  • Committed Limit

Process Memory Performance Information

  • Page File Bytes
  • Private Bytes
  • Virtual Bytes
  • Working Set
  • Working Set – Private

官方文件網址有列出如何透過Windows API取得上述指標,因此我在這邊著重在各項指標的解釋。

  • Commit Bytes
    • 這是一個系統層級的指標。
    • number of bytes of virtual(not reserved) memory that has been committed. 整個系統中,保證該virtual memory在實體RAM或paging files中會留有一席之地的記憶體數量。
  • Commit Limit
    • 這是一個系統層級的指標
    • 整個系統上可以保證分配給Virtual Memory的空間,包含了RAM和paging files, 當然如果paging files能夠expanding的話,該限制會放寬(是一個soft limit)。
  • Private Bytes, Page File Bytes
    • 這兩個值是一樣的
    • 這是一個Process層級的指標
    • The current size of memory committed to a worker process, which cannot be shared with other processes.
    • 該process已經要求的記憶體量,你可以想像成是Process中Commit Bytes的數量,該指標之所以稱之為Private,是因為排除了像是dll檔案的memory-mapped files所占用的記憶體,注意你無法分辨private bytes的變動來自該process本身,還是所引用的dll執行所造成的。
    • 這個指標並不等於實際的記憶體使用量,commit的記憶體只是系統會留一塊空間,但程式並不保證會使用到。
    • 這個和Physical Memory無關,所以你無法分辨這個數值中,有多少在disk或ram上。
  • Process: Virtual Bytes
    • 這是一個Process層級的指標
    • 該Process所有Virtual Memory Area的範圍總和。
    • The total virtual memory allocation of the process, including mapped regions, private committed regions, and private reserved regions.
  • Working Set
      • 這是一個Process層級的指標
      • 簡單但有一點小瑕疵的解釋是,access不會產生page fault的記憶體量。更簡略的解釋是該Process存在於實體RAM中的記憶體量,而不是該Process所有的記憶體量。所謂的小瑕疵指的是已經備份到page file但還是RAM中的的standby page list並不會計入Working Set。
    • Working Set有計入memory-mapped files,也就是有計入程式間共用的dll。如果把所有Process的Working Set Memory加總會會重複計算到共用的部分。
  • Working Set Private
    • 這是一個Process層級的指標
    • 同Working Set,但只包含該Process自己使用的部分,不包含共用的memory mapping file(如dll)。
    • 關於Working Set和Working Set Private, 這個stackoverflow有個有趣的蠟筆比喻。

雜項

順帶一提,上述那些Memory相關的指標,對於Memory Leak除錯基本上都沒太大幫助。

Working Set顯示的是在實體記憶體中的數量。和程式所消耗的總記憶體相關性有限。Private Bytes稍微有幫助一點,但是你無法分辨該數值的增加是來自程式本身還是他所引用的dll檔。Virtual Bytes是個不能精確反映程式實際分配記憶體量的指標。

工具

Windows Internals提供許多分析Memory的工具,有些蠻好玩的可以裝,玩一玩會覺得Task Manager很弱XD。都是免費的,google查一查直接裝就好。

  • VMMap 看虛擬記憶體
  • RAMMap 看實體記憶體
  • Process Explorer 比Task Manager更詳細的Process資訊
  • Process Monitor

Reference:

繼學vim的啟示之後,終於解掉心中的困惑

這篇算是上Visual Studio極速開發的課後心得吧。

在三年前,我寫過一篇文章叫「學vim的啟示」。內容大概是記錄自己從不熟悉vim,想mastering vim,想把vim tuning到跟IDE一樣所經歷的崩潰過程。

學Vim的啟示

每個學vim的使用者大概都會有一段崩潰的經歷,從入門到放棄。少數人會堅持下來,並且可以在日常生活中使用vim。我也是過來人,這三年期間,vim的日常操作沒太大問題,但心中一直有個遺憾在:「我還是不知道高手到底是怎麼達到開發如喝水般的境界。」

接下來三年我做機器、我當兵、我出社會開始第一份工作,這個疙瘩就慢慢隨時間淡忘。直到去年的時候,我看到91 Po出了他行雲流水的開發影片,心中整個一震:「這不就是我當時幻想的開發速度嗎,前輩竟然做到了!」

後來換第二份工作,從寫JS轉到寫C++,因為工作的緣故日常開發開始使用Visual Studio,便迫不及待的報名91開的「Visual Studio極速開發」,想一窺背後的秘密。這門課支援三種語言C#/Java/PHP,但多數人是使用C#開發,之前沒有接觸過C#,課前兩週還特地看微軟官網、自己試寫幾個小程式把語法弄熟,深怕上課過程會跟不上進度,浪費了報名費、害我那兩週幾乎沒有在重訓還熬夜,體重掉了兩公斤。

上完課後腦洞大開,從第一天上課台上什麼都程式都寫不出來的挫樣,回家刻意練習了一整個禮拜,到了第七天終於在22分鐘內以TDD的方式完成了Tennis Kata。

不論是技藝還是心態,這門課都給了我很大的啟發

技藝上最大的啟發是,我終於知道自己用vim的瓶頸在哪裡。

我在三年前的文章裡,寫過這麼一段話:

建立你自己的Vim,因為只有你知道你自己的需求、習慣的工作方式,這樣的練習對你才有意義

這段話基本上並沒錯,很政治正確但是沒啥意義,因為真正的問題在:「你不知道自己的需求。」

不知道需求的意思是:「你不知道你現在要做什麼」,舉一些case像是

  • 不知道自己接下來該寫什麼,於是思考會卡住
  • 不知道自己想找的檔案的位置,於是要花時間找檔案
  • 不知道自己想怎麼重構、想怎麼寫,於是花時間在打字刪除打字刪除

如果不知道自己在做什麼,就會按很多多餘的按鍵、就會開始用滑鼠上下瀏覽看code、表面上看起來很認真在寫程式,實際上只是在裝忙,因為那些行為都沒有實際的產出。

如果你很清楚你要做什麼,你就有了目標。目標越明確,就可以進行最佳化(跟深度學習一樣XD),就可以去一步一步改善,進入iteration tuning的cycle。

光是知道想做什麼還不夠,還要有能力做到這件事。也就是對工具的熟悉,知道該如何用最短的方式下vim指令,而這過程有非常多坑,當坑多到一個程度的時候,大多數人就會被困住動彈不得,放棄並回到自己熟悉的方式,就跟三年前的我一樣。

想要破坑的方法在於「看到有人真的做到了」、「了解他是怎麼做的」、「刻意練習並內化成自己的技能」

「看到有人做到了」這件事情最難,因為大多數人會被「不知道自己不知道什麼」這件事情困住!於是自己在爛泥坑打轉,想要破坑的方法在於參加社群、脫離自己日常的小圈圈、知道比自己厲害的人是怎麼做事的。

「了解他是怎麼做到的」,這件事情稍微容易一點,透過培訓或上課是最快速的方式,主動發問也是個不錯的方式,但如果發問者和實力差距太大的話,一樣會陷入「坑太深被卡住」的困境。

「刻意練習並內化成自己的技能」,這大概是最容易的了,只要花時間就可以做到了。方法論在從籃球機看練習的重要性有提到過。但雖然說容易,沒花個數十個小時大概也是做不到的。

上完課後,我花了一週的時間,每天平日晚上練習三個小時、假日練習五個小時。第一天連環境都架設不好,第二天第三天進度崩潰的慢,花了兩三個小時一個鍵一個鍵對照他的效果、試按,連範例影片的前五分鐘都練不完。

到了第四天的時候,突然腦袋某個地方開關像打開一樣,意識到自己的問題是對TDD不熟悉,必須要以TDD的角度去看Tennis Kata為什麼要這樣寫。進步速度開始飆升,我開始能在45分鐘內完成、接下來每次練習都比之前再快一些些,在第七天,終於在22分鐘內完成。而三個禮拜前我還不會C#。

認知到「這個坑是填得起來」這件事讓我非常開心,後續要把這個技能transfter learning到C++上恐怕還要一些時間,一整個禮拜的練習過程頗辛苦,但感受到自己以穩定的速度進步這件事相當愉快。

非常感謝91大大傾囊相授,破除了我的盲點,除了學到很多技術之外,心態上也成長許多,讓我意識到「不知道自己不知道什麼」、及「該如何review自己並持續改善」,最可怕的是前輩每天都持續在進步,要達到可以把這些知識教給他人,背後要付出的努力根本不敢想像,希望有一天能看到前輩的車尾燈。

最近打算買本學徒模式來看,以前大學時看不懂,覺得這樣也可以出一本書,現在大概會非常有感吧。

清燉牛肉麵的經驗價值

華亞科附近有一間小店叫湯本源,賣得是湯和拌麵,店門口看起來乾乾淨淨,擺了一台點餐機器。某次覓食時看到店門口菜單,一碗肉湯、一碗麵上面放一撮醬,小疊子盛了三種小菜、一杯酸梅汁,要價300元上下,眉頭一皺覺得價格和預期落差太大,便打消入內用餐的念頭。

但我心中一直有個疙瘩在,這間店在Google評價上非常高分,身為一個鍵盤美食家,不踩踩雷說不過去。在門口的點餐機猶豫了一會,老闆娘走出來介紹餐點,哪些辣哪些不辣,決定當作被騙吃一次看看,點了一份320元的清燉牛肉湯,配干貝XO醬拌麵。

 

吃完後的感想是,實在是太便宜了。

 

當你吃到「用心製作的好吃食物時」,你會覺得,啊,這食物的味道就應該要這樣!

清燉牛肉湯中,牛肉塊給的毫不吝嗇,比一般牛肉麵店還多,切成適合入口的大小。牛肉燉得透徹,但形狀又完整,輕嚙即化,牛筋軟嫩輕咬即斷開,放入口中咀嚼,滋味雋永,清燉的湯味道不是厚重的路線,不鹹,喝起來神清清爽。

一旁的XO醬拌麵滋味也很棒。單吃麵條本身沒有什麼味道,但把上頭的XO醬拌開後,微辣和干貝的香氣非常下麵,XO醬本身不死鹹、鮮味誘人,單吃一小撮也沒什麼問題。太辣時配上酸梅汁解膩,中和掉辣味後,剛好可以再喝一口清燉牛肉湯。非常的愉快。

三碟份小菜也很用心,切開的溏心蛋蛋黃呈現凝膠狀,涼拌百香果青木瓜上有黑色一粒粒的百香果籽,咬下去清脆微甜。麵筋筍乾滋味簡單,就是他該有的味道。

店家有提供肉臊飯,如果吃不飽可以免費加點。吃完上面這些其實已經八分飽了,但我實在太想吃看看他的肉臊飯是什麼樣子,決定加點他的肉臊飯。

飯端上,看一眼就知道絕對不簡單。米粒光澤飽滿粒粒分明,上面的肉臊肥瘦比大約3:7,給的毫不小氣,整碗吃完非常滿足。最後一口乾掉酸梅湯當作收尾,杯子的底部還陳著碎花瓣。

後來跟老闆娘聊天,稱讚他們食物的用心,老闆娘說他們的飯是選用冠軍越光米。酸梅湯要煮四個小時。老闆有三十年的料理經驗,參加兩岸料理大賽拿到金牌,店內擺著四本老闆出過的書、其中一本還專門講醬汁怎麼調、另一本是最近出版的,講家常菜的食譜。

翻著這些書,我可以感受到主廚對料理的熱愛。料理不外乎是各種變因的集合,食材的選用、烹飪的材料、方式、時間。厲害的廚師之所以厲害,在於他持續保有熱情,花了很多時間去調整變因,為了一道菜的本味,可以反覆的做,直到調整成自己覺得適合的味道,並把這些經驗內化成自己的技能。這些都需要時間積累成經驗,而320元一餐,其實一點都不貴。

 

但一般人能夠理解這樣的價值嗎?

 

很遺憾,不見得。

這間店的生意並不算好,我中午來的時候,店內並沒有其他客人,過一陣子才來了兩三組。大多數人在店門口看到價格晃晃就卻步離開,Google評論上也有1顆星,嫌湯太淡、嫌CP值太低的評論。

不是每個人都能理解經驗與用心的價值,就連我一開始也覺得,320元吃那一碗湯一碗麵,真的好貴。但多數吃過的客人評價都很高,覺得物有所值、變成回頭客不在少數,有些華亞科園區主管也會帶客戶來這裡用餐。

 

食材的價值估算簡單,廚師的經驗價值估算困難。

工程師的薪水價值估算簡單,工程師的經驗價值估算困難。

可是親口吃過、一起工作過,你會知道兩者的差距是一天一地。

不要追求CP值,而是追求物有所值。

Cannot load relative line numbers in Visual Studio 2017 enterprise

After installing extension relative line numbers in Visual Studio 2017 success, I opened a new project and found that the relative line number plugin is not loaded correcly.

Open logs in ActivityLog.xml I saw following error

<description>System.Runtime.InteropServices.COMException (0x80131163): 
Type library exporter encountered an error while processing &apos;
RelativeLineNumbers.OptionPageGrid.SaveSettingsToXml(writer), 
RelativeLineNumbers&apos;.
 Error: Type library exporter encountered an error while processing &apos;
Microsoft.VisualStudio.Shell.Interop.VSSAVETREEITEM.pHier, 
Microsoft.VisualStudio.Shell.Interop.8.0&apos;. 
Error: Type library exporter encountered an error while processing &apos;
Microsoft.VisualStudio.Shell.Interop.SVsSolutionObject, 
Microsoft.VisualStudio.Shell.Interop&apos;. 
Error: Type &apos;SVsSolutionObject&apos; 
and type &apos;SVsSolution&apos; 
both have the same UUID.&#x000D;&#x000A; 
at EnvDTE._DTE.get_Properties(String Category, String Page)&#x000D;&#x000A; 
at RelativeLineNumbers.RelativeLineNumbers..ctor(IWpfTextView textView, IEditorFormatMap formatMap, DTE dte)&#x000D;&#x000A; 
at RelativeLineNumbers.MarginFactory.CreateMargin(IWpfTextViewHost textViewHost, IWpfTextViewMargin containerMargin)&#x000D;&#x000A; 
at Microsoft.VisualStudio.Text.Utilities.ContainerMargin.<AddMargins>b__25_1(IWpfTextViewMarginProvider mp)&#x000D;&#x000A; 
at Microsoft.VisualStudio.Text.Utilities.GuardedOperations.InstantiateExtension[TExtension,TMetadata,TExtensionInstance](Object errorSource, Lazy`2 provider, Func`2 getter)&#x000D;&#x000A;
--- End of stack trace from previous location where exception was thrown ---&#x000D;&#x000A; 
at Microsoft.VisualStudio.Telemetry.WindowsErrorReporting.WatsonReport.GetClrWatsonExceptionInfo(Exception exceptionObject)

 

You can fix this issue by reference this with a little revision

  • Launch “Developer Command Prompt for VS 2017” as Administrator
  • Go to VS 2017 installation folder cd C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise
  • gacutil -if Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.Shell.Interop.dll

 

key point is last line, Microsoft.VisualStudio.Shell.Interop.[8|11|12].0.dll won’t work for me.

Hope this helps, happy coding 🙂

什麼是成熟,寫在從第一份工作離職之後

什麼是成熟

上個月和一個愛看電影的朋友討論一個有趣的問題:「什麼是成熟?」

朋友最近看了一部電影,內容敘述一名高中女學生喜歡上老師,但這段戀情卻不被他人祝福,大人告訴他你的喜歡會對老師造成困擾,你的思考還不成熟、未來還有許多值得嘗試的東西。而女學生卻覺得,為什麼他不能喜歡一個人?她因為喜歡,唸書都變得很快樂、成績也變好了,為什麼不行呢?

朋友分享他看完電影後的想法:大人們總是告訴我們不該做這個、不該做那個,大人們所謂的成熟,似乎都是要避免困擾,與其追求或爭取想要的東西,避免負面的結果理性上才是對的。但這樣的想法真的能被稱作成熟嗎?

大人們通常阻止學生追求愛情的理由,多半是學生時期的女朋友很有可能未來會分手,既然遲早會分手,與其浪費時間談戀愛幫別人養老婆,不如先努力讀書賺錢還來得實際。但我認為與其阻止學生談戀愛,不如讓學生學習處理兩人相處中會遇到的問題,沒有人一出生就會談戀愛,這需要時間練習、調適,很多人最後沒有跟第一任對象結婚,但之前犯下的錯都會成為滋養感情觀的養分。

朋友聽完後說,成熟似乎總是和圍繞在過錯上,大概是因為成熟的相對詞是幼稚吧,幼稚給人的感受是容易犯錯、做事情不得體。但那些阻止學生談戀愛的大人們似乎覺得,避免犯下錯誤才是成熟,難道成熟是完善的思考與理性行動嗎?

我想想說道,大人們為了避免學生荒廢課業,預先禁止學生談戀愛,可能會讓自己成為經驗主義的受害者。就像心理學實驗中一群被關在籠子內的猴子,籠子頂掛了香蕉,每次有猴子嘗試拿香蕉,實驗人員就會把冷水從上面潑下來,一段時間之後,每當有猴子想拿香蕉,其他猴子就會毆打阻止,避免被冷水波及。後來實驗人員陸續把籠子內的猴子換掉,換成沒有被水潑過的猴子,結果每當有猴子想去拿香蕉,其他人就會去阻止,甚至籠子內已經沒有任何一隻猴子被水潑過了,他們還是重複前人的行為,用自己過去的經驗武斷地下結論,這樣的作法大概也稱不上是完善的思考與理性行動。

「那怎麼樣算是一個成熟的行為?追求愛情,還是乖乖讀書,哪個才算是成熟?」朋友好奇問道。

「更好的問題會是,成熟的行為應該帶來什麼樣的結果。舉個例子吧,一個追尋愛情的女生面臨兩個選擇,一個是喜歡他外表的有錢人,另一個是很愛他的窮書生,他該選擇誰才算是成熟?」我反問。

「那要看他要什麼。」朋友說。

「所以成熟是,知道自己要什麼。」語畢,朋友與我都安靜一陣子。

知道自己要什麼

知道自己要什麼,勇敢地做出決定並負起責任,是多麼困難的事情啊。大人們認為高中談戀愛的學生不夠成熟,因為他們涉世未深,沒有足夠的經驗判斷對象的好壞,而且可能會因此犧牲學業,在不久的將來後悔,於是阻止他們談戀愛。但那些乖乖聽話,白天上學晚上補習的學生們就知道自己要什麼嗎?

從小到大,學校教我們好好讀書,社會要我們找份體面的工作賺錢、找好對象結婚生子,卻從來沒有教我們認識自己,搞清楚自己要什麼、不要什麼,然後好好追尋自己想要的,讓自己每天快樂的過日子。社會只會在三十歲時關心你怎麼還沒有對象,卻不關心你想要過什麼樣的生活,商人透過廣告說服你需要買這個,卻不關心你真正的需求、沒有人在意你每天工作快不快樂,只有過年時親戚會問你領幾個月年終。

在高中畢業選填資工系時,其實我並不知道自己要什麼,經過兩年以上的修課、自主學習課外的知識,才逐漸對資工系在學什麼有比較清晰的掌握。但其實上完四年課,儘管我會寫程式,但我依然不確定自己是否喜歡寫code、是否願意把寫code變成一生的志業,我只知道自己喜歡打造東西的樂趣。

在大四時決定做自動飲料機時,我其實並不知道自己要什麼,只覺得我不想繼續碰純軟了,想要開發硬一點的東西。結果沒想到頭一洗下去就花了14個月,途中遇到各種困難,還因此延畢了一學期。這專案讓我一窺硬體開發的世界,就像是在薩爾達傳說探索了新地圖一樣,我有了其他資工系學生沒有過的經驗。

後來我選擇不念研究所,因為我不想要在自己想要什麼都不清晰的情況下花兩年拿碩士學位。當完兵後我選了一間以分紅不錯聞名、同事都是高手的公司,結果不到一年我就離職了。我發現我對公司的產品沒有足夠的熱情。儘管我繼續待下去,兩三年之後可以有不錯的收入,但金錢對我而言並沒那麼重要。

後來我向一間做機械手臂的公司投了履歷,展開了職涯的第二份工作,現在的我開始新工作第三週,薪水比第一份工作少很多,但我每天上班比以前都快樂。

我在第一份工作的時間是不是浪費了?如果我畢業後直接先做第二份工作,會不會更好?不,不是這樣的。第二份工作有優點,也有缺點。如果沒有第一份工作經驗,我不會看到高手同事用什麼樣的方式看待程式碼,讓我在進行第二份工作時,迅速自信地看出可以改善的部分。如果我沒有做過硬體專案,我可能無法看出軟硬整合的價值。如果我一畢業直接做第二份工作,可能會把第二份工作的做事方法定錨成衡量公司好壞的標準,在不確定自己的喜好的情況下,或許並不喜歡這份工作。

你通常不會在第一次就做出正確的決定,你的第一個伴侶往往不會和你走入禮堂。你不太可能第一份工作就做到65歲退休。你不會一覺醒來就突然知道自己要什麼,也不會在考完學測填志願時就確定自己一生的志業,你20歲想要的東西和30歲可能很不一樣。

認識自己其實和軟體開發很像,顧客往往不知道自己真正要的是什麼。如果你照著傳統的瀑布式規劃走,先寫規格(e.g. 決定自己想讀什麼科系),按表實作(e.g.乖乖讀完四年大學),最後給客戶驗收(e.g.畢業當完兵開始找工作),很可能會發現產品和現實有很大的落差(e.g.學非所用)。唯一的應對方法是保持敏捷,相信需求本來就會一直改變。你本來就該持續和自己溝通。盡早嘗試,不追求完美,早點生出第一版原型並下水測試,犯錯,然後修正。當你修正的速度夠快的時候,早期犯下的錯誤根本無關緊要。多和其他人交流、溝通,看看世界上其他比你厲害的人是怎麼做事、思考。實作、實習、實踐,做中學才會看到更深入的東西。最怕的是在不確定自己要什麼的情況下,做出一個高成本高代價,而且無法修正的決定。

如何儘早用低成本的方式知道自己要什麼,活出無悔人生,早在2001年2月,一群去山中小屋滑雪的軟體開發專家們早已提出了解答。你只要將敏捷軟體開發宣言中的「軟體」代換成「生命經驗」,將「需求」改稱「自我實現的需求」,一切就豁然開朗。

我們遵守這些原則:
我們最優先的任務,
是透過及早並持續地交付有價值的軟體
來滿足客戶需求。
竭誠歡迎改變需求,甚至已處開發後期亦然。
敏捷流程掌控變更,以維護客戶的競爭優勢。
經常交付可用的軟體,
頻率可以從數週到數個月,
以較短時間間隔為佳。
業務人員與開發者
必須在專案全程中天天一起工作。
以積極的個人來建構專案,
給予他們所需的環境與支援,
並信任他們可以完成工作。
面對面的溝通
是傳遞資訊給開發團隊及團隊成員之間
效率最高且效果最佳的方法。
可用的軟體是最主要的進度量測方法。
敏捷程序提倡可持續的開發。
贊助者、開發者及使用者應當能不斷地維持穩定的步調。
持續追求優越的技術與優良的設計,
以強化敏捷性。
精簡──或最大化未完成工作量之技藝──是不可或缺的。
最佳的架構、需求與設計皆來自於
能自我組織的團隊。
團隊定期自省如何更有效率,
並據之適當地調整與修正自己的行為。

認識自己並不是一張職業性向量表就可以解決的事情,你必須要做各種嘗試,取得回饋、遇到困難、遇到挑戰,看到更大的世界、每一步的決定都會讓你更認識自己一點,你會遇到很多人、發生許多有好有壞的事、隨著你的閱歷逐漸豐富,你會知道對你而言什麼是重要的,而生命有限,什麼是可以割捨的,那才是真正形塑你樣子的東西。

成熟,就是知道自己要什麼。

成年人,就是知道自己要什麼,並且能夠替自己的決定負責的人。

為什麼透視圖的消失點會連成一條消失線?

最近在研究影像處理,在思考照片中的物體,會如何隨著相機拍攝的角度改變而變形。因此產生了一個有趣的疑問:為什麼照片中的透視點能夠連成一條線?


圖片引用自DesignByFoot blog

可以看到上圖中,不論是地上的抽屜、櫃子,只要將平行線的兩端延伸,最後就會交於一點,而這些不同平行線延伸出的交點,最後竟然會交於同一條水平線上,我覺得這實在是太神奇了,不禁好奇為什麼會有這個現象?

這個問題看似簡單,其實並不好思考。我想說學設計的應該會對透視比較了解,所以問了幾位學設計的朋友,他們其實也不太會解釋為什麼,就說是一種FU(馬的到底是什麼FU啦XD),我看了好幾篇文章,有設計的、也有計算機圖學的,平常在走廊走路時像怪人一樣不斷的看天花板無限延伸的平行線發呆,想了幾天,才總算搞懂這是怎麼一回事。

什麼是透視?

這裡的透視不是透明的意思,而是一種投影的現象。其實你每天張開眼睛,看到的畫面通通都是透視圖。無限延伸的鐵軌會在遠方交於一點是最經典的例子。

這是個很有趣的現象,現在想像假設地面上有不只一條的鐵軌,通通朝正前方延伸,有一個很重要的現象是,這些鐵軌通通會交於一點。

會這樣

而不會這樣

同樣地,如果我們從高空俯視大樓,會發現所有的牆壁邊線無限延伸後,都會交於同一點。

上面這三張圖都出自巴哈姆特 Arrogant(傲慢的羊)寫的透視教學,他的文章是針對繪畫的透視做介紹,是我目前看來最清晰易懂的教學,相當推薦

這個現象很直觀,我們可以從這裡切入,第一個要思考的問題是,為什麼會有這個現象?

攝像機模型

讓我們用更具體的模型來觀察透視現象。


圖片出自the university of edinburgh

所謂的透視,就是將三維世界座標系的物體,投影到某個平面上的現象。可以看到上圖的原點代表投影中心,投影中心前方有一個平面。而所謂的投影,就是將三維世界的物體,和投影中心連成一線,交於該平面上產生的圖樣。


圖片引用自Wiki Vanishing Point

現在我們將剛剛的延伸的鐵路,透過這個攝像機模型來表示,上圖可以看到隨著鐵軌的延伸,遠方的鐵軌和投影中心的連線越來越接近平行地面的直線,最後那個無限遠的點,其實就是一條由投影中心發出,平行於鐵軌方向的直線,交於平面上的點

推廣到不同角度的鐵軌

現在,想像另一種情況。假設這個鐵軌不是往正前方無限延伸,而是例如朝右45度無限延伸,那麼他會交於圖片上哪一點?

圖都找給你看了,答案很明顯吧。會在偏右的位置。

如果用剛剛所提的,這個點其實會在一條由投影中心發出,平行於該偏右鐵軌的直線,交於平面上的點
廢廢的手繪圖
自己畫的廢廢手繪圖

注意我講的偏右的意思是,你的視線朝正前方看,此時腳下的鐵軌往右偏。你的視線往前看,但用眼角餘光觀測鐵軌,發現鐵軌的盡頭位於整個視野中偏右的位置。如果你轉個身,讓視線和鐵軌朝同一個方向,那麼鐵軌的無限遠處自然就在視野的正中央了。

現在我們得到一個非常重要,基本上是整個透視投影界(?)最重要的觀念:所有同樣方向平行線的無限遠處,會落在投影平面上的同一點

所有朝正前方平行的鐵軌的無限遠處會落在同一點,所有朝右方45度平行的鐵軌的無限遠處也會落在同一點。而這些點都被叫做消失點,而消失點的座標恰好是一條從投影中心發出,平行於該鐵軌方向的直線,落在投影平面上的點。

為什麼這些透視點會連成一條線

掌握這些觀念後,現在我們要回答本文一開始的問題:為什麼這些透視點會連成一條線?

因為這些不同方向的鐵軌(或物體)都平行於地面,因此從透視中心發出到無限遠處的射線也會平行於地面。他們可能有不同的方向,朝正前方的鐵軌的消失點在正前方,偏右的鐵軌消失點在偏右的地方,但是無論是哪個方向的鐵軌,從透視中心延伸到投影平面上消失點的射線都平行於地面,因此他們都在同一個高度上,成為一條由消失點構成的水平線。

看看一開始的圖,不論是地板上的抽屜、櫥櫃裡的抽屜、櫥櫃本身的邊邊,這些平行線在三維空間中通通都平行於地面。因此這些平行線的消失點會落在照片上同一條水平線上。

換句話說,要是該平行線組並沒有平行於地面,那麼他的消失點就不會落在該線上。舉例來說像下圖,V1和V2這兩個消失點會在同一條水平線上,是因為空間中朝向V1, V2方向的平行線平行於地面。而斜屋頂的消失點V3因為不平行於地面,所以消失點不會在V1和V2的連線上。

那位什麼V3, V1, V4會在同一條線上呢? 因為這些的平行線雖然方向不同,但都平行於該房子的左面,所以他們的消失點會落在同一條線上。


圖片出自HSUEH Gallery

一點透視是個假議題

想通了這一點後,你會發現,大家平常在說的一點透視、二點透視、三點透視,通通都是假議題。

所謂的一點透視,只是剛好圖片適合用一組最有代表性的平行方向來畫

圖片出自GCS ARTS

兩點透視,只是剛好圖片適合用兩組最有代表性的平行方向來畫

圖片出自D’source

三點透視

思考的關鍵在於,你的投影中心、投影平面,和整個三維世界的連線。只要記得大原則「所有同樣方向平行線的無限遠處,會落在投影平面上的同一點」,其他就可以一一推導出來。

網路上大部分談透視的文章都是繪畫類型的,用作畫的角度來看透視,另外一小部份是計算機圖學的類型,用矩陣和齊次座標系來看透視。要用簡單的方式把透視的觀念講清楚,我覺得不是件很容易的事情,昨天把這個想法告訴朋友,他們都有種醍醐灌頂的感覺,故寫成文章以茲紀念。

如何克服低潮期

人生何處不迷茫

前一陣子,我再度對未來陷入了迷茫,甚至到了有點輕度憂鬱的地步。

茫然的因素主要有三個,一個是我對系統程式找不到成就感。另一個是對台灣資訊產業的悲觀。最後一個是金錢的問題。

剛離開成功嶺,開始服替代役時,我那時不斷問自己,未來想走什麼領域。我寫過幾個網站、做了一台飲料機,入伍前還上完Andrew Ng的機器學習線上課,最後我決定去找Jserv研究系統程式。

最後會決定系統程式的原因是有脈絡的,我不想繼續寫網站,大學期間我寫了三年網站,前端的技術更迭太快了,我努力了三個月才上手的AnglarJS很快就過時,JS邁向ECMA6,react.js當紅,之後又來個vue.js,我在乎的是解決眼前的問題,做出我想要的東西,而不是一直追網站的最新技術。

我覺得做網站能解決的問題有限,又不想當個接案者。於是我就跑去弄硬體,大四那年我花了一年的時間搞了一台自動做手搖飲料的機器,這個專案讓我深刻理解,現實世界的問題的難度不太可能靠一個人攻克,跨領域合作和專業化分工是必然的結果。

上完Andrew Ng的機器學習課,我曾一度考慮要不要研究Deep Learning。但後來讓我卻步的理由是,目前Deep Learning的發展方向還很亂,而且就像是個黑盒子一樣,it just works but we don’t know why。而且台灣做深度學習的公司實在有限,就先把這個選項擺到一旁。

剛離開成功嶺,思考一段時間要走什麼領域後,最後我選擇向Jserv學習系統程式。當時我給自己的理由是,這個東西很基礎、很紮實,技術的變動性應該不會像網站那麼大,而且Jserv有那麼多學生畢業後都找到不錯的工作,未來跟產業界應該也能銜接。

Jserv提供了兩個題目,一個是記憶體分配的機制,另一個是程式的並行性。我花了幾個月的時間研究這兩個題目,但後來卻漸漸失去學習的動力。回過頭來看失去興趣的原因,最關鍵的因素是,缺乏短期的成就感。

研究記憶體分配是為了ARRC的火箭系統,需要一個合理的機制來分配記憶體。我們研究了幾個記憶體管理套件的分配機制,我努力告訴自己這些東西很重要,但這些東西的成就感實在是很小眾,大多數人並不會對降低的25%的記憶體碎片到興奮。

我們後來研究程式的並行性(Concurrency),這個領域沒有多少中文資料,英文的資料也很分散,我花了幾周,啃了好幾本原文書的相關章節,才大致了解背景知識,包括處理器的指令重排、快取、程式碼執行的順序。但我距離寫出一個可以使用的lock-free演算法還好遠,這東西本身是為了提昇效能而存在的,讓系統能夠跑得更快、更順,我相信這領域很重要,但是那種膚淺的相信沒辦法帶給我繼續投入的動力。

在心情鬱卒的那段日子,我常常上104和ptt科技版,看看未來能從事什麼相關的工作。結果上頭充斥著各種薪水不上不下、工作十年也沒什麼前景的工作,只有少數幾間外商、厲害的公司才端得出稍微像樣的牛肉來。科技版上頭各種悲觀的仇恨發言,只求爽、只求有錢賺,不求進步的價值觀,看久了心情都會受到影響。

我才更明確的意識到,這是整個產業的問題。如果公司沒辦法做出有價值、有技術差異、能夠甩開競爭對手的產品,最後就會陷入低價競爭的迴圈當中,最後公司賺不到錢,員工也不會成長進步,大家比誰加班比較多,看起來比較努力而已。

同一時間,我手上的各種3C產品的折舊也漸漸到期了。我的筆電已經用了六年,開始會不定期過熱,鍵盤也開始鬆脫。因為沒錢所以先墊著用的ASUS手機時常會讀不到sim卡、網路連不上要重開機,頻率高到惱人的程度。鞋子破了、衣服舊了、眼鏡鏡片也早已磨損到會影響心情的地步。但我只是個薪水6000塊的役男,在台北等級消費下,光是伙食費就入不敷出。這些揮之不去的因素像夢魘一樣,不斷的侷限我思考的可能性。

負面情緒的迴圈

我突然理解憂鬱症是怎麼一回事。你向罹患憂鬱症的病人說看開點是沒有用的。因為現實把他所有可能性都封死了。他會陷入一個負面的迴圈,因為沒有錢,於是找工作的時候錢變成很重要的因素,但大多數的職缺薪水都不高,發展性也堪慮。而且如果把錢放到第一順位,你便無法傾聽內心真正想要的工作是什麼。這些道理憂鬱症的人都知道,他很努力的想要傾聽自己內心的聲音,但外在的惡劣的產業環境不斷的干擾他的思考,我該寫driver嗎?但driver是個枯燥乏味的工作。我該去系統廠嗎?但每個人都說系統廠是屎缺。還是該選比較高薪的外商?但外商大多數的工作是作客戶支援,並不是個長久之計。Jserv的包袱依舊掛在身上,他覺得自己不去學系統程式彷彿背棄了老師對自己的期望,但缺乏成就感的學習方式讓他無法繼續前進。

更糟糕的是,一旦這樣的負面想法持續超過一週,腦袋裡的某些神經迴路似乎會定型下來。你的思考會開始往悲觀的方向走,因為缺錢,所以變得更不敢投資自己,到書店連要不要買一本書都會猶豫半天。錢本身是帶來更多可能性的工具,但因為缺錢,不敢花錢,你等於是把大半的可能性都封死了。你不斷想找未來工作的方向,比平常變得更現實、更功利,但這不是好事,過於現實、悲觀會讓你看不到未來的希望,嘗試把一切怪罪在環境上,急功近利讓你無法做更長遠、短期內無法帶來回報的投資。

幸好兩三週之後,我總算脫離低潮與悲觀的負面迴圈,我試著整理出一些我自己的作法:

  • 運動,我在低潮這段時間依然繼續保持著重訓的習慣。一週將近4~5次,運動時大腦會分泌腦內啡,產生愉悅感,抵銷一部分的負面情緒,不讓其無上限的繼續累積。
  • 和朋友聊聊,線上或約出去吃個飯都好
  • 出去玩耍,脫離現實,我和朋友跑去馬拉灣玩水、跑去烏石被浪衝、跑去龍洞跳水

上述是較為消極作法,目的是避免情況惡化,但這些對於解決問題並沒有太大的助益。要脫離負面迴圈,我認為最有效的作法是

  • 中斷惡性循環,花一筆大錢在你目前最需要的地方上。
  • 找一個發自內心相信、並且願意投入努力的目標去實踐

脫離省錢的惡性循環

這樣的日子持續數周後,某天起床,我的內心突然浮現一個聲音:「不能再這樣下去了,我必須趕快把眼鏡換掉。」後來我去找了專業的配鏡店,配了一副上萬元的眼鏡。

戴上新的眼鏡後,透過毫無刮痕的新鏡片看世界,整個世界都光輝起來。每時每刻抬起頭來,都可以重新感受到世界的美好。但比起眼前的光明,更重要的是內心的轉變。你知道戴在你鼻樑上東西很貴,他不斷在提醒你,你是有價值的,你值得這些,你會意識到,自己該重新展開新的人生,思考新的可能性。

花錢的目的,並不是讓你大玩特玩,像壓力很大的上班族週末跑去百貨公司血拼轉移注意力。花錢真正的目的,是為了體驗投資自己帶來的好處,讓自己不要因為怕窮而省錢,結果越省越窮。穿一件用料好的衣服,讓自己出門時充滿自信、戴一副清澈明亮眼鏡、換掉那台sim卡常常接觸不良、鏡頭模組爛到根本不會想拿來拍照的ASUS手機。花錢只是手段,真正的目的是消除那些生活中帶來負面情緒的因素。

低潮是反省的好時機

在低潮的期間,我依舊不斷的反省,過去所做的決策與判斷,哪些是因為正確的觀念而產生好的結果,哪些想法是有問題的。

  • 追尋穩定的技術而跳去系統程式錯誤的觀念(但這不代表跳到系統程式是錯誤的),每個領域的技術都會不斷進步、演變,網頁也是、系統程式也是。真正的問題不是在技術變得太快,而是你沒有找到願意投入十年的方向
  • 認清你的失敗,是因為方向錯誤,還是方法錯誤。所謂的方向錯誤,是你本身就不適合、不喜歡、不該做這件事情,就像矮冬瓜跑去打籃球一樣。而方法錯誤,是你用錯方法,導致你在學習的過程中,充滿挫折。錯誤的方法包括找一堆書來讀,卻缺乏實做經驗、欠缺成就感等。
  • 把尋找目標這件事情當成一個長期的過程,請找到自己的內在動機,因為它們往往比外在動機更能長久。

人的內心其實相當複雜、難解,而且常常會自我矛盾。願意投入一件事情的理由,往往是許多不同的因素揉合在一起的結果,例如我決定投入系統程式這件事,其實包含了「我不想一輩子寫應用,我想知道我還能做什麼」、「理解電腦的底層原理好像很重要,這樣就可以活在工程師的鄙視鍊最上層」、「我很敬佩Jserv」、「感覺台灣產業需要這樣的人才」、「我覺得我的個性蠻踏實、有毅力的,應該可以勝任這樣的題目」。

但你看也知道,這些理由多半是自己內心的投射和想像,遇到現實很容易就直接被打臉。我們的心靈很容易被其他人所影響。賈伯斯就是這方面的佼佼者,他可以點燃工程師的熱情,把他們逼到極限來加快Mac的開機速度。厲害的業務員並不會讓你感受到他在推銷,但卻能夠讓你心甘情願掏出皮夾。心儀對象的一句話就讓你心甘情願的做牛做馬,反觀父母怎麼威脅利誘都沒有用。

有人會爭辯,幹嘛管那麼多?只要能夠驅使你自動自發努力的動機不就是好動機嗎?

不,不是的。一個顯而易見的例子是,為了錢工作的人,和為了理想工作的人,他們的表現絕對是不一樣的。過於理性的動機,例如錢多事少離家近,並沒辦法打動人心。羨慕與欽佩你的偶像,在遇到困難的時候,不見得有辦法帶你渡過難關。只是覺得「底層好像很重要」,如果沒有實際解決真實世界的問題,並不會感受何謂很重要,這樣的膚淺幻想很容易在遇到挫折時破滅。

我們往往是先透過感性下決策,最後才用理性解釋為什麼這麼做。而感性往往比理性更能夠打動人心,。你需要的是一個發自內心相信的動機,你願意學習、鑽研,不是因為背負著其他人的期望,不是因為薪水,而是像許多人一開始決定學吉他的原因一樣:「台上的人好酷,我想變得跟他一樣」。矽谷創業教父Paul Graham在如何才能去做你喜歡的事情裡談到:「不僅要做自己喜歡的事,而且是令人佩服的事,是那種做完可以說“哇,太酷了”的工作」。但不要誤會我的意思,你得先發自內心覺得很酷,進而把事情做好,才會受人尊敬,而不是為了受人尊敬,去尋找那些看起來能夠獲得聲望的工作。

每個人都是自己人生的駕駛,由自己決定開往的目的地。找到內心想前往的方向,才能夠享受旅途的美景,注意到各種有趣的機會,如果只是為了找到寶藏而前進,不斷患得患失是一件很可惜的事情。旅途中,你需要找到加油站,才能讓你的車子充滿動力,開往下一個里程碑,加油站就是你的成就感。

成就感就像油料一樣

沒有成就感的努力不會持久。為什麼大多數人沒辦法持之以恆的健身?但少數人可以練就一身好身材?因為大多數人持續了兩三個禮拜,沒看到效果,很容易就倦怠,不知不覺就放棄了。但少數人卻能夠撐過那段看不見陽光的日子。

我不願意把這件事情簡單的歸因於毅力,把那些放棄的人歸因於不夠努力。那些成功跨過無成就感之谷的人,可能是因為厲害的朋友邀約他們一起去運動,因此比較不會偷懶。前輩現身教學,幫助他們克服動作、姿勢的問題,變得比其他人更容易感受到自己的進步,遇到問題也能夠立刻修正。經過一兩個月之後,他們的身形漸漸有了改變,旁人開始稱讚,他們也掌握了常見的動作和肌肉的出力方式,他們對自己更有自信,也更願意繼續練習下去。

要失去成就感實在是太容易了,你只需要在達到自己的目標前,不斷遇到挫折就行了。例如不斷地看書,卻做不出有用的東西。不斷嘗試各種作法,但卻不知道這些作法有沒有效。把努力的對象放錯重點,鑽研一旁的小問題,你想解決的目標卻沒有太大的進展。自己一直撞牆,卻因為害羞不願意請教別人。久久看不見成果,缺乏他人的稱讚等。想要脫離這樣的循環,統統反過來做就好了。

不需要在一開始就想要把所有事情都全部搞懂。你需要經過一段時間,才會了解一個知識領域中最重要的20%是什麼,其他80%自然而然會漸漸清晰,因此不要死命的硬啃,盡可能讓你學習的過程充滿樂趣,享受旅途的風景。多多動手練習而不是看書,書只是參考資料,偶爾停下來看一下確認方向就好。最重要的是,盡快產出一些東西,你才有辦法獲得成就感,並且快速的修正。

找方向,找方法

搞清楚自己要什麼,找到正確的方向,試著釐清自己內心的聲音,哪部份的動機是基於其他人的期望,哪部份是基於自己的幻想,哪部份的動機才是最底層、最核心,最能夠打動自己的部份?用正確的方法努力,對成果有正確的期待,盡快累積成就感。不用急著擔心錢的問題,想想你那些揹著學貸的朋友還是咬著牙撐過來了。把目光從台灣的產業移開,從世界的需求思考自己的定位。把Ptt的tech_job版關掉,脫離負面悲觀的迴圈,從更高的層次去思考,找方向,找方法,才能夠看到更多可能性。

Concurrency系列(五): Sequential Consistency的代價

Memory Consistency Models

我們前面系列提及到,實際上程式在編譯與執行的時候,不一定會真的照你所寫的順序發生。而是可能改變順序、盡量最佳化,同時營造出彷彿一行一行執行下來的幻象,只要實際的結果和照順序執行沒有差別就好。

這樣的幻象要成立,在於程式設計師和該系統(硬體、編譯器等產生、執行程式的平台)達成了一致的協定,系統保證程式設計師只要照著規則走,程式執行結果會是正確的。

但什樣叫做正確?正確的意思不是保證只會發生一種執行結果,而是定義在所有可能發生的執行結果中,哪些是允許的。我們把這樣的約定稱為Memory Consistency Models,系統要想辦法在保證正確的情況下,盡可能的最佳化,讓程式跑的又快又好。

Memory Consistency Models存在於許多不同的層次中,像是組合語言跑在硬體上時,因為處理器可以做指令重排和最佳化,雙方得確保執行結果和預期相同。或者,在將高階語言轉換成組語時,因為編譯器能夠將組合語言重排,雙方也得確保產生的結果和預期一致。換言之,從原始碼到最後實際執行的硬體上,大家都必須做好約定,才會跑出預期的結果。

最直覺的約定,Sequential Consistency

在1970年代,Lamport大大就在思考這個問題了。他提出一個如今最常見的Memory Consistency Model: Sequential Consistency,並且定義如下

A multiprocessor system is sequentially consistent if the result of any execution is
the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.

我們可以分成兩個觀點來看Sequential Consistency的定義,
1. 對於每個獨立的處理單元,執行時都維持程式的順序(Program Order)
2. 整個程式以某種順序在所有處理器上執行

Lamport的定義濃縮的很精煉,對於第一次看到的人會抓不太他想表達的重點,因為這實在是太蠢、太顯而易見了。第一點講你的程式在處理器內會照順序跑,第二個講所有處理器會以某種順序執行你的程式。你一定覺得,幹這不是廢話嗎XD

之所以會這樣覺得,是因為你一直以來都活在這樣的世界,就像活在牛頓時代以前的人,覺得拿手上的東西放開就會掉下來一樣自然。接下來我們會告訴你,想要保證這樣的現象,在現代的處理器上會限制很多最佳化的手段,讓程式執行的沒那麼快。如果你同意放棄一些約定,例如不保證每個處理單元維持程式執行的順序,我們還能榨出更多效能出來。

讓我再額外補充一點,Memory Consistency Model只是一個幻象的約定,程式執行的結果必須看起來是這樣,但是實際程式編譯完、跑在硬體上,想怎麼改變執行順序都可以,只要結果和約好的定義相同就好。

確保執行順序

我們將用這張圖來闡述Sequential Consistency的兩個要點。上圖左邊是Dekker’s Alogrithm,一個關於critical section的演算法。如果我們確保Sequential Consistency,在每個處理器核心內維持program order,那麼這個程式就能確保同一時間只有一個處理器進入critical section。因為你一定是先立Flag,再檢查對方Flag是否立起,如果沒有才進入Critical Section。

但想像另一個情況,同一個處理器他可能直覺的認為Flag1, Flag2兩個變數沒有相依性,因此就算違反SC調換執行順序也沒差。如果P1和P2都先執行第二行,才執行第一行,那麼就會發生同時進入critical section的窘境。

確保對所有處理器都是一致的

上圖右邊,當P1執行A=1的效果發生後,P2進行if判斷為真,於是執行B=1,P3執行if判斷B等於1為真,最後把register1寫入A的值。這樣的程式要保證register1讀到A的值是1,前提是P1寫入共享變數A=1後,P2和P3都能保證讀到A=1,也就是確保整個程式以某種順序在所有處理器上執行,對每個處理器而言,都能看到其他先執行的指令所發生的效果。

我們接下來看三個典型的範例,就算是沒有cache的硬體架構,稍不注意也可能會違反Sequential Consistency。

Write Bufers with Bypassing Capability

這個範例會告訴我們維持Write->Read順序的重要性。


如上圖左,每個處理器都有自己的write buffer,程式在執行時處理器可以先寫到Write Buffer,晚點再寫到Memory上。

我們使用最佳化的手法,當處理器寫入write buffer時,不等待寫入到記憶體完成,直接繼續執行下面的程式。而接下來若發生read,只要讀的位址不是write buffer內等待寫入memory的位址,就允許讀取。這在單核心處理器上是個很常見的最佳化手法,不用等待耗時的寫入就繼續執行,可以縮短等待的時間。

但這種做法會導致違反Sequential Consistency,看看上圖右邊的程式,假設程式雖然看起來是一行一行執行下來,但實際上執行write時,是先寫到Buffer上,然後直接允許下一行read從主記憶體讀取。因此實際程式對記憶體的操作,會是上圖左的t1(讀取Flag2)->t2(讀取Flag1)->t3(寫入Flag1)->t4(寫入Flag2),兩個Flag都讀到0,統統進入critical section,並且違反SC。

Overlapping Write Operations

這個範例會告訴我們維持Write->Write順序的重要性。

假設在一個有多個記憶體模組,沒有bus且彼此互向連結的系統上,因為沒有bus,所以執行時不需要照順序執行,而是可以同時執行多個操作。我們假設處理器一樣照著程式的順序發出write請求,而且不等待前一個執行完畢,就直接發出下一個請求。

在執行右邊的程式時,如果遵守SC,應該可以看到Data會是最新的值2000。但這個架構上並不保證發生,因為P1寫入Data和Head時,可能會發生Head先抵達記憶體,Data後抵達記憶體的情況。因此實際的操作可能變成t1(寫入Head成功)–>t2(讀取Head為1)–>t3(讀取Data讀到舊的值)–>t4(寫入Data成功),變得完全違反SC了。

在單處理器上,對於寫入不同的位址,修改寫入的順序是不會有大問題的,只要維持data的相依性就好。但在此處的範例就可能出狀況,想要解決這樣的問題,必須等待上一個write完成之後,也就是等待acknowledgement response,才發出下一個寫入的請求。

Non-Blocking Read Operations

這個範例會告訴我們維持Read->Read, Read->Write順序的重要性。


這也是一個常見的最佳化,允許我們更改讀取的順序。假設P1很乖,程式都照順序寫入,但P2不等待read Head完成就繼續發出read Data的請求。就有可能發生t1(Read Data先回傳結果為0)–>t2(寫入Data 2000)–>t3(寫入Head 1)–>t4(讀取Head為1),產生違反SC的結果。

Cache Architechture 與 Sequential Consistency

上述三個是很典型的案例,要是程式存取記憶體的順序發生改變,可能會違反Sequentical Consistency。在有設計cache的系統內,也會遭遇上述的問題。

對於有cache的系統架構,同一份資料可能會存在於多個處理器的cache上。如果想要維持Sequential Consistency,系統要確保同一份資料在不同處理器的cache上保持一致,不然有的處理器讀到比較新的資料,有的讀到比較舊的,每顆處理器所見到的行為不一致,很容易就違反SC。就算是發現要讀取的資料剛好在cache內,也不能立刻讀出來,必須要確保前一個操作完成,才能進行讀取。

Cache Coherence and Sequential Consistency

我們需要一個機制確保不同處理器上的cache在系統中保持一致,稱之為cache coherence protocol。如果cache coherent protocal夠嚴格,那麼我們就可以保證這個系統上的程式不會出現違反Sequential Consistency的結果。

我們可以把cache coherent protocal想像成 “所有的寫入最終都會被所有的處理器看見”, 以及”寫入相同的位址的順序對於所有處理器而言都是一致的”(因此寫入相同位址不會出現交換順序的情況),如果想要遵守Sequential Consistency,還要確保”寫入不同位址的順序對於所有處理器而言都是一致的”(因此所有寫入不會出現交換順序的情況)。

Detecting the Completion of Write Operations

想要維持Program Order,代表我們需要確保上個寫入完成了,才能執行下一個指令。因此我們需要從記憶體模組收到一個完成的信號代表該次寫入完成。對於沒有cache的系統來說很簡單,就從主記憶體回傳信號即可。

但對於有cache的系統,所謂的寫入完成,真正的意思是,對所有處理器而言都能看到新的寫入值,因此必需要確保每份cache都被正確的更新或是無效化(必須重新從記憶體抓正確的值出來)

Maintaining the Illusion of Atomicity for Writes

在把新的值更新到每個cache上時,要知道這樣的操作並不是atomic的,並不是一瞬間,所有的cache統統都更新完成。可能有的cache會先被更新,有的之後才更新。


如上圖,假設P1和P2都照著Program Order執行,但要是寫入A=1和A=2用不同的順序抵達P3和P4,就會發生register1和register2讀到兩個不同的值的情形。例如P3看到的是A=1 A=2 B=1 C=1, P4看到的是A=2 A=1 B=1 C=1,使得P3, P4明明是讀取相同的A值,卻出現不一致的情形。避免這種狀況的方式是確保”寫入相同的位址的順序對於所有處理器而言都是一致的”。


但只保證寫入相同位址時所有處理器都看到同樣的更新順序是不夠的。回到一開始的圖右邊的程式碼範例,P1寫入A=1,假設P2已經可見A=1,於是執行B=1,但是對P3來說,還沒收到A=1的修改,但是已經看到B=1的修改。於是便讀出A的值為0。

想要避免這種情況發生,在讀取一個剛寫入的值之前,必須要確保所有的處理器的cache都正確的更新,如此一來對所有處理器來說,整個程式的順序就一致了,就能夠滿足Sequential Consistency。

小結

這些東西說破不值錢,但對於第一次接觸Memory Consistency Model和Sequential Consistency的人而言,一開始要理解並不容易。但不用緊張看久了就有fu了,SC雖然容易理解,但其實限制了很多最佳化的手段,如果我們可以放寬對Sequential Consistency的依賴,就可以讓程式跑得更快,後續我們會往更weak的memory consistency model邁進。

Reference

Concurrency系列(四): 與Synchronizes-With同在

在談論Concurrency時,常常會看到許多文件、文章使用Synchronized-with這個詞彙,但是深入google你會發現,網路上關於這個詞的資訊並不多,C++官方文件也沒有提出很明確的定義或解釋,但是他們依舊繼續使用這個詞,只說明有一些操作可以建立synchronizes-with的關係,這個詞大家並沒有給出一個很明確的定義。

我先用自己的話向大家解釋什麼是synchronized-with

synchronized-with是個發生在兩個不同thread間的同步行為,當A synchronized-with B的時,代表A對記憶體操作的效果,對於B是可見的。而A和B是兩個不同的thread的某個操作。

你會發現,其實synchronized-with就是跨thread版本的happens-before。

從Java切入

當在一個multithread的環境下,你要如何確定thread A執行someVariable = 3,那麼其他thread能夠看到3真的被寫入someVariable?

實際上,有很多原因會讓其他thread不會立刻看到someVariable為3,可能是compiler做instruction reorder,把指令重排讓程式更有效率,也可能是someVariable還在register內、或是被處理器寫到cache,但還沒被寫到到main memory上,甚至是其他thread嘗試讀取someVariable的時後讀到舊的cache資料。

因此,Java必須要定義一些特殊的語法,像是volatile, synchronized, final來確保針對同一個變數的跨thread記憶體操作能夠正確的同步。

synchronized keyword

public class SynchronizedCounter {
  private int c = 0;
  public synchronized void increment() {
    c++;
  }

  public synchronized void decrement() {
    c--;
  }

  public synchronized int value() {
    return c;
  }
}

可以看到上面的程式碼中,每個方法前面都加了一個關鍵字synchronized,這個關鍵字有兩個效果
– Mutual Exclusive
– 對同一個物件而言,不可能有兩個前綴synchronized的方法同時交錯執行,當一個thread正在執行前綴synchronized的方法時,其他想執行synchronized方法的thread會被擋住。
– 建立Happens Before關係
– 對同一個物件而言,當一個thread離開synchronized方法時,會自動對接下來呼叫synchronized方法的thread建立一個happens-before關係,前一個synchronized的方法對該物件所做的修改,保證對接下來進入synchronized方法的thread可見。

要確保這件事情,代表JVM必須要做兩件事,一個是在離開synchronized區段時,把local processor的cache寫入到記憶體內,另一個是在進入下一個synchronized前,要讓local cache失效,使處理器重新去main memory抓正確的值。這樣才能夠確保每次進入synchronized區段時,物件的狀態是最新的。

Volatile keyword

另一個常見的用法是Java的volatile,如果你將物件內的變數宣告為volatile,那麼不同thread對該變數的讀寫,保證是atomic的,而且會讀到最新寫入的值。如果我用正式一點的術語描述,就是 A write to a volatile field happens-before every subsequent read of that same volatile

thread create/join

Java在開新thread時,也會建立起跨thread的happens-before關係(其實就是synchronized-with)。當thread A呼叫Thread.start建立thread B時,thread A呼叫start之前對記憶體產生的影響對於thread B可見。

當thread A要結束時,thread B呼叫Thread.join等待thread A結束,此時也會建立起跨thread的happens-before關係,thread A結束前對記憶體的影響對於呼叫join之後的thread B可見。

圖解

img
這樣我們就能理解上圖了,左邊thread A確保每一行都happens-before下一行,右邊的thread B也確保每一行都happens-before下一行,因此如果我對unlock M和lock M建立了synchronized-with(跨thread的happens-before)的關係,那麼所有unlock M之前的效果,對於lock M之後都可見。

再來輪到C++

C++比Java討厭的地方在於,Java努力把各種底層的複雜性藏起來,讓上層的JVM提供一個一致的環境,得以達成Write Once, Run Anywhere的理想。但C++盡可能提供你所有你能做到的事情,即使你可能會誤用這些工具。

在2014年C++的官方標準文件(Standard for Programming Language C++)N4296的第12頁,提示了C++提供的同步操作,也就是使用atomic operation或是mutex:

The library defines a number of atomic operations and operations on mutexes that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another.

如上所述,C++定義了一系列的atomic operation和mutex,來協助你建立跨thread間的同步關係。實際上還有更多語法可用,但我們來看一個簡單的mutex例子:

include<iostream> // std::cout
include<thread>   // std::thread
include<mutex>    // std::mutex

std::mutex mtx;   // mutex for critical section

int count = 0;

void printthreadid (int id) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  std::cout << "thread #" << id << " count:" << count << '\n';
  count++;
  mtx.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i&lt;10; ++i)
    threads[i] = std::thread(printthreadid,i+1);

  for (auto&amp; th : threads) th.join();

  return 0;
}

上述的程式碼一次開10個thread,每個thread做的事情都一樣,印出傳入的參數和counter目前的值,之後把counter++。要是沒有加上mutex lock,因為thread間交錯執行,無法確保synchronized-with的關係,上個thread執行的效果無法保證傳遞給下一個thread,於是印的亂七八糟,counter數字也亂跳。

沒有加上mutex lock後果如下

thread #thread #thread #thread #12 count: count:00

3 count:0
thread #5 count:0
thread #4 count:0
6 count:2
thread #7 count:6
thread #8 count:7
thread #9 count:8
thread #10 count:9

加上lock之後,結果就正常多了,每個thread都正確地把內容印出來

thread #2 count:0
thread #1 count:1
thread #8 count:2
thread #4 count:3
thread #5 count:4
thread #6 count:5
thread #9 count:6
thread #7 count:7
thread #3 count:8
thread #10 count:9

再次回到Synchronizes-with

實際上,要建立synchornizes-with的關係有很多種不同層次的方法,Jeff Preshing介紹Synchorinzes-with的文章內提供了下面這張詳盡的圖,這張圖只是個大致的示意,實際上可能還有其他方法可以建立同步關係。

可以看到synchronizes-with是一種跨thread間的happens-before關係,此外我們可以透過mutex lock, thread create/join、Aquire and release Semantic來建立synchronized-with關係,因為Aquire and Release是另外一個較為複雜的概念,所以我打算另外開一篇文章再來談,其餘下有更底層的C++ atomic type, memory fence和volatile types等語法,之後再來介紹。

Reference

Concurrency系列(三): 朝Happens-Before邁進

我們先前談過了Sequenced-Before,現在我們來談什麼是Happens-Before

Java對Happens-Before的定義

Java的官方文件定義了什麼是Happens-Before,先不管那些volatile, synchronized等用詞,我只看最簡單的一句

If one action happens-before another, then the first is visible to and ordered before the second.

解釋的很簡單,當行為A happens-before B時,代表A的效果可見,而且發生於B之前。

通俗的解釋

讓我們用比較通俗一點的方式解釋happens-before的概念,這是由Jeff Preshing所提供的解釋

Let A and B represent operations performed by a multithreaded process. If A happens-before B, then the memory effects of A effectively become visible to the thread performing B before B is performed.

可以看到和Java的定義是差不多的,都在說明前一個操作的效果在後一個操作執行之前必須要可見。舉個簡單的例子就是

// example provided by Jeff Preshing
int A, B;
void foo()
{
  // This store to A ...
  A = 5;

  // ... effectively becomes visible before the following loads. Duh!
  B = A * A;
}

 

在上述的簡單的程式碼中,第一行的效果必須要讓第二行的效果可見,B才會正確的得到25,你說這不是很理所當然嗎?寫在前面一行的程式不是本來就應該先執行,之後才執行下一行嗎?

不,並不見得。

這裡有個關鍵是,Happens-before強調的是visible,而不是實際上執行的順序。
實際上程式在執行時,只需要”看起來有這樣的效果”就好,編譯器有很大的空間可以對程式執行的順序做優化。

舉例來說,像是下面的程式,

int A = 0;
int B = 0;

void foo()
{
  A = B + 1; // (1)
  B = 1; // (2)
}

int main()
{
  foo();
}

如果你只下gcc file.c,產生的組語節錄如下

movl B(%rip), %eax
addl $1, %eax
movl %eax, A(%rip)
movl $1, B(%rip)

可以看到先把B放到eax,之後eax+1放到A,然後才執行B=1。

但如果下gcc -O2 file.c

movl B(%rip), %eax
movl $1, B(%rip)
addl $1, %eax
movl %eax, A(%rip)

可以看到變成先把B放到eax,然後把B=1,最後再執行eax+1,然後才把結果存到A。
B比A更早先完成。

但這有違反happens-before的關係嗎?答案是沒有,因為happens-before只關注是否看起來有這樣的效果,從外界看起來,就彷彿是先執行第一行,完成之後,再執行第二行。

因此我們學到了一個重要的關鍵,A happens-before B並不代表實際上A happening before B。關鍵在於只要A的效果在B執行之前,對於B可見就可以了,實際上怎麼執行的並不需要深究。

現在我們來看C++對happens-before的定義,其實也是相同的概念

C++對Happens-Before的定義

再來看C++的定義

Regardless of threads, evaluation A happens-before evaluation B if any of the following is true:
1) A is sequenced-before B
2) A inter-thread happens before B

在C++的解釋中,Happens-before包含兩種情況,一種是同一個thread內的happens-before關係,另一個是不同thread間的happens-before關係。

我們平常程式一行一行寫下來,我們本來就預期上一行的程式效果會對下一行的程式可見。我們先前已經清楚的解釋什麼是Sequenced-before,現在你可以發現,Sequenced-before其實就是同一個thread內的happens-before。

在跨thread的情況下,如果沒有保證happens-before的關係,程式常常會出現意料之外的結果。舉例來說

int counter = 0;

現在有兩個thread同時執行,thread A執行counter++,thread B把counter的值印出來。因為這兩個thread沒有具備happens-before的關係,沒有保證counter++後的效果對於印出counter是可見的,導致印出來的結果可能是1,也可能是0。

因此,語言必須提供適當的手段,讓程式設計師能夠建立跨thread間的happens-before的關係,如此一來才能確保程式執行的結果正確。這也就是剛剛C++ happens-before定義裡的第二點,A inter-thread happens before B

Inter-thread happens before

[Java定義清楚用何種語法能夠建立happens-before的關係,在此先不贅述。
C++定義了五種情況都能夠建立跨thread間的happens-before,如下
1) A synchronizes-with B (A和B有適當的同步)
2) A is dependency-ordered before B (A和B有相依的順序關係)
3) A synchronizes-with some evaluation X, and X is sequenced-before B
4) A is sequenced-before some evaluation X, and X inter-thread happens-before B
5) A inter-thread happens-before some evaluation X, and X inter-thread happens-before B

其中第3, 4, 5點都是遞迴定義,因此我們只關注前兩點,不過再解釋下去會讓篇幅過長,影響閱讀和理解的順暢,目前只要先有個概念就好,我們後續再解釋什麼是Synchoronizes-with和dependency-ordered before。

Reference