1. 程式人生 > 程式設計 >如何高效的管理快取?--LoopBuffer

如何高效的管理快取?--LoopBuffer

我們需要一種快取結構,可以未預知資料大小的情況下高效的管理記憶體。每次資料到來的時候都能保證有效的寫入,即使動態的擴充套件記憶體也不會對原有的資料進行任何挪移操作。讀取資料的時候只能順序的讀取,也不會對未讀取到的資料進行移動。

CppNet的資料流緩衝通過CBuffer類來實現,實際的資料儲存在CLoopBuffer中,loop buffer實現如其名,通過在一塊固定大小的記憶體上移動指標來實現順序的讀寫操作。

每個loop buffer都持有一塊來自記憶體池的固定大小的記憶體。然後通過四個指標來嚴格標識資料的位置,注意這裡是嚴格標識,所以我們申請到的記憶體不用memset初始化,每次讀寫通過移動指標來控制資料流動,下面著重說下指標的幾種移動情況:

start : 指向分配記憶體的起始地址。
end: 指向分配記憶體的末端地址。
read: 當前讀取遊標。
write: 當前寫入遊標。
當loop buffer第一次被建立時,指標的位置如圖1:

圖1

start,read,write三個指標都指向記憶體的起始位置,這個時候 read = write 可讀取資料為空。接下來進行資料寫入,如圖2:

圖2

write指標開始向右移動,記錄著下一次寫入的位置。現在可讀取的資料量是 write - read,剩餘可寫入的記憶體大小是 end - write。

接下來 我們進行一次資料讀取, 如圖3:

圖3

read 指標開始向右移動,讀取到的資料量是 read - start,剩餘可讀取資料大小是 write - read,剩餘可寫入的記憶體大小是 end - write + (read - start)。

接下來我們將所有的資料讀取出來, 如圖4:

圖4

read 指標向右移動直到追上了write,現在read == write,當讀寫指標相等的時候,有兩種情況,要麼是記憶體被寫滿,要麼是記憶體塊為空,需要一個額外的成員變數來標識。現在read追上了write,記憶體塊為空,可讀取資料大小是0,可寫大小是整個記憶體塊的大小,為了使可寫快取更為完整,以方便writev和readv的呼叫,每次read指標追上write指標的時候,我們都將所有指標狀態重置,恢復到圖1的狀態。

接下來又有新的資料到來, 如圖5:

圖5

我們看到 write 到了read 的左邊,這是因為write 一直向右移動的時候,當指向了 end 指標,則需要重新調整指向 start,這就是loop的由來,而此時read 和 start之間有不少的距離,我們接著從start開始寫入資料,write又重新開始向右移動。現在可讀資料大小是 end - read + (write - start),可寫資料大小是 read - write。

如果接下來還是資料寫入的話, write 就會向右移動一直追上 read。這時 read == write, 但是記憶體已經被寫滿了。

為了配合readv的呼叫,需要有一個介面能返回當前可寫記憶體的起始位置和大小,通過上述的幾個過程我們可以觀察到有兩種情況:

1> 圖1,圖2,圖5的時候(圖4狀態會被重置為圖1),只有一個可寫快取區,起始地址是write指標,長度是read - write 或 end - write。
2> 圖3的時候有兩個可寫區域,起始地址是write和start,可寫長度分別是end - write 和 read - start。 writev時需要返回所有的資料區域,與上述情況類似但操作的指標剛好相反,不再詳述。

以上的幾個過程就是loop buffer寫入和讀取的全部情況,可以看到每次資料寫入和讀取的時候只有必要資料的複製,並沒有對其他資料的移動拷貝操作,而且每次資料流動的時候,都不會超出限定的記憶體區域。

但是loop buffer只有固定大小的記憶體,若是寫滿了之後還有新的資料寫入請求怎麼辦?這就是buffer表演的時候了。

CBuffer實現上其實和CLoopBuffer非常的相似,也是通過四個指標來控制資料的讀取和寫入,甚至每個指標的作用都與其相同,只不過CLoopBuffer中指標指向的記憶體塊的具體位置,而CBuffer中的指標指向的是CLoopBuffer記憶體塊。其內部通過一個單向連結串列管理所有的記憶體塊節點,當有資料未滿的時候,幾個指標的移動操作和loop buffer的指標完全相同。
唯一不同的是,當所有的記憶體塊被寫滿的時候,read == write,這時CBuffer需要重新從記憶體池中申請新的記憶體塊,並將其新增到連結串列中。

以上的實現方式存在一個問題,CLoopBuffer的指標不是順序申請的,無法通過比較指標地址來判斷讀寫的先後順序,所以每個CLoopBuffer在實現的時候都攜帶了一個自身所處佇列的索引,每次查詢的時候都需要過載操作符<或>的呼叫來判斷順序關係,valgrind效能分析時發現這裡呼叫頻次極高,所以重新優化了CBuffer的實現。

重構之後的CBuffer用一個單向連結串列來管理loop buffer,寫入資料的時候如何空間不夠,則從記憶體池中申請新的節點新增到連結串列後邊,write指標向後移動。讀取資料的時候,一旦當前loop buffer節點的資料全部讀取完成,則將當前塊歸還給記憶體池,read指標向後移動。實現起來像是紅白機遊戲裡的馬裡奧過浮橋,每次踩過的磚塊都會析構掉,前邊會生成新的磚塊拼成浮橋。整個讀寫過程都是從左往右順序移動的過程。

以上就是CppNet快取管理的核心實現。

github請戳這裡