「預設情況下,c++標準庫提供了合理的效能」。如果你對「合理的」一詞暗含的意思有過好奇,請接著讀下去……
引言假設我們希望從乙個檔案中將一串型別為double的值讀進乙個資料結構中,從而允許我們高效地訪問這些值,通常的方法如下:
vector
values;
double x;
while (cin >> x)
values.push_back(x);
當迴圈結束時,values會容納有所有的值,我們將可以通過values[i]高效地訪問任何值。
在直覺上,標準庫vector類就像乙個內建陣列:我們可以認為它在單塊連續的記憶體中容納其元素。實際上,儘管c++標準沒有明確要求vector的元素要占用連續的記憶體,然而標準委員會在2023年10月份的會議上裁定此項要求的遺漏歸因於工作上的疏忽,並且投票表決將其作為技術勘誤的一部分而包含進來。這個遲到的要求談不上是多大的痛苦,因為每乙個現有的vector實現本來就是以這種方式工作的。
如果乙個vector的元素位於連續的記憶體中,我們就很容易明白它是如何高效地訪問個體元素的 — 只要使用與內建陣列相同的機制就可以了。不過,要弄明白乙個vector實 現是如何處理高效增長的問題就不是這麼簡單了,因為這種增長將不可避免地涉及到將元素從一塊記憶體區域拷貝到另外一塊記憶體區域。儘管現代處理器通常特別擅長 於將一塊連續的資料從記憶體的乙個地方拷貝到另乙個地方,然而這樣的拷貝並非是免費的午餐。因此,思考乙個標準庫實現可能是如何處理vector的增長而又不消耗過量的時間或空間,很有意義。
本文的餘下部分將討論乙個用於管理vector增長的簡單而高效的策略。
大小和容量
要想搞清楚vector類的工作機制,首先要清楚它並不僅僅是一塊記憶體。相反,每乙個vector都關聯有兩個「尺寸」:乙個稱為 大小(size),表示vector容納的元素的數量;另乙個稱為容量(capacity),表示可被用來儲存元素的記憶體總量。比方說,假如v是乙個vector,那麼v.size()和v.capacity()則分別返回v的 大小和容量。你可以想象乙個vector看起來如下:
當然了,在vector尾部留有額外的記憶體的用意在於,當使用push_back向vector追加元素時無需分配更多的記憶體。如果鄰接於vector尾部的記憶體當時恰好未被占用,那麼vector的增長只要將那塊記憶體合併過來即可。然而這樣的好運氣極其罕見,大多數情況下需要分配新的記憶體,然後將vector現有的元素拷貝到那塊記憶體中,然後銷毀原來的元素,最後歸還元素先前占用的記憶體。在vector中留有額外的記憶體的好處在於,這樣的重新分配(代價可能很昂貴)不會每當試圖向vector追加乙個元素時都發生。
重新分配記憶體的代價有多高昂?它涉及如下四個步驟:
如果元素的數目為n,那麼我們知道步驟(2)和(3)都要占用o(n)的時間,除非分配或歸還內
存的代價的增長超過o(n),否則這兩步將在全部執行時間中占居支配地位。因此我們可以得出結論:無論用於重新分配的容量(capacity)是多少,重新分配乙個 大小(size)為n的vector需要占用o(n)的時間。
這 個結論暗示了一種折衷權衡。假如在重新分配時請求大量的額外記憶體,那麼在相當長的時間內將無需再次進行重新分配,因此總體重新分配操作消耗的時間相對較 少,這種策略的代價在於將會浪費大量的空間。另一方面,我們可以只請求一點點額外的記憶體,這麼做將會節約空間,但後繼的重新分配操作將會耗費時間。換句話 說,我們面臨乙個經典的抉擇:拿時間換空間,或者相反。
重新分配策略
作為乙個極端的例子,假定每當填充vector一次我們就將其容量增加1個單位,這種策略耗費盡可能少的記憶體空間,但每當追加乙個元素時都要重新分配整個vector。我們說過,重新分配乙個具有n個元素的vector占用o(n)的時間,因此,如果我們從乙個空vector開始並將其增長到k個元素,那麼占用的總時間將會是o(1+2+...+k)或者o(k2),這太可怕了!有沒有更好的辦法呢?
比方說,假如不是以步幅1來增長vector的容量,而是以乙個常量c的步幅來增長它將會如何?很明顯這個策略將會減少重新分配的次數(基於因子c),所以這當然是一種改進,但這個改進到底有多大呢?
理解這個改進的方式之一是要認識到此一新策略將針對每c個元素塊進行一次重新分配。假設我們為總量為kxc個元素分配k塊記憶體,那麼,第一次重新分配將會拷貝c個元素,第二次將會拷貝2xc個元素,等等。big-o表示法不考慮常量因子,因此我們可以將所有的c因子分攤開來而獲得o(1+2+...+k)或者o(k2)的總時間。換句話說,時間仍然是元素個數的二次方程,不過是帶有乙個小得多的因子罷了。
撇 開較小的因子不談,「二次行為」仍然太糟糕,即使有乙個快速的處理器也是如此。實際上,對於快速的處理器來說尤其糟糕,因為快速的處理器通常伴有大量的內 存,而訪問具有大量記憶體的快速處理器的程式設計師常常試圖用盡那些記憶體(這是遲早的事)。這些程式設計師往往會發現,如果在執行乙個二次演算法的話,處理器的速度於 事無補。
我們剛剛證實,乙個希望能以小於「二次時間」而分配大型vector的實現是不能使用「每次填充時以常量步幅增長vector容量」的策略的,相反,被分配的附加記憶體的數量必須隨著vector的增長而增長。這個事實暗示存在一種簡單的策略:vector從單個元素開始而後每當重新分配時倍增其容量,如何?事實證明這種策略允許我們以o(n)的時間構建乙個有著n個元素的vector。
為了理解是如何獲得這樣的效率的,考慮當我們已經完全填滿它並打算對其重新分配時的vector的狀態:
自最近一次重新分配記憶體以來被追加到vector中的元素有一半從未被拷貝過,而對於那些被拷貝的元素而言,其中一半只被拷貝了一次,其餘的一半被拷貝了兩次,以此類推。
換句話說,有n/2的元素被拷貝了一次或多次,有n/4的元素被拷貝了兩次或多次,等等。因此,拷貝元素的總數目為n/2 + n/4 +...,結果可以近似為n(隨著n的增大,這個近似值越發精確)。撇開拷貝動作不談,有n個元素被追加到了vector中,但操作占用的時間總量仍然是o(n)而不是o(n2)。
討論c++標準並沒有規定vector類必須以某種特定的方式管理其記憶體,它只是要求通過重複呼叫push_back而建立乙個具有n個元素的vector耗費的時間不得超過o(n),我們剛才討論的策略可能是滿足此項要求的最直截了當的一種。
因為對於這樣的操作來說vector具有優秀的時間效能,所以沒有什麼理由避免使用如下迴圈:
vector
values;
double x;
while (cin >> x)
values.push_back(x);
是的,當其增長時,實現將會重新分配vector的元素,但是,如果我們事先能夠**vector最終 大小的話,這個重新分配耗費的時間將不會超過「乙個常量因子」可能會占用的時間。
練習1.設想我們通過以如下方式編寫**而努力使我們那個小型迴圈速度更快:
while (cin >> x)
效果將會如何?成員函式reserve進行一次重新分配,從而改變vector的capacity,使其大於或等於其引數。
2.設想不是每次倍增vector的大小,而是增大三倍,在效能上將會產生什麼樣的影響?特別是,建立乙個具有n個元素的vector的執行時間仍然為o(n)嗎?
3.設想你知道你的vector最終將擁有多少元素,在這種情況下,在填充元素之前你可以呼叫reserve來預先分配數量合適的記憶體。試一試你手 頭的vector實現,看看呼叫reserve與否對你的程式的執行時間有多大的影響。
讓puppet agent同步變得更加隨機和離散
puppe的2種同步方式 對於puppet agent的同步,有2種方式可以去做 1.在客戶端執行乙個agent程序,通過配置 etc puppet puppet.conf配置檔案中的 agent 項下的runinterval去控制agent的同步時間間隔。2.通過crontab,每隔一段時間去執行...
讓編寫C 屬性更加規範 容易
通常,在寫c 類的屬性時,會這樣寫 public int count 大多數軟體公司都會要求新增注釋,並對屬性進行必要的擴充套件 private int count 玩具的數量 public int count set 我還有個習慣,是對屬性更改前後提供事件通知,並且將跟屬性相關的字段 屬性 事件用...
讓你工作變得更加有趣
今日去打球的途中,同事聊起最近工作感覺無聊,問問我們感覺如何。我隨口說我們沒有啊,我和小j同學經常爭爭吵吵,感覺非常有意思。聽者無心,說者有意。我回家後感覺這件事,說小是小,說大是大。工作如何才能有趣,應該也是一門學問啊。我倒沒有什麼方法能指導所有人都能將自己的工作變得有趣。但可以肯定的是,我就有這...