Сжатие памяти в управляемой среде

Я работаю над жидкостным симулятором на С#. Каждому циклу я должен рассчитать скорость жидкости в дискретных точках в пространстве. В рамках этого расчета мне нужно несколько десятков килобайт для пространства царапин для хранения нескольких двумерных массивов (точный размер массивов зависит от некоторых входных данных). Массивы нужны только в течение всего используемого им метода, и существует несколько разных методов, для которых требуется такое пространство для царапин.

Как я вижу, для построения массивов царапин существует несколько различных решений:

  • Используйте '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];
}