Эффективный способ хранения списка целых чисел в памяти

Я делаю некоторую обработку javascript 3D, и у меня очень большое количество объектов (скажем, объект A), каждый из которых содержит некоторые вещи и массив положительных целых чисел, таких как [0, 1, 4], [1, 5, 74, 1013], etc. Им не нужно иметь частное значение, все объекты могут использовать один и тот же список. Эти числа могут идти от 0 до нескольких тысяч, скажем, 65k (короткий).

профилирование показало, что массивы thoses съедают много памяти. При вычислениях моя программа достигает более 2GB выделенный mem, это не какой-то случай глупой предварительной оптимизации.

У меня есть 2 вывода для оптимизации памяти:

  1. найти более эффективный для памяти способ хранения списков thoses (возможно, массив битов в больших числах?)
  2. найдите способ избежать дублирования. Например, я обнаружил, что некоторые массивы (например, [0,1,2,3,4,5,6]) присутствовали в более чем 40 000 объектах A. возможно, хранение массивов thoses в древовидной структуре и создание моих объектов точкой к ней помочь?

у вас есть предложения?

EDIT: я забыл упомянуть об этом, но это важно: каждое целое число в списке уникально. EDIT2: единственное, что важно получить, - это набор целых чисел, порядок не важен.

Я думаю о преобразовании массивов thoses в "большие целые числа" с побитовыми операциями, т. е. создать большое целое число с некоторым классом, установить биты 1, 5, 74, 1013, преобразовать большой int в строку (массив из 8 bytes) и сохранить строку, но это не всегда будет усиление (например, массив [4000] будет представлен в виде строки длиной 500 байт...)

объем проекта (бесполезно, но меня попросили об этом)

Я создаю 3D-сетку объемных объектов, чтобы упростить, давайте просто предположим, что у меня много сфер. Я знаю их положение (центр, Луч), и я хочу нарисовать их в одной 3D-сетке. Для этого у меня есть структура памяти называется восьмеричного дерева набора это позволяет мне разделить 3D-пространство в нижних ячейках (узлах октрея) вокруг поверхности моего объекта. Затем я могу построить сетку из клеток thoses.

ячейки Thoses - это то, что я назвал объектом A в описании. Каждая ячейка содержит список идентификаторов (положительных целых чисел), которые указывают на объекты сферы, пересекаемые ячейкой.

факт в том, что профилирование показало, что массивы ячеек thoses сохраняют несколько сотен МБ в памяти. Я хотел бы уменьшить это число, найдя способ удалить все дубликаты и / или, если возможно, найти более эффективный способ хранения списка положительных идентификаторов (который может идти от 0 до 65k).

14 ответов


проверьте эту страницу https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays?redirectlocale=en-US&redirectslug=JavaScript%2FTyped_arrays, я содержит низкоуровневые контейнеры javascript, я думаю, что один из них будет соответствовать вашим потребностям


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

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

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

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


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

тогда я бы предложил оценить следующие варианты:

  • попробуйте использовать простые объекты в качестве хэшей вместо массивов
  • перепишите код в Дарт или Java и скомпилировать его в JS, используя компилятор GWT и проверить результаты
  • попробуйте использовать шейдеры WebGL
  • реализуйте части своего кода с помощью собственный клиент SDK или NPAPI (но имейте в виду, что NPAPI устарел)

вот мой ход на это:

  1. Если вы контролируете свой набор данных, вы можете попробовать использовать числовой JS (длиной 8 байт) для хранения большего количества значений (возможно, 4 значения, каждые 2 байта) - точное увеличение памяти зависит от фактического диапазона; также побитовые операции, как правило, довольно быстрые, поэтому не должно быть никакого хита производительности

  2. также вы можете попробовать обернуть свои значения в объект (например, OpNumeric), который является неизменяемым. Каждый раз, когда вам нужно числовое значение, вы спрашиваете NumericManager для обернутого экземпляра и сохраняете ссылку на OpNumeric. Таким образом, все значения 5 будут хранить ссылки на один и тот же объект OpNumeric(5); я не уверен, что размер ссылки в JS (это, вероятно, зависит от возможностей реализации и машины), но стоит попробовать. Также OpNumeric является хорошим кандидатом для реализации (1). Это немного снизит производительность и, вероятно, временно увеличит использование памяти, но это должно быть только при разрешении OpNumeric ссылок.

  3. Если вы генерируете свои массивы на лету (а не добавляя значения один за другим), возможно, стоит хэшировать и повторно использовать их. Поскольку вы уже знаете диапазон, вы можете придумать хэш, который даст вам как можно меньше коллизий; и даже если у вас есть пара коллизий, это должно быть довольно удобно для процессора, чтобы выбрать правильную ссылку по значению по сравнению значений.

извините, это не прямой ответ, но с такими вещами нет прямого способа достижения цели. Может быть, стоит посмотреть на asm.js. Похоже, у вас есть что-то общее с тем, что вы делаете.

спасибо,


Я работал над системами, которые отображали на веб-странице таблицы, содержащие до двадцати пяти тысяч строк данных о клиентах, включая 10 столбцов имени, возраста, адреса, телефона и т. д. и, как вы можете себе представить, мы искали способы оптимизировать отображение этих данных.

очевидно, что создание одной большой таблицы для поиска и сортировки на веб-странице и попытка сохранить все эти данные были свиньей памяти (вы можете себе представить, что она сделала с IE7/8).

четыре решения я придумал:

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

  2. Если локальное хранилище недоступно, я бы загрузил Flash-объект в страница и она извлекла данные с сервера, и я использовал ExternalInterface для доступа к данным по мере необходимости.

  3. Если бы Flash был недоступен, я бы его сохранил в апплете Java и доступе это в аналогичной манере.

  4. не хватает любого из этих вариантов, то пользователь должен был немного пострадать. Им нужно будет ввести некоторые параметры поиска, и они увидят только данные, возвращаемые с помощью вызова ajax и только в управляемом куски. Но если у них не было локального хранилища, Flash или Java свободные, они получили то, что заслужили. ;)


в зависимости от того, насколько разрежен Ваш массив, вы можете хранить диапазоны целых чисел, где у вас обычно есть непрерывные последовательности целых чисел в вашем массиве (т. е. [[1, 5], [10, 14]] вместо [1, 2, 3, 4, 5, 10, 11, 12, 13, 14] или даже [1, 5, 10, 14] поскольку ваш код может предполагать, что он отформатирован в парах). Если Ваш массив очень разрежен, вы все равно можете использовать этот метод, сохраняя диапазоны пробелов в последовательности. Чтобы дать вам представление:

function IntegerArray(integers) {
  this.ranges = [];
  // Convert integers to ranges (Don't have time to overview algorithm,
  // but I think a good start would be sorting the integers and searching
  // for gaps)
}
IntegerArray.prototype = {
  insert: function (n) {
    // Could achieve O(log n) time complexity with binary search since
    // the ranges are sorted.
    // * n is between two ranges = insert [n, n] range.
    // * n is 1 less than the start of a range = decrease range start by 1.
    // * n is 1 more than the end of a range = increase range end by 1.
  },
  remove: function (n) {
    // Also O(log n) time complexity with binary search.
    // * n has [n, n] range = remove range.
    // * n is at beginning of range = shift range.
    // * n is at end of range = pop range.
    // * n is in middle of range = split range.
  }
};

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


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

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

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


идея использования специальной структуры (string или big int с битовой маской) для воссоздания массива каждый раз, я думаю, неверна. Это заставит вас повторно создавать массивы снова и снова, и накладные расходы, которые вы введете при этом (дополнительное время процессора + время GC), вероятно, не стоит. Как и в базе данных, вычисляемое поле является честной игрой для небольших объемов данных, но взрывается у вас перед носом, когда у вас есть большие объемы-лучше хранить вычисленный и непосредственно используемый результат, если производительность имеет значение.

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

каждая ячейка содержит список идентификаторов (положительных целых чисел), которые указывают на объекты сферы, пересекаемые ячейкой.

Это, я читаю как: в зависимости от распределения сферы, вы можете оказаться в патологических случаях, когда вычисление и сохранение extra integer не только использует дополнительный процессор, но и-что самое главное-сохраняет еще больше объектов, чем у вас уже есть. По этой причине, я думаю, я бы тоже исключил это: преимущества в большинстве случаев, пока он не взорвется у вас перед лицом.

Imo, рассмотрите возможность перечисления объектов напрямую, вместо использования и сохранения их идентификаторов. Каждый объект будет существовать в любом случае, и я предполагаю, что каждый должен держать массив пересекающихся сфер вокруг по соображениям производительности. Так мог бы ну, не добавляйте накладные расходы (память + время доступа) наличия дополнительных идентификаторов.

в более общем плане (без полного понимания того, что вы делаете, трудно быть более точным): рассмотрите возможность пересмотра вашей модели данных, чтобы она была более эффективной для начала; не сохраняйте объекты, которые вам не нужны, и попробуйте использовать график объектов вместо хэша объектов, на которые ссылаются идентификаторы.

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


я осмеливаюсь высказать мифологическую мысль.

в этой мифологической мысли,

  1. у вас будет статический массив из 0 - 65k ints-отсортированных.
  2. массив, содержащий metadata для каждого из ваших объектов, и...
    1. slice(x,y) получите кусок массива, начиная с x и заканчивая y из статического массива Шаг 1.
    2. dupeof(oldarray) будет на самом деле вернуть oldarray in во время выполнения.
    3. diff который вычисляет разницу двух массивов
    4. sort это будет сортировать массив ints ... [].sort(function(a,b){ return a-b; });
  3. массив dupes для хранения уникальных дубликатов массивов.

  1. при создании нового объекта (а), вы sort массив, slice из статического массива из первое значение этого A, до последнего значения этого A, найти diff на sliced один, и ваш текущий массив A, магазин, который когда-либо меньше.
  2. учитывая этот сценарий, пример записи (metadata для объекта A) будет выглядеть так:"0-11,[3,5]". На более простом английском языке это следует понимать как construct an array from static array, slice from 0 to 11, and remove 3, 5 from that slice. Мы получим [0,1,2,4,6,7,8,9,10,11,12].

  3. вы должны сделать свой конструктор A пройти через эту логику, пока он будет построен. Итак, на любом момент времени, у вас будет только metadata ваших объектов (ваших A:s). И ваш код будет читать эти данные, чтобы построить свою сетку на лету во время выполнения.

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


массив целых положительных чисел, таких как [0, 1, 4], [1, 5, 74, 1013], и т. д. Им не нужно иметь частное значение, все объекты могут использовать один и тот же список. Эти числа могут идти от 0 до нескольких тысяч, скажем, 65k (короткий).

в одну сторону

замена массива ints массивом флагов в массив int имела бы смысл, если бы массивы были большими объектами. Но поскольку они короткие целые числа, преимущество вероятно минимальный.

возможно заменить числа массивами 32-битных флагов чисел, т. е. например

[ 10, 13, 1029 ]

может быть переведен в блоки размером 32, где 10 и 13 будут падать в одном блоке, и может быть закодирован (1

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

 15, 21, 27, 29, 32, 40, 44

будет:

 15, (21-16=)5, (27-16=)11, 13, 16, 24, 28

и затем:

 15, 1<<5 + 1<<11 + 1<<13 + 1<<16 + 1<<24 + 1<<28

так, что вы сможете хранить 6 дополнительных shortints в одном 32-битном значении. Это работает только в том случае, если массивы "сгруппированы". Если у вас большинство номеров рассеяны, дальше, чем 32, это определенно не будет стоит усилий.

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

другой способ

вы можете попытаться сохранить структуру со всеми массивами, используемыми до сих пор, с методом извлечения массива с учетом его идентификатора. Древовидная структура позволит хранить массивы общих префиксов, но тогда у вас возникнут проблемы при их восстановлении по ID. Так что я боюсь лучшего. вы можете сделать, это использовать очень длинный массив массивов:

[ [ 0, 1, 4 ],             // ID#0
  [ 1, 5, 74, 1013 ],      // ID#1
]

это означает, что поиск того, существует массив или нет, будет стоить. Вам также нужно будет добавить индекс:

{ "1": { "5": { "74": { "1013": { "-1": 1 // The array ending here has id 1

теперь вы храните [ 1, 5, 74, 1013, 1089 ], когда вы придете к ключу 1013, вы не найдете ключа 1089, поэтому вы знаете, что это не дубликат, сохраните его в основном массиве, восстановите его индекс-скажем, 1234 - и добавьте "1089": { "-1": 1234 } к ключу 1013.

добавление массива достаточно быстро, как и доступ к его значениям.

память-мудрый, это стоит того? Каждый массив вместо N целых чисел теперь состоит из N целых чисел плюс (N+1) словарей по крайней мере с одним целым числом каждый, поэтому я бы сказал, что стоимость между тройным и четвертым. Если повторяющихся массивов очень много, это может быть выгодно; в противном случае это может быть не так. Если, скажем, менее трети массивов являются дубликатами, скорее всего, этого не будет.

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

 [ 1 ]       [ 1 ]      [ 1 ]
 [ 2 ]       [ 2 ]      [ 2 ]
 [ 3 ]  -->        -->  [ 5 ]
 [ 4 ]       [ 4 ]      [ 4 ]
 [ 5 ]       [ 5 ]      

поворот

вместо массива и дерева выше вы можете хэшировать список и использовать хэш в качестве идентификатора. Это рискованно коллизии, однако, если вы не использовали stringified list (stringified by you или JS - первый метод thriftier, второй быстрее) в качестве ключа. В этом случае вам понадобится в среднем, скажем, четыре байта на каждое целое число в ключе; меньшие числа будут весить меньше, хотя, поскольку "1.12.13.15.29" составляет всего около 14 байтов или около того. Объем памяти будет утроен для уникальных массивов и обнулен для дубликатов; опять же все дело в том, сколько дубликатов сравнивается с не дубликаты.


нечто подобное делается для lucene / solr. Не стесняйтесь проверять https://github.com/apache/lucene-solr/blob/trunk/solr/core/src/java/org/apache/solr/search/DocSetCollector.java Это java, но я уверен, что вы можете использовать ту же идею для javascript.

короче говоря, изначально они хранят массив int

// in case there aren't that many hits, we may not want a very sparse
// bit array.  Optimistically collect the first few docs in an array
// in case there are only a few.
final int[] scratch;

но позже, если количество совпадающих документов слишком много, они переключаются на Битсет, который просто

bits = new OpenBitSet(maxDoc);

здесь maxDoc-это максимум количество элементов в списке. Я не знаю, можете ли вы найти число в своей задаче, но, возможно, вы знаете, что в списке никогда не бывает больше N целых чисел. (похоже, вы упомянули 65k).

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


  1. найдите более эффективный способ хранения списков thoses

обратите внимание, что JavaScript не имеет целых чисел, только числа с плавающей точкой.

(может быть, массив битов в больших числах?)

Я уверен, что JavaScript не имеет битовых операторов. (Побитовые операции с плавающей точкой не имеют смысла)

найти способ избежать дубликаты.

это позволило бы избежать большого объема памяти, если бы вы могли 1) обнаружить dups, 2) указать их на тот же базовый объект. Это должно быть довольно тривиально, если вы строите структуру раньше времени. Но обнаружение dups во время выполнения снизит производительность. Вы будете иметь, чтобы тест, чтобы увидеть, сколько. С моей головы, я бы сказал, Посмотрите на хранение массивов в Бор. Ваши объекты будут иметь прямой указатель на массив, но при добавлении нового array, вы идете через Trie. Это предотвращает дубликатов.

у вас есть предложения?

если вы работаете в браузере, посмотрите на проект под названием asm.js. Это позволит вам использовать ints.


хранить каждый массив в виде строки. строка по существу представляет собой массив шорт (UTF16) и с интерн среда выполнения позволит избежать дополнительного хранения идентичных массивов / строк. Использовать String.fromCharCode() преобразование числового значения UTF16 в односимвольную строку. Использовать String.charCodeAt() для извлечения числа из строки.

поскольку JavaScript немой о таких вещах, как специальные символы Юникода, такие как объединение символов акцента и даже недопустимый символы length будет работать, как вы ожидаете. То есть, это даст вам число "charCodes" не количество символов Юникода.


вы можете использовать bitset. Существуют эффективные библиотеки, реализующие битовые наборы в JavaScript. См., например:

https://github.com/lemire/FastBitSet.js