Производительность массива очень похожа на LinkedList — что дает?

Так что название несколько вводит в заблуждение... Я буду говорить просто: я сравниваю эти две структуры данных:

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

Не беспокойтесь ни о каких других деталях этих структур данных. Это единственная функциональность, которая меня интересует в этом тестировании.

Теоретически LL должен работать лучше. Тем не менее, они почти идентичны по времени испытаний, включающих 10, 100, 1000... до 5 000 000 элементов.

Я чувствую, что куча большая. Я думаю, что сегмент данных по умолчанию составляет 10 МБ на Redhat? Я могу ошибаться. В любом случае, realloc() сначала проверяет, доступно ли место в конце уже выделенного непрерывного участка памяти (0-[n-1]). Если n-я позиция свободна, перемещения элементов нет. Вместо этого realloc() просто резервирует старое пространство + следующее за ним пространство. Мне трудно найти доказательства этого, и мне еще труднее доказать, что этот массив на практике должен работать хуже, чем LL.

Вот некоторый дальнейший анализ, после прочтения сообщений ниже:

[Обновление № 1] Я изменил код, чтобы иметь отдельный список, который выделяет память через каждую 50-ю итерацию как для LL, так и для массива. На 1 миллион добавлений к массиву почти всегда приходится 18 ходов. Для LL нет концепции переезда. Я сделал сравнение времени, они все еще почти идентичны. Вот некоторый результат для 10 миллионов добавлений:

(Array)
time ./a.out a 10,000,000
real    0m31.266s
user    0m4.482s
sys     0m1.493s
(LL)
time ./a.out l 10,000,000
real    0m31.057s
user    0m4.696s
sys     0m1.297s

Я ожидаю, что время будет совершенно другим с 18 ходами. Добавление массива требует еще 1 присваивания и еще 1 сравнения, чтобы получить и проверить возвращаемое значение realloc, чтобы убедиться, что перемещение произошло.

[Обновление № 2] Я провел ltrace для тестирования, которое я опубликовал выше, и я думаю, что это интересный результат... Похоже, что realloc (или какой-то менеджер памяти) упреждающе перемещает массив в более крупные смежные местоположения. исходя из текущего размера. Для 500 итераций перемещение памяти было запущено на итерациях: 1, 2, 4, 7, 11, 18, 28, 43, 66, 101, 154, 235, 358, что довольно близко к последовательности суммирования. Я нахожу это довольно интересным — решил опубликовать это.


person Jmoney38    schedule 30.09.2010    source источник
comment
Вы можете легко найти свидетельство того, что realloc() расширяет блок, а не перемещает его — если указатель realloc() возвращает тот же самый, что и тот, который вы ему передали, то он расширил блок.   -  person caf    schedule 30.09.2010
comment
Если вам интересно, действительно ли realloc должен копировать данные, просто добавьте код, который сравнивает указатель до и после вызова. Времена, когда они разные, это когда он сделал копию. Складывайте размер массива каждый раз, когда он копируется, и вы увидите, сколько накладных расходов имеет массив.   -  person Gabe    schedule 30.09.2010
comment
Некоторые системы умножают выделение памяти на 2 (или около того) каждый раз, когда требуется перераспределение; это значительно сокращает время операций.   -  person PP.    schedule 01.10.2010


Ответы (8)


Итак, вы проверяете, насколько быстро вы можете расширить массив по сравнению со связанным списком?

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

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

Ожидайте изменения производительности там, где вы смешиваете вызовы malloc() и realloc().

person PP.    schedule 30.09.2010
comment
Другое предположение заключается в том, что время от времени realloc() будет выплевывать манекен и выделять большой кусок памяти в другом месте, потому что он не может получить непрерывные куски на текущей выделенной странице. Это именно то, что я ожидаю вызвать деградация производительности, но этого не происходит. - person Jmoney38; 30.09.2010

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

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

person harald    schedule 30.09.2010
comment
+1, это будет хороший тест. Проверить, происходит ли перераспределение памяти, также довольно просто: сравните новый указатель, возвращаемый функцией realloc, со старым указателем. - person nos; 30.09.2010

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

Предполагая, что realloc должен переместить массив в новое место, он должен пройти массив, чтобы скопировать его. Это O(n) операция.

По сложности обе операции равны. Однако, как указывали другие, realloc может избегать перемещения массива, и в этом случае добавление элемента в массив равно O(1). Другие также указали, что подавляющее большинство времени вашей программы, вероятно, тратится на malloc/realloc, которые обе реализации вызывают один раз для добавления.

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

person R.. GitHub STOP HELPING ICE    schedule 30.09.2010
comment
Любая разумная реализация связанного списка будет поддерживать указатель на оба конца списка, поэтому вам не нужно проходить всю его длину, чтобы добавить в хвост. - person JeremyP; 30.09.2010
comment
Связанный список, в котором я отслеживаю голову, хвост и размер. - person Jmoney38; 30.09.2010

Производительность решения на основе массива, расширенного с помощью realloc(), будет зависеть от вашей стратегии создания большего пространства.

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

Если вы увеличите объем пространства, добавив часть вашего текущего количества элементов (удвоение является довольно стандартным), вы получите расширение, которое в среднем занимает постоянное время.

person Vatine    schedule 30.09.2010

Будет ли сильно различаться вывод компилятора в этих двух случаях?

person dwarFish    schedule 30.09.2010

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

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

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

person JeremyP    schedule 30.09.2010

(Обновлено.) Как отмечали другие, если между reallocs нет других распределений, то копирование не требуется. Также, как отмечали другие, риск копирования памяти уменьшается (но, конечно, и его влияние) для очень маленьких блоков, меньше страницы.

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

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

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

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

(Спасибо за комментарии ниже, я сначала сам был сбит с толку тем, как эти вещи работают.)

person Prof. Falken    schedule 30.09.2010
comment
Хороший ответ; не могли бы вы привести несколько цитат? - person San Jacinto; 30.09.2010
comment
Я не знаю, почему вы думаете, что фиксация означает отсутствие копирования. На каждый процесс по-прежнему остается только одно виртуальное адресное пространство, и если память не может быть расширена непрерывно, ее необходимо скопировать. - person Matthew Flaschen; 30.09.2010
comment
realloc очень часто копирует данные в реальных сценариях просто потому, что между ними могут располагаться другие объекты, и адресные пространства перестают быть непрерывными. В тестовом сценарии, где вы только перераспределяете массив и не выполняете другие операции malloc, часть памяти может быть просто расширена. Это не имеет ничего общего с overcommit, хотя. - person nos; 30.09.2010
comment
@ Мэтью, я думаю, ты прав насчет Linux. Если другие указатели уже занимают место, где находится ваш указатель для расширения (для перераспределения), тогда да, данные должны быть скопированы. - person Prof. Falken; 30.09.2010
comment
Это верно и для OpenBSD. Единственным назначением защитной страницы является обнаружение определенных недопустимых обращений к памяти. realloc по-прежнему копирует, когда это необходимо. - person Matthew Flaschen; 30.09.2010

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

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

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

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

person Matthew Flaschen    schedule 30.09.2010
comment
realloc будет копировать только тогда, когда это необходимо, в то время как каждый узел связанного списка требует выделения и (при условии двойного связанного списка) двух операций записи. - Вы забываете, что realloc по-прежнему будет выделять место для новой записи. Он по-прежнему должен проверять кучу и резервировать пространство n+1 после массива (при условии, что перемещение не происходит). - person Jmoney38; 30.09.2010
comment
@Jmoney, я уточнил. Вы правы в том, что ему, возможно, придется обновить бухгалтерскую информацию. Однако это не всегда так. Например. если вы malloc 10 байтов, это может дать вам 16-байтовый фрагмент. Если вы realloc до 11 байт, ничего делать не нужно. - person Matthew Flaschen; 30.09.2010