2013-03-31 15 views
10

Pracuję nad symulacją płynów w języku C#. Każdy cykl muszę obliczyć prędkość płynu w dyskretnych punktach w przestrzeni. W ramach tych obliczeń potrzebuję kilkudziesięciu kilobajtów na przestrzeń scratch, aby pomieścić kilka podwójnych tablic [] (dokładny rozmiar macierzy zależy od niektórych danych wejściowych). Tablice są potrzebne tylko na czas metody, która ich używa, i istnieje kilka różnych metod, które wymagają takiej przestrzeni.Scratch memory in managed environment

Jak ja to widzę, istnieje kilka różnych rozwiązań do budowy tablic podstaw:

  1. Wykorzystanie „nowe”, aby pobrać z pamięci sterty każdym razem, gdy wywoływana jest metoda. To właśnie robiłem na początku, jednak wywiera ona duży nacisk na śmieciarza, a kilka-ms pików raz lub dwa razy na sekundę jest naprawdę denerwujące.

  2. Przygotuj tablice rysunkowe jako parametry podczas wywoływania metod. Problem polega na tym, że zmusza użytkownika do zarządzania nimi, w tym do odpowiedniego ich doboru, co jest ogromnym problemem. A to sprawia, że ​​korzystanie z mniej lub bardziej porysowanej pamięci jest trudne, ponieważ zmienia interfejs API.

  3. Użyj stosu alokacji w niebezpiecznym kontekście, aby przydzielić pamięć scratch ze stosu programów. To działałoby dobrze, z wyjątkiem tego, że musiałbym skompilować się z/unsafe i ciągle kropić niebezpieczne bloki w całym kodzie, czego chciałbym uniknąć.

  4. Przydzielać prywatne tablice raz podczas uruchamiania programu. To jest w porządku, ale niekoniecznie znam rozmiar tablic, których potrzebuję, dopóki nie spojrzę na niektóre dane wejściowe. I robi się naprawdę nieporządnie, ponieważ nie można ograniczyć zakresu tych prywatnych zmiennych do jednej metody, więc stale zanieczyszczają przestrzeń nazw. I skaluje się słabo, ponieważ liczba metod, które wymagają pamięci na zarysowania, wzrasta, ponieważ przydzielam dużo pamięci, która była używana tylko przez ułamek czasu.

  5. Utwórz pewną pulę centralną i przydzielaj tablice pamięci scratch z puli. Głównym problemem jest to, że nie widzę łatwego sposobu przydzielania dynamicznie wielkości macierzy z puli centralnej. Mógłbym użyć przesunięcia początkowego i długości i mieć całą pamięć podstawową w zasadzie współdzielić pojedynczą dużą tablicę, ale mam dużo istniejącego kodu, który zakłada podwójne [] s. I musiałbym być ostrożny, aby taki wątek był bezpieczny.

...

Czy ktoś ma jakieś doświadczenia z podobnym problemem? Wszelkie porady/lekcje do zaoferowania z doświadczenia?

+0

Czy naprawdę miałeś na myśli kilka dziesiątków kilobajtów? Ponieważ jest to tak mała ilość, nie martwiłbym się o zarządzanie pamięcią ... –

+0

To nie brzmi zbyt dobrze, ale jeśli mam 2000 cykli/sekundę, nagle jest to około 60 MB/s, i GC zaczyna zauważać. –

+0

@JayLemmon, myślę, że dbasz o te szczegóły ze względu na wydajność, prawda? Jeśli twój projekt się nie zakończył, sugeruję, żebyś nie przejmował się wydajnością aż do końca. Zobacz ten artykuł na temat [Przedwczesna optymalizacja] (http://c2.com/cgi/wiki?PrematureOptimization). Jeśli projekt jest skończony, artykuł zawiera ciekawe spostrzeżenia również na temat __optymalizacji__ w ogóle. Cytuję część: "Częstym błędnym przekonaniem jest to, że zoptymalizowany kod jest z konieczności bardziej skomplikowany [...] lepszy, dobrze napisany kod często działa szybciej i zużywa mniej pamięci [...]". – jay

Odpowiedz

3

Można owinąć kod, który wykorzystuje tezy zarysować tablic w użyciu instrukcji tak:

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

To zwolni pamięć jawnie przez wywołanie descructor na koniec użyciu instrukcji.

Niestety wygląda na to, że powyższe stwierdzenie nie jest prawdziwe! Zamiast tego możesz spróbować czegoś podobnego do funkcji pomocnika, która zwraca tablicę o odpowiednim rozmiarze (najbliższa potęga 2 większa niż rozmiar), a jeśli nie istnieje, to ją tworzy. W ten sposób masz tylko logarytmiczną liczbę tablic. Jeśli chcesz, aby był bezpieczny dla wątków, musisz trochę więcej problemów.

mogłoby to wyglądać mniej więcej tak: (używając pow2roundup z 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]; 
} 

EDIT: Temat bezpieczna wersja: ten będzie nadal mieć rzeczy, które się śmieci zebrane, ale to będzie wątek specyficznych i powinno być znacznie mniej narzutów.

[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

Niestety, Disposed! = Collected :(Zobacz http://stackoverflow.com/questions/655902/using-and-garbage-collection –

+0

To niefortunne. Zaktualizuję moją odpowiedź innym pomysłem: –

+0

Ah, nie zrobiłem tego Wiesz o ThreadStatic, co znacznie ułatwia pisanie wątków w lokalnej pamięci ... –

7

Współczuję Twojej sytuacji; kiedy pracowałem nad Roslyn, bardzo uważnie zastanawialiśmy się, jakie potencjalne problemy z wydajnością powodowane są przez presję gromadzenia z przydzielania tymczasowych tablic roboczych. Rozwiązaniem, na które się zdecydowaliśmy, była strategia łączenia.

W kompilatorze rozmiary macierzy są zwykle małe, a zatem często powtarzane. W twojej sytuacji, jeśli masz duże tablice, to chciałbym postępować zgodnie z sugestią Toma: uprość problem zarządzania i marnuj trochę miejsca. Kiedy poprosisz pulę o tablicę o rozmiarze x, zaokrąglij x do najbliższej potęgi dwóch i przydzielij tablicę o tym rozmiarze lub weź ją z puli. Dzwoniący dostaje tablicę, która jest trochę za duża, ale można ją napisać, żeby sobie z tym poradzić. Nie powinno być zbyt trudno przeszukiwać pulę dla tablicy o odpowiednim rozmiarze. Można też utrzymywać kilka pul, jedną pulę dla tablic o rozmiarze 1024, jedną dla 2048 i tak dalej.

Zapisywanie puli wątków nie jest zbyt trudne lub można ustawić wątek puli statycznie i mieć jedną pulę na wątek.

Trudnym bitem jest odzyskanie pamięci w puli. Jest kilka sposobów na rozwiązanie tego problemu. Po pierwsze, po prostu można wymagać od użytkownika puli pamięci, aby wywołać metodę "z powrotem w puli" po zakończeniu pracy z tablicą, jeśli nie chcą ponosić kosztów nacisku zbierania.

Innym sposobem jest napisanie okienka elewacji wokół tablicy, sprawienie, by implementował IDisposable, aby można było użyć "use" (*), i wykonać finalizator na tym, który umieszcza obiekt z powrotem w puli, wskrzeszając go. (Upewnij się, że finalizator zawrócił z powrotem do "Muszę być sfinalizowany".) Finalizatorzy, którzy robią zmartwychwstanie, denerwują mnie; Osobiście wolałbym poprzednie podejście, co zrobiliśmy w Roslyn.


(*) Tak, to narusza zasadę, że „przy pomocy” powinien wskazywać, że zasobem niekontrolowana jest zwracane do systemu operacyjnego. Zasadniczo traktujemy pamięć zarządzaną jako niezarządzane zasoby, wykonując własne zarządzanie, więc nie jest tak źle.

+0

Dzięki, to jest dokładnie taka historia wojenna, na którą liczyłem :) Basen brzmi jak najbardziej rozsądne rozwiązanie do tej pory, a ja nie "T mind" marnuje "pamięć taką jak ta. Nadal nie jestem zbytnio sprzedawany, polegając na tym, że użytkownicy zwalniają zasób lub próbują go wrzucić do metody Dispose, ale myślę, że właśnie tak jest. –