2011-07-20 49 views
6

在C++快速內存訪問方面開發遊戲時,我應該考慮什麼?C++中的快速內存訪問?

我加載的內存是靜態的,所以我應該放在一個連續的內存塊中嗎?

另外,我應該如何組織結構中的變量來提高性能?

+7

如果您不得不提問這樣的問題,您最好專注於功能和高級設計,而不是在低級細節處進行猜測,因爲您可能無法實現任何目標。即使你知道你在這個層面上做了什麼,(1)在實際完成任何事情方面,性能的優先級較低,(2)這些都是微觀優化,從本質上來說,這只是微小的改進。 – delnan

+1

您的問題是關於如何提高訪問內存的速度,或者提高內存管理模塊各個方面(分配,釋放,分段等)的總體高效設計? – FireAphis

回答

11

內存性能是非常含糊。

我認爲你正在尋找的是關於處理CPU高速緩存,因爲高速緩存中的訪問和主內存中的訪問之間存在大約10倍的因子。

有關緩存背後機制的完整參考資料,您可能需要閱讀Ulrich Drepper on lwn.net這篇優秀的系列文章。

簡而言之:

瞄準地點

你不應該在內存中跳來跳去,所以嘗試(如果可能),以將要一起使用組合在一起的項目。

瞄準預見性

如果你的內存訪問是可預見的,將有可能預取工作的下一個塊的內存,所以它是可立即或不久,完成當前大塊後的CPU。

典型的例子是for循環數組上:

for (int i = 0; i != MAX; ++i) 
    for (int j = 0; j != MAX; ++j) 
    array[i][j] += 1; 

變化array[i][j] += 1;array[j][i] += 1;和性能在低優化級別變化...;)

編譯器應該抓住那些明顯的情況下,但有些更隱蔽。例如,使用基於節點的容器(鏈接列表,二叉搜索樹)而不是基於數組的容器(向量,一些哈希表)可能會降低應用程序的速度。

不要浪費空間......提防假共享

嘗試收拾你的結構。這與對齊有關,並且由於結構中的對齊問題,您可能會浪費空間,這會人爲地誇大結構大小並浪費緩存空間。

典型的經驗法則是通過減小尺寸來排序結構中的項目(使用sizeof)。這是愚蠢的,但效果很好。如果您對尺寸和對齊更瞭解,請避開漏洞:) 注意:僅適用於具有大量實例的結構...

但是,要小心虛假分享。在多線程程序中,併發訪問兩個足夠接近以共享相同高速緩存行的變量是很昂貴的,因爲它涉及大量高速緩存失效和CPU佔用高速緩存行。

檔案

不幸的是,這是HARD弄清楚。

如果您碰巧在Unix上編程,Callgrind(Valgrind套件的一部分)可以使用高速緩存仿真運行,並確定觸發高速緩存未命中的代碼部分。

我想還有其他的工具,我從來沒有用過它們。

7

你不在乎。這樣的事情很可能是最小自然的微觀優化。首先讓它工作,如果它太慢然後找出哪些部分是慢的,並優化那些(提示:這很可能是你調用庫等的方式,而不是內存訪問)。

+2

+1。 「不成熟的優化是萬惡之源。」 - D. Knuth – DevSolar

+4

@DevSolar -1如果可以的話。組織內存訪問在許多情況下不是過早的優化。 –

+0

@DevSolar:通過以導致多次緩存未命中的方式訪問內存,您可以輕鬆地讓您的編程速度降低十倍。現在添加頁面文件,你不再談論像這樣的優化。 – sharptooth

0

在出現問題之前尋找解決方案並不富有成效。

您最好將精力集中在您的設計上,以便稍後留下這些細節,誰知道,由於良好的整體設計,最終可能不會出現任何性能問題。

0

內存使用情況不必是連續的。如果你可以減半使用的內存大小,這可能會有所幫助。

就結構組織而言,你應該保持字節在一起,然後一起短路,等等。否則,編譯器會浪費內存對齊較小的字節和短路來增加字位置。

另一個提示。如果你正在使用一個類,你可以把它放在堆棧上,而不是用新的分配。

我的意思是

CmyClass x; 

instead of 

Cmyclass px = new CmyClass; 
... 
delete px; 

**編輯 當調用新的()或MALLOC調用到C++堆,有時堆在幾個週期返回新的存儲塊,有時它不」噸。當你在棧上聲明一個類的時候,你仍然吃了相同數量的內存(可能比這更復雜),但是這個類只是被「推入」堆棧,並且不需要函數調用。永遠。當函數退出時,堆棧被清理並且堆棧縮回。

+0

你能否詳細說明爲什麼棧上的類比堆上的更好? – KillianDS

0

從緩存讀取的地址比從主存儲器讀取時快得多。因此,儘量保持您正在閱讀的地址儘可能接近彼此。

例如,當建立一個鏈表時,你可能會更好地爲所有節點mallocing一個大塊(可以按順序放置或多或少),而不是每個節點使用一個malloc(這可能會碎片化你的數據結構)

+1

我認爲數據位置的可預測性實際上比緊湊打包的數據更具有問題。您在其中跳轉的連續塊仍可能會引入大量開銷。固定的跨步訪問(類似於數組)是可預測的,因此可以由CPU預取。 – KillianDS

+1

如果您的所有數據都適合緩存,則可以儘可能多地跳轉。當需要獲取新數據時,最舊的高速緩存行被逐出,如果你回到被驅逐的內存,則需要從主內存中重新獲取數據。 – doron

+0

確實如此,但緩存行通常小於kibibyte,實際上並不是那麼多。因此,充其量,你最好有256個整數的完整列表,但在大多數情況下,你已經被分成2個緩存行。數據可預測性考慮到您可以訪問相同的緩存(在這種情況下,它不會執行任何操作)。隨機跳躍強烈依賴於緩存關聯。 – KillianDS

1

我同意以前的說法。你應該寫下你的遊戲,然後找出時間花在什麼地方,並在那裏改進。

然而,在提供精神一些潛在的幫助[從真正的問題可能分散注意力:-)]建議有一些常見的陷阱,你會發現:

  • 函數指針和虛擬方法提供了很多的設計靈活性,但如果非常頻繁地使用它們,你會發現它們比可以內聯的東西慢。這主要是因爲CPU很難通過函數指針對調用執行分支預測。在C++中對此的一個很好的緩解就是使用模板,它可以在編譯時爲您提供類似的設計靈活性。

    此方法的一個潛在缺點是內聯會增加您的代碼大小。好消息是你的編譯器決定是否內聯,並且它可能會做出比你更好的決定。在許多情況下,你的優化器知道你的特定CPU架構,並可以做出適當的猜測。

  • 避免在經常訪問的數據結構中間接尋址。

例如這樣的:

struct Foo 
{ 
    // [snip] other members here... 

    Bar *barObject; // pointer to another allocation owned by Foo structure 
}; 

就可能會造成低效率的內存佈局比這

struct Foo 
{ 
    // [snip] other members here... 

    Bar barObject; // object is a part of Foo, with no indirection 
}; 

這聽起來可能很傻,而且在大多數情況下,你不會注意到任何區別。但總體思路是「不必要的間接」是一件好事情,可以避免。不要過多地去做,但要記住這一點。

一個潛在缺點這種方法,它可以使你的緩存對象Foo不再適合整齊...

  • 除了前兩個子彈的線......在C++中,STL容器和算法可以導致一些相當有效的目標代碼。在<algorithm>的情況下,傳遞給各種算法的仿函數可以很容易地進行內聯,幫助您避免不必要的指針調用,同時仍然允許泛型例程。在容器的情況下,STL可以在列表節點等內恰當地聲明類型參數爲T的對象,有助於避免數據結構中不必要的間接尋址。

  • 是的,內存訪問可以有所作爲...一個例子是循環通過大圖像中的像素。如果您一次處理圖像列,它可能比一次處理一行更糟糕。在最常見的圖像格式中,(x,y)處的像素通常與(x + 1,y)處的像素相鄰,而(x,y)處的像素通常距離(x, Y + 1)。

  • 沿着與第二個項目符號相同的路線,一次用圖像處理來處理項目(儘管按照今天的標準在舊硬件上),但我看到即使涉及確定像素位置的算術運算也會導致放緩。例如,如果你正在處理座標(x,y),一個直觀的事情就是在buf[y * bytes_per_line + x]處引用一個像素。如果你的CPU在乘法運算速度慢並且圖像很大,這可能會加起來。在這種情況下,一次循環最好比保持計算各個座標的(x,y)的位置更好。

當然,您的遊戲的整體設計應該推動您的早期決策,測量應指導您的性能改進。如果不能讓您完成「實際工作」或使項目難以理解,您不應該竭盡全力實施這些要點。但是,這些要點旨在提供一些您可能會遇到麻煩的示例,並介紹一些可能會導致實踐中出現性能問題的背景,除了算法複雜性等其他措施。

+0

在現代的CPU上(我猜從第四代英特爾酷睿i7開始)非內聯函數調用比內聯更快,我猜這是因爲分支預測機制開始處理函數的主體並與調用它的代碼並行。你可以用循環增加一個volatile變量來測試它,而不是增加'virtual'/DLL /'__declspec(noinline)'函數中的volatile變量。 –