В C ++ есть std :: vector, а в Java - ArrayList, а во многих других языках есть собственная форма динамически выделяемого массива. Когда в динамическом массиве заканчивается пространство, он перераспределяется в большую область, а старые значения копируются в новый массив. Центральным вопросом для производительности такого массива является то, насколько быстро массив увеличивается в размере. Если вы всегда становитесь достаточно большим, чтобы соответствовать текущему толчку, вы будете каждый раз перераспределять. Поэтому имеет смысл удвоить размер массива или умножить его, скажем, на 1,5 раза.
Есть ли идеальный фактор роста? 2x? В 1,5 раза? Под идеалом я подразумеваю математически оправданный, лучший баланс производительности и потраченной впустую памяти. Я понимаю, что теоретически, учитывая, что ваше приложение может иметь любое потенциальное распределение толчков, это в некоторой степени зависит от приложения. Но мне любопытно узнать, есть ли значение, которое «обычно» лучше всего, или считается лучшим в рамках каких-то строгих ограничений.
Я слышал, что где-то есть бумага по этому поводу, но мне не удалось ее найти.
В идеале (в пределе n → ∞) это золотое сечение : ϕ = 1,618 ...
На практике вам нужно что-то близкое, например 1.5.
Причина в том, что вы хотите иметь возможность повторно использовать старые блоки памяти, чтобы воспользоваться преимуществами кеширования и не заставлять ОС постоянно предоставлять вам больше страниц памяти. Уравнение, которое вы должны решить, чтобы убедиться, что это сводится к x n - 1 - 1 = x n + 1 - x n , решение которого приближается к x = ϕ для больших n .
источник
Один из подходов к ответам на подобные вопросы - просто «обмануть» и посмотреть, что делают популярные библиотеки, исходя из предположения, что широко используемая библиотека, по крайней мере, не делает чего-то ужасного.
Итак, просто очень быстро проверяя, Ruby (1.9.1-p129), похоже, использует 1,5x при добавлении в массив, а Python (2.6.2) использует 1,125x плюс константа (in
Objects/listobject.c
):newsize
выше указано количество элементов в массиве. Обратите внимание, чтоnewsize
это добавленоnew_allocated
, поэтому выражение с битовыми сдвигами и тернарным оператором на самом деле просто вычисляет избыточное распределение.источник
Допустим, вы увеличили размер массива на
x
. Итак, предположим, вы начали с размераT
. В следующий раз, когда вы увеличите массив, его размер будетT*x
. Потом будетT*x^2
и так далее.Если ваша цель состоит в том, чтобы иметь возможность повторно использовать память, которая была создана ранее, вы должны убедиться, что новая выделяемая вами память меньше суммы предыдущей памяти, которую вы освободили. Следовательно, имеем это неравенство:
Мы можем удалить T с обеих сторон. Получаем вот что:
Неформально мы говорим, что при
nth
распределении мы хотим, чтобы вся наша ранее освобожденная память была больше или равна потребности в памяти при n-м распределении, чтобы мы могли повторно использовать ранее освобожденную память.Например, если мы хотим сделать это на 3-м шаге (т. Е.
n=3
), То у нас естьЭто уравнение верно для всех x таких, что
0 < x <= 1.3
(примерно)Посмотрите, какие x мы получаем для разных n ниже:
Обратите внимание, что коэффициент роста должен быть меньше, чем
2
с тех порx^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2
.источник
Это действительно зависит от обстоятельств. Некоторые люди анализируют распространенные варианты использования, чтобы найти оптимальное количество.
Я видел 1.5x 2.0x phi x и раньше использовал power of 2.
источник
Если у вас есть распределение по длинам массива и у вас есть функция полезности, которая говорит, насколько вам нравится тратить пространство впустую, а не тратить время, то вы определенно можете выбрать оптимальную стратегию изменения размера (и начального изменения размера).
Причина, по которой используется простое постоянное кратное, очевидно, заключается в том, что каждое добавление имеет амортизированное постоянное время. Но это не значит, что вы не можете использовать другое (большее) соотношение для небольших размеров.
В Scala вы можете переопределить loadFactor для хэш-таблиц стандартной библиотеки с помощью функции, которая смотрит на текущий размер. Как ни странно, массивы с изменяемым размером просто удваиваются, что большинство людей и делает на практике.
Я не знаю никаких массивов с удвоением (или 1.5 * ing), которые действительно вылавливают ошибки памяти и в этом случае становятся меньше. Кажется, что если бы у вас был один огромный массив, вы бы захотели это сделать.
Я бы также добавил, что если вы достаточно долго храните массивы с изменяемым размером и предпочитаете пространство с течением времени, может иметь смысл сначала резко перераспределить (для большинства случаев), а затем перераспределить до точно нужного размера, когда вы сделанный.
источник
Еще два цента
old*3/2
поэтому нет необходимости в операциях с плавающей запятой. (Я говорю/2
потому что компиляторы заменят это сдвигом бит в сгенерированном коде сборки, если сочтут нужным.)Как сказал кто-то, 2 чувствует себя лучше, чем 8. И также 2 чувствует себя лучше, чем 1.1.
Я считаю, что 1.5 - хороший вариант по умолчанию. В остальном это зависит от конкретного случая.
источник
n + n/2
для задержки переполнения. Использованиеn*3/2
сокращает вашу возможную емкость наполовину.Я согласен с Джоном Скитом, даже мой друг-теоретик настаивает на том, что это может быть доказано как O (1), если установить коэффициент равным 2x.
Соотношение между временем процессора и объемом памяти на разных машинах разное, поэтому коэффициент будет также меняться. Если у вас есть машина с гигабайтами оперативной памяти и медленным процессором, копирование элементов в новый массив будет намного дороже, чем на быстрой машине, которая, в свою очередь, может иметь меньше памяти. На этот вопрос теоретически можно дать ответ для единого компьютера, который в реальных сценариях вам совершенно не помогает.
источник
Я знаю, что это старый вопрос, но есть несколько вещей, которые всем, кажется, не хватает.
Во-первых, это умножение на 2: size << 1. Это умножение на что-либо от 1 до 2: int (float (size) * x), где x - это число, * - математика с плавающей запятой, а процессор имеет для запуска дополнительных инструкций по приведению типов между float и int. Другими словами, на машинном уровне для поиска нового размера для удвоения требуется одна очень быстрая инструкция. Для умножения на значение от 1 до 2 требуется как минимумодна инструкция для преобразования размера в число с плавающей запятой, одна инструкция для умножения (это умножение с плавающей запятой, поэтому, вероятно, потребуется как минимум в два раза больше циклов, если не в 4 или даже в 8 раз больше) и одна инструкция для возврата к int, и это предполагает, что ваша платформа может выполнять вычисления с плавающей запятой в регистрах общего назначения, вместо того, чтобы требовать использования специальных регистров. Короче говоря, вы должны ожидать, что математика для каждого распределения займет как минимум в 10 раз больше времени, чем простой сдвиг влево. Если вы копируете много данных во время перераспределения, это не может иметь большого значения.
Во-вторых, и это, вероятно, самый важный момент: все, кажется, полагают, что освобождаемая память является смежной с самой собой, а также с недавно выделенной памятью. Если вы заранее не распределяете всю память, а затем используете ее как пул, это почти наверняка не так. ОС может иногдав конечном итоге это произойдет, но в большинстве случаев фрагментации свободного пространства будет достаточно, чтобы любая полуприличная система управления памятью смогла найти небольшую дыру, в которой ваша память просто поместится. Как только вы доберетесь до действительно битовых блоков, у вас больше шансов получить непрерывные части, но к тому времени ваши выделения будут достаточно большими, чтобы вы не выполняли их достаточно часто, чтобы это больше не имело значения. Короче говоря, забавно представить, что использование некоторого идеального числа позволит наиболее эффективно использовать свободное пространство памяти, но на самом деле этого не произойдет, если ваша программа не будет работать на голом железе (например, нет ОС под ним принимаются все решения).
Мой ответ на вопрос? Нет, идеального числа не существует. Это настолько специфично для приложения, что никто даже не пытается. Если ваша цель - идеальное использование памяти, вам в значительной степени не повезло. Для производительности лучше использовать менее частое распределение, но если бы мы пошли именно так, мы могли бы умножить на 4 или даже на 8! Конечно, когда Firefox перескакивает с 1 ГБ на 8 ГБ за один раз, люди будут жаловаться, так что это даже не имеет смысла. Вот несколько практических правил, которые я бы придерживался:
Если вы не можете оптимизировать использование памяти, по крайней мере, не теряйте циклы процессора. Умножение на 2 по крайней мере на порядок быстрее, чем вычисления с плавающей запятой. Это может не иметь большого значения, но, по крайней мере, будет иметь какое-то значение (особенно на ранних этапах, при более частом и меньшем распределении).
Не зацикливайтесь на этом. Если вы потратили 4 часа, пытаясь понять, как сделать что-то, что уже было сделано, вы просто зря потратили время. Честно говоря, если бы был вариант лучше, чем * 2, это было бы сделано в векторном классе C ++ (и во многих других местах) несколько десятилетий назад.
Наконец, если вы действительно хотите оптимизировать, не переживайте по мелочам. В наши дни никого не волнует потеря 4 КБ памяти, если только они не работают со встроенными системами. Когда вы получаете 1 ГБ объектов размером от 1 до 10 МБ каждый, удвоение, вероятно, слишком много (я имею в виду, что это от 100 до 1000 объектов). Если вы можете оценить ожидаемую скорость расширения, вы можете выровнять ее до линейной скорости роста в определенный момент. Если вы ожидаете около 10 объектов в минуту, то увеличение размера от 5 до 10 объектов за шаг (от 30 секунд до минуты), вероятно, будет нормальным.
Все сводится к тому, что не думайте слишком много, оптимизируйте то, что вы можете, и при необходимости настраивайте свое приложение (и платформу).
источник
n + n >> 1
же, как1.5 * n
. Достаточно легко придумать аналогичные приемы для каждого практического фактора роста, о котором вы только можете подумать.