C++ 性能优化篇三《测量性能》
測(cè)量可測(cè)量之物,將不可測(cè)量之物變?yōu)榭蓽y(cè)量。 ——伽利略 ? 伽利雷(1564—1642)
測(cè)量和實(shí)驗(yàn)是所有改善程序性能嘗試的基礎(chǔ)。本章將介紹兩種測(cè)量性能的工具軟件:分析 器和計(jì)時(shí)器軟件。我將討論如何設(shè)計(jì)性能測(cè)量實(shí)驗(yàn),使得測(cè)量結(jié)果更有指導(dǎo)意義,而不是 誤導(dǎo)我們。
最基本和最頻繁地執(zhí)行的軟件性能測(cè)量會(huì)告訴我們“需要多長(zhǎng)時(shí)間”。執(zhí)行函數(shù)需要多長(zhǎng) 時(shí)間?從磁盤讀取配置文件需要多長(zhǎng)時(shí)間?啟動(dòng)和退出程序需要多長(zhǎng)時(shí)間?
這些測(cè)量問(wèn)題的解答方法有時(shí)簡(jiǎn)單得令人覺(jué)得可笑。牛頓通過(guò)用物體掉落至地面的時(shí)間除 以他的心跳速度測(cè)量出了重力常數(shù) 1 。我相信每位開(kāi)發(fā)人員都有通過(guò)大聲數(shù)數(shù)進(jìn)行計(jì)時(shí)的經(jīng) 歷。在美國(guó),我們通過(guò)喊“one-Mississippi, two-Mississippi, three-Mississippi...” 來(lái)得到比較 精確的秒數(shù)。帶有秒表功能的電子手表曾經(jīng)是計(jì)算機(jī)極客的必備之物,而非僅僅是潮流的 象征。在嵌入式開(kāi)發(fā)中,熟悉硬件的開(kāi)發(fā)人員有很多優(yōu)秀的工具可以使用,其中有頻率計(jì) 數(shù)器和信號(hào)示波器等甚至可以精確地測(cè)量極短例程的時(shí)間的工具。軟件廠商也會(huì)出售專業(yè) 工具,由于數(shù)量太多,這里不會(huì)逐一介紹。
本章將主要介紹兩種被廣泛使用的、具有通用性且價(jià)格低廉的工具。第一個(gè)工具是編譯器 廠商通常在編譯器中都會(huì)提供的分析器(profiler)。分析器會(huì)生成各個(gè)函數(shù)在程序運(yùn)行過(guò) 程中被調(diào)用的累積時(shí)間的表格報(bào)表。對(duì)性能優(yōu)化而言,它是一個(gè)非常關(guān)鍵的工具,因?yàn)樗鼤?huì)列出程序中最熱點(diǎn)的函數(shù)。
第二個(gè)工具是計(jì)時(shí)器軟件(software timer)。開(kāi)發(fā)人員可以自己實(shí)現(xiàn)這個(gè)工具,就像絕地武 士自己打造他們的光劍一樣(請(qǐng)?jiān)徫以谶@里引用了《星球大戰(zhàn)》中的內(nèi)容打比喻)。如 果帶有分析器的豪華版編譯器太過(guò)昂貴,或是編譯器廠商在某些嵌入式平臺(tái)上不提供分析 器,開(kāi)發(fā)人員依然可以通過(guò)測(cè)量長(zhǎng)時(shí)間運(yùn)行的活動(dòng)來(lái)進(jìn)行性能實(shí)驗(yàn)。計(jì)時(shí)器軟件還可以用 于測(cè)量不受計(jì)算限制的任務(wù)。
第三個(gè)工具是非常古老的“實(shí)驗(yàn)筆記本”,許多開(kāi)發(fā)人員認(rèn)為它已經(jīng)完全過(guò)時(shí)了。但是實(shí) 驗(yàn)筆記本或是其他文本文件仍然是不可或缺的優(yōu)化工具。
3.1 優(yōu)化思想
在開(kāi)始介紹測(cè)量和實(shí)驗(yàn)之前,我想談一點(diǎn)點(diǎn)我一直在實(shí)踐的、也是我想在本書(shū)中教授的優(yōu) 化哲學(xué)。
3.1.1 必須測(cè)量性能
人的感覺(jué)對(duì)于檢測(cè)性能提高了多少來(lái)說(shuō)是不夠精確的。人的記憶力不足以準(zhǔn)確地回憶起以 往多次實(shí)驗(yàn)的結(jié)果。書(shū)本中的知識(shí)可能會(huì)誤導(dǎo)你,使你相信了一些并非總是正確的事情。 當(dāng)判斷是否應(yīng)當(dāng)對(duì)某段代碼進(jìn)行優(yōu)化的時(shí)候,開(kāi)發(fā)人員的直覺(jué)往往差得令人吃驚。他們 編寫(xiě)了函數(shù),也知道這個(gè)函數(shù)會(huì)被調(diào)用,但他們并不清楚調(diào)用頻率以及會(huì)被什么代碼所調(diào) 用。于是,一段低效的代碼混入了核心組件中并被調(diào)用了無(wú)數(shù)次。經(jīng)驗(yàn)也可能會(huì)欺騙你。 編程語(yǔ)言、編譯器、庫(kù)和處理器都在不斷地發(fā)展。之前曾經(jīng)肯定是熱點(diǎn)的函數(shù)可能會(huì)變得 非常高效,反之亦然。只有測(cè)量才能告訴你到底是在優(yōu)化游戲中取勝了還是失敗了。 那些具有最讓我折服的優(yōu)化技巧的開(kāi)發(fā)人員都會(huì)系統(tǒng)地完成他們的優(yōu)化任務(wù):
- 他們做出的預(yù)測(cè)都是可測(cè)試的,而且他們會(huì)記錄下預(yù)測(cè);
- 他們保留代碼變更記錄;
- 他們使用可以使用的最優(yōu)秀的工具進(jìn)行測(cè)量;
- 他們會(huì)保留實(shí)驗(yàn)結(jié)果的詳細(xì)筆記。
| 請(qǐng)回過(guò)頭來(lái)再次閱讀上節(jié)中的內(nèi)容。其中包含了本書(shū)中最重要的建議。多數(shù)開(kāi)發(fā)人員 (包括筆者)都會(huì)想當(dāng)然地,而不是按照以上方式有條不紊地進(jìn)行優(yōu)化。這是一項(xiàng)必須 不斷實(shí)踐的技能。 |
3.1.2 優(yōu)化器是王牌獵人
我說(shuō)起飛后用核彈炸掉這地方。這是唯一的方法。 ——艾倫 ? 蕾普莉(西格麗 ? 維弗飾演),《異形 2》,1986 年
優(yōu)化器是王牌獵人。如果只能讓程序的運(yùn)行速度提高 1% 是不值得冒險(xiǎn)去修改代碼的,因?yàn)樾薷拇a可能會(huì)引入 bug。只有能顯著地提升性能時(shí)才值得修改代碼。而且,這 1% 的 速度提升可能只是將測(cè)量套件的誤差當(dāng)作了性能改善。因此,我們必須用隨機(jī)抽樣統(tǒng)計(jì) 和置信水平來(lái)證明速度的提升。但是完全沒(méi)有必要為了這么一點(diǎn)點(diǎn)性能提升花費(fèi)這么大氣 力。本書(shū)中不會(huì)推薦大家這么做。
當(dāng)性能提升 20% 的時(shí)候,事情就完全不同了。它會(huì)消除所有反對(duì)方法論的聲音。本書(shū)中雖 然沒(méi)有太多統(tǒng)計(jì)數(shù)字,不過(guò)我并不會(huì)為此感到抱歉。本書(shū)的重點(diǎn)是幫助開(kāi)發(fā)人員找到這樣 的性能改善點(diǎn):其顯著的效果足以戰(zhàn)勝任何對(duì)其價(jià)值的質(zhì)疑。這些性能改善點(diǎn)可能仍然取 決于操作系統(tǒng)和編譯器等因素,因此它們可能會(huì)在其他操作系統(tǒng)上或是其他時(shí)間點(diǎn)沒(méi)有太 好的效果。但是即使開(kāi)發(fā)人員把他們的代碼移植到新操作系統(tǒng)上,這些修改也幾乎從來(lái)都 不會(huì)反過(guò)來(lái)降低程序性能。
3.1.3 90/10規(guī)則
性能優(yōu)化的基本規(guī)則是 90/10 規(guī)則:一個(gè)程序花費(fèi) 90% 的時(shí)間執(zhí)行其中 10% 的代碼。這 只是一條啟發(fā)性的規(guī)則,并非自然法則,但對(duì)于我們的思考和計(jì)劃卻具有指導(dǎo)性。這條規(guī) 則有時(shí)也被稱為 80/20 規(guī)則,但思想是一樣的。直觀地說(shuō),90/10 規(guī)則表示某些代碼塊是會(huì) 被頻繁地執(zhí)行的熱點(diǎn)(hot spot),而其他代碼則幾乎不會(huì)被執(zhí)行。這些熱點(diǎn)就是我們要進(jìn) 行性能優(yōu)化的對(duì)象。
| 我是在作為專業(yè)開(kāi)發(fā)人員研發(fā)一種叫作 9010A 的帶鍵盤的嵌入式設(shè)備(圖 3-1)的項(xiàng) 目中初識(shí) 90/10 規(guī)則的。程序中有個(gè)函數(shù)會(huì)輪詢鍵盤,查看用戶是否按下了 STOP 鍵。這個(gè)函數(shù)會(huì)被每個(gè)例程 頻繁地執(zhí)行。手動(dòng)優(yōu)化 C 編譯器輸出的這個(gè)函數(shù)的 Z80 匯編代碼(耗費(fèi)了 45 分鐘) 將整體吞吐量提高了 7%,對(duì)這臺(tái)設(shè)備來(lái)說(shuō),非常不錯(cuò)了。一般情況下,這是一條典型的性能優(yōu)化經(jīng)驗(yàn)。在優(yōu)化過(guò)程的初期,大量的運(yùn)行時(shí)間都 集中消耗在程序中的某個(gè)位置。這個(gè)位置也非常明顯:在每個(gè)循環(huán)的每次迭代中都要 重復(fù)進(jìn)行的處理,就像每天的家務(wù)勞動(dòng)一樣。想要優(yōu)化這些代碼需要做出一項(xiàng)痛苦的 選擇——用匯編語(yǔ)言重寫(xiě)這些 C 語(yǔ)言代碼。但是由于使用匯編語(yǔ)言的代碼范圍極其有 限,選擇使用匯編語(yǔ)言降低了需要承受的風(fēng)險(xiǎn)。當(dāng)這段代碼被頻繁執(zhí)行時(shí),這條經(jīng)驗(yàn)同樣很典型。當(dāng)我們改善了這段代碼后,另一段 代碼成為了最頻繁地被執(zhí)行的代碼——不過(guò)它對(duì)整體運(yùn)行時(shí)間的影響已經(jīng)小多了。它 實(shí)在是太小了,以至于我們?cè)谶M(jìn)行了這一處改動(dòng)后就停止了性能優(yōu)化。我們甚至找不 到改動(dòng)后可以將程序執(zhí)行速度提高 1% 的地方了。 |
90/10 規(guī)則的一個(gè)結(jié)論是,優(yōu)化程序中的所有例程并沒(méi)有太大幫助。優(yōu)化一小部分代碼事 實(shí)上已經(jīng)足夠提供你所想要的性能提升了。識(shí)別出 10% 的熱點(diǎn)代碼是值得花費(fèi)時(shí)間的,但 靠猜想選擇優(yōu)化哪些代碼可能只是浪費(fèi)時(shí)間。
這里我想再一次引用第 1 章中曾經(jīng)引用過(guò)的高德納的一句名言。不過(guò),此處是那句名言一 個(gè)較長(zhǎng)的版本:
程序員浪費(fèi)了太多的時(shí)間去思考和擔(dān)憂程序中那些非關(guān)鍵部分的速度,而且考慮 到調(diào)試和維護(hù),這些為優(yōu)化而進(jìn)行的修改實(shí)際上是有很大負(fù)面影響的。我們應(yīng)當(dāng) 忘記小的性能改善,97% 的情況下,過(guò)早優(yōu)化都是萬(wàn)惡之源。
? ——高德納,“使用 goto 語(yǔ)句進(jìn)行結(jié)構(gòu)化編程”, ACM Computing Surveys 6 (Dec 1974): 268. CiteSeerX: 10.1.1.103.6084(http://citeseerx.ist.psu.edu/ viewdoc/summary?doi=10.1.1.103.6084)
正如有些人所建議的那樣,高德納博士也并非警告我們所有的優(yōu)化都是罪惡的。他只是說(shuō) 浪費(fèi)時(shí)間去優(yōu)化那非關(guān)鍵的 90% 的程序是罪惡的。很明顯,他也意識(shí)到了 90/10 規(guī)則。
3.1.4 阿姆達(dá)爾定律
阿姆達(dá)爾定律是由計(jì)算機(jī)工程先鋒基恩 ? 阿姆達(dá)爾(Gene Amdahl)提出并用他的名字命名 的,它定義了優(yōu)化一部分代碼對(duì)整體性能有多大改善。阿姆達(dá)爾定律有多種表達(dá)方式,不 過(guò)就優(yōu)化而言,可以表示為下面的等式:
其中 ST 是因優(yōu)化而導(dǎo)致程序整體性能提升的比率,P 是被優(yōu)化部分的運(yùn)行時(shí)間占原來(lái)程序 整體運(yùn)行時(shí)間的比例,SP 是被優(yōu)化部分 P 的性能改善的比率。
例如,假設(shè)一個(gè)程序的運(yùn)行時(shí)間是 100 秒。通過(guò)分析(請(qǐng)參見(jiàn) 3.3 節(jié))發(fā)現(xiàn)程序花費(fèi)了 80 秒多次調(diào)用函數(shù) f?,F(xiàn)在假設(shè)修改 f 使其運(yùn)行速度提升了 30%,那么這對(duì)程序整體運(yùn)行時(shí) 間有多大改善呢?
P 是函數(shù) f 的運(yùn)行時(shí)間占原來(lái)程序整體運(yùn)行時(shí)間的比例,即 0.8;SP 是被優(yōu)化的部分 P 的 性能改善的比率,即 1.3。將它們代入到阿姆達(dá)爾定律的公式中:
也就是說(shuō),將這個(gè)函數(shù)的性能提升 30% 會(huì)將程序整體運(yùn)行時(shí)間縮短 22%。在這個(gè)例子中, 阿姆達(dá)爾定律證明了 90/10 規(guī)則,而且通過(guò)這個(gè)例子向我們展示了,對(duì) 10% 的熱點(diǎn)代碼進(jìn) 行適當(dāng)?shù)膬?yōu)化,就可以帶來(lái)如此大的性能提升。
下面我們?cè)賮?lái)看一個(gè)例子。我們還是假設(shè)一個(gè)程序的運(yùn)行時(shí)間是 100 秒。通過(guò)分析,你發(fā) 現(xiàn)有一個(gè)函數(shù) g 的運(yùn)行時(shí)間是 10 秒?,F(xiàn)在假設(shè)你修改了函數(shù) g,將它的運(yùn)行速度提高了 100 倍。那么這對(duì)程序整體性能的提升有多大呢?
P 是函數(shù) g 的運(yùn)行時(shí)間占原來(lái)程序整體運(yùn)行時(shí)間的比例,即 0.1;SP 是 100。將它們代入到 阿姆達(dá)爾定律的公式中:
在這個(gè)例子中阿姆達(dá)爾定律是具有警示性的。即使有異常優(yōu)秀的編碼能力或是黑科技將函 數(shù) g 的運(yùn)行時(shí)間縮短為 0,它仍然是那并不重要的 90% 代碼中的一部分。將性能提升的比 率精確到兩個(gè)小數(shù)位后,對(duì)程序整體性能的提升依然只有 11%。阿姆達(dá)爾定律告訴我們, 如果被優(yōu)化的代碼在程序整體運(yùn)行時(shí)間中所占的比率不大,那么即使對(duì)它的優(yōu)化非常成功 也是不值得的。阿姆達(dá)爾定律的教訓(xùn)是,當(dāng)你的同事興沖沖地在會(huì)議上說(shuō)他知道如何將一 段計(jì)算處理的速度提高 10 倍,這并不一定意味著性能優(yōu)化工作就此結(jié)束了。
3.2 進(jìn)行實(shí)驗(yàn)
開(kāi)發(fā)軟件在某種意義上就是一項(xiàng)實(shí)驗(yàn)。你想讓程序做一些事情,然后開(kāi)始編程,最后觀察 程序的運(yùn)行結(jié)果是否與預(yù)想的一樣。性能調(diào)優(yōu)則是更有正式意義的實(shí)驗(yàn)。在開(kāi)始性能調(diào)優(yōu) 前,必須要有正確的代碼,即在某種意義上可以完成我們所期待的處理的代碼。你需要擦 亮眼睛審視這些代碼,然后問(wèn)自己:“為什么這些代碼是熱點(diǎn)?”為什么某個(gè)函數(shù)與程序 中的上百個(gè)函數(shù)不同,出現(xiàn)在了分析器的最差性能列表中的最前面?是這個(gè)函數(shù)浪費(fèi)了很 多時(shí)間在冗余處理上嗎?有其他更快的方法進(jìn)行相同的計(jì)算嗎?這個(gè)函數(shù)使用了緊缺的計(jì) 算機(jī)資源嗎?是這個(gè)函數(shù)自身已經(jīng)是非常快了,只不過(guò)它被調(diào)用了太多次,已經(jīng)沒(méi)有優(yōu)化 的余地了嗎?
你對(duì)于“為什么這些代碼是熱點(diǎn)”這個(gè)問(wèn)題的回答構(gòu)成了你要測(cè)試的假設(shè)。實(shí)驗(yàn)要對(duì)程序 的兩種運(yùn)行時(shí)間進(jìn)行測(cè)量:一種是修改前的運(yùn)行時(shí)間,一種是修改后的運(yùn)行時(shí)間。如果后 者比前者短,那么實(shí)驗(yàn)驗(yàn)證了你的假設(shè)。
請(qǐng)注意這里的用詞。實(shí)驗(yàn)并不需要證明任何事情。修改后的代碼可能會(huì)因?yàn)槟承┰蜻\(yùn)行 得更快或者更慢,但這些原因卻與你修改的部分沒(méi)有任何關(guān)系。比如:
- 當(dāng)你在測(cè)量運(yùn)行時(shí)間時(shí),計(jì)算機(jī)可能在接收郵件或是檢查 Java 是否有版本更新;
- 在你重編譯之前,一位同事剛剛簽入了一個(gè)性能改善后的庫(kù);
- 你的修改可能運(yùn)行得更快,但是處理邏輯卻是不正確的。
優(yōu)秀的科學(xué)家是懷疑論者。他們總是對(duì)事物持有懷疑。如果沒(méi)有出現(xiàn)所期待的實(shí)驗(yàn)結(jié)果, 或是實(shí)驗(yàn)結(jié)果太好了,不像是對(duì)的,那么懷疑論者會(huì)再進(jìn)行一次實(shí)驗(yàn)或者質(zhì)疑她的假設(shè), 抑或檢查是否有 bug。
優(yōu)秀的科學(xué)家會(huì)接受新知識(shí),即使這些知識(shí)與他們腦海中的知識(shí)相悖。我在編寫(xiě)本書(shū)的過(guò) 程中學(xué)到了一些出乎意料的優(yōu)化知識(shí)。本書(shū)的技術(shù)審核者也從本書(shū)中學(xué)到了知識(shí)。優(yōu)秀的 科學(xué)家從不會(huì)停止學(xué)習(xí)。
| 在第 5 章有一個(gè)查找關(guān)鍵字的示例函數(shù)。我為這個(gè)示例函數(shù)編寫(xiě)了幾個(gè)不同的版本。 其中一個(gè)版本是線性查找(linear search),另一個(gè)版本則是二分查找(binary search)。 當(dāng)測(cè)量這兩個(gè)函數(shù)的性能時(shí),我發(fā)現(xiàn)線性查找的速度比二分查找快幾個(gè)百分點(diǎn)。這讓 我覺(jué)得不可思議。二分查找本應(yīng)當(dāng)更快,但是測(cè)量結(jié)果卻不是這樣的。 我注意到有人在互聯(lián)網(wǎng)上發(fā)表報(bào)告說(shuō)線性查找經(jīng)常會(huì)更快,因?yàn)橄啾榷植檎?#xff0c;它的 緩存局部性(cache locality)更好,而且確實(shí)我實(shí)現(xiàn)的線性查找應(yīng)當(dāng)具有非常優(yōu)秀的緩 存局部性。但是這個(gè)結(jié)果卻與我的經(jīng)驗(yàn)以及我從受人尊崇的書(shū)本上學(xué)到的有關(guān)查找算 法性能的知識(shí)相違背。 進(jìn)行了更深入的調(diào)查后我發(fā)現(xiàn),在測(cè)試時(shí)所使用的測(cè)試表格中只有幾個(gè)單詞,而且要 查找的關(guān)鍵字我自己都能從表格中找到。如果一個(gè)表格有 8 個(gè)項(xiàng)目,那么線性查找平 均會(huì)檢查其中半數(shù)(4)后返回結(jié)果。而二分查找每次被調(diào)用時(shí)都會(huì)將表格一分為二 (共 4 次),然后才能查找到關(guān)鍵字。這兩種算法對(duì)小的關(guān)鍵字集有著完全相同的平均 性能。直覺(jué)告訴我二分查找總是比線性查找更快,但這個(gè)結(jié)果告訴我我錯(cuò)了。 但是這并非我想證明的結(jié)果。所以我擴(kuò)大了測(cè)試數(shù)據(jù)表格,想著這個(gè)表格在達(dá)到某 個(gè)大小時(shí),一定會(huì)出現(xiàn)二分查找更快的結(jié)果。另外,我還向其中加入了一些原本不 在測(cè)試表格中的單詞??墒菧y(cè)試結(jié)果依然不變,線性查找更快。這時(shí),我不得不將 編寫(xiě)這份示例代碼的任務(wù)擱置了幾天,但是這個(gè)結(jié)果卻一直折磨著我。 我仍然相信二分查找應(yīng)當(dāng)更快。我檢查了兩種查找方式的單元測(cè)試代碼,最終發(fā)現(xiàn)線 性查找在進(jìn)行第一次比較后總是返回成功。我的測(cè)試用例檢查了是否返回了非零值, 而不是檢查是否返回了正確值。接著,我慚愧地修改了線性查找算法和測(cè)試用例?,F(xiàn) 在,實(shí)驗(yàn)結(jié)果與我所期待的一樣,二分查找的速度更快了。 在這個(gè)例子中,實(shí)驗(yàn)結(jié)果先否定然后又驗(yàn)證了我的假設(shè)——整個(gè)過(guò)程中一直在挑戰(zhàn) 我的假設(shè)。 |
3.2.1 記實(shí)驗(yàn)筆記
優(yōu)秀的優(yōu)化人員(如同所有優(yōu)秀的科學(xué)家)都會(huì)關(guān)心可重復(fù)性。這時(shí)實(shí)驗(yàn)室筆記本就可以 發(fā)揮作用了。為了驗(yàn)證猜想,優(yōu)化人員在對(duì)代碼進(jìn)行一處或多處修改后,利用輸入數(shù)據(jù)集 對(duì)代碼進(jìn)行性能測(cè)試,而測(cè)試則會(huì)在若干毫秒后結(jié)束。在與下次運(yùn)行時(shí)間進(jìn)行比較前一直 記著上次程序的運(yùn)行時(shí)間,這事兒并不難。如果每次代碼改善都是成功的,用腦袋記住就 足夠了。
不過(guò),開(kāi)發(fā)人員的猜想可能會(huì)出錯(cuò),這將導(dǎo)致最近一次的程序運(yùn)行時(shí)間比上一次的更長(zhǎng)。 這時(shí),無(wú)數(shù)的疑問(wèn)會(huì)充斥在開(kāi)發(fā)人員的腦中。雖然 5 號(hào)測(cè)試的運(yùn)行時(shí)間比 4 號(hào)長(zhǎng),但是它 比 3 號(hào)短嗎?在進(jìn)行 3 號(hào)測(cè)試時(shí)修改了哪些代碼?兩次測(cè)試間的速度差異是由其他因素造 成的,還是的確變快了?
如果每次的測(cè)試運(yùn)行情況都被記錄在案,那么就可以快速地重復(fù)實(shí)驗(yàn),回答上述問(wèn)題就會(huì) 變得很輕松了。否則,開(kāi)發(fā)人員必須回過(guò)頭去重新做一次實(shí)驗(yàn)來(lái)獲取運(yùn)行時(shí)間——前提是 他還記得應(yīng)該修改哪些代碼或是撤銷哪些修改。如果測(cè)試運(yùn)行很簡(jiǎn)單,開(kāi)發(fā)人員的記憶力 也非常好,那么他很幸運(yùn),只需要花費(fèi)一點(diǎn)時(shí)間即可重復(fù)實(shí)驗(yàn)。但是也有可能沒(méi)那么幸 運(yùn),明明想重復(fù)實(shí)驗(yàn)卻偏離了正確的前進(jìn)道路,或是毫無(wú)意義地浪費(fèi)一天去重復(fù)實(shí)驗(yàn)。
每當(dāng)我給出這條建議時(shí),總會(huì)有人說(shuō):“我不需要筆和紙就能做到!我可以寫(xiě)一段 Perl 腳 本去修改代碼版本管理工具的命令,讓它幫忙將每次運(yùn)行的測(cè)試結(jié)果和所修改的代碼一起 保存起來(lái)。如果我將測(cè)試結(jié)果保存在文件中……如果我在不同的目錄下做測(cè)試……”
我并不想妨礙開(kāi)發(fā)人員創(chuàng)新。如果你是一位主動(dòng)吸收最佳實(shí)踐的高級(jí)開(kāi)發(fā)經(jīng)理,那么盡管 這么做吧。不過(guò)我想說(shuō)的是,使用紙和筆記錄是一種很穩(wěn)健、容易使用而且有著千年歷史 的技術(shù)。即使在開(kāi)發(fā)團(tuán)隊(duì)替換了版本管理工具或測(cè)試套件的情況下,這項(xiàng)技術(shù)仍然可用。 它還適用于開(kāi)發(fā)人員的下一份工作。這項(xiàng)傳統(tǒng)的解決方案仍然可以節(jié)省開(kāi)發(fā)人員的時(shí)間。
3.2.2 測(cè)量基準(zhǔn)性能并設(shè)定目標(biāo)
獨(dú)立開(kāi)發(fā)人員可以隨意地、迭代地進(jìn)行優(yōu)化,直到他覺(jué)得性能足夠好了為止。不過(guò)工作于 團(tuán)隊(duì)中的開(kāi)發(fā)人員需要滿足經(jīng)理和其他利益相關(guān)人員的需求。優(yōu)化工作受兩個(gè)數(shù)字主導(dǎo): 優(yōu)化前的性能基準(zhǔn)測(cè)量值和性能目標(biāo)值。測(cè)量性能基準(zhǔn)不僅對(duì)于衡量每次獨(dú)立的改善是否 成功非常重要,而且對(duì)于向其他利益相關(guān)人員就優(yōu)化成本開(kāi)銷做出解釋也是非常重要的。
而優(yōu)化目標(biāo)值之所以重要,是因?yàn)樵趦?yōu)化過(guò)程中優(yōu)化效果會(huì)逐漸變小。在優(yōu)化過(guò)程的最初 階段,樹(shù)上總是有些容易摘取的掛得很低的水果:一些獨(dú)立的進(jìn)程或是想當(dāng)然地編寫(xiě)的函 數(shù),優(yōu)化它們后可以使性能提升很多。但是一旦實(shí)現(xiàn)了這些簡(jiǎn)單的優(yōu)化目標(biāo)后,下一輪性 能提升就需要付出更多的努力。
許多團(tuán)隊(duì)之所以在一開(kāi)始沒(méi)有為性能或是響應(yīng)性設(shè)定目標(biāo),只是因?yàn)樗麄儾⒉涣?xí)慣這么 做。幸運(yùn)的是,差勁的性能往往表現(xiàn)得非常明顯(例如用戶界面長(zhǎng)時(shí)間不響應(yīng)、托管服 務(wù)器的規(guī)模沒(méi)有可擴(kuò)展性、按照 CPU 時(shí)間付費(fèi)的成本非常高等)。一旦團(tuán)隊(duì)研究下性能問(wèn) 題,那么目標(biāo)數(shù)字很容易被設(shè)定下來(lái)。用戶體驗(yàn)(UX)設(shè)計(jì)的一個(gè)學(xué)科分支專門研究用 戶如何看待等待時(shí)間。下面是一份常用的性能測(cè)試項(xiàng)目清單,你可以從為這些項(xiàng)目設(shè)定性能目標(biāo)開(kāi)始。這其中有足夠多的與用戶體驗(yàn)相關(guān)的數(shù)字,可以讓你意識(shí)到危險(xiǎn)性。
啟動(dòng)時(shí)間
從用戶按下回車鍵直至程序進(jìn)入主輸入處理循環(huán)所經(jīng)過(guò)的時(shí)間。通常,開(kāi)發(fā)人員可以通 過(guò)測(cè)量程序進(jìn)入 main() 函數(shù)到進(jìn)入主循環(huán)的時(shí)間來(lái)得到啟動(dòng)時(shí)間,但是有時(shí)候也有例 外。為程序提供認(rèn)證的操作系統(tǒng)廠商對(duì)程序在計(jì)算機(jī)啟動(dòng)時(shí)或某個(gè)用戶登入時(shí)就運(yùn)行有 嚴(yán)格的要求。例如,對(duì)那些尋求認(rèn)證的硬件廠商,微軟會(huì)要求 Windows shell 必須在啟 動(dòng)后 10 秒內(nèi)能夠進(jìn)入它們的主循環(huán)。這限制了在忙碌的啟動(dòng)環(huán)境中,廠商可以預(yù)載和 啟動(dòng)的其他程序的數(shù)量。為此,微軟提供了專用工具來(lái)幫助硬件廠商測(cè)量啟動(dòng)時(shí)間。
退出時(shí)間
從用戶點(diǎn)擊關(guān)閉圖標(biāo)或是輸入退出命令直至程序?qū)嶋H完全退出所經(jīng)過(guò)的時(shí)間。通常,開(kāi) 發(fā)人員可以通過(guò)測(cè)量主窗口接收到關(guān)閉命令到程序退出 main() 的時(shí)間來(lái)得到退出時(shí)間, 但是有時(shí)候也有例外。退出時(shí)間也包含停止所有的線程和所依賴的進(jìn)程所需的時(shí)間。為 程序提供認(rèn)證的操作系統(tǒng)廠商對(duì)程序的退出時(shí)間有嚴(yán)格的要求。退出時(shí)間同樣非常重 要,因?yàn)橹貑⒁粋€(gè)服務(wù)或是長(zhǎng)時(shí)間運(yùn)行的程序所需的時(shí)間等于它的退出時(shí)間加上它的啟 動(dòng)時(shí)間。
響應(yīng)時(shí)間
執(zhí)行一個(gè)命令的平均時(shí)間或最長(zhǎng)時(shí)間。對(duì)于網(wǎng)站來(lái)說(shuō),平均響應(yīng)時(shí)間和最長(zhǎng)響應(yīng)時(shí)間 都會(huì)影響用戶對(duì)網(wǎng)站的滿意度。響應(yīng)時(shí)間可以粗略地以 10 的冪為單位劃分為以下幾 個(gè)級(jí)別。
| 低于 0.1 秒:用戶在直接控制 | 如果響應(yīng)時(shí)間低于 0.1 秒,用戶會(huì)感覺(jué)他們?cè)谥苯涌刂朴脩艚缑?#xff0c;他們的操作直接 改變了用戶界面。這是用戶開(kāi)始拖動(dòng)對(duì)象至對(duì)象發(fā)生移動(dòng),或是用戶點(diǎn)擊輸入框至 輸入框變?yōu)楦吡林g的最小延遲。任何高于這個(gè)值的延遲都會(huì)讓用戶覺(jué)得他們發(fā)送 了一條命令讓計(jì)算機(jī)去執(zhí)行。 |
| 0.1 秒至 1 秒:用戶在控制命令 | 如果響應(yīng)時(shí)間在 0.1 秒至 1 秒之間,用戶雖然仍然會(huì)覺(jué)得他們處于掌控狀態(tài),但是 這個(gè)短暫的延遲會(huì)被用戶理解為計(jì)算機(jī)執(zhí)行了一條命令導(dǎo)致 UI 發(fā)生了變化。用戶可 以忍受這種程度的延遲,不至于分散注意力。 |
| 1秒至 10 秒:計(jì)算機(jī)在控制 | 如果響應(yīng)時(shí)間在 1 秒至 10 秒之間,用戶會(huì)覺(jué)得他們?cè)趫?zhí)行了一條命令后失去了對(duì) 計(jì)算機(jī)的控制,雖然這時(shí)候計(jì)算機(jī)仍然在處理命令。用戶可能會(huì)分散注意力,忘記 一件剛才發(fā)生的事情——他們需要完成自己的任務(wù)。10 秒是用戶能保持注意力的 最長(zhǎng)時(shí)間。如果他們多次遇到這種長(zhǎng)時(shí)間等待 UI 發(fā)生改變的情況,用戶滿意度會(huì) 急速下降。 |
| 高于 10 秒:喝杯咖啡休息一下 | 如果響應(yīng)時(shí)間高于 10 秒,用戶會(huì)覺(jué)得他們有足夠的時(shí)間去做一些其他的事情。如 果他們的工作需要用到 UI,那么他們會(huì)利用等待計(jì)算機(jī)進(jìn)行計(jì)算的時(shí)間去喝一杯 咖啡。如果可以的話,他們甚至?xí)P(guān)閉程序,然后去其他地方找找滿足感。 |
雅各布 ? 尼爾森(Jakob Nielsen)就用戶體驗(yàn)中的響應(yīng)時(shí)間范圍寫(xiě)了一篇非常有意思的 文章(https://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/),這是一份出于 好奇而進(jìn)行的學(xué)術(shù)研究。
吞吐量
與響應(yīng)時(shí)間相對(duì)。通常,吞吐量表述為在一定的測(cè)試負(fù)載下,系統(tǒng)在每個(gè)時(shí)間單位內(nèi)所 執(zhí)行的操作的平均數(shù)。吞吐量所測(cè)量的東西與響應(yīng)時(shí)間相同,但是它更適合于評(píng)估批處 理程序,如數(shù)據(jù)庫(kù)和 Web 服務(wù)等。通常,這個(gè)數(shù)字越大越好。
有時(shí),也可能會(huì)發(fā)生過(guò)度優(yōu)化的情況。例如,在許多情況下,用戶認(rèn)為響應(yīng)時(shí)間小于 0.1 秒就是一瞬間的事了。在這種情況下,即使將響應(yīng)時(shí)間從 0.1 秒改善為了 1 毫秒,也不會(huì) 增加任何價(jià)值,盡管響應(yīng)速度提升了 100 倍。
3.2.3 你只能改善你能夠測(cè)量的
優(yōu)化一個(gè)函數(shù)、子系統(tǒng)、任務(wù)或是測(cè)試用例永遠(yuǎn)不等同于改善整個(gè)程序的性能。由于測(cè)試 時(shí)的設(shè)置在許多方面都與處理客戶數(shù)據(jù)的正式產(chǎn)品不同,在所有環(huán)境中都取得在測(cè)試過(guò)程 中測(cè)量到的性能改善結(jié)果是幾乎不可能的。盡管某個(gè)任務(wù)在程序中負(fù)責(zé)大部分的邏輯處 理,但是使其變得更快可能仍然無(wú)法使整個(gè)程序變得更快。 例如,一個(gè)數(shù)據(jù)庫(kù)開(kāi)發(fā)人員通過(guò)執(zhí)行 1000 次某個(gè)特定的查詢語(yǔ)句分析了數(shù)據(jù)庫(kù)性能,然 后基于分析結(jié)果進(jìn)行了優(yōu)化,但這可能并不會(huì)提升整個(gè)數(shù)據(jù)庫(kù)的速度,而是只提升了該查 詢語(yǔ)句的速度。這也可能會(huì)提升其他查詢語(yǔ)句的速度,但它可能不會(huì)改善刪除或更新查 詢、建立索引或是數(shù)據(jù)庫(kù)可以進(jìn)行的其他處理的速度。
3.3 分析程序執(zhí)行
分析器是一個(gè)可以生成另外一個(gè)程序的執(zhí)行時(shí)間的統(tǒng)計(jì)結(jié)果的程序。分析器可以輸出一份 包含每個(gè)語(yǔ)句或函數(shù)的執(zhí)行頻度、每個(gè)函數(shù)的累積執(zhí)行時(shí)間的報(bào)表。
許多編譯器套件,如 Windows 上的 Visual Studio 和 Linux 上的 GCC 都帶有分析器,可以 幫助我們找到程序中的熱點(diǎn)。微軟曾經(jīng)只在價(jià)格昂貴的 Visual Studio 版本中提供了分析 器,但是自 Visual Studio 2015 社區(qū)版開(kāi)始,微軟開(kāi)始向開(kāi)發(fā)者提供免費(fèi)的分析器。當(dāng)然, 在 Windows 上還有其他開(kāi)源的分析器以及對(duì)應(yīng)早期的 Visual Studio 版本的分析器。
有幾種方式可以實(shí)現(xiàn)一個(gè)分析器。一種可以同時(shí)支持 Windows 和 Linux 的方法如下。
分析器的輸出結(jié)果可能會(huì)有多種形式。一種形式是一份標(biāo)記有每行代碼的執(zhí)行次數(shù)的源代 碼清單。另一種形式是一份由函數(shù)名和該函數(shù)被調(diào)用的次數(shù)組成的清單。第三種形式同樣 也是函數(shù)清單,不過(guò)里面記錄的是每個(gè)函數(shù)的累計(jì)執(zhí)行時(shí)間和在每個(gè)函數(shù)中進(jìn)行的函數(shù)調(diào) 用。還有一種形式是一份函數(shù)和在每個(gè)函數(shù)中花費(fèi)的總時(shí)間的清單,但不包括調(diào)用其他函 數(shù)的時(shí)間、調(diào)用系統(tǒng)代碼的時(shí)間和等待事件的時(shí)間。
分析器的分析功能都是量身設(shè)計(jì)的,它自身的性能開(kāi)銷非常小,因此它對(duì)程序整體運(yùn)行時(shí) 間的影響也很小。通常,程序中每個(gè)操作的執(zhí)行速度只會(huì)被降低幾個(gè)百分點(diǎn)。第一種方法 的分析結(jié)果會(huì)非常精確,代價(jià)是更高的間接成本和禁用了某些優(yōu)化。第二種方法的測(cè)量結(jié) 果是近似值,而且可能會(huì)遺漏一些非頻繁地被調(diào)用的函數(shù),但是它的優(yōu)點(diǎn)是可以直接運(yùn)行 于正式產(chǎn)品之上。
分析器的最大優(yōu)點(diǎn)是它直接顯示出了代碼中最熱點(diǎn)的函數(shù)。優(yōu)化過(guò)程被簡(jiǎn)化為列出需要調(diào)查 的函數(shù)的清單,確認(rèn)各個(gè)函數(shù)優(yōu)化的可能性,修改代碼,然后重新運(yùn)行代碼得到一份新的分 析結(jié)果。如此反復(fù),直至沒(méi)有特別熱點(diǎn)的函數(shù)或是你無(wú)能為力了為止。由于分析結(jié)果中的熱 點(diǎn)函數(shù)從定義上來(lái)說(shuō)就是代碼中發(fā)生大量計(jì)算的地方,因此,通常這個(gè)過(guò)程是直截了當(dāng)?shù)摹?/p>
以我個(gè)人的分析經(jīng)驗(yàn)來(lái)看,對(duì)調(diào)試構(gòu)建(debug build)的分析結(jié)果和對(duì)正式構(gòu)建(release build)的分析結(jié)果是一樣的。在某種意義上,調(diào)試構(gòu)建更易于分析,因?yàn)槠渲邪械?函數(shù),包括內(nèi)聯(lián)函數(shù),而正式構(gòu)建則會(huì)隱藏這些被頻繁調(diào)用的內(nèi)聯(lián)函數(shù)。
| 在 Windows 上分析調(diào)試構(gòu)建的一個(gè)問(wèn)題是,調(diào)試構(gòu)建所鏈接的是調(diào)試版本的運(yùn)行時(shí) 庫(kù)。調(diào)試版本的內(nèi)存管理器函數(shù)會(huì)執(zhí)行一些額外的測(cè)試,以便更好地報(bào)告重復(fù)釋放的 內(nèi)存和內(nèi)存泄漏問(wèn)題。這些額外測(cè)試的開(kāi)銷會(huì)顯著地增加某些函數(shù)的性能開(kāi)銷。有一 個(gè)環(huán)境變量可以讓調(diào)試器不要使用調(diào)試內(nèi)存管理器:進(jìn)入控制面板→系統(tǒng)屬性→高級(jí) 系統(tǒng)設(shè)置→環(huán)境變量→系統(tǒng)變量,然后添加一個(gè)叫作 _NO_DEBUG_HEAP 的新變量并設(shè)定 其值為 1。 |
使用分析器是一種幫助我們找到要優(yōu)化的代碼的非常好的方式,但也有它的問(wèn)題。
- 分析器無(wú)法告訴你有更高效的算法可以解決當(dāng)前的計(jì)算性能問(wèn)題。去優(yōu)化一個(gè)低效的算 法只是浪費(fèi)時(shí)間。
- 對(duì)于會(huì)執(zhí)行許多不同任務(wù)的待優(yōu)化的程序,分析器無(wú)法給出明確的結(jié)果。例如,一個(gè) SQL 數(shù)據(jù)庫(kù)在執(zhí)行 insert 語(yǔ)句時(shí)和在執(zhí)行 select 語(yǔ)句時(shí)所運(yùn)行的代碼是不一樣的。因 此,當(dāng)使用 insert 加載數(shù)據(jù)庫(kù)時(shí)的熱點(diǎn)代碼,可能在數(shù)據(jù)庫(kù)執(zhí)行 select 語(yǔ)句的時(shí)候完 全不會(huì)被運(yùn)行。除非在分析時(shí)會(huì)進(jìn)行大量計(jì)算,否則請(qǐng)?jiān)跍y(cè)試中混合加載數(shù)據(jù)庫(kù)操作和 查詢數(shù)據(jù)庫(kù)操作,使執(zhí)行 insert 語(yǔ)句的代碼在分析結(jié)果中不那么突出。因此,要想容易地找出最熱點(diǎn)的函數(shù),請(qǐng)盡量一次僅優(yōu)化一個(gè)任務(wù)。這對(duì)于分析整個(gè)程 序中的一個(gè)子系統(tǒng)在測(cè)試套件上的運(yùn)行情況非常有幫助。不過(guò),如果每次只優(yōu)化一個(gè)任 務(wù),那么也會(huì)引入另外一種不確定性:即它不一定會(huì)改善程序的整體性能。而實(shí)際上當(dāng) 程序運(yùn)行多個(gè)任務(wù)時(shí),優(yōu)化的效果可能就體現(xiàn)得不那么明顯了。
- 當(dāng)遇到 IO 密集型程序或是多線程程序時(shí),分析器的結(jié)果中可能會(huì)含有誤導(dǎo)信息,因?yàn)?分析器減去了系統(tǒng)調(diào)用的時(shí)間和等待事件的時(shí)間。不計(jì)算這些時(shí)間在理論上是完全合理 的,因?yàn)槌绦虿⒉恍枰獮檫@些等待時(shí)間負(fù)責(zé)。但是結(jié)果卻是分析器可以告訴我們程序做 了多少事情,而不是花了多少實(shí)際時(shí)間去做這些事情。有些分析器不僅統(tǒng)計(jì)了函數(shù)調(diào)用 的次數(shù),還計(jì)算出了每個(gè)函數(shù)的調(diào)用時(shí)間。如果函數(shù)調(diào)用次數(shù)非常多,意味著分析器可 能隱藏了實(shí)際時(shí)間。
分析器并不完美。有些優(yōu)化可能性可能不會(huì)被分析出來(lái),而且程序員在理解分析器的輸出 結(jié)果時(shí)也可能會(huì)有問(wèn)題。不過(guò),對(duì)于許多程序來(lái)說(shuō),分析器的分析結(jié)果已經(jīng)足夠好了,不 需要再使用其他的優(yōu)化方法了。
3.4 測(cè)量長(zhǎng)時(shí)間運(yùn)行的代碼
如果程序只是運(yùn)行一個(gè)計(jì)算密集型的任務(wù),那么分析器會(huì)自動(dòng)地告訴我們程序中的熱點(diǎn)在 哪里。不過(guò)如果程序要做許多不同的處理,可能在分析器看來(lái),沒(méi)有任何一個(gè)函數(shù)是熱 點(diǎn)。程序還有可能會(huì)花費(fèi)大量的時(shí)間等待 I/O 或是外部事件,這樣降低了程序的性能,增 加了程序的實(shí)際運(yùn)行時(shí)間。在這種情況下,我們需要測(cè)量程序中各個(gè)部分的時(shí)間,然后試 著減少其中低效部分的運(yùn)行時(shí)間。
開(kāi)發(fā)人員通過(guò)不斷地縮小長(zhǎng)時(shí)間運(yùn)行的任務(wù)的范圍直至定位其中一段代碼花費(fèi)了太長(zhǎng)時(shí) 間,感覺(jué)不對(duì)勁這種方式來(lái)查找代碼中的熱點(diǎn)。在找出這些可疑代碼后,開(kāi)發(fā)人員會(huì)在測(cè) 試套件中對(duì)小的子系統(tǒng)或是獨(dú)立的函數(shù)進(jìn)行優(yōu)化實(shí)驗(yàn)。
測(cè)量運(yùn)行時(shí)間是一種測(cè)試關(guān)于“如何減少某個(gè)特定函數(shù)的性能開(kāi)銷”的假設(shè)的有效方式。
一般,我們很難意識(shí)到可以通過(guò)編程在計(jì)算機(jī)上實(shí)現(xiàn)秒表功能。你可以非常方便地使用手 機(jī)或是手提電腦在工作日的 6:45 叫醒你,或是在早上 10 點(diǎn)的站立會(huì)議前 5 分鐘提醒你 參加會(huì)議。但是在現(xiàn)代計(jì)算機(jī)上測(cè)量亞微秒級(jí)的運(yùn)行時(shí)間卻是有點(diǎn)難度的,特別是因?yàn)樵?普通的 Window/PC 平臺(tái)上存在沒(méi)有可以穩(wěn)定地工作于不同型號(hào)的硬件和不同的軟件版本 上的高精度計(jì)時(shí)器的歷史遺留問(wèn)題。
因此,作為一名開(kāi)發(fā)人員,你需要隨時(shí)準(zhǔn)備好制作一個(gè)自己的秒表,而且必須知道它們以 后可能會(huì)發(fā)生變化。為了使這成為可能,接下來(lái)我會(huì)討論如何測(cè)量時(shí)間以及有哪些工具可 用于在計(jì)算機(jī)上測(cè)量時(shí)間。
3.4.1 一點(diǎn)關(guān)于測(cè)量時(shí)間的知識(shí)
淺學(xué)害人。
? ——亞歷山大 ? 蒲柏,“批評(píng)論”(http://poetry.eserver.org/ essay-oncriticism.html), 1774 年
一次完美的測(cè)量是指精確地得到大小、重量或者在本書(shū)中是某個(gè)事件每次持續(xù)的時(shí)間。完 美的測(cè)量就像是將弓箭不斷地精準(zhǔn)地射中靶心一樣。這種箭術(shù)只存在于故事書(shū)中,測(cè)量也 是一樣的。
真正的測(cè)量實(shí)驗(yàn)(就像真正的弓箭)必須能夠應(yīng)對(duì)可變性(variation):可能破壞完美測(cè) 量的誤差源??勺冃杂袃煞N類型:隨機(jī)的和系統(tǒng)的。隨機(jī)的可變性對(duì)每次測(cè)量的影響都不 同,就像一陣風(fēng)導(dǎo)致弓箭偏離飛行線路一樣。系統(tǒng)的可變性對(duì)每次測(cè)量的影響是相似的, 就像一位弓箭手的姿勢(shì)會(huì)影響他每一次射箭都偏向靶子的左邊一樣。
可變性自身也是可以測(cè)量的。衡量一次測(cè)量過(guò)程中的可變性的屬性被稱為精確性(precision) 和正確性(trueness)。這兩種屬性組合成的直觀特性稱為準(zhǔn)確性(accuracy)。
很明顯,對(duì)測(cè)量感到興奮的科學(xué)家就相關(guān)的專業(yè)用語(yǔ)展開(kāi)了喋喋不休的爭(zhēng)論。你只需在維 基百科上查找一下“準(zhǔn)確性”這個(gè)詞,就會(huì)發(fā)現(xiàn)關(guān)于究竟應(yīng)該使用哪些詞來(lái)解釋已經(jīng)達(dá)成 一致的概念有多少爭(zhēng)議了。我選擇使用 1994 版的 ISO 5725-1 中的上下文來(lái)解釋術(shù)語(yǔ):“測(cè) 量方法和結(jié)果中的準(zhǔn)確性(正確性和精確性)——卷 1:通用原則和定義”(1994)。
如果測(cè)量不受隨機(jī)可變性的影響,它就是精確的。也就是說(shuō),如果反復(fù)地測(cè)量同一現(xiàn)象, 而且這些測(cè)量值之間非常接近,那么測(cè)量就是精確的。一系列精確的測(cè)量中可能仍然包含 系統(tǒng)的可變性。盡管一位弓箭手將一組弓箭射到了偏離靶心的一塊區(qū)域中,但我們?nèi)匀豢?以說(shuō)這是精確的,盡管不太準(zhǔn)確。他射中的靶子的樣子可能如圖 3-2 所示。
? 圖 3-2:高精確性(但低正確性)的射箭結(jié)果
如果測(cè)量一個(gè)事件(比如一個(gè)函數(shù)的運(yùn)行時(shí)間)10 次,而且 10 次的結(jié)果完全相同,我們 可以認(rèn)為測(cè)量是精確的。(像在任何實(shí)驗(yàn)中一樣,我應(yīng)當(dāng)會(huì)對(duì)此持懷疑態(tài)度,直到找到足 夠的證據(jù)為止。)如果其中只有 6 次結(jié)果相同,3 次結(jié)果略微有些不同,1 次結(jié)果的差異非 常大,那么測(cè)量就是不夠精確的。
如果測(cè)量不受系統(tǒng)可變性的影響,它就是正確的。也就是說(shuō),如果反復(fù)地測(cè)量同一現(xiàn)象, 而且所有測(cè)量結(jié)果的平均值接近實(shí)際值,那可以認(rèn)為測(cè)量是正確的。每次獨(dú)立的測(cè)量可能 受到隨機(jī)可變性的影響,所以測(cè)量結(jié)果可能會(huì)更接近或是偏離實(shí)際值。正確性并不受弓箭手的技能影響。在圖 3-3 中,將四箭的平均值看作是一把箭的話,那么它應(yīng)當(dāng)是正中靶心 的。而且,就環(huán)數(shù)(離靶心的距離)而言,這四箭具有相同的準(zhǔn)確性。
圖 3-3:弓箭手的箭找到了正確的靶心
測(cè)量的準(zhǔn)確性是一個(gè)取決于每次獨(dú)立的測(cè)量結(jié)果與實(shí)際值有多接近的非正式的概念。與實(shí) 際值的差異由隨機(jī)可變性與系統(tǒng)可變性兩部分組成。只有同時(shí)具有精確性和正確性的測(cè)量 才是準(zhǔn)確的測(cè)量。
本書(shū)中涉及的軟件性能測(cè)量要么是測(cè)量持續(xù)時(shí)間(兩個(gè)事件之間的時(shí)間),要么是測(cè)量速 率(單位時(shí)間內(nèi)事件的數(shù)量,與持續(xù)時(shí)間相對(duì))。用于測(cè)量持續(xù)時(shí)間的工具是時(shí)鐘。
所有時(shí)鐘的工作原理都是周期性地計(jì)數(shù)。某些時(shí)鐘的計(jì)數(shù)會(huì)表示為時(shí)、分、秒,有些則是 直接顯示時(shí)標(biāo)的次數(shù)。但是時(shí)鐘(除了日晷外)是并不會(huì)直接測(cè)量時(shí)、分、秒的。它們只 會(huì)對(duì)時(shí)標(biāo)進(jìn)行計(jì)數(shù),然后只有將時(shí)標(biāo)計(jì)數(shù)值與秒基準(zhǔn)的時(shí)鐘進(jìn)行比較后才能校準(zhǔn)時(shí)鐘,顯 示出時(shí)、分、秒。
周期性地改變的東西受到可變性的影響也會(huì)出現(xiàn)誤差。有些可變性是隨機(jī)的,有些可變性 則是系統(tǒng)的。
- 日晷利用了地球的周期性旋轉(zhuǎn)。從定義上說(shuō),一次完整的旋轉(zhuǎn)是一天。地球并非完美的 時(shí)鐘,不僅是因?yàn)橹芷谔L(zhǎng),而且我們發(fā)現(xiàn)由于大陸在它表面上緩慢地移動(dòng),它的旋轉(zhuǎn) 速度時(shí)快時(shí)慢(微秒級(jí)別)。這種可變性是隨機(jī)的;來(lái)自月球和太陽(yáng)的潮汐力會(huì)降低地 球的整體旋轉(zhuǎn)速率。這種可變性是系統(tǒng)的。
- 老式時(shí)鐘會(huì)對(duì)鐘擺有規(guī)律的擺動(dòng)計(jì)數(shù)。齒輪會(huì)隨著鐘擺驅(qū)動(dòng)指針旋轉(zhuǎn)來(lái)顯示時(shí)間。鐘擺 擺動(dòng)的間隔可以手動(dòng)調(diào)整,這樣所顯示的時(shí)間可以與地球旋轉(zhuǎn)同步。鐘擺擺動(dòng)的周期取 決于鐘擺的重量和它的長(zhǎng)度,這樣就可以根據(jù)需要讓擺動(dòng)得更快或是更慢。這種可變性 是系統(tǒng)的;而即使在最開(kāi)始鐘擺的擺動(dòng)非常精準(zhǔn),但摩擦、氣壓和累積的灰塵都會(huì)對(duì)擺 動(dòng)造成影響。這些都是隨機(jī)可變性因素。
- 電子時(shí)鐘使用它的交流電源的周期性的 60Hz 正弦波驅(qū)動(dòng)同步電機(jī)。齒輪會(huì)下分基本振 蕩和驅(qū)動(dòng)指針來(lái)顯示時(shí)間。電子時(shí)鐘也并非完美的時(shí)鐘,因?yàn)楦鶕?jù)慣例(不是自然法 則),交流電源的周期只有 60Hz(在美國(guó))。當(dāng)負(fù)荷過(guò)高時(shí),電力公司會(huì)先降低振蕩周期,稍后又提高振蕩周期,這樣電子時(shí)鐘并不會(huì)走慢。所以,在炎熱夏日的午后電子時(shí)鐘的 一秒可能會(huì)比涼爽夜晚的一秒快(雖然我們總是對(duì)此表示懷疑)。這種可變性是隨機(jī)的。 將一個(gè)為美國(guó)用戶制造的電子時(shí)鐘插入到歐洲 50Hz 的交流電源插座中,它會(huì)走得慢。 與氣溫引起的隨機(jī)可變性相比,這種由歐洲電源插座引起的可變性是系統(tǒng)的。
- 數(shù)字腕表采用石英晶體的誘導(dǎo)振動(dòng)作為基本振動(dòng)。邏輯電路會(huì)下分基本振動(dòng)并驅(qū)動(dòng)時(shí)間 顯示。石英晶體的振動(dòng)周期取決于它的大小、溫度以及加載的電壓。石英晶體的大小的 影響是系統(tǒng)的可變性,而溫度和電壓的可變性則是隨機(jī)的。
時(shí)標(biāo)計(jì)數(shù)值肯定是一個(gè)無(wú)符號(hào)的值。不可能存在 -5 次時(shí)標(biāo)。我之所以在這里提醒大家這 個(gè)看似非常明顯的事實(shí),是因?yàn)檎缟院髸?huì)向大家展示的,許多開(kāi)發(fā)人員實(shí)現(xiàn)計(jì)時(shí)函數(shù)時(shí) 選擇有符號(hào)類型來(lái)表示持續(xù)時(shí)間。我不知道為什么他們這么做。我那十幾歲的兒子應(yīng)該會(huì) 說(shuō):“這沒(méi)什么大不了?!?/p>
測(cè)量的分辨率是指測(cè)量所呈現(xiàn)出的單位的大小。
一位弓箭手只要將弓箭射在指定環(huán)內(nèi)的任意位置,所得到的分?jǐn)?shù)都是相同的。靶心并非是 無(wú)限小的點(diǎn),而是一個(gè)給定直徑的圓環(huán)(請(qǐng)參見(jiàn)圖 3-4)。一支箭要么設(shè)在靶心,或是九 環(huán)、八環(huán)等。每一環(huán)的寬度就是射箭得分的分辨率。
圖 3-4:分辨率:一支箭設(shè)在一環(huán)中任意地方的得分是相同的
時(shí)間測(cè)量的有效分辨率會(huì)受到潛在波動(dòng)的持續(xù)時(shí)間的限制。時(shí)間測(cè)量結(jié)果可以是一次或者 兩次時(shí)標(biāo),但不能是這兩者之間。這些時(shí)標(biāo)之間的間隔就是時(shí)鐘的有效分辨率。
觀察人員可能會(huì)察覺(jué)到一個(gè)走得很慢的時(shí)鐘的兩次時(shí)標(biāo)之間發(fā)生的事情,例如鐘擺的一次 擺動(dòng)。這只是說(shuō)明在人類腦海中有一個(gè)更快的時(shí)鐘(雖然沒(méi)有那么準(zhǔn)確),他們會(huì)將這個(gè) 時(shí)鐘的時(shí)間與鐘擺的時(shí)間進(jìn)行比較。觀察人員如果想測(cè)量那些不可感知的持續(xù)時(shí)間,例如 毫秒級(jí)別,只能用時(shí)鐘的時(shí)標(biāo)。
在測(cè)量的準(zhǔn)確性與它的分辨率之間是沒(méi)有任何必需的關(guān)聯(lián)的。例如,假設(shè)我記錄了我每天 的工作,那么我可以報(bào)告說(shuō)我花了兩天來(lái)編寫(xiě)本節(jié)內(nèi)容。在這個(gè)例子中,測(cè)量的有效分辨 率是“天”。如果我想把這個(gè)時(shí)間換成秒,那么可以報(bào)告說(shuō)成我花了 172 800 秒來(lái)編寫(xiě)本節(jié)
內(nèi)容。但除非我手頭上有一個(gè)秒表,否則以秒為單位進(jìn)行報(bào)告會(huì)讓人誤認(rèn)為比之前更加準(zhǔn) 確,或是給人一種沒(méi)有吃飯和睡覺(jué)的錯(cuò)覺(jué)。
測(cè)量結(jié)果的單位可能會(huì)比有效分辨率小,因?yàn)閱挝徊攀菢?biāo)準(zhǔn)。我有一個(gè)可以以華氏溫度為 單位顯示溫度的烤箱。恒溫器控制著烤箱,但是有效分辨率只有 5°F。所以在烤箱加熱的 過(guò)程中,顯示屏上顯示的溫度會(huì)是 300°F,接著是 305°F、310°F、315°F 等。以一度為單 位顯示溫度應(yīng)該比恒溫器的單位更合理。有效分辨率只有 5°F 只是表示測(cè)量的最低有效位 只能是 0 或者 5。
當(dāng)讀者知道他們身邊廉價(jià)的溫度計(jì)、尺子和其他測(cè)量設(shè)備的有效分辨率后可能會(huì)感到吃驚 和失望,因?yàn)檫@些設(shè)備的顯示分辨率是 1 個(gè)單位或是 1/10 單位。
只有一塊表的人知道現(xiàn)在的時(shí)間,而擁有兩塊表的人卻永遠(yuǎn)不能確定現(xiàn)在的時(shí)間。
? ——多認(rèn)為該名言出自 Lee Segall
當(dāng)兩個(gè)事件在同一個(gè)地點(diǎn)發(fā)生時(shí),很容易通過(guò)一個(gè)時(shí)鐘的時(shí)標(biāo)計(jì)數(shù)來(lái)測(cè)量事件的經(jīng)過(guò)時(shí) 間。但是如果這兩個(gè)事件發(fā)生在相距很遠(yuǎn)的不同地點(diǎn),可能就需要兩個(gè)時(shí)鐘來(lái)測(cè)量時(shí)間。 而兩個(gè)不同時(shí)鐘的時(shí)標(biāo)次數(shù)無(wú)法直接比較。
人類想到了一個(gè)辦法,那就是通過(guò)與國(guó)際協(xié)調(diào)時(shí)間(Coordinated Universal Time)同步。 國(guó)際協(xié)調(diào)時(shí)間與經(jīng)度 0 度的天文學(xué)上的午夜同步,而經(jīng)度 0 度這條線穿過(guò)了英格蘭格林威 治皇家天文臺(tái)中的一塊漂亮的牌匾(請(qǐng)參見(jiàn)圖 3-5)。這樣就可以將一個(gè)以時(shí)標(biāo)計(jì)數(shù)值表示 的時(shí)間轉(zhuǎn)換為以時(shí)分秒表示的相對(duì) UTC(Universal Time Coorinated,國(guó)際協(xié)調(diào)間,由 法國(guó)和英國(guó)的時(shí)鐘專家商定的一個(gè)既不是法式拼寫(xiě)也不是英式拼寫(xiě)的縮寫(xiě))午夜的時(shí)間。
圖 3-5:英格蘭格林威治皇家天文學(xué)館的本初子午線的標(biāo)記(攝影: ?var Arnfj?re Bjarmason, license CC BY-SA 3.0)
如果兩個(gè)時(shí)鐘都與 UTC 完美地同步了,那么其中一個(gè)時(shí)鐘的相對(duì) UTC 時(shí)間可以直接與另 外一個(gè)相比較。但是當(dāng)然,完全的同步是不可能的。兩個(gè)時(shí)鐘都有各自獨(dú)立的可變性因 素,導(dǎo)致它們與 UTC 之間以及它們互相之間產(chǎn)生誤差。
3.4.2 用計(jì)算機(jī)測(cè)量時(shí)間
要想在計(jì)算機(jī)上制作一個(gè)時(shí)鐘需要一個(gè)周期性的振動(dòng)源——最好有很好的精確性和正確 性——以及一種讓軟件獲取振動(dòng)源的時(shí)標(biāo)的方法。要想專門為了計(jì)時(shí)而制造一臺(tái)計(jì)算機(jī)是 很容易的。不過(guò),多數(shù)現(xiàn)在流行的計(jì)算機(jī)體系結(jié)構(gòu)在設(shè)計(jì)時(shí)都沒(méi)有考慮過(guò)要提供很好的時(shí) 鐘。我將會(huì)結(jié)合 PC 體系結(jié)構(gòu)和微軟的 Windows 操作系統(tǒng)講解問(wèn)題所在。Linux 和嵌入式 平臺(tái)上也存在類似的問(wèn)題。
PC 時(shí)鐘電路核心部分的晶體振蕩器的基本精度是 100PPM,即 0.01%,或者每天約 8 秒的 誤差。雖然這個(gè)精度只比數(shù)字腕表的精度高一點(diǎn)點(diǎn),但對(duì)性能測(cè)量來(lái)說(shuō)已經(jīng)足夠了,因?yàn)?對(duì)于極其非正式的測(cè)量結(jié)果,精確到幾個(gè)百分點(diǎn)就可以了。廉價(jià)的嵌入式處理器的時(shí)鐘電 路的精確度較低,但是最大的問(wèn)題并非周期性振動(dòng)的振動(dòng)源,更困難的是如何讓程序得到 可靠的時(shí)標(biāo)計(jì)數(shù)值。
起初的 IBM PC 是不包含任何硬件時(shí)標(biāo)計(jì)數(shù)器的。它確實(shí)有一個(gè)記錄一天之中的時(shí)間的 時(shí)鐘,軟件也可以讀取這個(gè)時(shí)間。最早的微軟的 C 運(yùn)行時(shí)庫(kù)復(fù)制了 ANSI C 庫(kù),提供了 time_t time(time_t*) 函數(shù)。該函數(shù)會(huì)返回一個(gè)距離 UTC 時(shí)間 1970 年 1 月 1 日 0:00 的秒 數(shù)。舊版本的 time() 函數(shù)返回的是一個(gè) 32 位有符號(hào)整數(shù),但是在經(jīng)歷了 Y2K3 之后,它被 修改成了一個(gè) 64 位的有符號(hào)整數(shù)。
起初的 IBM PC 會(huì)使用來(lái)自交流電源的周期性的中斷來(lái)喚醒內(nèi)核去進(jìn)行任務(wù)切換或是進(jìn)行 其他內(nèi)核操作。在北美,這個(gè)周期是 16.67 毫秒,因?yàn)榻涣麟娫词?60Hz 的。如果交流電 源是 50Hz 的話,這個(gè)周期就是 20 毫秒。
自 Windows 98(可能更早)以來(lái),微軟的 C 運(yùn)行時(shí)提供了 ANSI C 函數(shù) clock_t clock()。 該函數(shù)會(huì)返回一個(gè)有符號(hào)形式的時(shí)標(biāo)計(jì)數(shù)器。常量 CLOCKS_PER_SEC 指定了每秒鐘的時(shí)標(biāo)的 次數(shù)。返回值為 -1 表示 clock() 不可用。clock() 會(huì)基于交流電源的周期性中斷記錄時(shí)標(biāo)。 clock() 在 Windows 上的實(shí)現(xiàn)方式與 ANSI 所規(guī)定的不同,在 Windows 上它所測(cè)量的是經(jīng) 過(guò)時(shí)間而非 CPU 時(shí)間 4 。最近,clock() 被根據(jù) GetSystemTimeAsfileTime() 重新實(shí)現(xiàn)了。在 2015 年時(shí)它的時(shí)標(biāo)是 1 毫秒,分辨率也是 1 毫秒。這使得它成了 Windows 上一個(gè)優(yōu)秀的 毫秒級(jí)別的時(shí)鐘。
自 Windows 2000 開(kāi)始,可以通過(guò)調(diào)用 DWORD GetTickCount() 來(lái)實(shí)現(xiàn)基于 A/C 電源中斷的 軟件時(shí)標(biāo)計(jì)數(shù)器。GetTickCount() 的時(shí)標(biāo)計(jì)數(shù)值取決于 PC 的硬件,可能會(huì)遠(yuǎn)比 1 毫秒長(zhǎng)。 GetTickCount() 會(huì)進(jìn)行一次將時(shí)標(biāo)轉(zhuǎn)換為毫秒的計(jì)算來(lái)消除部分不確定性。這個(gè)方法的一 個(gè)升級(jí)版是 ULONGLONG GetTickCount64(),它會(huì)以 64 位無(wú)符號(hào)整數(shù)的形式返回相同的時(shí)標(biāo)計(jì)數(shù)值,這樣可以測(cè)量更長(zhǎng)的處理時(shí)間。雖然沒(méi)有辦法知道當(dāng)前的中斷周期,但下面這對(duì) 函數(shù)可以縮短和然后恢復(fù)周期:
MMRESULT timeBeginPeriod(UINT) MMRESULT timeEndPeriod(UINT)這兩個(gè)函數(shù)作用于全局變量上,會(huì)影響所有的進(jìn)程和其他函數(shù),如取決于交流電源的中斷 周期的 Sleep()。另外一個(gè)函數(shù) DWORD timeGetTime() 可以通過(guò)另一種方法獲取相同的時(shí)標(biāo) 計(jì)數(shù)值。
自奔騰體系結(jié)構(gòu)后,英特爾提供了一個(gè)叫作時(shí)間戳計(jì)數(shù)器(Time Stamp Counter,TSC)的 硬件寄存器。TSC 是一個(gè)從處理器時(shí)鐘中計(jì)算時(shí)標(biāo)數(shù)的 64 位寄存器。RDTSC 指令可以非常 快地訪問(wèn)該寄存器。
自 Windows 2000 問(wèn)世后,可以通過(guò)調(diào)用函數(shù) BOOL Query PerformanceCounter(LARGE_ INTEGER*) 來(lái)讀取 TSC,這將會(huì)產(chǎn)生一次特殊的不帶分辨率的時(shí)標(biāo)計(jì)數(shù)??梢酝ㄟ^(guò)調(diào)用 BOOL QueryPerformanceFrequency(LARGE_INTEGER*) 來(lái)獲得分辨率,它會(huì)返回每秒鐘時(shí)標(biāo)的 頻率。LARGE_INTEGER 是一個(gè)帶有有符號(hào)格式的 64 位整數(shù)的結(jié)構(gòu)體,因?yàn)樵诋?dāng)時(shí)引入了以 上這些函數(shù)的 Visual Studio 中還沒(méi)有原生的 64 位有符號(hào)整數(shù)類型。
初始版本的 QueryPerformanceCounter() 的一個(gè)問(wèn)題是,它的時(shí)標(biāo)速率取決于處理器的時(shí)鐘。不同處理器和主板的處理器時(shí)鐘不同。在當(dāng)時(shí),老式的 PC,特別是那些使用超微半導(dǎo)體公司(Advanced Micro Devices,AMD)處理器的 PC 是沒(méi)有 TSC 的。在當(dāng)時(shí)沒(méi)有 TSC 可用的情況下,QueryPerformanceCounter() 會(huì)返回 GetTickCount() 返回的低分辨率的時(shí)標(biāo)計(jì)數(shù)值。
在 Windows 2000 中還新增加了一個(gè) void GetSystemTimeAsfileTime(fiLETIME*) 函數(shù),它 會(huì)返回一個(gè)自 1601 年 1 月 1 日 00:00 UTC 開(kāi)始計(jì)算的以 100 納秒為時(shí)標(biāo)的計(jì)數(shù)值。其中, fiLETIME 也是一個(gè)帶有 64 位整數(shù)的結(jié)構(gòu)體,不過(guò)這次是無(wú)符號(hào)的形式。盡管該時(shí)標(biāo)計(jì)數(shù) 器顯示出來(lái)的分辨率看起來(lái)非常高,有些實(shí)現(xiàn)卻使用了與 GetTickCount() 所使用的低分辨 率計(jì)數(shù)器相同的計(jì)數(shù)器。
很快,QueryPerformanceCounter() 的更多問(wèn)題暴露出來(lái)了。有些處理器實(shí)現(xiàn)了可變時(shí)鐘頻率來(lái)管理功耗。這會(huì)導(dǎo)致時(shí)標(biāo)周期發(fā)生了變化。在擁有多個(gè)獨(dú)立處理器的多處理器系統(tǒng) 中,QueryPerformanceCounter() 返回的值取決于線程運(yùn)行于哪個(gè)處理器之上。處理器開(kāi) 始實(shí)現(xiàn)指令重排序之后,導(dǎo)致 RDTSC 指令可能會(huì)發(fā)生延遲,降低使用了 TSC 的軟件的 準(zhǔn)確性。
為了解決這些問(wèn)題,Windows Vista 為 QueryPerformanceCounter() 使用了一種不同的計(jì) 數(shù)器,稱為 Advanced Configuration and Power Interface(ACPI)電源管理計(jì)時(shí)器。使用 這個(gè)計(jì)數(shù)器雖然能夠解決多處理器的同步問(wèn)題,但是卻顯著地增加了延遲。與此同時(shí), 英特爾重新指定了 TSC 為最大且不變的時(shí)鐘頻率。此外,英特爾還增加了不可重排序的 RDTSCP 指令。
自 Windows 8 開(kāi)始,Windows 提供了一種基于 TSC 的、可靠的、高分辨率的硬件時(shí)標(biāo)計(jì) 數(shù)。只要該系統(tǒng)運(yùn)行于 Windows 8 或者之后的版本上,void GetSystemTimePreciseAsfileT ime(fiLETIME*) 就可以生成一個(gè)固定頻率和亞微秒準(zhǔn)確度的高分辨率時(shí)標(biāo)。
一句話總結(jié)本堂歷史課的內(nèi)容就是,**PC 從來(lái)都不是設(shè)計(jì)作為時(shí)鐘的,因此它們提供的時(shí) 標(biāo)計(jì)數(shù)器是不可靠的。**如果以過(guò)去 35 年的歷史為鑒,未來(lái)的處理器和操作系統(tǒng)可能依然 無(wú)法提供穩(wěn)定的、高分辨率的時(shí)標(biāo)計(jì)數(shù)值。
歷代 PC 都提供的唯一可靠的時(shí)標(biāo)計(jì)數(shù)器就是 GetTickCount() 返回的時(shí)標(biāo)計(jì)數(shù)器了,盡管它也有缺點(diǎn)。clock() 返回的毫秒級(jí)的時(shí)標(biāo)更好,而且近 10 年生產(chǎn)的 PC 應(yīng)該都是支持該函數(shù)的。如果只考慮 Windows 8 及之后的版本和新的處理器的話, GetSystemTimePreciseAsfileTime() 返回的 100 納秒級(jí)別的時(shí)標(biāo)計(jì)數(shù)器是非常精確的。不 過(guò),就我個(gè)人的經(jīng)驗(yàn)來(lái)看,對(duì)時(shí)間測(cè)量來(lái)說(shuō)毫秒級(jí)別的準(zhǔn)確性已經(jīng)足夠了。
返轉(zhuǎn)(wraparound)是指當(dāng)時(shí)鐘的時(shí)標(biāo)計(jì)數(shù)器值到達(dá)最大值后,如果再增加就變?yōu)?0 的 過(guò)程。12 小時(shí)制的模擬時(shí)鐘在每天的正午和午夜各會(huì)進(jìn)行一次返轉(zhuǎn)。Windows 98 在連續(xù) 運(yùn)行 49 天后會(huì)因 32 位毫秒時(shí)標(biāo)計(jì)數(shù)器的返轉(zhuǎn)而掛起(請(qǐng)參見(jiàn) Q216641)。當(dāng)兩位數(shù)的年 份返轉(zhuǎn)時(shí)會(huì)發(fā)生 Y2K 問(wèn)題?,斞湃諝v在 2012 年返轉(zhuǎn),因?yàn)楝斞湃苏J(rèn)為那就是世界末日。 UNIX 時(shí)間戳(自 UTC1970 年 1 月 1 日 00:00 起的帶符號(hào)的 32 位秒數(shù))會(huì)在 2038 年 1 月 發(fā)生返轉(zhuǎn),這可能會(huì)稱為某些“歷史悠久”的嵌入式系統(tǒng)的“世界末日”。返轉(zhuǎn)的問(wèn)題出在缺少額外的位去記錄數(shù)據(jù),導(dǎo)致下次時(shí)間增加后的數(shù)值比上次時(shí)間的數(shù)值小。會(huì)返轉(zhuǎn)的時(shí)鐘僅適用于測(cè)量持續(xù)時(shí)間小于返轉(zhuǎn)間隔的時(shí)間。
例如,在 Windows 上,GetTickCount() 函數(shù)會(huì)返回一個(gè)分辨率為 1 毫秒的 32 位無(wú)符號(hào)的 整數(shù)值作為時(shí)標(biāo)計(jì)數(shù)值。那么,GetTickCount() 的返回值會(huì)每 49 天返轉(zhuǎn)一次。也就是說(shuō), GetTickCount() 適用于測(cè)量那些所需時(shí)間小于 49 天的操作。如果一個(gè)程序在某個(gè)操作開(kāi)始時(shí)和結(jié)束時(shí)分別調(diào)用了 GetTickCount(),兩個(gè)返回值之間的差值就是兩次調(diào)用之間經(jīng)過(guò)的毫秒數(shù)。例如:
DWORD start = GetTickCount();DoBigTask(); DWORD end = GetTickCount(); cout << "Startup took " << end-start << " ms" << endl;C++ 實(shí)現(xiàn)無(wú)符號(hào)算術(shù)的方式去確保了即使在發(fā)生返轉(zhuǎn)時(shí)也可以得到正確的結(jié)果。 GetTickCount() 對(duì)于記住自程序啟動(dòng)后所經(jīng)過(guò)的時(shí)間是比較低效的。許多“歷史悠久”的 服務(wù)器可以持續(xù)運(yùn)行數(shù)個(gè)月甚至數(shù)年。返轉(zhuǎn)的問(wèn)題在于,由于缺少位數(shù)去記錄返轉(zhuǎn)的次 數(shù),end-start 的結(jié)果中可能體現(xiàn)不出發(fā)生了返轉(zhuǎn),或是體現(xiàn)出一個(gè)或者多個(gè)返轉(zhuǎn)。 自 Windows Vista 開(kāi)始,微軟加入了 GetTickCount64() 函數(shù),它會(huì)返回一個(gè) 64 位無(wú)符號(hào)且 顯示分辨率為 1 毫秒的時(shí)標(biāo)計(jì)數(shù)值。GetTickCount64() 的結(jié)果只有在數(shù)百萬(wàn)年后才會(huì)發(fā)生返轉(zhuǎn)。這就意味著,幾乎不會(huì)有人能夠見(jiàn)證返轉(zhuǎn)發(fā)生了。
在 Windows 上,GetTickCount() 會(huì)返回一個(gè)無(wú)符號(hào)的 32 位整數(shù)值。如果一個(gè)程序在某個(gè)操作開(kāi)始和結(jié)束時(shí)分別調(diào)用了 GetTickCount(),兩個(gè)返回值之間的差值就是兩次調(diào)用之間 經(jīng)過(guò)的毫秒單位的執(zhí)行時(shí)間。因此,GetTickCount() 的分辨率是 1 毫秒。
例如,下面這段代碼通過(guò)在循環(huán)中反復(fù)調(diào)用 Foo(),在 Windows 上測(cè)量了名為 Foo() 的函數(shù)的相對(duì)性能。通過(guò)在代碼塊開(kāi)始和結(jié)束時(shí)得到的時(shí)標(biāo)計(jì)數(shù)值,我們可以計(jì)算出循環(huán)處理 所花費(fèi)的時(shí)間:
DWORD start = GetTickCount(); for (unsigned i = 0; i < 1000; ++i) {Foo(); } DWORD end = GetTickCount(); cout << "1000 calls to Foo() took " << end-start << "ms" << endl;如果 Foo() 中包含了大量的計(jì)算,那么這段代碼的輸出結(jié)果可能如下:
1000 calls to Foo() took 16ms不幸的是,從微軟網(wǎng)站中關(guān)于 GetTickCount() 的文檔(https://msdn.microsoft.com/en-us/ library/windows/desktop/ms724408(v=vs.85).aspx)來(lái)看,調(diào)用 GetTickCount() 的準(zhǔn)確性 可能是 10 毫秒或 15.67 毫秒。也就是說(shuō),如果連續(xù)調(diào)用兩次 GetTickCount(),那么結(jié)果之間的差值可能是 0 或者 1 毫秒,也可能是 10、15 或 16 毫秒。因此,測(cè)量的基礎(chǔ)精度 是 15 毫秒,額外的分辨率毫無(wú)價(jià)值。之前代碼塊的輸出結(jié)果可能會(huì)是 10ms、20ms 或精 確的 16ms。
GetTickCount() 特別讓人沮喪的一點(diǎn)是,除了分辨率是 1 毫秒外,無(wú)法確保在兩臺(tái) Windows 計(jì)算機(jī)中該函數(shù)是以某種方式或是相同方式實(shí)現(xiàn)的。
我在 Windows 上測(cè)試了許多計(jì)時(shí)函數(shù),試圖找出它們?cè)谀骋慌_(tái)計(jì)算機(jī)(基于 i7 處理器的 Surface 3 平板電腦)的某個(gè)操作系統(tǒng)(Windows 8.1)上的可用分辨率。示例代碼 3-1 中的 測(cè)試循環(huán)地調(diào)用了測(cè)量時(shí)間的函數(shù),并檢查這些連續(xù)的函數(shù)調(diào)用的返回值之間的差值。如果時(shí)標(biāo)的可用分辨率大于函數(shù)調(diào)用的延遲,那么這些連續(xù)的函數(shù)調(diào)用的返回值將要么相同,要么它們之間的差值是若干個(gè)基礎(chǔ)時(shí)標(biāo),單位是函數(shù)的分辨率。我計(jì)算了非零差值的 平均值,以排除操作系統(tǒng)偷用時(shí)間片段去執(zhí)行其他任務(wù)的誤差。
代碼清單 3-1 測(cè)量 GetTickCount() 的時(shí)標(biāo)
unsigned nz_count = 0, nz_sum = 0; ULONG last, next; for (last = GetTickCount(); nz_count < 100; last = next) {next = GetTickCount();if (next != last) {nz_count += 1;nz_sum += (next - last);} } std::cout << "GetTickCount() mean resolution "<< (double)nz_sum / nz_count<< " ticks" << std::endl;我將測(cè)量結(jié)果總結(jié)在了表 3-1 中。
表3-1 :在i7的Surface Pro 3(Windows 8.1)上測(cè)量的時(shí)標(biāo)結(jié)果:
| time() | 1 秒 |
| GetTickCount() | 15.6 毫秒 |
| GetTickCount64() | 15.6 毫秒 |
| timeGetTime() | 15.6 毫秒 |
| clock() | 1.0 毫秒 |
| GetSystemTimeAsFileTime() | 0.9 毫秒 |
| GetSystemTimePreciseAsFileTime() | 約 450 納秒 |
| QueryPerformanceCounter() | 約 450 納秒 |
需要特別注意的是 GetSystemTimeAsfileTime() 函數(shù)。它的顯示分辨率是 100 納秒,但 是看起來(lái)卻似乎是基于同樣低分辨率的 1 毫秒時(shí)標(biāo)的 clock() 實(shí)現(xiàn)的,而 GetSystemTime PreciseAsfileTime() 看起來(lái)則是用 QueryPerformanceCounter() 實(shí)現(xiàn)的。
現(xiàn)代計(jì)算機(jī)的基礎(chǔ)時(shí)鐘周期已經(jīng)短至了數(shù)百皮秒(100 皮秒是 10-10 秒)。它們可以以幾納秒的速度執(zhí)行指令。但是在這些 PC 上卻沒(méi)有提供可訪問(wèn)的皮秒級(jí)或是納秒級(jí)的時(shí)標(biāo)計(jì)數(shù)器。在 PC 上,可使用的最快的時(shí)標(biāo)計(jì)數(shù)器的分辨率是 100 納秒級(jí)的,而且它們的基礎(chǔ)準(zhǔn)確性可能遠(yuǎn)比它們的分辨率更低。這就導(dǎo)致不太可能測(cè)量函數(shù)的一次調(diào)用的持續(xù)時(shí)間。讀者可以參見(jiàn) 3.4.3 節(jié)看看如何應(yīng)對(duì)這個(gè)問(wèn)題。
延遲是指從發(fā)出命令讓活動(dòng)開(kāi)始到它真正開(kāi)始之間的時(shí)間。延遲是從丟下一枚硬幣到井水 中到聽(tīng)見(jiàn)井水濺落之間的時(shí)間(請(qǐng)參見(jiàn)圖 3-6)。它也是發(fā)令員鳴槍至選手出發(fā)之間的時(shí)間。
圖 3-6:延遲:從丟下一枚硬幣到井水中到聽(tīng)見(jiàn)井水濺落之間的時(shí)間
就計(jì)算機(jī)上的時(shí)間測(cè)量而言,之所以會(huì)有延遲是因?yàn)閱?dòng)時(shí)鐘、運(yùn)行實(shí)驗(yàn)和停止時(shí)鐘是一 系列的操作。整個(gè)測(cè)量過(guò)程可以分解為以下五個(gè)階段。
- (1) “啟動(dòng)時(shí)鐘”涉及調(diào)用函數(shù)從操作系統(tǒng)中獲取一個(gè)時(shí)標(biāo)計(jì)數(shù)。這個(gè)調(diào)用的時(shí)間大于 0。 在函數(shù)調(diào)用過(guò)程中,會(huì)實(shí)際地從處理器寄存器中獲取時(shí)標(biāo)計(jì)數(shù)器的值。這個(gè)值就是開(kāi)始 時(shí)間。我們稱其為間隔 t1。
- (2) 在讀取時(shí)標(biāo)計(jì)數(shù)器的值后,它仍然必須被返回和賦值給一個(gè)變量。這些動(dòng)作也需要花費(fèi) 時(shí)間。實(shí)際的時(shí)鐘已經(jīng)在計(jì)時(shí)的過(guò)程中了,但是時(shí)標(biāo)計(jì)數(shù)值還沒(méi)有增加。我們稱其為間 隔 t2。
- (3) 測(cè)量實(shí)驗(yàn)開(kāi)始,然后結(jié)束。我們稱其為間隔 t3。
- (4) “停止時(shí)鐘”涉及另外一個(gè)函數(shù)調(diào)用去獲取一個(gè)時(shí)標(biāo)計(jì)數(shù)值。雖然實(shí)驗(yàn)已經(jīng)結(jié)束了,但 是在函數(shù)運(yùn)行至讀取時(shí)標(biāo)計(jì)數(shù)值的過(guò)程中,計(jì)時(shí)器仍然在計(jì)時(shí)。我們稱其為間隔 t4。
- (5) 讀取時(shí)標(biāo)計(jì)數(shù)器的值后,它仍然必須被返回和賦值給一個(gè)變量。這時(shí),雖然時(shí)鐘仍然在 計(jì)時(shí),但是由于已經(jīng)讀取了時(shí)標(biāo)計(jì)數(shù)器的值,因此測(cè)量結(jié)果并不會(huì)繼續(xù)錯(cuò)誤地累加。我 們稱其為間隔 t5。
因此,雖然實(shí)際上測(cè)量時(shí)間應(yīng)當(dāng)是 t3,但測(cè)量到的值卻更長(zhǎng)一些,是 t2+t3+t4。因此,延遲就 是 t2+t4。如果延遲對(duì)相對(duì)實(shí)驗(yàn)運(yùn)行時(shí)間的比例很大,實(shí)驗(yàn)員必須從實(shí)驗(yàn)結(jié)果中減去延遲。
假設(shè)獲取一次時(shí)標(biāo)計(jì)數(shù)值的時(shí)間是 1 微秒(μs),而且時(shí)標(biāo)計(jì)數(shù)值是由當(dāng)時(shí)執(zhí)行的最后一條 指令獲得的。在以下這段偽代碼中,直到第一次函數(shù)調(diào)用的最后一條指令調(diào)用 get_tick() 才開(kāi)始測(cè)量時(shí)間,因此在測(cè)量活動(dòng)之前是沒(méi)有延遲的。在測(cè)試的最后調(diào)用 get_tick() 的延 遲則被計(jì)算到了測(cè)量結(jié)果中:
start = get_tick() // 測(cè)量開(kāi)始之前的1μs延遲沒(méi)有影響 do_activity() stop = get_tick() // 測(cè)量開(kāi)始之后的1μs延遲被計(jì)算到測(cè)量結(jié)果中 duration = stop-start如果被測(cè)量的活動(dòng)的執(zhí)行時(shí)間是 1 微秒,那么測(cè)量結(jié)果就是 2 微秒,誤差達(dá)將會(huì)到 100%;而如果被測(cè)量的活動(dòng)的執(zhí)行時(shí)間是 1 毫秒,那么測(cè)量結(jié)果就是 1.001 微秒,誤差 只有 0.1%。
如果同一個(gè)函數(shù)既在實(shí)驗(yàn)前被調(diào)用了,也在實(shí)驗(yàn)后被調(diào)用了,那么有 t1=t4 和 t2=t5。也就是 說(shuō),延遲就是計(jì)時(shí)函數(shù)的執(zhí)行時(shí)間。
我在 Windows 上測(cè)試了計(jì)時(shí)函數(shù)的調(diào)用延遲,也就是它們的執(zhí)行時(shí)間。代碼清單 3-2 展示 了一個(gè)典型的用于計(jì)算 GetSystemTimeAsfile() 函數(shù)的時(shí)間的測(cè)試套件。
代碼清單 3-2 Windows 計(jì)時(shí)函數(shù)的延遲
ULONG start = GetTickCount(); LARGE_INTEGER count; for (counter_t i = 0; i < nCalls; ++i)QueryPerformanceCounter(&count); ULONG stop = GetTickCount(); std::cout << stop - start<< "ms for 100m QueryPerformanceCounter() calls"<< std::endl;表3-2:Windows計(jì)時(shí)函數(shù)的延遲(2013,i7,Win 8.1)
| GetSystemTimeAsFileTime() | 2.8 納秒 |
| GetTickCount() | 3.8 納秒 |
| GetTickCount64() | 6.7 納秒 |
| QueryPerformanceCounter() | 8.0 納秒 |
| clock() | 13 納秒 |
| time() | 15 納秒 |
| TimeGetTime() | 17 納秒 |
| GetSystemTimePreciseAsFileTime() | 22 納秒 |
測(cè)試結(jié)果中非常有趣的地方是,在我的 i7 平板電腦上,所有的延遲都在若干納秒的范圍 內(nèi)。所以,這些函數(shù)調(diào)用是相當(dāng)高效的。這就意味著延遲不會(huì)對(duì)在循環(huán)中連續(xù)調(diào)用函數(shù)約 1 秒的測(cè)量結(jié)果的準(zhǔn)確性造成影響。不過(guò),對(duì)于那些讀取相同的低分辨率時(shí)標(biāo)的函數(shù),這 些時(shí)間開(kāi)銷的差距仍然在 10 倍左右。GetSystemTimePreciseAsfileTime() 的延遲最高,而 這個(gè)最高的延遲相對(duì)于它的時(shí)標(biāo)占到了大約 5%。延遲問(wèn)題在慢速處理器上更嚴(yán)重。
計(jì)算機(jī)是帶有大量?jī)?nèi)部狀態(tài)的異常復(fù)雜的裝置,其中絕大多數(shù)狀態(tài)對(duì)開(kāi)發(fā)人員是不可見(jiàn)的。 執(zhí)行函數(shù)會(huì)改變計(jì)算機(jī)的狀態(tài)(例如高速緩存中的內(nèi)容),這樣每次重復(fù)執(zhí)行指令時(shí),情況 都會(huì)與前一條指令不同。因此,內(nèi)部狀態(tài)的不可控的變化是測(cè)量中的一個(gè)隨機(jī)變化源。
而且,操作系統(tǒng)對(duì)任務(wù)的安排也是不可預(yù)測(cè)的,這樣在測(cè)量過(guò)程中,在處理器和內(nèi)存總線上運(yùn)行的其他活動(dòng)會(huì)發(fā)生變化。這會(huì)降低測(cè)量的準(zhǔn)確性。
操作系統(tǒng)甚至可能會(huì)暫停執(zhí)行正在被測(cè)量的代碼,將 CPU 時(shí)間分配給其他程序。但是在暫停過(guò)程中,時(shí)標(biāo)計(jì)數(shù)器仍然在計(jì)時(shí)。這會(huì)導(dǎo)致與操作系統(tǒng)沒(méi)有將 CPU 時(shí)間分配給其他 程序相比,測(cè)量出的執(zhí)行時(shí)間變大了。這是一種會(huì)對(duì)測(cè)量造成更大影響的隨機(jī)變化源。
3.4.3 克服測(cè)量障礙
那么,這到底有多糟糕呢?我們能讓計(jì)算機(jī)完全用于計(jì)時(shí)嗎?我們需要做什么才能實(shí)現(xiàn)計(jì) 算機(jī)計(jì)時(shí)呢?在本節(jié)中,我將對(duì)我個(gè)人使用 3.4.4 節(jié)中的 stopclass 類來(lái)為本書(shū)測(cè)試函數(shù)的 經(jīng)驗(yàn)進(jìn)行總結(jié)。
好消息是測(cè)量誤差只要在幾個(gè)百分點(diǎn)以內(nèi)就足夠指引我們進(jìn)行性能優(yōu)化了。換種方式說(shuō), 如果希望從性能優(yōu)化中獲得線性改善效果,誤差只需要有兩位有效數(shù)字就可以了。即對(duì)于循環(huán)執(zhí)行某個(gè)函數(shù) 1000 毫秒的實(shí)驗(yàn)來(lái)說(shuō),大約 10 毫秒。我們將可能的誤差源整理在了表 3-3 中。
表3-3:各變化源對(duì)在Windows上測(cè)量1秒時(shí)間的影響度
| 時(shí)標(biāo)計(jì)數(shù)器函數(shù)延遲 | < 0.00001 |
| 基本時(shí)鐘穩(wěn)定性 | < 0.01 |
| 時(shí)標(biāo)計(jì)數(shù)器的可用分辨率 | < 0.1 |
優(yōu)化后代碼的運(yùn)行時(shí)間與優(yōu)化前代碼的運(yùn)行時(shí)間的比率被稱為相對(duì)性能。相對(duì)性能有眾多 優(yōu)點(diǎn),其一是它們抵消了系統(tǒng)可變性,因?yàn)閮纱螠y(cè)量受到的可變性影響是一樣的。同時(shí), 相對(duì)性能是一個(gè)百分比,比“多少毫秒”這種測(cè)量結(jié)果更加直觀。
模塊測(cè)試,即使用預(yù)錄入的輸入數(shù)據(jù)進(jìn)行的子系統(tǒng)測(cè)試,可以讓分析運(yùn)行或性能測(cè)量變得 具有可重復(fù)性。許多組織都有自己的模塊測(cè)試擴(kuò)展庫(kù),還可以為性能調(diào)優(yōu)加入新的測(cè)試。 對(duì)于性能調(diào)優(yōu),有一個(gè)常見(jiàn)的擔(dān)憂:“我的代碼就像一個(gè)大的毛線球,而且我沒(méi)有為其編 寫(xiě)任何測(cè)試用例。我必須在最新的輸入數(shù)據(jù)或最新的數(shù)據(jù)庫(kù)上測(cè)試它的性能,但這些數(shù)據(jù) 經(jīng)常會(huì)發(fā)生變化。我得不到一致的或者可重復(fù)的測(cè)量結(jié)果。我應(yīng)當(dāng)怎么辦呢?” 我沒(méi)有任何辦法來(lái)解決這種問(wèn)題。如果我用一組可重復(fù)的 Mock 輸入數(shù)據(jù)來(lái)測(cè)試模塊或是 子系統(tǒng),那么在這些測(cè)試中反映出來(lái)的性能改善效果通常適用于最新的測(cè)試數(shù)據(jù)。如果我 通過(guò)一次大型但不可重復(fù)的測(cè)試找到了熱點(diǎn)函數(shù),那么通過(guò)模塊測(cè)試用例去改善這些熱點(diǎn) 函數(shù),所得到的性能提升效果通常也適用于最新的測(cè)試數(shù)據(jù)。每位開(kāi)發(fā)人員都知道為什么 他們應(yīng)當(dāng)構(gòu)建由松耦合的模塊組合而成的軟件系統(tǒng);每位開(kāi)發(fā)人員都知道為什么他們應(yīng)當(dāng) 維護(hù)優(yōu)秀的測(cè)試用例庫(kù)。優(yōu)化只是應(yīng)當(dāng)這樣做的又一個(gè)理由。
開(kāi)發(fā)人員仍然有一線希望可以基于不可預(yù)測(cè)的最新數(shù)據(jù)優(yōu)化性能。這種方式就是不測(cè)量臨 界響應(yīng)時(shí)間等值,而是收集指標(biāo)、代碼統(tǒng)計(jì)數(shù)據(jù)(例如中間值和方差),或是響應(yīng)時(shí)間的 指數(shù)平滑平均數(shù)。由于這些統(tǒng)計(jì)數(shù)字是從大量的獨(dú)立事件中得到的,因此這些數(shù)字的持續(xù) 改善表明對(duì)代碼的修改是成功的。
以下是在根據(jù)指標(biāo)優(yōu)化性能時(shí)可能遇到的一些問(wèn)題。
- 代碼統(tǒng)計(jì)必須基于大量事件才有效。當(dāng)執(zhí)行“修改 / 測(cè)試 / 評(píng)估”這樣的循環(huán)改善過(guò)程時(shí), 與使用固定的輸入數(shù)據(jù)進(jìn)行直接測(cè)量相比,根據(jù)指標(biāo)優(yōu)化性能更加耗費(fèi)時(shí)間。
- 相比于分析代碼和測(cè)量運(yùn)行時(shí)間,收集指標(biāo)需要更完善的基礎(chǔ)設(shè)施。通常都需要持久化 的存儲(chǔ)設(shè)備來(lái)存放統(tǒng)計(jì)數(shù)據(jù)。而存儲(chǔ)這些數(shù)據(jù)的時(shí)間開(kāi)銷非常大,會(huì)對(duì)性能產(chǎn)生影響。 收集指標(biāo)的系統(tǒng)必須設(shè)計(jì)得足夠靈活,可以支持多種實(shí)驗(yàn)。
- 盡管有行之有效的方法去驗(yàn)證或是推翻基于統(tǒng)計(jì)的假設(shè),但是這種方法需要開(kāi)發(fā)人員能 夠妥當(dāng)?shù)貞?yīng)對(duì)一些統(tǒng)計(jì)的復(fù)雜性。
在實(shí)驗(yàn)中通過(guò)取多次測(cè)量的平均值可以提高單次測(cè)量的準(zhǔn)確性。當(dāng)開(kāi)發(fā)人員在循環(huán)中測(cè)量函數(shù) 調(diào)用的時(shí)間,或是讓程序處理那些會(huì)讓它多次執(zhí)行某個(gè)函數(shù)的輸入數(shù)據(jù)時(shí),就是在取平均值。
對(duì)一個(gè)函數(shù)調(diào)用進(jìn)行多次迭代測(cè)量的一個(gè)優(yōu)點(diǎn)是可以抵消隨機(jī)變化性。這種情況下,高速 緩存的狀態(tài)幾乎會(huì)聚集于一個(gè)值,讓我們可以在每次迭代測(cè)量的結(jié)果之間進(jìn)行公平合理的 比較。經(jīng)過(guò)一段足夠長(zhǎng)的時(shí)間間隔后,隨機(jī)調(diào)度程序的行為對(duì)原函數(shù)和優(yōu)化后函數(shù)的影響 是一樣的。盡管同樣的函數(shù)在另一個(gè)更大型程序中的絕對(duì)時(shí)間是不一樣的,但是通過(guò)測(cè)量 相對(duì)性能仍然能夠準(zhǔn)確地反映出性能改善的程度。 另外一個(gè)優(yōu)點(diǎn)是可以使用現(xiàn)成的但不精確的時(shí)標(biāo)計(jì)數(shù)器。現(xiàn)在,計(jì)算機(jī)的處理速度已經(jīng)足 夠在 1 秒內(nèi)處理數(shù)千次甚至數(shù)百萬(wàn)次迭代了。
通過(guò)提高測(cè)量進(jìn)程的優(yōu)先級(jí),可以減小操作系統(tǒng)使用 CPU 時(shí)間片段去執(zhí)行測(cè)量程序以外 的處理的幾率。在 Windows 上,可以通過(guò)調(diào)用 SetPriorityClass() 函數(shù)來(lái)設(shè)置進(jìn)程的優(yōu)先 級(jí),而 SetThreadPriority() 函數(shù)則可以用來(lái)設(shè)置線程的優(yōu)先級(jí)。下面這段代碼提高了當(dāng) 前進(jìn)程和線程的優(yōu)先級(jí):
SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);在測(cè)量結(jié)束后,通常應(yīng)當(dāng)將進(jìn)程和線程恢復(fù)至正常優(yōu)先級(jí):
SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL);我為性能優(yōu)化而測(cè)量性能的方式是極度非正式的。其中沒(méi)有深?yuàn)W的統(tǒng)計(jì)學(xué)知識(shí)。我的測(cè)試 只運(yùn)行幾秒鐘,而不是幾小時(shí)。但我認(rèn)為并不需要為這種非正式的方式感到愧疚。這些方 法可以將測(cè)量結(jié)果轉(zhuǎn)換為開(kāi)發(fā)人員可以理解的相對(duì)于程序整體運(yùn)行時(shí)間的性能改善結(jié)果, 因此,我知道我一定是在正確的優(yōu)化道路上前進(jìn)。
如果我以兩種不同的方式運(yùn)行相同的測(cè)量實(shí)驗(yàn),得到的結(jié)果的差異可能在 0.1% 至 1% 之 間。這毫無(wú)疑問(wèn)與我的 PC 的初始狀態(tài)不同有關(guān)。我沒(méi)有辦法控制這些狀態(tài),因此我并不擔(dān) 心。如果差異比較大,我就會(huì)讓測(cè)試程序運(yùn)行得時(shí)間更長(zhǎng)一些。由于這也會(huì)讓我的測(cè)試 / 調(diào) 試周期變長(zhǎng),所以除非萬(wàn)不得已,否則我不會(huì)這么做。
即使我發(fā)現(xiàn)兩次測(cè)量結(jié)果之間的差異達(dá)到了幾個(gè)百分點(diǎn),在單次測(cè)量中測(cè)量結(jié)果的相對(duì)變 化看起來(lái)仍然小于 1%。也就是說(shuō),通過(guò)在相同的測(cè)試中測(cè)量一個(gè)函數(shù)的兩種變化,我甚至 能看出發(fā)生了相當(dāng)微妙的變化。
我會(huì)盡量在一臺(tái)沒(méi)有播放視頻、升級(jí) Java 或是壓縮大文件的“安靜”的計(jì)算機(jī)上測(cè)量時(shí) 間。在測(cè)量過(guò)程中,我也會(huì)盡量不移動(dòng)鼠標(biāo)或是切換窗口。特別是當(dāng) PC 中只有一個(gè)處理 器時(shí),這一點(diǎn)非常重要。但是當(dāng)使用現(xiàn)代多核處理器時(shí),我發(fā)現(xiàn)即使我忘記了上面這些注 意事項(xiàng),測(cè)量結(jié)果也不會(huì)有什么大的變化。
如果在測(cè)量時(shí)間時(shí)調(diào)用了某個(gè)函數(shù) 10 000 次,這段代碼和相關(guān)的數(shù)據(jù)會(huì)被存儲(chǔ)在高速緩存 中。當(dāng)為一個(gè)實(shí)時(shí)系統(tǒng)測(cè)量最差情況下的絕對(duì)時(shí)間時(shí),這會(huì)有影響。但是現(xiàn)在我是在一個(gè) 內(nèi)核本身就充滿了非確定性的系統(tǒng)上測(cè)量相對(duì)時(shí)間。而且,我所測(cè)試的函數(shù)是我的分析器 指出的熱點(diǎn)函數(shù)。因此,即使是當(dāng)正式版本在運(yùn)行時(shí),它們也會(huì)被緩存于高速緩存中。這樣,迭代測(cè)試就確確實(shí)實(shí)地重現(xiàn)了真實(shí)運(yùn)行狀態(tài)。
如果修改后的函數(shù)看起來(lái)快了 1%,那么通常不值得進(jìn)行修改。根據(jù)阿姆達(dá)爾定律,該函數(shù)優(yōu)化結(jié)果對(duì)整個(gè)程序的運(yùn)行時(shí)間的貢獻(xiàn)會(huì)變得微不足道。速度提高 10% 是臨界值,而速 度提高 100% 則對(duì)縮短整個(gè)程序的運(yùn)行時(shí)間有非常大的幫助。只進(jìn)行有明顯效果的性能改 善可以將開(kāi)發(fā)人員從對(duì)方法論的擔(dān)憂中解放出來(lái)。
3.4.4 創(chuàng)建stopwatch類
我會(huì)用一個(gè) stopwatch 類來(lái)測(cè)量程序中部分代碼的執(zhí)行時(shí)間并分析代碼。這個(gè)類的工作 方式非常像一塊機(jī)械秒表。初始化秒表或是調(diào)用它的 start() 成員函數(shù)后,秒表將開(kāi)始 計(jì)時(shí);調(diào)用秒表的 stop() 成員函數(shù)或是銷毀秒表類的實(shí)例后,秒表將停止計(jì)時(shí)并顯示出計(jì)時(shí)結(jié)果。
編寫(xiě)一個(gè) stopwatch 類并不難,網(wǎng)上也有很多現(xiàn)成的代碼。代碼清單 3-3 展示了我所使用 的 stopwatch 類。
代碼清單 3-3 stopwatch 類
template <typename T> class basic_stopwatch : T {typedef typename T BaseTimer; public:// 創(chuàng)建一個(gè)秒表,開(kāi)始計(jì)時(shí)一項(xiàng)程序活動(dòng)(可選)explicit basic_stopwatch(bool start);explicit basic_stopwatch(char const* activity = "Stopwatch",bool start=true);basic_stopwatch(std::ostream& log,char const* activity="Stopwatch",bool start=true);// 停止并銷毀秒表~basic_stopwatch();// 得到上一次計(jì)時(shí)時(shí)間(上一次停止時(shí)的時(shí)間)unsigned LapGet() const;// 判斷:如果秒表正在運(yùn)行,則返回truebool IsStarted() const;// 顯示累計(jì)時(shí)間,一直運(yùn)行,設(shè)置/返回上次計(jì)時(shí)時(shí)間unsigned Show(char const* event="show");// 啟動(dòng)(重啟)秒表,設(shè)置/返回上次計(jì)時(shí)時(shí)間unsigned Start(char const* event_namee="start");// 停止正在計(jì)時(shí)的秒表,設(shè)置/返回上次計(jì)時(shí)時(shí)間unsigned Stop(char const* event_name="stop"); private: // 成員變量char const* m_activity; // "activity"字符串unsigned m_lap; // 上次計(jì)時(shí)時(shí)間(上一次停止時(shí)的時(shí)間)std::ostream& m_log; // 用于記錄事件的流 };這段代碼只是重新定義了類。為了使性能最優(yōu)化,成員函數(shù)將會(huì)被內(nèi)聯(lián)展開(kāi)。
stopwatch 的類型模板參數(shù) T 的值的類是一個(gè)更加簡(jiǎn)單的計(jì)時(shí)器,它提供了依賴于操作系 統(tǒng)和 C++ 標(biāo)準(zhǔn)的函數(shù)去訪問(wèn)時(shí)標(biāo)計(jì)數(shù)器。我編寫(xiě)過(guò)多個(gè)版本的 TimerBase 類,去測(cè)試各 種不同的時(shí)標(biāo)計(jì)數(shù)器的實(shí)現(xiàn)方式。在有些現(xiàn)代 C++ 處理器上,T 的值的類可以使用 C++ 庫(kù),或是可以直接從操作系統(tǒng)中得到時(shí)標(biāo)。 代碼清單 3-4 展示的 TimerBase 類使 用了在 C++11 及之后的版本中提供的 C++ 庫(kù)。
代碼清單 3-4 使用了 的 TimerBase 類
# include <chrono> using namespace std::chrono; class TimerBase { public:// 清除計(jì)時(shí)器TimerBase() : m_start(system_clock::time_point::min()) { }// 清除計(jì)時(shí)器void Clear() {m_start = system_clock::time_point::min();}// 如果計(jì)時(shí)器正在計(jì)時(shí),則返回truebool IsStarted() const {return (m_start.time_since_epoch() != system_clock::duration(0));}// 啟動(dòng)計(jì)時(shí)器void Start() {m_start = system_clock::now();}// 得到自計(jì)時(shí)開(kāi)始后的毫秒值unsigned long GetMs() {if (IsStarted()) {system_clock::duration diff;diff = system_clock::now() - m_start;return (unsigned)(duration_cast<milliseconds>(diff).count());}return 0;}private:system_clock::time_point m_start; };這種實(shí)現(xiàn)方式的優(yōu)點(diǎn)是在不同操作系統(tǒng)之間具有可移植性,但是它需要用到 C++11。
代碼清單 3-5 中的 TimerBase 類與這個(gè)類的功能相同,不過(guò)其中使用的是在 Windows 上和 Linux 上都可以使用的 clock() 函數(shù)。
代碼清單 3-5 使用了 clock() 的 TimerBase 類
class TimerBaseClock { public: 46 | 第 3 章// 清除計(jì)時(shí)器TimerBaseClock() { m_start = -1; }// 清除計(jì)時(shí)器void Clear() { m_start = -1; }// 如果計(jì)時(shí)器正在計(jì)時(shí),則返回truebool IsStarted() const { return (m_start != -1); }// 啟動(dòng)計(jì)時(shí)器void Start() { m_start = clock(); }// 得到自計(jì)時(shí)開(kāi)始后的毫秒值unsigned long GetMs() {clock_t now;if (IsStarted()) {now = clock();clock_t dt = (now - m_start);return (unsigned long)(dt * 1000 / CLOCKS_PER_SEC);}return 0;} private:clock_t m_start; };這種實(shí)現(xiàn)方式的優(yōu)點(diǎn)是在不同 C++ 版本和不同操作系統(tǒng)之間具有可移植性,缺點(diǎn)是在 Linux 上和 Windows 上,clock() 函數(shù)的測(cè)量結(jié)果略有不同。
代碼清單 3-6 中的 TimerBase 類可以工作于舊版本的 Windows 上和 Linux 上。如果是在 Windows 上,那么還必須顯式地提供 gettimeofday() 函數(shù),因?yàn)樗炔粚儆?Windows API,也不屬于 C 標(biāo)準(zhǔn)庫(kù)。
代碼清單 3-6 使用了 gettimeofday() 的 TimerBase
# include <chrono> using namespace std::chrono; class TimerBaseChrono { public:// 清除計(jì)時(shí)器TimerBaseChrono() :m_start(system_clock::time_point::min()) {}// 清除計(jì)時(shí)器void Clear() {m_start = system_clock::time_point::min();}// 如果計(jì)時(shí)器正在計(jì)時(shí),則返回truebool IsStarted() const {return (m_start != system_clock::time_point::min());}// 啟動(dòng)計(jì)時(shí)器void Start() {m_start = std::chrono::system_clock::now();}// 得到自計(jì)時(shí)開(kāi)始后的毫秒值unsigned long GetMs() {if (IsStarted()) {system_clock::duration diff;diff = system_clock::now() - m_start;return (unsigned)(duration_cast<milliseconds>(diff).count());}return 0;} private:std::chrono::system_clock::time_point m_start; }; 這種實(shí)現(xiàn)方式在不同的 C++ 版本和不同操作系統(tǒng)之間具有可移植性。但當(dāng)運(yùn)行于 Windows 上時(shí),需要實(shí)現(xiàn) `gettimeofday()` 函數(shù)。stopwatch 類的最簡(jiǎn)單的用法用到了 RAII(Resource Acquisition Is Initialization,資源獲取 就是初始化 5 )慣用法。程序會(huì)在由大括號(hào)包圍的語(yǔ)句塊中的開(kāi)頭處初始化 stopwatch 類, stopwatch 類的默認(rèn)操作是開(kāi)始計(jì)時(shí)。當(dāng) stopwatch 在語(yǔ)句塊結(jié)束前被析構(gòu)時(shí),它會(huì)輸出最 終計(jì)時(shí)結(jié)果。程序可以在執(zhí)行過(guò)程中通過(guò)調(diào)用 stopwatch 類的 show() 成員函數(shù)輸出中間計(jì) 時(shí)結(jié)果。這樣,開(kāi)發(fā)人員就可以只使用一個(gè)計(jì)時(shí)器來(lái)分析幾個(gè)互相聯(lián)系的代碼塊。例如:
{Stopwatch sw("activity");DoActivity(); }這段代碼將會(huì)在標(biāo)準(zhǔn)輸出中打印出以下結(jié)果 :
activity: start activity: stop 1234mSstopwatch 在運(yùn)行時(shí)不會(huì)產(chǎn)生間接開(kāi)銷。開(kāi)始計(jì)時(shí)和停止計(jì)時(shí)的延遲包括了獲取當(dāng)前時(shí)間 的系統(tǒng)調(diào)用的開(kāi)銷,如果調(diào)用了 show() 成員函數(shù)輸出計(jì)時(shí)結(jié)果,那么還要加上產(chǎn)生輸出的 開(kāi)銷。如果是測(cè)試那些需要花費(fèi)數(shù)十毫秒或者更長(zhǎng)時(shí)間的任務(wù),那么這個(gè)延遲可以忽略。 但是如果開(kāi)發(fā)人員試圖測(cè)試微秒級(jí)別的活動(dòng)的時(shí)間,那么間接開(kāi)銷的比重將會(huì)顯著增大, 測(cè)量結(jié)果的準(zhǔn)確度也因此會(huì)降低。
測(cè)量運(yùn)行時(shí)間的最大缺點(diǎn)可能是需要直覺(jué)和經(jīng)驗(yàn)去解釋這些結(jié)果。在通過(guò)多次測(cè)量縮小了 查找熱點(diǎn)代碼的范圍后,開(kāi)發(fā)人員必須接著檢查代碼或者進(jìn)行實(shí)驗(yàn)找出和移除熱點(diǎn)代碼。 檢查代碼時(shí)需要依靠開(kāi)發(fā)人員自己的經(jīng)驗(yàn)或是本書(shū)中概述的啟發(fā)式規(guī)則。這些規(guī)則的優(yōu)點(diǎn) 是可以幫助你找出那些長(zhǎng)時(shí)間運(yùn)行的代碼,缺點(diǎn)則是無(wú)法明確地指出最熱點(diǎn)的代碼。
3.4.5 使用測(cè)試套件測(cè)量熱點(diǎn)函數(shù)
一旦通過(guò)分析器或是運(yùn)行時(shí)分析找出了一個(gè)候選的待優(yōu)化函數(shù),一種簡(jiǎn)單的改善它的方法 是構(gòu)建一個(gè)測(cè)試套件,在其中多次調(diào)用該函數(shù)。這樣可以將該函數(shù)的運(yùn)行時(shí)間增大為一 個(gè)可測(cè)量的值,同時(shí)還可以抵消因后臺(tái)任務(wù)、上下文切換等對(duì)運(yùn)行時(shí)間造成的影響。采 用“修改 - 編譯 - 運(yùn)行”的迭代方式去獨(dú)立地測(cè)量一個(gè)函數(shù),會(huì)比采用“修改 - 編譯 - 運(yùn) 行”,然后運(yùn)行分析器并解析它的輸出更快。本書(shū)中的許多例子都會(huì)使用這種技巧。 這個(gè)計(jì)時(shí)測(cè)試套件(代碼清單 3-7)只是先調(diào)用了 stopwatch,然后循環(huán)中調(diào)用了 10000 次 需要被測(cè)量的函數(shù)。
代碼清單 3-7 計(jì)時(shí)測(cè)試套件
typedef unsigned counter_t; counter_t const iterations = 10000;... {Stopwatch sw("function_to_be_timed()");for (counter_t i = 0; i < iterations; ++i) {result = function_to_be_timed();} }迭代次數(shù)需要憑經(jīng)驗(yàn)估計(jì)。如果 stopwatch 使用的時(shí)標(biāo)計(jì)數(shù)器的有效分辨率是大約 10 毫 秒,那么測(cè)試套件在桌面處理器上的運(yùn)行時(shí)間應(yīng)當(dāng)在幾百到幾千毫秒。
這里,我使用了 counter_t 來(lái)替代 unsigned 或 unsigned long,這是因?yàn)閷?duì)于一些非常短 小的函數(shù),該變量的類型可能需要是 64 位 unsigned long long。相比于回過(guò)頭來(lái)重新修改 所有類型名稱,我更習(xí)慣于使用 typedef。這是對(duì)優(yōu)化過(guò)程自身的一種優(yōu)化。
最外層的一組大括號(hào)非常重要,因?yàn)樗x了 sw(也就是 Stopwatch 類的實(shí)例)的存在范 圍。由于 stopwatch 使用了 RAII 慣用法,sw 的構(gòu)造函數(shù)會(huì)得到第一次時(shí)標(biāo)計(jì)數(shù)值,而它 的析構(gòu)函數(shù)則會(huì)得到最后一次時(shí)標(biāo)計(jì)數(shù)值并將結(jié)果放入到標(biāo)準(zhǔn)輸出流中。
3.5 評(píng)估代碼開(kāi)銷來(lái)找出熱點(diǎn)代碼
經(jīng)驗(yàn)告訴我分析代碼和測(cè)量運(yùn)行時(shí)間是幫助找出需要優(yōu)化的代碼的兩種有效方法。分析器 會(huì)指出某個(gè)函數(shù)被頻繁地調(diào)用了或是在程序總運(yùn)行時(shí)間中所占的比率很大。但它不太可能 指出某個(gè)具體的 C++ 語(yǔ)句可以優(yōu)化。分析代碼的成本也可能是非常高的。測(cè)量時(shí)間也可能會(huì)表明一大段代碼很 慢,但不會(huì)指出其中存在的具體問(wèn)題。
開(kāi)發(fā)人員下一步需要做的是,對(duì)指出的代碼塊中的每條語(yǔ)句的開(kāi)銷進(jìn)行評(píng)估。這一步就像 是證明一條定理一樣,并不需要太精確。大多數(shù)情況下,只需大致觀察一下這些語(yǔ)句就能得到它們的開(kāi)銷,然后從中找出性能開(kāi)銷大的語(yǔ)句和語(yǔ)法結(jié)構(gòu)。
3.5.1 評(píng)估獨(dú)立的C++語(yǔ)句的開(kāi)銷
正如 2.2.1 節(jié)中所講述的,訪問(wèn)內(nèi)存的時(shí)間開(kāi)銷遠(yuǎn)比執(zhí)行其他指令的開(kāi)銷大。在烤箱和咖 啡機(jī)所使用的簡(jiǎn)單微處理器中,執(zhí)行一條指令所花費(fèi)的時(shí)間大致包含從內(nèi)存中讀取指令的 每個(gè)字節(jié)所需要的時(shí)間,加上讀取指令的輸入數(shù)據(jù)所需的時(shí)間,再加上寫(xiě)指令結(jié)果的時(shí) 間。相比之下,隱藏于內(nèi)存訪問(wèn)時(shí)間之下的解碼和執(zhí)行指令的時(shí)間就顯得微不足道了。
在桌面級(jí)微處理器上,情況就更加復(fù)雜了。許多處于不同階段的指令會(huì)被同時(shí)執(zhí)行。讀取 指令流的開(kāi)銷可以忽略。不過(guò),訪問(wèn)指令所操作的數(shù)據(jù)的開(kāi)銷則無(wú)法忽略。正是由于這個(gè) 原因,讀寫(xiě)數(shù)據(jù)的開(kāi)銷可以近似地看作所有級(jí)別的微處理器上的執(zhí)行指令的相對(duì)開(kāi)銷。
有一條有效的規(guī)則能夠幫助我們?cè)u(píng)估一條 C++ 語(yǔ)句的開(kāi)銷有多大,那就是計(jì)算該語(yǔ)句對(duì) 內(nèi)存的讀寫(xiě)次數(shù)。例如,有一條語(yǔ)句 a = b + c;,其中 a、b 和 c 都是整數(shù),b 和 c 的值 必須從內(nèi)存中讀取,而且它們的和必須寫(xiě)入至內(nèi)存中的位置 a。因此,這條語(yǔ)句的開(kāi)銷是 三次內(nèi)存訪問(wèn)。這個(gè)次數(shù)不依賴于微處理器的指令集。這是語(yǔ)句不可避免的、必然會(huì)發(fā)生 的開(kāi)銷。
再比如,r = *p + a[i]; 這條語(yǔ)句訪問(wèn)內(nèi)存的次數(shù)如下:一次訪問(wèn)用于讀取 i,一次讀取 a[i],一次讀取 p,一次讀取 *p 所指向的數(shù)據(jù),一次將結(jié)果寫(xiě)入至 r。也就是說(shuō) , 總共進(jìn) 行了 5 次訪問(wèn)。7.2.1 節(jié)中講解了函數(shù)調(diào)用訪問(wèn)內(nèi)存的開(kāi)銷。
理解這是一條啟發(fā)式規(guī)則是非常重要的。在實(shí)際的硬件中,獲取執(zhí)行語(yǔ)句的指令會(huì)發(fā)生額 外的內(nèi)存訪問(wèn)。不過(guò),由于這些訪問(wèn)是順序的,所以它們可能非常高效。而且這些額外的 開(kāi)銷與訪問(wèn)數(shù)據(jù)的開(kāi)銷是成比例的。編譯器可能會(huì)在優(yōu)化時(shí)通過(guò)復(fù)用之前的計(jì)算或是發(fā)揮 代碼靜態(tài)分析的優(yōu)勢(shì)來(lái)省略一些內(nèi)存訪問(wèn)。單位時(shí)間內(nèi)的開(kāi)銷也取決于 C++ 語(yǔ)句要訪問(wèn)的 內(nèi)容是否在高速緩存中。
但是其他因素是等價(jià)的,有影響的是訪問(wèn)語(yǔ)句要用到的數(shù)據(jù)需要多少次讀寫(xiě)內(nèi)存。這條啟 發(fā)式規(guī)則并不完美,但這是所有你能做的了,除非你想去查看編譯器輸出的冗長(zhǎng)無(wú)味、收 效甚微的匯編代碼。
3.5.2 評(píng)估循環(huán)的開(kāi)銷
由于每條 C++ 語(yǔ)句都只會(huì)進(jìn)行幾次內(nèi)存訪問(wèn),通常情況下熱點(diǎn)代碼都不會(huì)是一條單獨(dú)的 語(yǔ)句,除非受其他因素的作用,讓其頻繁地執(zhí)行。這些因素之一就是該語(yǔ)句出現(xiàn)在了循環(huán) 中。這樣,合計(jì)開(kāi)銷就是該語(yǔ)句的開(kāi)銷乘以該語(yǔ)句被執(zhí)行的次數(shù)了。
如果你很幸運(yùn),可能會(huì)偶然找到這樣的代碼。分析器可能會(huì)指出一條單獨(dú)的語(yǔ)句被執(zhí)行了 100 萬(wàn)次,或者其他的熱點(diǎn)函數(shù)包含以下這樣的循環(huán):
for (int i=1; i<1000000; ++i) {do_something_expensive();if (mostly_true) {do_more_stuff();even_more();} }這個(gè)循環(huán)中的語(yǔ)句很明顯會(huì)被執(zhí)行 100 萬(wàn)次,因此它是熱點(diǎn)語(yǔ)句??雌饋?lái)你需要花點(diǎn)精力 去優(yōu)化。
當(dāng)一個(gè)循環(huán)被嵌套在另一個(gè)循環(huán)里面的時(shí)候,代碼塊的循環(huán)次數(shù)是內(nèi)層循環(huán)的次數(shù)乘以外 層循環(huán)的次數(shù)。例如:
for (int i=0; i<100; ++i) {for (int j=0; j<50; ++j) {fiddle(a[i][j]);} }在這里,代碼塊的循環(huán)次數(shù)是 100*50=5000。
上面的代碼塊非常直接。但是實(shí)際上這里可能有無(wú)數(shù)種變化。例如,當(dāng)進(jìn)行數(shù)學(xué)運(yùn)算時(shí), 在有些重要的情況下會(huì)對(duì)三角矩陣進(jìn)行循環(huán)計(jì)算。而且有時(shí)候,代碼編寫(xiě)得非常糟糕,需要花費(fèi)很大氣力才能看清嵌套循環(huán)的輪廓。
嵌套循環(huán)可能并非一眼就能看出來(lái)。如果一個(gè)循環(huán)調(diào)用了一個(gè)函數(shù),而這個(gè)函數(shù)中又包含 了另外一個(gè)循環(huán),那么內(nèi)層循環(huán)就是嵌套循環(huán)。正如我們稍后會(huì)在 7.1.8 節(jié)中看到的,有 時(shí)在外層循環(huán)中重復(fù)地調(diào)用函數(shù)的開(kāi)銷也是可以消除的。
內(nèi)存循環(huán)可能被嵌入在標(biāo)準(zhǔn)庫(kù)函數(shù)中,特別是處理字符串或字符的 I/O 函數(shù)。如果這些 函數(shù)被重復(fù)調(diào)用的次數(shù)非常多,那么可能值得去重新實(shí)現(xiàn)標(biāo)準(zhǔn)函數(shù)庫(kù)中的函數(shù)來(lái)回避調(diào) 用開(kāi)銷。
不是所有循環(huán)中的循環(huán)次數(shù)都是很明確的。許多循環(huán)處理會(huì)不斷重復(fù)直至滿足某個(gè)條件為 止,比如有些循環(huán)會(huì)重復(fù)地處理字符,直至找到空格為止;還有些循環(huán)則會(huì)重復(fù)地處理數(shù) 字,直到遇到非數(shù)字為止。這種循環(huán)的重復(fù)次數(shù)也是可以估算出來(lái)的。當(dāng)然,只需要大致 地估算一下即可,例如每個(gè)數(shù)字的平均位數(shù)是 5,或是每個(gè)單詞的平均字母數(shù)是 6。估算 的目的是找出可能需要優(yōu)化的代碼。
響應(yīng)事件的程序(例如 Windows UI 程序)在最外層都會(huì)有一個(gè)隱式循環(huán)。這個(gè)循環(huán)甚至 在程序中是看不到的,因?yàn)樗浑[藏在了框架中。如果這個(gè)框架以最大速率接收事件的 話,那么每當(dāng)事件處理器取得程序控制權(quán),或是在事件分發(fā)前,抑或是在事件分發(fā)過(guò)程中都會(huì)被執(zhí)行的代碼,以及最頻繁地被分發(fā)的事件中的代碼都可能是熱點(diǎn)代碼。
不是所有的 while 或者 do 語(yǔ)句都是循環(huán)語(yǔ)句。我就曾經(jīng)遇到過(guò)使用 do 語(yǔ)句幫助控制流程 的代碼。下面這段示例代碼還有更好的實(shí)現(xiàn)方式,不過(guò)使用了更復(fù)雜的 if-then-else 邏 輯的話,這種慣用法就有其用武之地了。下面這個(gè)“循環(huán)”只會(huì)被執(zhí)行一次。當(dāng)它遇到 while(0) 后就會(huì)退出:
do {if (!operation1())break;if (!operation2(x,y,z))break; } while(0);這種慣用法也時(shí)常被用于將幾條語(yǔ)句“打包”為 C 風(fēng)格的宏。
3.6 其他找出熱點(diǎn)代碼的方法
如果開(kāi)發(fā)人員熟悉需要優(yōu)化的代碼,可以選擇僅憑直覺(jué)去推測(cè)影響程序整體運(yùn)行時(shí)間的代 碼塊在哪里,然后做實(shí)驗(yàn)去驗(yàn)證對(duì)這些代碼的修改是否可以提高程序整體性能。 我不建議選擇這種方法,除非整個(gè)項(xiàng)目只有你一個(gè)人。通過(guò)使用分析器或是計(jì)時(shí)器分析代 碼,開(kāi)發(fā)人員可以向同事和經(jīng)理展示他們?cè)谛阅軆?yōu)化工作中取得的進(jìn)展。如果你僅憑直覺(jué) 進(jìn)行優(yōu)化,也不發(fā)表結(jié)果,有時(shí)甚至即使你發(fā)表了結(jié)果,團(tuán)隊(duì)成員也會(huì)質(zhì)疑你的方法,使 你無(wú)法專心于你的工作。他們也應(yīng)該如此。這是因?yàn)樗麄兎植磺迥愕降资窃谑怯媚愀叨葘?業(yè)的直覺(jué)進(jìn)行優(yōu)化還是只是在碰運(yùn)氣。
總結(jié)
以上是生活随笔為你收集整理的C++ 性能优化篇三《测量性能》的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: jquery ajax 异步分页,jqu
- 下一篇: linux 关联数组,linux 普通数