Уменьшение объема памяти приложения C#

Я разрабатываю приложение c#, которое должно обрабатывать приблизительно 4,000,000 английских предложений. Все эти предложения хранятся в дереве. Где каждый узел в дереве является классом, который имеет следующие поля:

class TreeNode
{
    protected string word;
    protected Dictionary<string, TreeNode> children;
}

моя проблема в том, что приложение использует всю ОЗУ (у меня есть 2 ГБ ОЗУ), когда она достигает 2,000,000-го предложения. Таким образом, ему удается обработать только половину предложений, а затем он резко замедляется.

что я могу сделать, чтобы попытаться уменьшить объем памяти приложения?

EDIT: позвольте мне объяснить немного больше моего приложения. Таким образом, у меня есть приблизительно 300 000 английских предложений, и из каждого предложения я генерирую следующие предложения:

пример: Предложение: футбол-очень популярный вид спорта Sub предложения мне нужно:

  1. футбол-очень популярный вид спорта
  2. - это очень популярный вид спорта
  3. очень популярный вид спорта
  4. очень популярный вид спорта
  5. популярный вид спорта
  6. спорт

каждое предложение хранится в дереве слово за словом. Поэтому, учитывая приведенный выше пример, у меня есть класс TreeNode со словом field = "футбол", а в списке детей есть TreeNode для слова "is". Ребенок "- это" узел "а" узел. Ребенка на "а" узел "очень" узел. Мне нужно хранить предложения слово за словом, так как мне нужно иметь возможность искать все предложения, начинающиеся с Пример: "футбол есть".

поэтому в основном для каждого слова в предложении я создаю новое (под-предложение). И именно по этой причине я в конечном итоге получаю 4 миллиона различных предложений. Хранение данных в базе данных-это не вариант, так как приложение должно работать на всю структуру сразу. И это еще больше замедлит процесс, если мне придется продолжать записывать все данные в базу данных.

спасибо

9 ответов


что вы используете в качестве ключа? Откуда вы берете данные? Если это слова (не полные setences), мне интересно, есть ли у вас много дублированный ключи (разные string экземпляры с тем же фундаментальным значением), в этом случае вы можете воспользоваться реализацией локального интернера для повторного использования значений (и позволить переходным копиям собирать мусор).

public sealed class StringCache {
    private readonly Dictionary<string,string> values
        = new Dictionary<string,string>(StringComparer.Ordinal);
    public string this[string value] {
        get {
            string cached;
            if (!values.TryGetValue(value, out cached)) {
                values.Add(value, value);
                cached = value;
            }
            return cached;
        }
    }
}

создайте экземпляр этого при построении дерева и используйте (когда вы думаю, значение, скорее всего, будет дублироваться):

StringCache cache = new StringCache(); // re-use this instance while building
                                       // your tree
...
string s = ... // whatever (from reading your input)
s = cache[s];

сам тип словаря может потреблять много памяти. Вы рассматривали возможность использования ? Родовое List использует намного меньше памяти на экземпляр, чем универсальный Dictionary.

конечно, ограничение использования списка вместо словаря заключается в том, что вы не получаете автоматического индексирования по строкам. Это явный обмен между временем и пространством. Если списки короткие, это может быть даже быстрее, чем словарь (линейный поиск ~10 ключей часто происходит чтобы быть быстрее, чем поиск hashtable). Даже если по крайней мере большинство из списков короткие, это все еще может быть большим улучшением (например, если 95% списков имеют 10 или меньше элементов, а другие 5% имеют максимум, возможно, 100 элементов).

вы даже можете использовать Collection<KeyValuePair<string, TreeNode>>, который использует даже меньше памяти, чем List<T>.


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


не могли бы вы сопоставить каждое слово в int? Таким образом, у вас есть одна карта int to string, которая содержит уникальные английские слова и древовидную структуру, содержащую такие предложения:

class TreeNode
{
    protected int word;
    protected Dictionary<int, TreeNode> children;
}

Dictionary<string, int> _AllWords;

теперь _AllWords коллекция не является оптимальным для поиска слов на основе ключа как. То, что вы, вероятно, хотите здесь,-это что-то вроде списка с несколькими ключами, где вы можете сделать быстрый поиск на основе ключа и значения. CodeProject есть статья об этом.


Это может быть излишним для вашей ситуации, но вы могли бы хранить свои узлы в файлах на диске и использовать B-Tree реализация для максимизации производительности ввода-вывода. Это то, что большинство баз данных используют внутри, потому что слишком много данных для хранения в памяти.


некоторые моменты, о которых нужно подумать.

  1. когда вы инициализируете свой словарь, передайте максимальное количество элементов, которые вам нужны. Это заставит его выделить достаточное количество ведер при запуске. По умолчанию инициализируется с помощью 0 ведер, что равно 3 (prime). После добавления дополнительных элементов словарь должен повторно инициализировать и скопировать все элементы в новое более крупное хранилище. Если программа никогда не бездельничает, то ГК не будет собирать старые словари.
  2. вы могли бы сэкономить пространство кодировка строк. Строки будут использовать два байта на символ в памяти. С помощью некоторых вспомогательных функций вы можете иметь свой класс следующим образом:
    class TreeNode
    {
        protected byte[] word;
        protected Dictionary<byte[], TreeNode> children;

        public string Word
        {
            get { return Encoding.UTF8.GetString(word); }
            set { word = Encoding.UTF8.GetBytes(value); }
        }

        public TreeNode GetChildByKey( string key )
        {
            TreeNode node;
            if(children.TryGetValue( Encoding.UTF8.GetBytes(key), out node  ))
            {
                return node;
            }
            return null;
        }
    }

[редактирование] И я забыл, что вам также нужен новый компаратор для ключа byte [].

var children = new Dictonary<string,TreeNode>(new ByteArrayComparer);

public class ByteArrayComparer : IEqualityComparer<byte[]>
{
    public bool Equals(byte[] x, byte[] y)
    {
        if (x.Length != y.Length)
            return false;

        for (int i = 0; i < x.Length; i++)
        {
            if (x[i] != y[i])
                return false;
        }

        return true;
    }

    public int GetHashCode(byte[] a)
    {
        return a[0] | (int)a[1] << 8 | (int)a[2] << 16 | (int)a[3] << 24;
    }
}

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

чего вы пытаетесь достичь? Зачем ты строишь дерево? Если вы что-то подсчитываете, подсчитайте и отбросьте строки по мере их чтения. Если вы строите график (т. е. чтобы проанализировать отношения между предложением и/или словами), попробуйте перечислить предложения и слова, чтобы они могли быть уникальными / ключевыми по этому идентификатору. Используйте этот id в памяти вместо.

надеюсь, это поможет.


чтобы уменьшить объем памяти, вы должны искать Последовательный Кэш Данных.

Это позволяет снизить нагрузку на память в коллекции. (Элемент коллекции должен быть помечен как [Serializable])

вы даже можете сделать коллекцию постоянной, передав deleteOnClose: false параметр

пример

using (var c = SequentialDataCache<TreeNode>.Initialize(deleteOnClose: false))
        {
            //add items to collection
            for (int i = 0; i < 1000; i++)
            {
                var treeNode = new TreeNode()
                                   {
                                       Word = string.Format("Word{0}", i),
                                       Children = new Dictionary<string, TreeNode>()
                                   };
                for (int j = 0; j < 100; j++)
                {
                    var child = new TreeNode() { Word = string.Format("Word{0}", j) };
                    treeNode.Children.Add(string.Format("key{0}{1}", i, j), child);
                }
                c.Add(treeNode);
            }

            //assert query
            Assert.AreEqual("Word0", c[0].Word);
            Assert.AreEqual("Word1", c[0].Children["key01"].Word);
            Assert.AreEqual("Word100", c[100].Word);
        }

и TreeNode...

    [Serializable]
    class TreeNode
    {
        private string word;
        private Dictionary<string, TreeNode> children;

        public string Word
        {
            get { return word; }
            set { word = value; }
        }

        public Dictionary<string, TreeNode> Children
        {
            get { return children; }
            set { children = value; }
        }
    }

Отличный вопрос, и некоторые отличные ответы. Я многому научился. Идея StringCache заслуживает некоторого исследования.

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

считайте, что надежный SQL Database engine (я парень MSSQL):

  • может содержать гораздо больше данных, размер диска, а не размер памяти и пространства подкачки. (База данных SQL может также используйте память и диск на отдельной машине, тем самым увеличивая доступный след с компромиссом задержки сети.)
  • индексирует данные для быстрого извлечения
  • динамически кэширует данные, используемые чаще всего и освобождает менее используемые данные, когда давление памяти диктует.
  • использует алгоритмы хранения, извлечения и кэширования, разработанные большой командой на протяжении многих лет и настроенные для динамической адаптации к широкому спектру положения.

динамическое кэширование может быть огромным преимуществом для данного множества решений. Предполагая, что ваш корпус состоит только из" нормальных " предложений, распределение слов не будет равномерным. Наиболее частые слова будут доступны на несколько порядков чаще, чем наименее частые. Также вероятно, что частые слова будут добавлены в словарь очень рано, и поэтому будут храниться близко друг к другу в базе данных. Хороший SQL-движок будет кэшировать больше всего часто используемые блоки в памяти, что, естественно, способствует типу поиска, который вы описываете.

гибридное решение может выглядеть так:

  • таблица с соответствующими индексами

    create table myWords (wordKey int identity, word varchar(50))
    create unique index iword 
      on myWords(word)  -- used for adds and retrieval
    create unique index iwordKey 
      on myWords(wordKey) -- used for mapping keys back to words
    
  • хранимая процедура добавления / поиска слов. Хранимые процедуры удобно возвращают int.

    create procedure addWord (@word varchar(50))
    as
    begin
      declare @wordKey int, @rows int
      insert myWords (word)
        select @word
        where not exists (select 1 from myWords where word = @word)
      select @wordKey = @@identity, @rows = @@rowcount
      if @rows = 0
      begin
        select @wordKey = wordKey
          from myWords
          where word = @word
      end
      return @wordKey
    end
    
  • приложение добавляет слова в базу данных, строит дерево в памяти, используя только wordKey ценности.

  • поиск соответствующего предложения начнется с запроса, чтобы получить значения wordKey для рассматриваемых слов, затем проанализируйте дерево, соберите wordKeys, необходимые для построения полных предложений, и, наконец, получить эти слова с помощью второго запроса.

вы можете торговать небольшой скоростью создания базы данных для дальнейшей оптимизации выгоды от кэширования наиболее частых слов.

  1. добавить поле в таблицу (usageCount int). Вставки установите его в 1, обновляет инкремент.
  2. только с индексом на word, заполнить таблицу словаря из вашего корпуса
  3. добавьте кластеризованный индекс в usageCount (desc), который будет реорганизован, чтобы приблизить наиболее частые слова. (Может быть, отбросьте его снова - хорошая работа завершена.)
  4. построить свое дерево.

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