Сжатие памяти в управляемой среде
Я работаю над жидкостным симулятором на С#. Каждому циклу я должен рассчитать скорость жидкости в дискретных точках в пространстве. В рамках этого расчета мне нужно несколько десятков килобайт для пространства царапин для хранения нескольких двумерных массивов (точный размер массивов зависит от некоторых входных данных). Массивы нужны только в течение всего используемого им метода, и существует несколько разных методов, для которых требуется такое пространство для царапин.
Как я вижу, для построения массивов царапин существует несколько различных решений:
-
Используйте 'new' для захвата памяти из кучи каждый раз, когда вызывается метод. Это то, что я делал вначале, однако он оказывает сильное давление на сборщик мусора, а несколько-миллисекундные всплески один или два раза в секунду действительно раздражают.
-
При вызове методов передаются массивы царапин в качестве параметров. Проблема в том, что это заставляет пользователя управлять ими, включая их правильное определение, что является огромной болью. И это затрудняет использование более или менее проблемной памяти, поскольку оно изменяет API.
-
Используйте stackalloc в небезопасном контексте, чтобы выделить память нуля из стека программ. Это будет работать очень хорошо, за исключением того, что мне нужно будет компилировать/небезопасно и постоянно посыпать небезопасные блоки по всему моему коду, чего я бы хотел избежать.
-
Предварительно выделяйте частные массивы один раз, когда программа запускается. Это прекрасно, за исключением того, что я не обязательно знаю размер массивов, которые мне нужны, пока я не смогу посмотреть на некоторые из входных данных. И это становится очень грязным, поскольку вы не можете ограничить область этих частных переменных только одним методом, поэтому они постоянно загрязняют пространство имен. И он плохо масштабируется, так как количество методов, требующих памяти с нуля, увеличивается, поскольку я выделяю много памяти, которая использовала только часть времени.
-
Создайте какой-то центральный пул и выделите массивы с памятью из пула. Основная проблема заключается в том, что я не вижу простого способа выделения массивов динамического размера из центрального пула. Я мог бы использовать начальное смещение и длину и иметь всю память с нуля, по существу, разделяющую один большой массив, но у меня есть много существующего кода, который предполагает double [] s. И я должен быть осторожным, чтобы сделать такой поток потока безопасным.
...
Есть ли у кого-нибудь опыт подобной проблемы? Любые советы/уроки, которые можно предложить из опыта?
Ответы
Ответ 1
Я сочувствую вашей ситуации; когда я работал над Roslyn, мы очень тщательно рассмотрели потенциальные проблемы с производительностью, вызванные давлением сбора из распределения временных рабочих массивов. Решение, на котором мы остановились, было стратегией объединения.
В компиляторе размеры массива имеют тенденцию быть небольшими и поэтому часто повторяются. В вашей ситуации, если у вас большие массивы, то то, что я сделаю, следует за предложением Тома: упростите проблему управления и потратите некоторое пространство. Когда вы задаете пул для массива размера x, округлите x до ближайшей мощности двух и выделите массив этого размера или возьмите один из пула. Вызывающий получает массив, который немного велик, но их можно написать, чтобы справиться с этим. Не следует слишком сложно искать пул для массива соответствующего размера. Или вы можете поддерживать пул пулов, один пул для массивов размером 1024, один для 2048 и т.д.
Написание пула потоков не слишком сложно, или вы можете сделать поток пула статическим и иметь один пул на поток.
Сложный бит возвращает память в пул. Есть несколько способов справиться с этим. Во-первых, вы можете просто потребовать от пользователя объединенной памяти вызвать метод "обратно в пул", когда они будут сделаны с массивом, если они не хотят брать на себя расходы на сбор.
Другой способ - написать оболочку фасада вокруг массива, сделать его реализацией IDisposable, чтобы вы могли использовать "использование" (*) и сделать финализатор на том, что помещает объект обратно в пул, воскрешая его. (Удостоверьтесь, что финализатор вернется к бит "Я должен быть доработан".) Финализаторы, которые делают воскрешение, заставляют меня нервничать; Я лично предпочел бы прежний подход, что мы и сделали в Рослине.
(*) Да, это нарушает принцип, согласно которому "использование" должно указывать на то, что неуправляемый ресурс возвращается в операционную систему. По сути, мы рассматриваем управляемую память как неуправляемый ресурс, делая свое собственное управление, поэтому это не так уж плохо.
Ответ 2
Вы можете обернуть код, который использует тезисы массивов царапин в операторе using следующим образом:
using(double[] scratchArray = new double[buffer])
{
// Code here...
}
Это освободит память явным образом, вызвав дескриптор в конце инструкции using.
К сожалению, похоже, что это не так! Вместо этого вы можете попробовать что-то по линии вспомогательной функции, которая возвращает массив соответствующего размера (ближайшая мощность 2 больше размера), а если он не существует, создавая его. Таким образом, у вас будет только логарифмическое количество массивов. Если вы хотите, чтобы он был потокобезопасным, хотя вам нужно будет немного потрудиться.
Он может выглядеть примерно так: (используя pow2roundup из Алгоритм для нахождения наименьшей мощности двух, более или равных заданному значению)
private static Dictionary<int,double[]> scratchArrays = new Dictionary<int,double[]>();
/// Round up to next higher power of 2 (return x if it 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 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];
}