鍍金池/ 教程/ Java/ 個(gè)人技能
個(gè)人技能
服務(wù)你的團(tuán)隊(duì)
機(jī)智地妥協(xié)
附錄 B - 歷史
團(tuán)隊(duì)技能
Contributions
個(gè)人技能
詞匯表
評(píng)判
附錄 A - 書(shū)目/網(wǎng)站目錄
技術(shù)評(píng)判
團(tuán)隊(duì)技能

個(gè)人技能

學(xué)會(huì) Debug

調(diào)試(Debug)是作為一個(gè)程序員的基石。調(diào)試這個(gè)詞第一個(gè)含義即是移除錯(cuò)誤,但真正有意義的含義是,通過(guò)檢查來(lái)觀察程序的運(yùn)行。一個(gè)不能調(diào)試的程序員等同于瞎子。

那些認(rèn)為設(shè)計(jì)、分析、復(fù)雜的理論或其他東西是更基本的東西的理想主義者們不是現(xiàn)實(shí)的程序員?,F(xiàn)實(shí)的程序員不會(huì)活在理想的世界里。即使你是完美的,你周?chē)矔?huì)有,并且也需要與主要的軟件公司或組織,比如 GNU,或者與你的同事,寫(xiě)的代碼打交道。這里面大部分的代碼以及它們的文檔是不完美的。如果沒(méi)有獲得代碼的執(zhí)行過(guò)程可見(jiàn)性的能力,最輕微的顛簸都會(huì)把你永遠(yuǎn)地拋出去。通常這種可見(jiàn)性只能從實(shí)驗(yàn)獲得,也就是,調(diào)試。

調(diào)試是一件與程序運(yùn)行相關(guān)的事情,而非與程序本身相關(guān)。你從主要的軟件公司購(gòu)買(mǎi)一些產(chǎn)品,你通常不會(huì)看到(產(chǎn)品背后的)程序本身。但代碼不遵循文檔這樣的情況(讓你整臺(tái)機(jī)器崩掉是一個(gè)常見(jiàn)又特殊的例子)或者文檔沒(méi)有說(shuō)明的情況仍然會(huì)出現(xiàn),不可避免的,這意味著你做的一些假設(shè)并不對(duì),或者一些你沒(méi)有預(yù)料到的情況發(fā)生了。有時(shí)候,神奇的修改源代碼的技巧可能會(huì)生效。當(dāng)它無(wú)效時(shí),你必須調(diào)試了。

為了獲得一個(gè)程序執(zhí)行的可見(jiàn)性,你必須能夠執(zhí)行代碼并且從這個(gè)過(guò)程中觀察到什么。有時(shí)候這是可見(jiàn)的,比如一些正在呈現(xiàn)在屏幕上的東西,或者兩個(gè)事件之間的延遲。在許多其他的案例中,它與一些不一定可見(jiàn)的東西相關(guān),比如代碼中一些變量的狀態(tài),當(dāng)前真正在執(zhí)行的代碼行,或者是否一些斷言持有了一個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。這些隱藏的細(xì)節(jié)必須被顯露出來(lái)。

通常的(一些)觀察一個(gè)正在執(zhí)行的程序的內(nèi)部的方法可以如下分類(lèi):

  • 使用一個(gè)調(diào)試工具;
  • Printlining - 對(duì)程序做一個(gè)臨時(shí)的修改,通常是加一些行去打印一些信息;
  • 日志 - 用日志的形式為在程序的運(yùn)行中創(chuàng)建一個(gè)永久的視窗。

當(dāng)調(diào)試工具穩(wěn)定可用時(shí),它們是非常美妙的,但 Printlining 和寫(xiě)日志是更加重要的。調(diào)試工具通常落后于編程語(yǔ)言的發(fā)展,所以在任何時(shí)間點(diǎn)它們都可能是無(wú)效的。另外,調(diào)試工具可能輕微改變程序?qū)嶋H執(zhí)行的方式。最后,調(diào)試有許多種,比如檢查一個(gè)斷言和一個(gè)巨大的數(shù)據(jù)結(jié)構(gòu),這需要寫(xiě)代碼并改變程序的運(yùn)行。當(dāng)調(diào)試工具可用時(shí),知道怎樣使用調(diào)試工具是好的,但學(xué)會(huì)使用其他兩種方式是至關(guān)重要的。

當(dāng)需要修改代碼時(shí),一些初學(xué)者會(huì)害怕調(diào)試。這是可以理解的,這有點(diǎn)像探索型外科手術(shù)。但你需要學(xué)會(huì)打破代碼,讓它跳起來(lái),你需要學(xué)會(huì)在它上面做實(shí)驗(yàn),并且需要知道你臨時(shí)對(duì)它做的任何事情都不會(huì)使它變得更糟。如果你感受到了這份恐懼,找一位導(dǎo)師 - (否則)在許多人面對(duì)這種恐懼的脆弱的開(kāi)始時(shí)刻,我們會(huì)因此失去很多優(yōu)秀的程序員。

如何通過(guò)分割問(wèn)題空間來(lái) Debug

調(diào)試是有趣的,因?yàn)樗婚_(kāi)始是個(gè)迷。你認(rèn)為它應(yīng)該這樣做,但實(shí)際上它卻那樣做。很多時(shí)候并不僅是這么簡(jiǎn)單---我給出的任何例子都會(huì)被設(shè)計(jì)來(lái)與一些偶爾在現(xiàn)實(shí)中會(huì)發(fā)生的情況相比較。調(diào)試需要?jiǎng)?chuàng)造力與智謀。如果說(shuō)調(diào)試有簡(jiǎn)單之道,那就是在這個(gè)謎題上使用分治法。

假如,你創(chuàng)建了一個(gè)程序,它會(huì)在一個(gè)序列里做十件事情。當(dāng)你運(yùn)行它的時(shí)候,它崩潰了。因?yàn)槟銓?xiě)的代碼并不想讓它崩潰,所以現(xiàn)在你有一個(gè)謎題了。當(dāng)你查看輸出時(shí),你可以看到序列里前七件事情運(yùn)行成功了。最后三件事情在輸出里卻看不到,所以你的謎題變小了:“它是在執(zhí)行第 8、9、10 件事的時(shí)候崩潰的”。

你可以設(shè)計(jì)一個(gè)實(shí)驗(yàn)來(lái)觀察它是在哪件事情上崩潰的嗎?當(dāng)然,你可以用一個(gè)調(diào)試器或者我們可以在第 8 第 9 件事后面加一些 printlining 的語(yǔ)句(或者你正在使用的任何語(yǔ)言里的等價(jià)的事情),當(dāng)我們重新運(yùn)行它的時(shí)候,我們的謎題會(huì)變得更小,比如“它是在做第九件事的時(shí)候崩潰的”。我發(fā)現(xiàn),把謎題是怎樣的一直清楚地記在心里能讓我們保持注意力。當(dāng)幾個(gè)人在一個(gè)問(wèn)題的壓力下一起工作時(shí),很容易忘記最重要的謎題是什么。

調(diào)試技術(shù)中分治的關(guān)鍵和算法設(shè)計(jì)里的分治是一樣的。你只要從中間開(kāi)始劃分,就不用劃分太多次,并且能快速地調(diào)試。但問(wèn)題的中點(diǎn)在哪里?這就是真正創(chuàng)造力和經(jīng)驗(yàn)需要參與的地方。

對(duì)于一個(gè)真正的初學(xué)者來(lái)說(shuō),可能發(fā)生錯(cuò)誤的地方好像在代碼的每一行里都有。一開(kāi)始,你看不到一些其他的你稍后將會(huì)學(xué)到的維度,比如執(zhí)行過(guò)的代碼段,數(shù)據(jù)結(jié)構(gòu),內(nèi)存管理,與外部代碼的交互,一些有風(fēng)險(xiǎn)的代碼,一些簡(jiǎn)單的代碼。對(duì)于一個(gè)有經(jīng)驗(yàn)的程序員,這些其他的維度為整個(gè)可能出錯(cuò)的事情展示了一個(gè)不完美但是有用的思維模型。擁有這樣的思維模型能讓一個(gè)人更高效地找到謎題的中點(diǎn)。

一旦你最終劃分出了所有可能出錯(cuò)的地方,你必須試著判斷錯(cuò)誤躲在哪個(gè)地方。比如:這樣一個(gè)謎題,哪一行未知的代碼讓我的程序崩潰了?你可以這樣問(wèn)自己,出錯(cuò)的代碼是在我剛才執(zhí)行的程序中間的那行代碼的前面還是后面?通常你不會(huì)那么幸運(yùn)就能知道錯(cuò)誤在哪行代碼甚至是哪個(gè)代碼塊。通常謎題更像這個(gè)樣子的:“圖中的一個(gè)指針指向了錯(cuò)誤的結(jié)點(diǎn)還是我的算法里變量自增的代碼沒(méi)有生效?”,在這種情況下你需要寫(xiě)一個(gè)小程序去確認(rèn)圖中的指針是否都是對(duì)的,來(lái)決定分治后的哪個(gè)部分可以被排除。

如果移除一個(gè)錯(cuò)誤

我曾有意把檢查程序執(zhí)行和修復(fù)錯(cuò)誤分割開(kāi)來(lái),但是當(dāng)然,調(diào)試也意味著移除 bug。理想狀況下,當(dāng)你完美的發(fā)現(xiàn)了錯(cuò)誤以及它的修復(fù)方法時(shí),你會(huì)對(duì)代碼有完美的理解,并且有一種頓悟(啊哈!)的感覺(jué)。但由于你的程序會(huì)經(jīng)常使用不具有可視性的、沒(méi)有一致性注釋的系統(tǒng),所以完美是不可能的。在其他情況下,可能代碼是如此的復(fù)雜以至于你的理解可能并不完美。

在修復(fù) bug 時(shí),你可能想要做最小的改變來(lái)修復(fù)它。你可能看到一些其他需要改進(jìn)的東西,但不會(huì)同時(shí)去改進(jìn)他們。試圖使用科學(xué)的方法去改進(jìn)一個(gè)東西,并且一次只改變一個(gè)東西。修復(fù) bug 最好的方式是能夠重現(xiàn) bug,然后把你的修復(fù)替換進(jìn)去,重新運(yùn)行你的程序,觀察 bug 不再出現(xiàn)。當(dāng)然,有時(shí)候不止一行代碼需要修改,但你在邏輯上仍然需要使用一個(gè)獨(dú)立原子(譯者注:以前人們認(rèn)為原子不可再分,所以用用原子來(lái)代表不可再分的東西)的改變來(lái)修復(fù)這個(gè) bug。

有時(shí)候,可能實(shí)際上有幾個(gè) bug,但表現(xiàn)出來(lái)好像是一個(gè)。這取決于你怎么定義 bug,你需要一個(gè)一個(gè)地修復(fù)它們。有時(shí)候,程序應(yīng)該做什么或者原始作者想要做什么是不清晰的。在這種情況下,你必須多加練習(xí),增加經(jīng)驗(yàn),評(píng)判并為代碼賦予你自己的認(rèn)知。決定它應(yīng)該做什么,并注釋/或用其他方式闡述清楚,然后修改代碼以遵循你賦予的含義。這是一個(gè)進(jìn)階或高級(jí)的技能,有時(shí)甚至比一開(kāi)始用原始的方式創(chuàng)建這些代碼還難,但真實(shí)的世界經(jīng)常是混亂的。你必須修復(fù)一個(gè)你不能重寫(xiě)的系統(tǒng)。

如何使用日志調(diào)試

Logging(日志)是一種編寫(xiě)系統(tǒng)的方式,可以產(chǎn)生一系列信息記錄,被稱(chēng)為 log。Printlining只是輸出簡(jiǎn)單的,通常是臨時(shí)的日志。初學(xué)者一定要理解并且使用日志,因?yàn)樗麄儗?duì)編程的理解是局限的。因?yàn)橄到y(tǒng)的復(fù)雜性,系統(tǒng)架構(gòu)必須理解與使用日志。理想地,程序運(yùn)行時(shí),日志產(chǎn)生的信息的數(shù)量需要是可配置的。通常,日志提供了下面三個(gè)基本的優(yōu)點(diǎn):

  • 日志可以提供一些難以重現(xiàn)的 bug 的有效信息,比如在產(chǎn)品環(huán)境中發(fā)生的、不能在測(cè)試環(huán)境重現(xiàn)的 bug。
  • 日志可以提供統(tǒng)計(jì)和與性能相關(guān)的數(shù)據(jù),比如語(yǔ)句間流逝過(guò)的時(shí)間。
  • 可配置的情況下,日志允許我們獲取普通的信息,使得我們可以在不修改或重新部署代碼的情況下調(diào)試以處理具體的問(wèn)題。

需要輸出的日志數(shù)量總是一個(gè)簡(jiǎn)約與信息的權(quán)衡。太多的信息會(huì)使得日志變得昂貴,并且造成滾動(dòng)目盲,使得發(fā)現(xiàn)你想要的信息變得很困難。但信息太少的話(huà),日志可能不包含你需要的信息。出于這個(gè)原因,讓日志的輸出可配置是非常有用的。通常,日志中的每個(gè)記錄會(huì)標(biāo)記它在源代碼里的位置,執(zhí)行它的線程(如果可用的話(huà)),時(shí)間精度,并且,通常有,一些額外的有效信息,比如一些變量的值,剩余內(nèi)存大小,數(shù)據(jù)對(duì)象的數(shù)量,等等。這些日志語(yǔ)句撒遍源碼,但只出現(xiàn)在主要的功能點(diǎn)和一些可能出現(xiàn)危機(jī)的代碼里。每個(gè)語(yǔ)句可以被賦予一個(gè)等級(jí),并且將會(huì)在系統(tǒng)設(shè)置輸出這個(gè)等級(jí)時(shí)輸出這個(gè)記錄。你應(yīng)該設(shè)計(jì)好日志語(yǔ)句來(lái)標(biāo)記你預(yù)期的問(wèn)題。預(yù)估測(cè)量程序表現(xiàn)的必要性。

如果你有一個(gè)永久的日志,printling 現(xiàn)在可以用日志的形式來(lái)完成,并且一些調(diào)試語(yǔ)句可能會(huì)永久地加入日志系統(tǒng)。

如何理解性能問(wèn)題

學(xué)習(xí)理解運(yùn)行的程序的性能問(wèn)題與學(xué)習(xí) debug 是一樣不可避免的。即使你完美地理解了你寫(xiě)的代碼的代價(jià),你的代碼也會(huì)調(diào)用其他你幾乎不能控制的或者幾乎不可看透的軟件系統(tǒng)。然而,實(shí)際上,通常性能問(wèn)題和調(diào)試有點(diǎn)不一樣,而且往往要更簡(jiǎn)單些。

假如你或你的客戶(hù)認(rèn)為你的一個(gè)系統(tǒng)或子系統(tǒng)運(yùn)行太慢了。在你把它變快之前,你必須構(gòu)建一個(gè)它為什么慢的思維模型。為了做到這個(gè),你可以使用一個(gè)圖表工具或者一個(gè)好的日志,去發(fā)現(xiàn)時(shí)間或資源真正被花費(fèi)在什么地方。有一句很有名的格言:90%的時(shí)間會(huì)花費(fèi)在 10%的代碼上。在性能這個(gè)話(huà)題上,我想補(bǔ)充的是輸入輸出開(kāi)銷(xiāo)的重要性。通常大部分時(shí)間是以某種形式花費(fèi)在 I/O 上。發(fā)現(xiàn)昂貴的 I/O 和昂貴的 10%代碼是構(gòu)建思維模型的一個(gè)好的開(kāi)始。

計(jì)算機(jī)系統(tǒng)的性能有很多個(gè)維度,很多資源會(huì)被消耗。第一種資源是“掛鐘時(shí)間”,即執(zhí)行程序的所有時(shí)間。記錄“掛鐘時(shí)間”是一件特別有價(jià)值的事情,因?yàn)樗梢愿嬖V我們一些圖表工具表現(xiàn)不了的不可預(yù)知的情況。然而,這并不總是描繪了整幅圖景。有時(shí)候有些東西只是花費(fèi)了稍微多一點(diǎn)點(diǎn)時(shí)間,并且不會(huì)引爆什么問(wèn)題,所以在你真實(shí)要處理的計(jì)算機(jī)環(huán)境中,多一些處理器時(shí)間可能會(huì)是更好的選擇。相似的,內(nèi)存,網(wǎng)絡(luò)帶寬,數(shù)據(jù)庫(kù)或其他服務(wù)器訪問(wèn),可能最后都比處理器時(shí)間要更加昂貴。

競(jìng)爭(zhēng)共享的資源被同步使用,可能導(dǎo)致死鎖和線程饑餓,如果這是可預(yù)見(jiàn)的,最好有一種方式來(lái)合適地測(cè)量這種競(jìng)爭(zhēng)。即使競(jìng)爭(zhēng)不會(huì)發(fā)生,能夠斷言這種情況也是非常有幫助的。

如何修復(fù)性能問(wèn)題

大部分軟件都可以通過(guò)相對(duì)小得多的努力,變得比它們剛發(fā)布時(shí),在時(shí)間上快 10 到 100 倍。在市場(chǎng)發(fā)布時(shí)間的壓力下,選擇一個(gè)簡(jiǎn)單快速的解決性能問(wèn)題的方法而非其他方法是聰明而有效率的。然而,性能是可用性的一部分,而且通常它也需要被更仔細(xì)地考慮。

提高一個(gè)非常復(fù)雜的系統(tǒng)的性能的關(guān)鍵是,充分分析它,以發(fā)現(xiàn)“瓶頸”,或者資源耗費(fèi)的地方。優(yōu)化一個(gè)只占用 1%執(zhí)行時(shí)間的函數(shù)是沒(méi)有多大意義的。一個(gè)簡(jiǎn)要的原則是,你在做任何事情之前必須仔細(xì)思考,除非你認(rèn)為它能夠使系統(tǒng)或者它的一個(gè)重要部分至少快兩倍。通常會(huì)有一種方法來(lái)達(dá)到這個(gè)效果??紤]你的修改會(huì)帶來(lái)的測(cè)試以及質(zhì)量保證的工作需要。每個(gè)修改帶來(lái)一個(gè)測(cè)試負(fù)擔(dān),所以最好這個(gè)修改能帶來(lái)一點(diǎn)大的優(yōu)化。

當(dāng)你在某個(gè)方面做了一個(gè)兩倍提升后,你需要至少重新考慮并且可能重新分析,去發(fā)現(xiàn)系統(tǒng)中下一個(gè)最昂貴的瓶頸,并且攻破那個(gè)瓶頸,得到下一個(gè)兩倍提升。

通常,性能的瓶頸的一個(gè)例子是,數(shù)牛的數(shù)目:通過(guò)數(shù)腳的數(shù)量然后除以 4,還是數(shù)頭的數(shù)量。舉些例子,我曾犯過(guò)的一些錯(cuò)誤:沒(méi)能在關(guān)系數(shù)據(jù)庫(kù)中,為我經(jīng)常查詢(xún)的那一列提供適當(dāng)?shù)乃饕?,這可能會(huì)使得它至少慢了 20 倍。其他例子還包括在循環(huán)里做不必要的 I/O 操作,留下不再需要的調(diào)試語(yǔ)句,不再需要的內(nèi)存分配,還有,尤其是,不專(zhuān)業(yè)地使用庫(kù)和其他的沒(méi)有為性能充分編寫(xiě)過(guò)的子系統(tǒng)。這種提升有時(shí)候被叫做“低垂的水果”,意思是它可以被輕易地獲取,然后產(chǎn)生巨大的好處。

你在用完這些“低垂的水果”之后,應(yīng)該做些什么呢?你可以爬高一點(diǎn),或者把樹(shù)鋸倒。你可以繼續(xù)做小的改進(jìn)或者你可以嚴(yán)肅地重構(gòu)整個(gè)系統(tǒng)或者一個(gè)子系統(tǒng)。(不只是在新的設(shè)計(jì)里,在信任你的 boss 這方面,作為一個(gè)好的程序員,這是一個(gè)非常好的使用你的技能的機(jī)會(huì))然而,在你考慮重構(gòu)子系統(tǒng)之前,你應(yīng)該問(wèn)你自己,你的建議是否會(huì)讓它好五倍到十倍。

如何優(yōu)化循環(huán)

有時(shí)候你會(huì)遇到循環(huán),或者遞歸函數(shù),它們會(huì)花費(fèi)很長(zhǎng)的執(zhí)行時(shí)間,可能是你的產(chǎn)品的瓶頸。在你嘗試使循環(huán)變得快一點(diǎn)之前,花幾分鐘考慮是否有可能把它整個(gè)移除掉,有沒(méi)有一個(gè)不同的算法?你可以在計(jì)算時(shí)做一些其他的事情嗎?如果你不能找到一個(gè)方法去繞開(kāi)它,你可以?xún)?yōu)化這個(gè)循環(huán)了。這是很簡(jiǎn)單的,move stuff out。最后,這不僅需要獨(dú)創(chuàng)性而且需要理解每一種語(yǔ)句和表達(dá)式的開(kāi)銷(xiāo)。這里是一些建議:

  • 刪除浮點(diǎn)運(yùn)算操作。
  • 非必要時(shí)不要分配新的內(nèi)存。
  • 把常量都放在一起聲明。
  • 把 I/O 放在緩沖里做。
  • 盡量不使用除法。
  • 盡量不適用昂貴的類(lèi)型轉(zhuǎn)換。
  • 移動(dòng)指針而非重新計(jì)算索引。

這些操作的具體代價(jià)取決于你的具體系統(tǒng)。在一些系統(tǒng)中,編譯器和硬件會(huì)為你做一些事情。但必須清楚,有效的代碼比需要在特殊平臺(tái)下理解的代碼要好。

如何處理 I/O 代價(jià)

在很多問(wèn)題上,處理器的速度比硬件交流要快得多。這種代價(jià)通常是小的 I/O,可能包括網(wǎng)絡(luò)消耗,磁盤(pán) I/O,數(shù)據(jù)庫(kù)查詢(xún),文件 I/O,還有其他與處理器不太接近的硬件使用。所以構(gòu)建一個(gè)快速的系統(tǒng)通常是一個(gè)提高 I/O 的問(wèn)題,而非在緊湊的循環(huán)里優(yōu)化代碼或者甚至優(yōu)化算法。

有兩種基本的技術(shù)來(lái)優(yōu)化 I/O:緩存和代表(譯者注:比如用短的字符代表長(zhǎng)的字符)。緩存是通過(guò)本地存儲(chǔ)數(shù)據(jù)的副本,再次獲取數(shù)據(jù)時(shí)就不需要執(zhí)行 I/O,以此來(lái)避免 I/O(通常避免讀取一些抽象的值)。緩存的關(guān)鍵在于讓?zhuān)ㄉ蠈訉?duì)于)哪些數(shù)據(jù)是主干的,哪些數(shù)據(jù)是副本,完全透明。主干的數(shù)據(jù)只有一份-周期。緩存有這樣一種危險(xiǎn):副本有時(shí)候不能立刻反映主干的修改。

代表是通過(guò)更高效地表示數(shù)據(jù)來(lái)讓 I/O 更廉價(jià)。這通常會(huì)限制其他的要求,比如可讀性和可移植性。

代表通??梢杂盟麄兊谝粚?shí)現(xiàn)中的兩到三個(gè)因子來(lái)做優(yōu)化。實(shí)現(xiàn)這點(diǎn)的技術(shù)包括使用二進(jìn)制表示而非人類(lèi)可識(shí)別的方式,傳遞數(shù)據(jù)的同時(shí)也傳遞一個(gè)符號(hào)表,這樣長(zhǎng)的符號(hào)就不需要被編碼,極端的,可能會(huì)像哈弗曼編碼。

一個(gè)偶爾可行的第三方技術(shù)是讓計(jì)算更接近數(shù)據(jù),來(lái)優(yōu)化本地引用。例如,如果你正在從數(shù)據(jù)庫(kù)讀取一些數(shù)據(jù)并且在它上面執(zhí)行一些簡(jiǎn)單的計(jì)算,比如求和,試著讓數(shù)據(jù)庫(kù)服務(wù)器去做這件事,這高度依賴(lài)于你正在工作的系統(tǒng)的類(lèi)型,但這個(gè)方面你必須自己探索。

如何管理內(nèi)存

內(nèi)存是一種你不可以耗盡的珍貴資源。在一段時(shí)期里,你可以無(wú)視它,但最終你必須決定如何管理內(nèi)存。

堆內(nèi)存是在單一子程序范圍外,需要持續(xù)(保留)的空間。一大塊內(nèi)存,在沒(méi)有東西指向它的時(shí)候,是無(wú)用的,因此被稱(chēng)為垃圾。根據(jù)你所使用的系統(tǒng)的不同,你可能需要自己顯式釋放將要變成垃圾的內(nèi)存。更多時(shí)候你可能使用一個(gè)有垃圾回收器的系統(tǒng)。一個(gè)垃圾回收器會(huì)自己注意到垃圾的存在并且在不需要程序員做任何事情的情況下釋放它的內(nèi)存空間。垃圾回收器是奇妙的:它減小了錯(cuò)誤,然后增加了代碼的簡(jiǎn)潔性。如果可以的話(huà),使用垃圾回收器。 但是即使有了垃圾回收機(jī)制,你還是可能把所有的內(nèi)存填滿(mǎn)垃圾。一個(gè)典型的錯(cuò)誤是把哈希表作為一個(gè)緩存,但是忘了刪除對(duì)哈希表的引用。因?yàn)橐萌匀淮嬖?,被引用者是不可回收但卻無(wú)用的。這就叫做內(nèi)存泄露。你應(yīng)該盡早發(fā)現(xiàn)并且修復(fù)內(nèi)存泄露。如果你會(huì)長(zhǎng)時(shí)間運(yùn)行系統(tǒng),內(nèi)存可能在測(cè)試中不會(huì)被耗盡,但可能在用戶(hù)那里被耗盡。

創(chuàng)建新對(duì)象在任何系統(tǒng)里都是有點(diǎn)昂貴的。然而,在子程序里直接為局部變量分配內(nèi)存通常很便宜,因?yàn)獒尫潘牟呗院芎?jiǎn)單。你應(yīng)該避免不必要的對(duì)象創(chuàng)建。

當(dāng)你可以定義你一次需要的數(shù)量的上界的時(shí)候,一個(gè)重要的情況出現(xiàn)了:如果這些對(duì)象都占用相同大小的內(nèi)存,你可以使用單獨(dú)的一塊內(nèi)存,或緩存,來(lái)持有所有的這些對(duì)象。你需要的對(duì)象可以在這個(gè)緩存里以循環(huán)的方式分配和釋放,所以它有時(shí)候被稱(chēng)為環(huán)緩存。這通常比堆內(nèi)存分配更快。(譯者注:這也被稱(chēng)為對(duì)象池。)

有時(shí)候你需要顯式釋放已分配的內(nèi)存,所以它可以被重新分配而非依賴(lài)于垃圾回收機(jī)制。然后你必須在每塊內(nèi)存上使用謹(jǐn)慎的智慧,并且為它設(shè)計(jì)一種在合適的時(shí)候重新分配的方式。這種銷(xiāo)毀的方式可能隨著你創(chuàng)建的對(duì)象的不同而不同。你必須確定每個(gè)內(nèi)存分配方法的執(zhí)行與最終都匹配一個(gè)內(nèi)存釋放操作。(譯者注:在 C 里面,no malloc no free,在 C++ 里面,no new no free)。這通常是很困難的,所以程序員通常會(huì)實(shí)現(xiàn)一種簡(jiǎn)單的方式或者垃圾回收機(jī)制,比如引用計(jì)數(shù),來(lái)為它們做這件事情。

如何處理偶現(xiàn)的 Bugs

偶現(xiàn) bug 是外部不可見(jiàn)的 50 足的蝎子的親戚。這種噩夢(mèng)是如此稀少以至于它很難觀察,但其出現(xiàn)頻率使得它不能被忽視。你不能調(diào)試因?yàn)槟悴荒苷业剿?/p>

盡管在 8 個(gè)小時(shí)后你會(huì)開(kāi)始懷疑,偶現(xiàn)的 bug 必須像其他事情一樣遵循相同的邏輯規(guī)律。但困難的是它只發(fā)生在一些未知的情形。嘗試記錄這個(gè) bug 會(huì)出現(xiàn)的情況,這樣你可以猜測(cè)真實(shí)的影響變量是什么。情況可能跟數(shù)據(jù)的值相關(guān),比如“這只是在我們把 Wyoming 作為一個(gè)值輸入時(shí)發(fā)生”,如果這不是變量的根源,下一個(gè)懷疑應(yīng)該是不合適的同步并發(fā)。

嘗試,嘗試,嘗試去在一種可控的方式下重現(xiàn)這個(gè) bug。如果你不能重現(xiàn)它,用日志系統(tǒng)給它設(shè)置一個(gè)圈套,來(lái)在你需要的時(shí)候,在它真的發(fā)生的時(shí)候,記錄你猜想的,需要的東西。重新設(shè)計(jì)這個(gè)圈套,如果這個(gè) bug 只發(fā)生在產(chǎn)品中,且不在你的猜想中的話(huà),這可能是一個(gè)長(zhǎng)的過(guò)程。你從日志中得到的(信息)可能不能提供解決方案,但可能給你足夠的信息去優(yōu)化這個(gè)日志。優(yōu)化后的日志系統(tǒng)可能花很長(zhǎng)時(shí)間才能被放入產(chǎn)品中使用。然后,你必須等待 bug 重新出現(xiàn)以獲得更多的信息。這個(gè)循環(huán)可能會(huì)繼續(xù)好幾次。

我曾創(chuàng)建過(guò)的最愚蠢的偶現(xiàn) bug 出現(xiàn)在,一個(gè)函數(shù)式編程語(yǔ)言里為類(lèi)工程做多線程實(shí)現(xiàn)。我非常仔細(xì)地保證了函數(shù)式程序的并發(fā)估計(jì),還有好的 CPU 使用(在這個(gè)例子里,是 8 個(gè) CPU)。我簡(jiǎn)單地忘記了同步垃圾回收器。系統(tǒng)可能運(yùn)行了很長(zhǎng)一段時(shí)間,經(jīng)常結(jié)束在我開(kāi)始任何一個(gè)任務(wù)的時(shí)候,在任何能被注意到的事情出錯(cuò)之前。我很遺憾地承認(rèn)在我理解我的錯(cuò)誤之前,我甚至開(kāi)始懷疑硬件了。

在工作中我們最近有這樣一個(gè)偶現(xiàn)的 bug 讓我們花了幾個(gè)星期才發(fā)現(xiàn)。我們有一個(gè)多線程的基于 Apache? 的 Java?web 服務(wù)器,在維護(hù)第一個(gè)頁(yè)面跳轉(zhuǎn)的時(shí)候,我們?cè)谒膫€(gè)獨(dú)立線程里以 4 個(gè)獨(dú)立的線程而非頁(yè)面跳轉(zhuǎn)線程為一個(gè)小的集合執(zhí)行所有的 I/O 操作。每一次跳轉(zhuǎn)會(huì)產(chǎn)生明顯的卡頓然后停止做任何有用的事情,直到幾個(gè)小時(shí)后,我們的日志允許我們?nèi)チ私獍l(fā)生了什么。因?yàn)槲覀冇兴膫€(gè)線程,在一個(gè)線程內(nèi)部發(fā)生這種情況并不是什么大問(wèn)題,除非所有的四個(gè)線程都阻塞了。然后被這些線程排空的隊(duì)列會(huì)迅速填充所有可用的內(nèi)存,然后導(dǎo)致我們的服務(wù)器崩潰。這個(gè) bug 花了我們一個(gè)星期去揪住這個(gè)問(wèn)題,但我們?nèi)匀徊恢朗裁磳?dǎo)致了這個(gè)現(xiàn)象,不知道它什么時(shí)候會(huì)發(fā)生,甚至不知道它們阻塞的時(shí)候,線程們?cè)诟墒裁础?/p>

這表明了有關(guān)使用第三方軟件的一些風(fēng)險(xiǎn)。我們?cè)谑褂靡欢问跈?quán)的代碼,從文本中移除 HTML 標(biāo)簽。受它的起源的影響,我們把它叫做法國(guó)脫衣舞者。盡管我們有源代碼(由衷感謝?。?,我們沒(méi)有仔細(xì)研究它,直到查看我們服務(wù)器的日志的時(shí)候,我們最終意識(shí)到“法國(guó)脫衣舞者”中,通信線程阻塞了。

這個(gè)工具在大多數(shù)時(shí)候工作得很好,除了處理一些長(zhǎng)而不常見(jiàn)的文本時(shí)。在那些文本里,代碼復(fù)雜度是 N 平方或者更糟。這意味著處理時(shí)間與文本的長(zhǎng)度的平方成正比。由于這些文本通常都會(huì)出現(xiàn),我們可以馬上發(fā)現(xiàn)這個(gè) bug。如果他們從來(lái)都不會(huì)出現(xiàn),我們永遠(yuǎn)都不會(huì)發(fā)現(xiàn)這個(gè)問(wèn)題。當(dāng)它發(fā)生時(shí),我們花了幾個(gè)星期去最終理解并且解決了這個(gè)問(wèn)題。

如何學(xué)習(xí)設(shè)計(jì)技能

為了學(xué)習(xí)如何設(shè)計(jì)軟件,你可以在導(dǎo)師做設(shè)計(jì)的時(shí)候,出現(xiàn)在他身邊,學(xué)習(xí)他的行為。然后學(xué)習(xí)精心編寫(xiě)過(guò)的軟件片段(譯者注:比如 android 系統(tǒng)中的谷歌官方應(yīng)用)。在這之后,你可以讀一些關(guān)于最新設(shè)計(jì)技術(shù)的書(shū)。

然后你必須自己動(dòng)手了。從一個(gè)小的工程開(kāi)始,當(dāng)你最后完成時(shí),考慮為什么這個(gè)設(shè)計(jì)失敗了或成功了,你是怎樣偏離你最初的設(shè)想的。然后繼續(xù)去著手大一點(diǎn)的工程,在與其他人結(jié)合時(shí)會(huì)更有希望。設(shè)計(jì)是一種需要花很多年去學(xué)習(xí)的關(guān)于評(píng)判的事情。一個(gè)聰明的程序員可以學(xué)習(xí)在兩個(gè)月內(nèi)充分學(xué)好這種基礎(chǔ)然后從這里開(kāi)始進(jìn)步。

發(fā)展出你自己的風(fēng)格是自然而有用的,但記住,設(shè)計(jì)是一種藝術(shù),而不是一種科學(xué)。人們寫(xiě)的關(guān)于這個(gè)主題的書(shū)都有一種使得它好像是科學(xué)的既定的興趣。不要武斷對(duì)待特定的設(shè)計(jì)風(fēng)格。

如何進(jìn)行實(shí)驗(yàn)

已故的偉大的 Edsger Dijkstra 曾經(jīng)充分解釋過(guò):計(jì)算機(jī)科學(xué)不是一門(mén)實(shí)驗(yàn)科學(xué)[ExpCS],并且不依賴(lài)于電子計(jì)算機(jī)。當(dāng)他提出這個(gè)觀點(diǎn)時(shí),他指的是 19 世紀(jì) 60 年代。[Knife]

...危害已經(jīng)出現(xiàn):主題現(xiàn)在已經(jīng)變成了“計(jì)算機(jī)科學(xué)” - 這實(shí)際上,像是把外科手術(shù)引用為“手術(shù)刀科學(xué)” - 這在人們心中深深植入了這樣一個(gè)概念:計(jì)算機(jī)科學(xué)是關(guān)于機(jī)器和它們的外圍設(shè)備的。

編程不應(yīng)該是一門(mén)實(shí)驗(yàn)科學(xué),但大多數(shù)職業(yè)程序員并沒(méi)有保衛(wèi) Dijkstra 對(duì)于計(jì)算機(jī)科學(xué)的解釋的榮耀。我們必須在實(shí)驗(yàn)的領(lǐng)域里工作,正如一部分,但非所有的,物理學(xué)家做的。如果三十年后,編程可以在不進(jìn)行任何實(shí)驗(yàn)的前提下進(jìn)行,這將是計(jì)算機(jī)科學(xué)的一個(gè)巨大成就。

你需要進(jìn)行的實(shí)驗(yàn)包括:

  • 用小的例子測(cè)試系統(tǒng)以驗(yàn)證它們遵循文檔,或者在沒(méi)有文檔時(shí),理解它們的反應(yīng);
  • 測(cè)試一些小的代碼修改去驗(yàn)證它們是否確實(shí)修復(fù)了一個(gè) bug;
  • 由于對(duì)一個(gè)系統(tǒng)不完全的理解,需要在兩種不同情況下測(cè)量它們的性能表現(xiàn);
  • 檢查數(shù)據(jù)的完整性,和
  • 對(duì)困難的或者難以重現(xiàn)的 bug,收集解決方案中可能提示的統(tǒng)計(jì)數(shù)據(jù)。

我不認(rèn)為在這篇文章里我可以講述實(shí)驗(yàn)的設(shè)計(jì),你會(huì)在實(shí)踐中學(xué)習(xí)到這方面的知識(shí)。然而,我可以提供兩點(diǎn)建議:

第一,對(duì)你的假設(shè)或者你要測(cè)試的斷言要非常清楚。把假設(shè)寫(xiě)下來(lái)也是很有用的,尤其是如果你有點(diǎn)迷惑或者與其他人合作時(shí)。

你會(huì)經(jīng)常發(fā)現(xiàn)你必須設(shè)計(jì)一系列的實(shí)驗(yàn),它們中的每個(gè)都基于對(duì)最后一個(gè)實(shí)驗(yàn)的理解。所以,你應(yīng)該設(shè)計(jì)你的實(shí)驗(yàn)去提供大部分可能的信息。不幸的是,這會(huì)影響保持實(shí)驗(yàn)簡(jiǎn)單的目的 - 你必須通過(guò)經(jīng)驗(yàn)來(lái)發(fā)展這種評(píng)判的能力。