C Lazy Evaluation 懶惰評估

2021-08-29 07:58:24 字數 4303 閱讀 7558

在c++中這裡的拖延戰術擁有乙個非常優雅的名字 -- lazy evalution。一旦你的程式中使用了lazy evaluation,那麼你就可以在你實際需要某些動作時編寫相應的**,如果不需要,那麼相應的動作也就永遠都不會執行。

對於引用技術,相信大部分人都不覺得陌生,在c++中的智慧型指標shared_ptr便是利用這一技術的最佳人選。

下面要講的是c++的string類的實現,string類的實現(可能有些庫並未採用lazy evaluation的技術)同樣採用了引用計數,比如下面是我們經常會遇到的**:

1

2

3

class 

string

;

string s1 =

"hexo";

string s2 = s1;

關於 string s2 = s1的行為,其實取決於string類的copy ctor的實現方式,如果string類採用的時eager evaluation的方式,這就意味著,當s2被建立的時候,copy ctor負責為s2分配記憶體空間,並且拷貝s1的內容到s2的記憶體中。設想一下,如果s2的值並未被使用或者是在之後的語句中對於s2只是讀取,並未修改,那麼上述的copy ctor的工作便是白白浪費掉了的。如果在乙個程式中大量出現這樣的物件的建立,那麼對於整個程式的衝擊無疑時很大的。

此時,lazy evaluation可以省去上述的很多任務作,因為此時s2與s1共享同一塊記憶體空間,直到不得不分配自己的空間時。這個過程省去了「分配記憶體」以及「字串拷貝」的工作,只需要做一些簡單的記錄,表示當前s2與s1共享的是同一塊記憶體空間即可。當然,這整個過程對於使用者來說則是透明的。比如下面的讀取操作:

1

2

cout

<< s1 <<

" "<< s2 << endl;

cout

<< s1 + s2 << endl;

這樣的共享直到某個不得已的瞬間出現,即其中的任何乙個字串發生改變的時候,比如:

1

s1.tolowercase(); 

//將s1中的所有的字母都變為小寫

上述**的意圖是修改s1的內容,但是現在s1與s2共享的是同一塊記憶體空間,那麼此時就需要為s2分配空間,並且複製字串的內容。如果整個過程都沒有出現字串被修改的操作,那麼lazy evaluation便做到了不必為你不需要的操作付出任何的代價。

其實這就是c++的string的cow(copy on write)的技術。大部分的程式庫現在都使用的是cow的技術,順帶一提,這樣的計數是執行緒不安全的。

繼續上面的string類,對於下面的**:

1

2

3

string s = 

"hello";

cout << s[

3];

//僅僅只是讀取

s[3] =

'x ;

上述兩句都是對operator的呼叫,我們希望區分上述兩種語句,讀或者寫,因為如果是讀,那麼我們還可以更lazy一點,直到寫時再分配記憶體以及複製記憶體。編譯器如何區分呢?很無奈,編譯器做不到,我們可以使用**類的技術來實現區分。

假設程式中使用了大型物件,其內部還有很多不同的字段,假設我們需要從乙個很大的資料庫中去讀取物件的內容,假設物件很大,那麼我們在需要某個物件時,不是直接將該物件完全讀入之後,再讀取需要的字段,而是在需要某個欄位的時候再去讀取該物件的相應字段即可。例如下面的乙個簡單的類;

1

2

3

4

5

6

7

8

9

struct 

type1

;

struct

type2

;

struct

type3

;

class

largestruct

;現在我有乙個本地的read函式,需要讀取mem1欄位,如:

1

2

3

4

5

6

7

8

9

void readmem1(

const

char *arg)

return;

}

那麼上述的**中只需要訪問到big物件的mem1欄位,但是我們卻需要構造出整個物件,這無疑是對資源的一大浪費,設想一下,如果資料儲存在遠端的伺服器,那麼每次拷貝乙個largestruct的物件,無疑是對網路頻寬的一大浪費,顯然,lazy fetching在這裡意味著 提供給你的資源絕對不要超出你想要的範圍,否則就是浪費。

那麼,像上述這樣的類,本地應該如何定義,才能使用lazy evaluation。也就是說lazy fetching的技術如何實現呢,其實很簡單,定義乙個指標的類,也就是說上述的mem1,mem2,mem3不是直接作為物件放在類中,而是使用指標。(這裡其實很容易想到,比如我們不需要該成員時,只需要將成員置為空,並且不會呼叫該成員的任何建構函式,直到需要時,才構造該成員變數的物件即可)。

1

2

3

4

5

6

7

8

9

10

11

struct 

type1

;

struct

type2

;

struct

type3

;

class

largestruct

};

只有在真正需要某個成員的時候,才會將該指標賦值,如果是在const成員函式中,怎麼改變成員變數呢,請參考 ***x

矩陣運算是乙個非常好的例子,比如:a b是兩個1000*1000的矩陣:

1

2

3

4

matrix a(

1000,

1000), b(

1000,

1000);

matrix c = a + b; //這裡先不必急著計算c的值。

... //此處的**中並未使用c的值,又或者是改變了c的值。

c = a+ a; // c的值又被改變了,上面的a+b的值,我們還沒計算呢,那就不必計算了。節約了資源,不是嗎?

對於上述這樣的**,程式設計師的不經意的語句將導致 100萬的數字的計算,計算成本是相當大的。

還有乙個值得稱頌的點,是部分值,也就是說我們只需要使用結果矩陣的部分值:

比如:

1

2

3

matrix a(

1000,

1000), b(

1000,

1000);

matrix c = a + b;

//這裡先不必急著計算c的值。

cout << c[

4] ;

// 這裡只需要第4行的資料,那麼只計算第4行即可。

那麼我們究竟有多幸運,使得我們不需要計算整個矩陣的值呢,其實概率是很大的,矩陣運算大部分都是採用了這樣的懶惰政策,大大節省了時間與資源,但是在下面的情況下,幸運就不會再出現了:

1

2

cout << 

c ; //需要列印整個矩陣的值,

b =

d; //b被改變了。那麼上述的

c = a+b的值,此時就必須計算

c的所有的元素,因為b的值被改變了。(a的值被改變亦是如此)。

緩式評估帶來的好處對於整個程式而言可能有很大的衝擊,所以在編寫**時:請謹記「不要付出任何額外的代價」,任何操作不必急著做,因為有可能這些操作永遠都不會被執行。

lazy 在此時最大的優點便是這些操作對使用者而言是完全透明的。不必更改客戶端的**便可以取得效能的大幅提公升。

貪婪與懶惰

當正規表示式中包含能接受重複的限定符時,通常的行為是 在使整個表示式能得到匹配的前提下 匹配盡可能多的字元。以這個表示式為例 a.b,它將會匹配最長的以a開始,以b結束的字串。如果用它來搜尋aabab的話,它會匹配整個字串aabab。這被稱為貪婪匹配。有時,我們更需要懶惰匹配,也就是匹配盡可能少的字...

懶惰的後果

1,我不做飯,但是如果連熱飯都懶得熱,那是什麼後果呢?週六打完羽毛球,中午沒吃飯,回來後現成的飯懶得熱了吃,胡亂吃了幾個酸奶,一堆餅乾,還有一堆乾果 核桃等 下午就肚子脹的難受 2,打完球,溼衣服懶得換 就開車回家,道上小風一吹,溼衣服貼在肚皮上,回到家,肚子又疼了.3,有時候忘了拿髒衣服出來,這下...

懶惰的奶牛

題目描述 夏天又到了,奶牛貝里斯開始變得非常懶惰。他想要站在乙個地方,然後只走很少的一段路,就能吃到盡可能多的美味的青草。有n塊草坪排列在一條直線上,第i個草坪擁有g i數量的青草,第i個草坪所在的位置是x i。奶牛貝里斯想要在直線上選擇乙個點作為他的初始點 初始點有可能和草坪的位置重合 這樣他就能...