2013-03-31 66 views
10

我正在C#中進行流體模擬。每個週期我需要計算流體在空間離散點的速度。作爲計算的一部分,我需要幾十千字節的空間來容納一些double []數組(數組的確切大小取決於某些輸入數據)。數組只是在使用它們的方法的持續時間中需要的,並且有幾種不同的方法需要像這樣的臨時空間。管理環境中的暫存內存

在我看來,有構建劃痕陣列幾個不同的解決方案:

  1. 使用「新」從堆中的每個方法被調用的時候搶內存。這是我一開始就在做的事情,但是它給垃圾收集器帶來了很大的壓力,而每秒一次或幾次的幾毫秒尖峯真的很煩人。

  2. 調用方法時,將scratch數組作爲參數傳遞。問題是這迫使用戶去管理它們,包括適當地調整它們,這是一個巨大的痛苦。由於它改變了API,所以它使得使用或多或少的暫存內存變得困難。

  3. 在不安全的上下文中使用stackalloc從程序堆棧中分配暫存內存。這將工作得很好,除非我需要使用/ unsafe進行編譯,並且不斷在我的代碼中散佈不安全的塊,我想避免這些塊。

  4. 當程序啓動時預先分配私有數組一次。這很好,除非我可以查看一些輸入數據之前並不知道所需數組的大小。由於不能將這些私有變量的範圍限制爲單一方法,所以它會變得非常混亂,所以它們不斷地污染命名空間。它隨着需要臨時存儲器的方法數量的增加而縮小,因爲我分配了大量只佔用了一小部分時間的內存。

  5. 創建某種中央池,並從池中分配暫存內存數組。這樣做的主要問題是我沒有看到從中央池中分配動態大小數組的簡單方法。我可以使用起始偏移量和長度,並讓所有暫存內存共享一個大型數組,但我有很多現有代碼假設爲double []。而且我必須小心使這樣的泳池線程安全。

...

有沒有人有類似的問題的經驗嗎?從經驗中提供的任何建議/課程?

+0

你真的意味着幾十千字節嗎?因爲這個數目非常小,所以我不用擔心內存管理... –

+0

聽起來不是很多,但是如果我運行2000周/秒,突然它就像60MB /秒,並且GC開始注意到。 –

+0

@JayLemmon,我認爲你出於性能原因關心這些細節,對吧?如果您的項目沒有完成,我建議您在完成之前不關心性能。請參閱本文[早熟優化](http://c2.com/cgi/wiki?PrematureOptimization)。如果項目完成,文章還會對__optimization__進行一些有趣的觀察。我引用了一個部分:「一個常見的誤解是,優化的代碼必然更復雜[...]更好的分解代碼通常運行速度更快,並且使用更少的內存[...]」。 – jay

回答

3

你可以用使用論文在using語句這樣劃傷陣列代碼:

using(double[] scratchArray = new double[buffer]) 
{ 
    // Code here... 
} 

這將通過調用在using語句的結束descructor明確地釋放內存。

不幸的是,看起來上面是不對的!代替這一點,你可以嘗試一些幫助函數,它返回一個合適大小的數組(最大冪次數大於2),如果它不存在,就創建它。這樣,你只有對數數組。如果你希望它是線程安全的,但你需要去更多的麻煩。

它可能看起來像這樣:(使用pow2roundup從Algorithm for finding the smallest power of two that's greater or equal to a given value

private static Dictionary<int,double[]> scratchArrays = new Dictionary<int,double[]>(); 
/// Round up to next higher power of 2 (return x if it's already a power of 2). 
public static int Pow2RoundUp (int x) 
{ 
    if (x < 0) 
     return 0; 
    --x; 
    x |= x >> 1; 
    x |= x >> 2; 
    x |= x >> 4; 
    x |= x >> 8; 
    x |= x >> 16; 
    return x+1; 
} 
private static double[] GetScratchArray(int size) 
{ 
    int pow2 = Pow2RoundUp(size); 
    if (!scratchArrays.ContainsKey(pow2)) 
    { 
     scratchArrays.Add(pow2, new double[pow2]); 
    } 
    return scratchArrays[pow2]; 
} 

編輯:線程安全版本: 這將仍然作爲垃圾收集的事情,但是這將是線程專用並且應該少得多。

[ThreadStatic] 
private static Dictionary<int,double[]> _scratchArrays; 

private static Dictionary<int,double[]> scratchArrays 
{ 
    get 
    { 
     if (_scratchArrays == null) 
     { 
      _scratchArrays = new Dictionary<int,double[]>(); 
     } 
     return _scratchArrays; 
    } 
} 

/// Round up to next higher power of 2 (return x if it's already a power of 2). 
public static int Pow2RoundUp (int x) 
{ 
    if (x < 0) 
     return 0; 
    --x; 
    x |= x >> 1; 
    x |= x >> 2; 
    x |= x >> 4; 
    x |= x >> 8; 
    x |= x >> 16; 
    return x+1; 
} 
private static double[] GetScratchArray(int size) 
{ 
    int pow2 = Pow2RoundUp(size); 
    if (!scratchArrays.ContainsKey(pow2)) 
    { 
     scratchArrays.Add(pow2, new double[pow2]); 
    } 
    return scratchArrays[pow2]; 
} 
+0

不幸的是,處置!=收集:(見http://stackoverflow.com/questions/655902/using-and-garbage-collection –

+0

這是不幸的,我會更新我的答案與另一個想法 –

+0

啊,我沒有'不知道ThreadStatic,這讓thread-local-ish臨時記憶容易得多 –

7

我同情你的情況;當我在羅斯林工作時,我們非常仔細地考慮了分配臨時工作陣列所帶來的收集壓力導致的潛在性能問題。我們解決的解決方案是集中策略。

在編譯器中,數組大小往往很小,因此經常重複。在你的情況下,如果你有大陣列,那麼我會做的就是遵循湯姆的建議:簡化管理問題並浪費一些空間。當你向泳池詢問一個尺寸爲x的數組時,可以將x乘以2,然後分配一個這個尺寸的數組,或者從泳池中取一個。調用者獲得一個有點太大的數組,但是可以編寫它們來處理這個問題。在池中搜索適當大小的數組並不困難。或者你可以維護一堆池,一個池大小爲1024,一個爲2048,等等。

寫一個線程安全池不是太難,或者你可以使得線程池靜態並且每個線程有一個池。

棘手的一點是讓內存回到池中。有幾種方法可以解決這個問題。首先,如果他們不想承擔收集壓力的代價,那麼您可以簡單地要求混合內存的用戶在完成陣列時調用「回到池中」方法。

另一種方法是在數組周圍編寫一個外觀包裝,使其實現IDisposable,以便您可以使用「using」(*),並在該對象上創建一個終結器,將對象放回池中,使其重新生成。 (請務必讓終結器回到「我需要定稿」位)。復活終結者讓我感到緊張;我個人更喜歡前一種方法,這正是我們在羅斯林所做的。


(*)是的,這違反了「使用」應表明非託管資源正在返回到操作系統的原則。從本質上講,我們通過自己的管理將託管內存視爲非託管資源,所以它不是那麼糟糕。

+0

謝謝,這正是我所希望的那種戰爭故事:)游泳池聽起來像迄今爲止最合理的解決方案,而且我不知道'不介意'浪費「這樣的記憶。依靠用戶釋放資源,或者試圖將它塞進Dispose方法中,我仍然沒有超級賣出,但我想它就是這樣。 –