C# OutOfMemory, сопоставленный файл памяти или временная база данных

Прошу совета, передового опыта и т.д.

Технология: C# .NET4.0, Winforms, 32 бит

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

Проблема в том, что мы выполняем обработку большого количества данных «в памяти», когда открывается «корзина». Проще говоря, когда загружается "корзина для покупок", мы выполняем следующие вычисления:

  1. Для каждого товара в «корзине для покупок» извлеките его историческую цену, начиная с даты, когда товар впервые появился на складе (это могут быть данные за два месяца, два года или два десятилетия). Исторические ценовые данные извлекаются из текстовых файлов через Интернет в любом формате, который поддерживается ценовым плагином.

  2. Для каждого товара, за каждый день с момента его первого появления на складе рассчитываются различные показатели, которые создают исторический профиль для каждого товара в корзине.

В результате мы потенциально можем выполнять сотни, тысячи и/или миллионы вычислений в зависимости от количества товаров в «корзине». Если в корзине слишком много элементов, мы рискуем столкнуться с исключением OutOfMemory.

Несколько предостережений;

  1. Эти данные необходимо рассчитать для каждого товара в «корзине для покупок», и данные хранятся до тех пор, пока «корзина для покупок» не будет закрыта.

  2. Несмотря на то, что мы выполняем шаги 1 и 2 в фоновом потоке, скорость важна, поскольку количество элементов в «корзине» может сильно повлиять на общую скорость вычислений.

  3. Память спасается сборщиком мусора .NET, когда «корзина для покупок» закрывается. Мы профилировали наше приложение и следим за тем, чтобы все ссылки правильно размещались и закрывались при закрытии корзины.

  4. После завершения всех вычислений результирующие данные сохраняются в IDictionary. «CalculatedData — это объект класса, свойства которого представляют собой отдельные метрики, рассчитанные с помощью описанного выше процесса.

Несколько идей, о которых я подумал;

Очевидно, что моей главной задачей является уменьшение объема памяти, используемой вычислениями, однако объем используемой памяти может быть уменьшен только в том случае, если я
1) уменьшу количество вычисляемых метрик для каждого дня или
2) уменьшу количество дней, используемых для расчета.

Оба эти варианта не являются жизнеспособными, если мы хотим выполнить наши бизнес-требования.

  • Файлы с отображением памяти
    Одна из идей заключалась в использовании файлов с отображением памяти, в которых будет храниться словарь данных. Будет ли это возможно/выполнимо и как мы можем реализовать это?

  • Используйте временную базу данных
    Идея состоит в том, чтобы использовать отдельную базу данных (не в памяти), которую можно создать для жизненного цикла приложения. Когда «корзины для покупок» открываются, мы можем сохранять рассчитанные данные в базе данных для повторного использования, устраняя необходимость повторного расчета для одной и той же «корзины для покупок».

Есть ли другие альтернативы, которые мы должны рассмотреть? Какова наилучшая практика, когда речь идет о вычислениях с большими данными и их выполнении вне оперативной памяти?

Любые советы приветствуются....


person paligap    schedule 26.02.2012    source источник
comment
OutOfMemoryException на самом деле недостаточно адресного пространства. Вы рассматривали возможность перехода на 64-разрядную версию?   -  person Damien_The_Unbeliever    schedule 26.02.2012


Ответы (3)


Самое простое решение — база данных, возможно, SQLite. Файлы с отображением памяти не становятся автоматически словарями, вам придется самостоятельно кодировать все управление памятью и, таким образом, бороться с самой системой .net GC за право владения данными.

person Joel Lucsy    schedule 26.02.2012

Если вы заинтересованы в том, чтобы попробовать подход с отображением файлов в память, вы можете попробовать его прямо сейчас. Я написал небольшой собственный пакет .NET под названием MemMapCache, который, по сути, создает базу данных ключей/значений, поддерживаемую MemMappedFiles. Это немного хакерская концепция, но программа MemMapCache.exe хранит все ссылки на отображаемые в память файлы, так что в случае сбоя вашего приложения вам не придется беспокоиться о потере состояния вашего кеша.

Он очень прост в использовании, и вы сможете вставить его в свой код без особых модификаций. Вот пример его использования: https://github.com/jprichardson/MemMapCache/blob/master/TestMemMapCache/MemMapCacheTest.cs

Возможно, вам было бы полезно хотя бы дополнительно выяснить, что вам нужно сделать для фактического решения.

Пожалуйста, дайте мне знать, если вы в конечном итоге используете его. Мне будут интересны ваши результаты.

Однако в долгосрочной перспективе я бы рекомендовал Redis.

person JP Richardson    schedule 26.02.2012
comment
Спасибо, ДжейПи. Redis — это база данных NoSQL, почему вы предпочитаете ее другим? Что вы считаете его плюсами и минусами? Спасибо за подсказку, я не знал об этом и буду разбираться. - person paligap; 26.02.2012
comment
Redis безумно быстр. Его набор команд очень прост, и человек может освоить его менее чем за 30 минут. Это сетевая БД, которая может масштабироваться горизонтально. Это не ограничивается объектами размером 1 МБ. - person JP Richardson; 28.02.2012
comment
не могли бы вы ответить на мой вопрос? /вопросы/9760073/ - person Royi Namir; 18.03.2012

В качестве обновления для тех, кто наткнулся на эту тему...

В итоге мы использовали SQLite в качестве решения для кэширования. Используемая нами база данных SQLite существует отдельно от основного хранилища данных, используемого приложением. Мы сохраняем рассчитанные данные в SQLite (diskCache) по мере необходимости, и у нас есть код, управляющий аннулированием кеша и т. д. Это было подходящим решением для нас, поскольку мы смогли добиться увеличения скорости записи и около 100 000 записей в секунду.

Для тех, кому интересно, это код, который управляет вставками в diskCache. Вся ответственность за этот код принадлежит Дж. П. Ричардсону (здесь он отвечает на вопрос) за его прекрасную запись в блоге.

internal class SQLiteBulkInsert
{
#region Class Declarations

private SQLiteCommand m_cmd;
private SQLiteTransaction m_trans;
private readonly SQLiteConnection m_dbCon;

private readonly Dictionary<string, SQLiteParameter> m_parameters = new Dictionary<string, SQLiteParameter>();

private uint m_counter;

private readonly string m_beginInsertText;

#endregion

#region Constructor

public SQLiteBulkInsert(SQLiteConnection dbConnection, string tableName)
{
    m_dbCon = dbConnection;
    m_tableName = tableName;

    var query = new StringBuilder(255);
    query.Append("INSERT INTO ["); query.Append(tableName); query.Append("] (");
    m_beginInsertText = query.ToString();
}

#endregion

#region Allow Bulk Insert

private bool m_allowBulkInsert = true;
public bool AllowBulkInsert { get { return m_allowBulkInsert; } set { m_allowBulkInsert = value; } }

#endregion

#region CommandText

public string CommandText
{
    get
    {
        if(m_parameters.Count < 1) throw new SQLiteException("You must add at least one parameter.");

        var sb = new StringBuilder(255);
        sb.Append(m_beginInsertText);

        foreach(var param in m_parameters.Keys)
        {
            sb.Append('[');
            sb.Append(param);
            sb.Append(']');
            sb.Append(", ");
        }
        sb.Remove(sb.Length - 2, 2);

        sb.Append(") VALUES (");

        foreach(var param in m_parameters.Keys)
        {
            sb.Append(m_paramDelim);
            sb.Append(param);
            sb.Append(", ");
        }
        sb.Remove(sb.Length - 2, 2);

        sb.Append(")");

        return sb.ToString();
    }
}

#endregion

#region Commit Max

private uint m_commitMax = 25000;
public uint CommitMax { get { return m_commitMax; } set { m_commitMax = value; } }

#endregion

#region Table Name

private readonly string m_tableName;
public string TableName { get { return m_tableName; } }

#endregion

#region Parameter Delimiter

private const string m_paramDelim = ":";
public string ParamDelimiter { get { return m_paramDelim; } }

#endregion

#region AddParameter

public void AddParameter(string name, DbType dbType)
{
    var param = new SQLiteParameter(m_paramDelim + name, dbType);
    m_parameters.Add(name, param);
}

#endregion

#region Flush

public void Flush()
{
    try
    {
        if (m_trans != null) m_trans.Commit();
    }
    catch (Exception ex)
    {
        throw new Exception("Could not commit transaction. See InnerException for more details", ex);
    }
    finally
    {
        if (m_trans != null) m_trans.Dispose();

        m_trans = null;
        m_counter = 0;
    }
}

#endregion

#region Insert

public void Insert(object[] paramValues)
{
    if (paramValues.Length != m_parameters.Count) 
        throw new Exception("The values array count must be equal to the count of the number of parameters.");

    m_counter++;

    if (m_counter == 1)
    {
        if (m_allowBulkInsert) m_trans = m_dbCon.BeginTransaction();
        m_cmd = m_dbCon.CreateCommand();

        foreach (var par in m_parameters.Values)
            m_cmd.Parameters.Add(par);

        m_cmd.CommandText = CommandText;
    }

    var i = 0;
    foreach (var par in m_parameters.Values)
    {
        par.Value = paramValues[i];
        i++;
    }

    m_cmd.ExecuteNonQuery();

    if(m_counter != m_commitMax)
    {
        // Do nothing
    }
    else
    {
        try
        {
            if(m_trans != null) m_trans.Commit();
        }
        catch(Exception)
        { }
        finally
        {
            if(m_trans != null)
            {
                m_trans.Dispose();
                m_trans = null;
            }

            m_counter = 0;
        }
    }
}

#endregion

}

person paligap    schedule 23.09.2012