Какова идеальная скорость роста для динамически выделяемого массива?

В C ++ есть std :: vector, а в Java - ArrayList, а во многих других языках есть собственная форма динамически выделяемого массива. Когда в динамическом массиве заканчивается пространство, он перераспределяется в большую область, а старые значения копируются в новый массив. Центральным вопросом для производительности такого массива является то, насколько быстро массив увеличивается в размере. Если вы всегда становитесь достаточно большим, чтобы соответствовать текущему толчку, вы будете каждый раз перераспределять. Поэтому имеет смысл удвоить размер массива или умножить его, скажем, на 1,5 раза.

Есть идеальный фактор роста? 2x? В 1,5 раза? Под идеалом я подразумеваю математически обоснованный, наилучший баланс между производительностью и потраченной впустую памятью. Я понимаю, что теоретически, учитывая, что ваше приложение может иметь любое потенциальное распределение толчков, это в некоторой степени зависит от приложения. Но мне любопытно узнать, есть ли значение, которое «обычно» лучше всего, или считается лучшим в рамках каких-то строгих ограничений.

Я слышал, что где-то есть бумага по этому поводу, но мне не удалось ее найти.


person Joseph Garvin    schedule 08.07.2009    source источник


Ответы (11)


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

Не существует такого понятия, как «идеальный фактор роста». Он не только теоретически зависит от приложения, он определенно зависит от приложения.

2 - довольно распространенный фактор роста - я почти уверен, что ArrayList и List<T> в .NET используют именно его. ArrayList<T> в Java использует 1.5.

РЕДАКТИРОВАТЬ: Как указывает Эрих, Dictionary<,> в .NET использует «удвоить размер, а затем увеличить до следующего простого числа», чтобы значения хэша можно было разумно распределить между сегментами. (Я уверен, что недавно видел документацию, в которой говорится, что простые числа на самом деле не так хороши для распределения хеш-ведер, но это аргумент в пользу другого ответа.)

person Jon Skeet    schedule 08.07.2009

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

Аргументация такова:

  1. Допустим, вы начинаете с выделения памяти размером 16 байт.
  2. Когда вам нужно больше, вы выделяете 32 байта, а затем освобождаете 16 байтов. Это оставляет в памяти 16-байтовую дыру.
  3. Когда вам нужно больше, вы выделяете 64 байта, освобождая 32 байта. Это оставляет 48-байтовое отверстие (если 16 и 32 были смежными).
  4. Когда вам нужно больше, вы выделяете 128 байтов, освобождая 64 байта. Это оставляет 112-байтовую дыру (при условии, что все предыдущие выделения являются смежными).
  5. И так и так далее.

Идея состоит в том, что при двукратном расширении нет момента времени, когда образовавшаяся дыра когда-либо станет достаточно большой для повторного использования для следующего распределения. Используя выделение 1.5x, мы получаем следующее:

  1. Начните с 16 байтов.
  2. Когда вам нужно больше, выделите 24 байта, затем освободите 16, оставив 16-байтовое отверстие.
  3. Когда вам нужно больше, выделите 36 байтов, затем освободите 24, оставив 40-байтовую дыру.
  4. Когда вам нужно больше, выделите 54 байта, затем освободите 36, оставив 76-байтовую дыру.
  5. Когда вам нужно больше, выделите 81 байт, затем освободите 54, оставив 130-байтовое отверстие.
  6. Когда вам нужно больше, используйте 122 байта (округляя в большую сторону) из 130-байтовой дыры.
person Chris Jester-Young    schedule 08.07.2009
comment
Я нашел случайное сообщение на форуме (objectmix.com/c /) рассуждает аналогично. Плакат утверждает, что (1 + sqrt (5)) / 2 - это верхний предел для повторного использования. - person Naaff; 09.07.2009
comment
Если это утверждение верно, то phi (== (1 + sqrt (5)) / 2) действительно является оптимальным числом для использования. - person Chris Jester-Young; 09.07.2009
comment
Мне нравится этот ответ, потому что он раскрывает разумное значение 1,5х против 2х, но Джона технически наиболее верен в том смысле, в котором я его сформулировал. Мне следовало просто спросить, почему в прошлом рекомендовали 1.5: p - person Joseph Garvin; 15.04.2010
comment
Facebook использует версию 1.5 в своей реализации FBVector, статья здесь объясняет, почему 1.5 оптимальна для FBVector. - person csharpfolk; 04.11.2014
comment
Это может быть менее важно для некоторых управляемых языков, потому что сборщик мусора может переупорядочивать кучу. - person jackmott; 16.08.2016
comment
@jackmott Правильно, именно так, как было отмечено в моем ответе: это, вероятно, не относится к управляемым языкам, где система времени выполнения может перемещать объекты по своему желанию. - person Chris Jester-Young; 16.08.2016
comment
Существуют ли какие-либо тесты, которые показывают от 1,5x до 2,0x? Статья в Facebook не включает ни одного. - person Andrew Bainbridge; 01.01.2017
comment
@AndrewBainbridge Выбор 1,5x не потому, что он быстрее 2x. Дело в том, что для неперемещаемых распределителей памяти он не оставляет больших неиспользуемых пробелов в памяти. Очевидно, что если вы используете платформу (например, JVM), которая выполняет перемещение памяти, это не имеет значения. - person Chris Jester-Young; 02.01.2017
comment
@Chris Хорошо, спасибо за это. В статье FBVector упоминается, что 2x недружелюбен к кешу. Я предположил, что 1.5x более дружественен к кешу и, следовательно, в некоторых случаях быстрее. Думаю, я слишком много вложил в это замечание. - person Andrew Bainbridge; 02.01.2017
comment
Я пойду с 2х. Просто попробуйте разместить новый 1x за концом, а затем разместить новый там. - person jdh8; 17.08.2017
comment
Мертвая ссылка @Naaff теперь ссылается на adv. Сохраненная ссылка: archive.li/ykdHM - person theGiallo; 05.05.2018
comment
Этот ответ был бы правильным, если бы память распределялась последовательно. Этого нет ни в одном современном распределителе с высокой производительностью. Фактически, выделения разных размеров поступают из разных блоков памяти, поэтому выделение большего размера никогда не может повторно использовать память, которая ранее использовалась для выделения меньшего размера. - person Ilya Popov; 12.05.2020

В пределе n → ∞, это будет золотое сечение : ϕ = 1,618 ...

Для конечного n вам нужно что-то близкое, например 1.5.

Причина в том, что вы хотите иметь возможность повторно использовать старые блоки памяти, чтобы воспользоваться преимуществами кеширования и не заставлять ОС постоянно предоставлять вам больше страниц памяти. Уравнение, которое вы должны решить, чтобы гарантировать, что последующее выделение может повторно использовать все предыдущие блоки, уменьшается до x n - 1 < / sup> - 1 = x n + 1 - x n , решение которого приближается к x = ϕ для больших n. На практике n конечно, и вы захотите иметь возможность повторно использовать последние несколько блоков каждые несколько выделений, поэтому 1.5 отлично подходит для этого.
(Подробнее см. Ссылку детальное объяснение.)

person user541686    schedule 09.12.2013
comment
(Не знаю, почему вы удалили все наши комментарии, но я хотел бы добавить несколько нейтральных пояснений для всех, кто сталкивается с этим.) Чтобы уточнить, n в этом ответе не является размером array, это минимальное количество перераспределений, прежде чем вы сможете повторно использовать память. Таким образом, n → ∞ не означает, что массив растет до бесконечности, это означает, что чем выше ваша устойчивость к потраченной впустую памяти, тем ближе к золотому сечению, по вашему желанию, будет ваш фактор роста. Обратите внимание, что этот расчет имеет практический смысл только для малых n и темпов роста дальше от ϕ, потому что - person Han Seoul-Oh; 24.05.2021
comment
большой, но конечный n, со скоростью роста, приближающейся к ϕ, означал бы, что вы сможете повторно использовать старые блоки памяти только после множества перераспределений; если ваш вариант использования настолько нечувствителен к потере памяти, скорость роста в 2 раза будет лучше, чем скорость, близкая к ϕ. - person Han Seoul-Oh; 24.05.2021

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

Так что просто очень быстро проверяя, Ruby (1.9.1-p129), кажется, использует 1.5x при добавлении в массив, а Python (2.6.2) использует 1.125x плюс константа (in _ 1_):

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
    PyErr_NoMemory();
    return -1;
} else {
    new_allocated += newsize;
}

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

person Jason Creighton    schedule 08.07.2009
comment
Таким образом, он увеличивает массив от n до n + (n / 8 + (n ‹9? 3: 6)), что означает, что коэффициент роста, в терминологии вопроса, составляет 1,25x (плюс константа). - person ShreevatsaR; 09.07.2009
comment
Разве это не было бы 1,125x плюс константа? - person Jason Creighton; 09.07.2009

Допустим, вы увеличили размер массива на x. Итак, предположим, вы начали с размера T. В следующий раз, когда вы увеличите массив, его размер будет T*x. Тогда будет T*x^2 и так далее.

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

T*x^n <= T + T*x + T*x^2 + ... + T*x^(n-2)

Мы можем удалить букву T с обеих сторон. Получаем вот что:

x^n <= 1 + x + x^2 + ... + x^(n-2)

Неформально мы говорим, что при nth распределении мы хотим, чтобы вся наша ранее освобожденная память была больше или равна потребности в памяти при n-м распределении, чтобы мы могли повторно использовать ранее освобожденную память.

Например, если мы хотим иметь возможность сделать это на 3-м шаге (т.е. n=3), то у нас есть

x^3 <= 1 + x 

Это уравнение верно для всех x таких, что 0 < x <= 1.3 (примерно)

Посмотрите, какой x мы получаем для разных n ниже:

n  maximum-x (roughly)

3  1.3

4  1.4

5  1.53

6  1.57

7  1.59

22 1.61

Обратите внимание, что коэффициент роста должен быть меньше 2 с x^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2.

person CEGRD    schedule 04.12.2012
comment
Кажется, вы утверждаете, что уже можете повторно использовать ранее освобожденную память при втором выделении с коэффициентом 1,5. Это не так (см. Выше). Дай мне знать, если я тебя неправильно понял. - person awx; 22.02.2013
comment
При 2-м распределении вы выделяете 1,5 * 1,5 * T = 2,25 * T, в то время как общее освобождение, которое вы будете делать до этого, составляет T + 1,5 * T = 2,5 * T. Итак, 2,5 больше 2,25. - person CEGRD; 23.02.2013
comment
Ах, я должен прочитать внимательнее; все, что вы говорите, это то, что общая освобожденная память будет больше, чем выделенная память при n-м распределении, не, что вы можете повторно использовать ее при n-м распределении. - person awx; 28.02.2013

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

Я видел 1.5x 2.0x phi x и раньше использовал power of 2.

person Unknown    schedule 08.07.2009
comment
Фи! Это хорошее число для использования. Я должен начать использовать его с этого момента. Спасибо! +1 - person Chris Jester-Young; 09.07.2009
comment
Я не понимаю ... почему фи? Какие свойства делают его подходящим для этого? - person Jason Creighton; 09.07.2009
comment
@Jason: phi создает последовательность Фибоначчи, поэтому следующий размер распределения - это сумма текущего и предыдущего размера. Это обеспечивает умеренную скорость роста, быстрее, чем 1,5, но не 2 (см. Мой пост о том, почему ›= 2 не является хорошей идеей, по крайней мере, для неуправляемых языков). - person Chris Jester-Young; 09.07.2009
comment
@Jason: Также, по словам комментатора моего сообщения, любое число ›фи на самом деле плохая идея. Я сам не делал математических расчетов, чтобы подтвердить это, так что относитесь к этому с недоверием. - person Chris Jester-Young; 09.07.2009
comment
@ ChrisJester-Young Для ясности: любой темп роста, даже близкий к фи (≈ 1,618), плох, если вашей целью является повторное использование памяти. Любая скорость роста ≥ phi, включая 2x, никогда не сможет повторно использовать память, а скорость роста чуть меньше phi приведет к потере большого количества памяти, прежде чем будет возможность повторно использовать какую-либо. Вы хотите быть намного меньше, чем phi, чтобы быстрее повторно использовать память и меньше тратить впустую, но это должно быть сбалансировано с более частыми перераспределениями и копиями: stackoverflow.com/a/67648650/362030 - person Han Seoul-Oh; 24.05.2021

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

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

В Scala вы можете переопределить loadFactor для хэш-таблиц стандартной библиотеки с помощью функции, которая смотрит на текущий размер. Как ни странно, массивы с изменяемым размером просто удваиваются, что большинство людей и делает на практике.

Я не знаю никаких массивов с удвоением (или 1.5 * ing), которые действительно вылавливают ошибки памяти и в этом случае становятся меньше. Кажется, что если бы у вас был один огромный массив, вы бы захотели это сделать.

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

person Jonathan Graehl    schedule 08.07.2009

Еще два цента

  • У большинства компьютеров есть виртуальная память! В физической памяти вы можете иметь случайные страницы повсюду, которые отображаются как единое непрерывное пространство в виртуальной памяти вашей программы. Разрешение косвенного обращения осуществляется аппаратно. Исчерпание виртуальной памяти было проблемой в 32-битных системах, но на самом деле это больше не проблема. Так что заполнение дыры больше не проблема (кроме особых условий). Поскольку Windows 7 даже Microsoft поддерживает 64-битную версию без лишних усилий. @ 2011
  • O (1) достигается с любым коэффициентом r> 1. То же математическое доказательство работает не только для параметра 2.
  • r = 1.5 можно вычислить с помощью old*3/2, поэтому нет необходимости в операциях с плавающей запятой. (Я говорю /2, потому что компиляторы заменят это сдвигом бит в сгенерированном коде сборки, если сочтут нужным.)
  • MSVC выбрал r = 1.5, поэтому есть по крайней мере один основной компилятор, который не использует 2 в качестве отношения.

Как сказал кто-то, 2 чувствует себя лучше, чем 8. А также 2 чувствует себя лучше, чем 1.1.

Я считаю, что 1.5 - хороший вариант по умолчанию. В остальном это зависит от конкретного случая.

person Notinlist    schedule 03.01.2017
comment
Было бы лучше использовать n + n/2 для задержки переполнения. Использование n*3/2 сокращает вашу возможную емкость вдвое. - person owacoder; 04.05.2019
comment
@owacoder Верно. Но когда n * 3 не подходит, а n * 1.5 подходит, мы говорим о большом количестве памяти. Если n - 32-битный беззнаковый, тогда n * 3 переполняется, когда n равно 4G / 3, то есть примерно 1,333G. Это огромное количество. Это достаточно много памяти для одного выделения. Еще больше, если элементы имеют размер не 1 байт, а, например, 4 байта каждый. Думаете о варианте использования ... - person Notinlist; 04.05.2019
comment
Это правда, что это может быть крайний случай, но крайний случай - это то, что обычно кусается. Привыкнуть искать возможное переполнение или другое поведение, которое может намекать на лучший дизайн, никогда не является плохой идеей, даже если в настоящее время это может показаться надуманным. В качестве примера возьмем 32-битные адреса. Теперь нам нужно 64 ... - person owacoder; 04.05.2019

Я согласен с Джоном Скитом, даже мой друг-теоретик настаивает на том, что это может быть доказано как O (1), если установить коэффициент равным 2x.

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

person Tom    schedule 08.07.2009
comment
Чтобы уточнить, удвоение размера массива означает, что вы получаете амотизированные вставки O (1). Идея состоит в том, что каждый раз, когда вы вставляете элемент, вы также копируете элемент из старого массива. Допустим, у вас есть массив размером m с элементами m. При добавлении элемента m + 1 места нет, поэтому вы выделяете новый массив размером 2m. Вместо того, чтобы копировать все первые элементы m, вы копируете один каждый раз, когда вставляете новый элемент. Это минимизирует дисперсию (за исключением распределения памяти), и после того, как вы вставите 2 м элементов, вы скопируете все элементы из старого массива. - person hvidgaard; 04.11.2014

Я знаю, что это старый вопрос, но есть несколько вещей, которые всем, кажется, не хватает.

Во-первых, это умножение на 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 секунд до минуты), вероятно, будет нормальным.

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

person Rybec Arethdar    schedule 21.05.2016
comment
Конечно, n + n >> 1 то же самое, что 1.5 * n. Достаточно легко придумать похожие приемы для каждого практического фактора роста, о котором вы только можете подумать. - person Björn Lindqvist; 13.10.2016
comment
Это хороший момент. Обратите внимание, однако, что за пределами ARM это как минимум удваивает количество инструкций. (Многие инструкции ARM, включая инструкцию добавления, могут выполнять необязательный сдвиг одного из аргументов, позволяя вашему примеру работать в одной инструкции. Однако большинство архитектур не могут этого сделать.) Нет, в большинстве случаев, удвоение числа Количество инструкций от одного до двух не является существенной проблемой, но для более сложных факторов роста, где математика более сложна, это может повлиять на производительность чувствительной программы. - person Rybec Arethdar; 23.10.2018
comment
@Rybec - Хотя могут быть некоторые программы, которые чувствительны к изменениям времени одной или двумя инструкциями, очень маловероятно, что какая-либо программа, использующая динамическое перераспределение, когда-либо будет обеспокоена этим. Если ему нужно точно контролировать время, он, вероятно, будет использовать вместо этого статически выделенное хранилище. - person owacoder; 04.05.2019
comment
Я занимаюсь играми, где одна или две инструкции могут существенно повлиять на производительность в неправильном месте. Тем не менее, если распределение памяти обрабатывается правильно, это не должно происходить достаточно часто, чтобы несколько инструкций имели значение. - person Rybec Arethdar; 06.05.2019

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

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

В частности, если вы посчитаете 1, вы обнаружите, что верхний предел по скорости роста - золотое сечение ϕ = 1,618…. Скорость роста больше ϕ (например, 2x) означает, что вы никогда не сможете повторно использовать старые блоки памяти. Скорость роста лишь немного ниже, чем ϕ, означает, что вы не сможете повторно использовать старые блоки памяти до тех пор, пока не будет выполнено много перераспределений, в течение которых вы будете тратить память впустую. Итак, вы хотите быть как можно ниже ϕ, не жертвуя при этом слишком большой производительностью.

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

  • ≈1,466x (решение x 4 = 1 + x + x 2 ) позволяет повторно использовать память всего после 3 перераспределений, одно раньше, чем позволяет 1,5x, при этом перераспределение выполняется лишь немного чаще.
  • ≈1,534x (решение x 5 = 1 + x + x 2 + x 3) позволяет повторно использовать память после 4 перераспределений, как и в 1,5 раза, при этом перераспределение происходит немного реже для повышения производительности.
  • ≈1,570x (решение x 6 = 1 + x + x 2 + x 3 + x 4) разрешает повторное использование памяти только после 5 перераспределений, но будет перераспределяться еще реже для еще большего повышения производительности (едва)

Очевидно, что здесь наблюдается некоторая убывающая отдача, поэтому я думаю, что глобальный оптимум, вероятно, находится среди них. Также обратите внимание, что 1,5x - отличное приближение к тому, что на самом деле является глобальным оптимумом, и имеет то преимущество, что оно чрезвычайно простое.

1 Благодарим @ user541686 за этот отличный источник.

person Han Seoul-Oh    schedule 22.05.2021