1. 程式人生 > >一段阿里筆試程式碼的(瞎)分析

一段阿里筆試程式碼的(瞎)分析

本文地址

前言

我是菜雞,如有不對的地方煩請指正。

起始

上週(好像是上週)的時候作業系統的老師丟擲了一個問題留給我們:

對以下程式在一臺主流配置 的PC上,呼叫f(36)所需要 的時間大概是多少?請給出時間估算的依據並對程式的執行情況進行詳細的解析說明。

int f(int x) {
	int s = 0;
	while (x++ > 0) s += f(x);
	return std::max(s, 1);
}

第一反應

顯然在棧資源無窮且使用超算的情況下,這個遞迴的壽命會比銀河系的壽命(140億年,我查的)還要長,因為總規模為2231362^{2^{31}-36}

36。 但題目要求考慮實際情況,所以考慮一些別的東西。

以下所有(瞎)分析建立在忽略編譯優化及O2優化的情形下。

硬體資源

拋開四萬塊起步的iMac Pro128GB記憶體和18核超執行緒Xeon),現在主流PC配置普遍是16GB記憶體和6核12執行緒i7 8700k(暫不考慮剛釋出的9700k,因為還沒有流片)或8核16執行緒Ryzen 7 2700k

但是!其實用什麼CPU都無所謂,因為單純地執行這個程式實際取決於記憶體和硬碟的一些引數和CPU的單核運算效能,而且由於作業系統的存在,只要不是上個世紀的電腦,實際執行中都不會對這個程式的執行時間產生多大的限制。

一般來說,在演算法競賽中普遍認為CPU的時間複雜度上界為1

09times/sec10^9\ times/sec,實際上要低於這個,大概5×108×log2(5×108)5\times10^8\times \log_2(5\times10^8)是極限了(我在CodeforcesNowCoder測試了5×1085\times10^8的快排以得到這個資料)。

程式碼(瞎)分析

對於每一次呼叫,它都會重複呼叫自己2311x+1=231x2^{31} - 1 - x + 1 = 2^{31} - x(次),試圖畫一下遞迴樹:

1.jpg

其中:每個節點上的數字x代表呼叫一次f(x),可見,從樹根開始,第一層有1

1個節點,第二層有23136=21474836122^{31} - 36 = 2147483612個節點,第三層有i=12147483611i\sum_{i=1}^{2147483611}i個節點,其真值為2147483612×214748361122.30×1018\frac{2147483612 \times 2147483611}{2}\approx2.30\times 10^{18},第四層的節點總數為i=12147483611j=1ij=i=12147483611(i+1)×i2\sum_{i=1}^{2147483611}\sum_{j=1}^{i}j = \sum_{i=1}^{2147483611}\frac{(i+1)\times i}{2},這個式子我不太會估值,隊友說很easy(Orz)。

但是一層一層的算節點個數並不符合實際情形,雖然這樣能比較容易地計算樹的實際規模,但是這不滿足深搜樹的實際增廣時的行為。

對於深搜樹來說,一條樹鏈的最大長度LL等於樹的高度,同時也是深度depthdepth,也就是說在記憶體中最多隻能同時存在21474836112147483611個這樣的遞迴子程式,我們來粗略計算一下它需要的記憶體資源。

取變數ssxx的記憶體佔用(8位元組)作為單個子程式的記憶體佔用,那麼實際佔用的資源約為2147483611×8÷1024÷1024÷1024=16.00745030492544(GB)16(GB)2147483611 \times 8 \div 1024 \div 1024 \div 1024 = 16.00745030492544(GB) \approx 16(GB),也就是說需要16GB的棧大小。

但是!Linux下的棧大小大約為8MB(比8MB小一點),垃圾Windows更是少得可憐,所以到這裡沒有繼續(瞎)分析下去的必要了。

結論

這個遞迴程式在爆棧之後就會立刻結束,所以說在寫入大概10610^6次之後程式就會結束,忽略遞迴的呼叫時間理論上應該在11000\frac{1}{1000}秒左右,但因為遞迴呼叫的存在,實際情況要大一些,但憑經驗判斷理應在11秒以內,這裡不再做深入的探究。

能否讓這個程式健康地執行

答案是可以的,可以用全域性棧來模擬遞迴呼叫,這樣實體記憶體有多大程式本身就可以用到多大的記憶體,此時如果你的記憶體足夠大,則取決於CPU的單核運算效能,如果記憶體小於16GB,那麼會發生記憶體交換,由於木桶效應的存在,此時程式的運算速度就會收到硬碟讀寫速度的制約。

2n2^n樹的規模證明

首先,可以將上文中提到的深搜樹剪為一顆左偏深搜樹: 2.jpg 易知,這棵樹的整體規模為i=1(23136)i\sum_{i=1}^{(2^{31}-36)}i,推廣到原樹上,每過一層就會多一層\sum,顯然,nn層這樣的\sum逼近於2n2^n

證畢。