上個禮拜才看完 2008 / 10 的 MSDN Magazine,才想說這期 MSDN 的內容,有一半以上的主題是討論多核心處理器效能問題 (平行處理,多核的 CACHE 機制對程式的影響... etc),不過剛才才想貼篇心得,上網一看竟然又縮回去只剩九月號? @_@
難道我是夢到我看過十月號的嘛? 哈哈... GOOGLE 找一下還真的給我找到庫存網頁 :D 當然要貼來紀念一下。不知道重新上架後內容會不會多幾篇文章?
這期的主題還真的都押在多核處理器的效能問題上。光是平行處理 ( TPL, PLINQ ) 的方法就好幾篇:
另外在探討多核心效能問題的也有幾篇:
不過看完只記得部份重點而以,本來想多寫點心得,不過原文網址都連不到了... 再等幾天吧 :D
無意間在網站上看到這則新聞:
GOOGLE 讓人變笨?網路便利後遺症
http://n.yam.com/bcc/life/200806/20080626204869.html
文內有這麼一段話:
但是大家都忽視了這種便捷要付出的代價。「網路似乎粉碎了人們專注與沉思的能力,到如今,腦袋只盼著以網路提供資訊的方式來獲取資訊」。
影響所及,傳統媒體也跟著零碎化,長篇大論的東西不再有人要看,一篇文章超過四個段落,讀者就想落跑,電視節目加入滾動字幕和不斷跳出的小廣告,報刊則儘量縮短文章長度,改以一小塊一小塊的摘要取代,在版面上堆砌各種易於瀏覽的零碎資訊。
這篇文章還沒找到真正源頭的那篇文章,不過這篇一定要推一下,真的是講到我心坎裡了。我要講的不是這篇文章,而是現在 INTERNET 環境下,GOOGLE 實在是太方便了,讓很多事開始可以 "速成" 起來。技術的進步不是壞事,不過在什麼都要 "速成" 的文化下,到底還有多少人願意去深入的研究某一件事物呢?
以我的老本行 "軟體開發" 為例,很多人半路出家,學了一些工具或是簡單的網頁程式設計,就可以出家了。天資好一點的,碰到問題還曉得到 GOOGLE 去查查有無合適的 SOLUTION,找到了就東拼西湊湊出個 "可以動" 的 SOLUTION。聽起來沒什麼不好啦,不過這樣一來,沒有幾個人還會願意再深入瞭解為什麼要這樣做等等議題了。我面試過的工程師也有幾十個了 (不知道破百了沒?),有幾個問題是我必問的,其中一個就是:
"你碰到不知道怎麼解決的問題時,你會怎麼做?"
我沒真的統計啦,不過印像中,問題答案的榜首是:
我期望的答案一直沒有聽到,而聽到的都是透過 INTERNET + SEARCH ENGINE 這類的回答。當然這是沒錯啦,因為 GOOGLE 實在太方便了,不過我期望聽到的 "再進修" "想辦法去瞭解問題的原因" 等等答案都沒聽到。就算要找資料,連知道要去查第一手資料 (Microsoft 的東西,大部份 MSDN 都有提供第一手的資訊,不然就是某技術產品的官方網站) 的觀念都沒了,只因為英文看不懂? 或是鄉民寫的東西看的比較簡單? 咳咳...。
而這件事的負面影響,就是人才越來越 M 型化了。INTERNET 只需要有 M 型頂端的人提供資訊,讓其它人來找資源就夠了。GOOGLE 越強,落在 M 型左邊人才的需求就越少。越來越多人依賴 GOOGLE找到零碎的資訊拼湊起來,而少了獨立思考解決問題的能力。我想人類的智能,重組的部份一定會越來越多,創造的部份一定會越來越少,會不會到最後創造的部份就會消失不見了? 難怪 INTEL 會如此預言,2050年後,機器的智慧會開始超越人類了。很有道理耶,因為搜尋 & 重組不就是電腦的專長嗎? 資料夠多運算速度夠快就能做的到。而電腦追不上的 "創造" 這種能力,人類卻漸漸的在喪失...
也許實際情況不會這麼悲觀,但是我相信這一定是個趨勢。能怎樣對抗這樣的 "退化" ? 想辦法讓自己的能力落在 M 型的左邊吧。跟上一篇講的一樣,沒有速成的方式,先把內功練好吧! 與其花時間研究那些兩三年就會淘汰掉,看起來很炫的 "新技術",不如多花點時間學一學能讓你終身受用的基礎能力吧。
會寫這篇是因為上禮拜,有個資深的同事問我個問題,如何把底下的 programmer 素質拉上來? 跟他講這問題害我那天拖到晚上十點才回家吃晚飯 @_@,不過我想這也是現在台灣軟體業普遍碰到的人才問題,就順手寫了這篇。這篇是打算要貼在公司的 BLOG 裡給同事看的,這裡先貼一下,到時整理好再搬過去…。
今天同事有個問題搞不定,就找我去處理,越弄越火... 嘖嘖,這裡就來還原一下現場,記錄一下這個鳥問題...。
最討厭處理這種問題了。現在客戶 IT 都委外,結果就變成 IT 本身不大管事,什麼事就交給外包商就好... 而這種 A 包商跟 B 包商之間的問題,往往就變成踢皮球... 除非有明確證據指出人就是他殺的,不然? 出問題的一方自己摸摸鼻子吧。幾年來吃了不少這種虧,這就是小廠的悲哀啊。
這次碰到的例子是客戶 A 系統建的資料,需要整理後匯到我們負責維護的 B 系統。而中間資料需要作些修正,所以建了一個中繼資料庫,透過 LINKED SERVER,從 A 系統的 DB 把整個 TABLE SELECT 一份到中繼資料庫,然後再進行一連串的修正...
一開始問題很單純,就兩邊碰到中文字編碼不同,直接 SELECT 就碰到這樣的亂碼..
看起來是個小問題,請對方 IT 確認了編碼的問題後,我在中繼資料庫作了點調整,convert 成 ntext 就搞定了。已經可以跑出正確的中文資料了。
正想把程式弄一弄就收工,然後很得意的回報問題搞定時,發現不大對勁,怎麼整個 BATCH 跑下來結果還是錯的? 還錯的不一樣? 真是奇了... 原本轉 UNICODE 問題,再怎麼樣也應該只是變亂碼,或是變 ? 而以,結果這次看到的是資料錯亂,跑出其它的字出來...
這張圖是我把問題簡化後抓到的,原本是有上百行的 SQL SCRIPT ... @_@ 被我抽絲撥繭剩這段。第 33 筆資料是有問題的,不過出現的資料不是原本 "馥瑈" 啊,原本是第二個字變成 ? 而以... 現在竟然變成上一筆資料 (第32筆) 的內容,而第三個字 '榮' 則是由前面好幾筆的 "XX榮" 留下來,第四個字 "子" 就真的不曉得從那裡來了...
好怪的問題,如果是程式碰到這種 BUG,一看就像某個 BUFFER 沒清掉就一直重複被使用,後面的值直接蓋掉前面的值,字串長一點沒被蓋掉,就這樣留下來了。而到了第 33 這筆不知為什麼原因,整個 BUFFER 的內容就跑出來... 這段 SQL SCRIPT 跟本沒作什麼事,不過是前面的 SELECT 指令,改 SELECT INTO 到 TEMP TABLE,然後再撈出來而以。直接 SELECT 沒問題,沒道理 SELECT INTO 就掛掉啊! 不過還真的被我碰到了,see the ghost ..
我沒有直接拼下去 GOOGLE 或是試各種解法,而是想了一下問題是怎麼回事? SQL / DB 的東西我只算外行人,沒本事跟他硬碰硬,我也不知道有啥工具可以看 SQL NATIVE CLIENT 的 TRACE INFO 之類的,只能靠想像,猜一下問題會在那。一開始我就否定掉是不是什麼編碼或是定序的問題,因為我可以正確的 SELECT 出來啊,而且如果 SELECT INTO 是變成 ?? 的話我也許還會頭痛一點,不過竟然是上一筆的資料跑過來了? 這個問題很明顯的,跟本就是 overflow (緩衝區溢位) 之類的 BUG。八成是什麼地方應該填個 0x00 作字串結尾的 CODE 錯掉,結果 SQL 就抓過頭,抓到不該抓的舊資料才會這樣。
拿我寫了十幾年程式的經驗,跟它賭下去了!! 於是我就沒再去跟一堆編碼定序之類的設定搏鬥了,因為我認定這是 SQL SERVER 或是 SQL NATIVE CLIENT 的 BUG。我採的方案是找可以繞過去的方式,於是我把我想的到的 COPY TABLE 都用上了,最後試出來的是笨方法...
用... 用... 用 CURSOR 一筆一筆的跑... @_@
哈哈,各位看倌請笑小聲一點,我這貴州的驢子就只會這種把戲而以.... 果然這樣就正確了。CURSOR 的 FETCH 指令,把欄位的值抓到 nvarchar 的變數,就一切 OK,然後再把這變數的值 update 回我的暫存 TABLE,就什麼問題都沒了 -_-
真是它ㄨㄨㄨㄨ的,最後的 SOLUTION 細節我就不一一的貼出來了,反正只是換個方式 COPY TABLE 而以。既然我賭是 Microsoft 的問題,而換條路的方式 (完全沒改任何定序 OR 編碼) 也成功了,我就不跟它奮鬥下去了,客戶的 IT 既然懶的上 UPDATE / SERVICE PACK,我也只好避掉問題了事... 咳咳。特地貼出來記念一下,如果你們也碰到一樣的問題,切記切記,別跟它硬碰硬啊... :D
繼上篇,講了一些 yield return 編譯後產生的 Code, 說明了 C# compiler 如何用簡單的語法替你實作了 IEnumerator 介面,而完全不會增加程式的複雜度,這是我認為 C# 提供最讚的 Syntax Sugar ...。 不過無意間我想到了 yield return 還有另一種應用方式。靈感來自之前 Darkthread 舉辦的 [黑暗盃程式魔人賽]。因為參賽題目 [xAxB猜數字遊戲] 原本就是考驗演算法,邏輯就不大簡單了,加上要配合 GameHost 的呼叫方式,難度更提高不少。因此之前貼了兩篇文章 [ThreadSync #1. 概念篇 - 如何化被動為主動?, #2. 實作篇 - 互相等待的兩個執行緒],介紹了我改寫的 AsyncPlayer,讓程式可以分別以獨立的執行緒執行 GameHost 及 Player 的程式碼。藉著這方式讓兩者都可以 "獨立思考",邏輯不會中斷,讓程式能夠簡單一些。 不過執行緒同步機制是很花時間的,因為兩方都要等來等去...。多了 Sync 的動作,就要至少 10 ms 的時間來完成這動作。跑個十幾萬次下來,額外花費的時間太多了,因此我貼了那兩篇文章後,就一直在思考這樣的作法有沒有其它效能較佳的方式? 有的,最後我找到的答案就是 yield return,不過大家看了一定很納悶... "yield return (Iteration) 跟執行緒同步機制有什麼關聯?" 不多說,先看看之前畫的兩張時序圖: 先看之前 ThreadSync #1 裡提到的圖,我這次加上紅線當 "輔助線",紅線代表執行 GameHost 的主程式,這個執行序必需反反覆覆的在 GameHost / Player 兩份類別的程式碼跑來跑去,主程式是 GameHost 發起的,當然被強迫切成好幾段的就只有 Player 了。 這是修改過後的版本,GameHost / Player 有各自的執行緒,紅色是 GameHost,藍色是 Player。當執行緒跑到中間時代表它在等待了,等另一方也跑到中間把執行結果放到共用變數,同時叫醒對方之後才交換過來。兩方都各自照著自己的邏輯跑,不過這種等待 & 喚醒的動作,相較於一般的 function call / return 而言,實在是太慢了...。我就是從這張圖得來的靈感,這個解決方式不就跟 yield return 很像嘛? 都是為了避免多次呼叫之間,被呼叫的另一方的邏輯被破切斷的問題... 因此我就開始思考 AsyncPlayer 是不是有機會用 yield return 寫出另一個版本...。 原本的結構很直覺,透過共用變數來傳遞資訊,用 AutoResetEvent 來通知另一個等待中的執行緒可以醒來拿資料去用。而 yield return 則要換個角度來想這件事。yield return 是實作 Iterator 的一種方式,目的是讓你的程式自己決定如何把 collection 裡的 element 照什麼方式丟出去,原本的問題就要想成: "GameHost 要跟 Player 拿所有 Player 會問的問題,而 Player 會透過 yield return 一次一次的把問題丟出去。" 看起來好像可行,不過方向只有單向,就是 Player 丟問題給 GameHost,還缺了 GameHost 把問題答案交給 Player 這段。不過這部份好解決,一樣用共用變數就搞定。細節我就不講太多,直接來看程式碼: 用 yield return 改寫過的 AsyncPlayer
public abstract IEnumerable<HintRecord> Think(); private HintRecord last_record = null; public override int[] StartGuess(int maxNum, int digits) { base.StartGuess(maxNum, digits); this._enum = this.Think().GetEnumerator(); this._enum.MoveNext(); return this._enum.Current.Number; } public override int[] GuessNext(Hint lastHint) { this._enum.Current.Hint = lastHint; if (this._enum.MoveNext() == true) return this._enum.Current.Number; throw new InvalidOperationException("Player Stopped!"); } public override void Stop() { base.Stop(); this._enum.Current.Hint = new Hint(this._digits, 0); try { this._enum.MoveNext(); } catch { Console.WriteLine("!!!!"); } } protected virtual HintRecord GameHost_AskQuestion(int[] number) { this.last_record = new HintRecord( (int[])number.Clone(), new Hint()); return this.last_record; } protected HintRecord GameHostAnswer { get { return this.last_record; } }程式碼一如往常,又是只有一點點 (謎之音: 你到底有沒有寫過長一點的程式碼? -_-) ...。 原本的 Think 改成會傳回 IEnumerable<HintRecord> 的型別,因此內部就可以透過一連串的 yield return xxxx; 指令來把問題交給 GameHost。而 GameHost 拿到題目就會開始計算答案,然後再呼叫 Player.GuessNext( ) 把上次的答案傳回去。透過 Player 的實作,GuessNext 會呼叫 _enum.MoveNext( ), 控制權會再交到 Think( ) 上次呼叫 yield return 的地方,直到又執行到下一個 yield return 為止。這時 GameHost 又取得下一個問題,不斷重複這樣的動作直到結束。 同樣的,我們用 DummyPlayer 改寫,看看用 yield return 的版本寫起來是怎麼樣? DummyYieldPlayer 的程式碼
public class DummyYieldPlayer : YieldPlayer { private Random _rnd = new Random(); private int[] randomGuess() { int[] _currAnswer = new int[this._digits]; List<int> lst = new List<int>(); for (int i = 0; i < _digits; i++) { int r = _rnd.Next(_maxNum); while (lst.Contains(r)) r = _rnd.Next(_maxNum); lst.Add(r); _currAnswer[i] = r; } return _currAnswer; } public override IEnumerable<HintRecord> Think() { while (true) { yield return this.GameHost_AskQuestion(this.randomGuess()); } } }跟上次的 DummyAsyncPlayer (用 ThreadSync 的版本) 一樣,超簡單,實在沒什麼需要說明的了。唯一要特別記得的是,如果你需要取得 GameHost 傳回的答案,應該在 22 ~ 23 行之間,使用 this.GameHostAnswer( ) 來取得答案。有人問我為什麼不把它包成 function call ? 在 function 內接到參數後呼叫 yield return, 而把答案 return 回來不是很好嗎? 很無奈,除非 C# 支援像 C/C++ 那樣的 MACRO 語法,不然這個東西是不可能單靠 yield return 就做出來。你使用 yield return 的條件就是 function return type 一定要是 IEnumerable<T>,這是配對的,代表你不能任易的把 yield return 移到其它 function call 內。除非你不靠 C# yield return 來自動產生對應的 IEnumerator,一切自己來就可以。不過這樣不就又回到原點了? 咳咳... 就乖乖的寫兩行吧。 這樣的寫法執行效率就好的多,我用 DummyYieldPlayer 來測試,跟 DarkThread 提供的版本不相上下,意思是差異小到可以不理它的地步了 :D 這樣的方式不會有太大的效能損失,因為最後要執行的程式碼,跟直接手寫是差不多的,只是中間難寫的那段 code 是 C# compiler 幫我們解決掉,而不是像上回 AsyncPlayer 是用兩個執行緒來解決的。 效果很滿意,當然最後參賽的版本就改這寫法了 :D。不過寫的太晚,來不及幫到其它參賽者 :P,想到這方法算是我佔了 C# compiler 一點便宜,有幸找到方法坳 C# compiler 幫我把最難的部份寫好了,我自己則樂的輕鬆,專心研究怎樣才能少猜幾次... 這裡把我另類應用 yield return 的方法貼給各位參考一下,也算作個筆記 :D,各位高手如果還有發現 yield return 解決過你什麼樣的怪問題,也歡迎到我這留個言 :D