Какова структура данных для NSMutableArray?
Обычно класс "изменчивый массив" реализуется как оболочка вокруг простого массива. Обертка выделяет больше памяти, когда вы добавляете элемент за конец. Это общая структура данных, и эффективность различных операций хорошо известна. Вы получаете O (1) доступ к элементу, O (N) вставляете и удаляете, или O (1) (в среднем) вставляете и удаляете в конце массива. Но NSMutableArray
- это нечто другое. Например, docs говорят [акцент мой]:
Примечание. Большинство операций над массивом принимают постоянное время: доступ к элементу, добавление или удаление элемента с любого конца и замена элемента. Вставка элемента в середину массива занимает линейное время.
Итак, что же такое NSMutableArray
? Это где-то документально?
Ответы
Ответ 1
Это оболочка вокруг круговой буфер.
Это не документировано и не открыто, но это сообщение в блоге показывает удивительную работу ревертора над NSMutableArray
, что я думаю вы найдете очень интересным.
Класс кластера NSMutableArray
поддерживается конкретным частным подклассом __NSArrayM
.
Самое большое открытие состоит в том, что NSMutableArray
не является тонкой оболочкой вокруг CFArray
, как можно разумно думать: CFArray
является открытым исходным кодом и не использует круговой буфер, тогда как __NSArrayM
делает.
Прочитав комментарии к статье, похоже, что это началось с iOS 4, тогда как в предыдущих SDK NSMutableArray
фактически использовался CFArray
внутри, а __NSArrayM
даже не был.
Прямо из сообщения в блоге, о котором я говорил выше
Структура данных
Как вы могли догадаться, __NSArrayM
использует круговой буфер. Эта структура данных чрезвычайно проста, но немного больше сложный, чем обычный массив/буфер. Содержание циркулярных буфер может обернуться, когда достигнут любой конец.
Циклический буфер имеет некоторые очень классные свойства. В частности, если буфер заполнен, вставка/удаление с любого конца не требует каких-либо памяти для перемещения.
Псевдокод для objectAtIndex:
выглядит следующим образом:
- (id)objectAtIndex:(NSUInteger)index {
if (_used <= index) {
goto ThrowException;
}
NSUInteger fetchOffset = _offset + index;
NSUInteger realOffset = fetchOffset - (_size > fetchOffset ? 0 : _size);
return _list[realOffset];
ThrowException:
// exception throwing code
}
где ivars определены как
-
_used
: количество элементов, которые содержит массив
-
_list
: указатель на круговой буфер
-
_size
: размер буфера
-
_offset
: индекс первого элемента массива в буфере
Опять же, я не беру на себя ответственность за всю информацию выше, поскольку они идут прямо из этого замечательного сообщения в блоге Bartosz Ciechanowski.
Ответ 2
Сделал некоторые измерения: начиная с пустого массива, добавив @ "Привет" 100 000 раз, а затем удалил его 100 000 раз. Различные шаблоны: добавление/удаление в конце, в начале, посередине, близко к началу (по показателю 20, когда это возможно), близко к концу (20 индексов от конца, когда это возможно), и один, где я чередовался между близкими к началу и концу. Здесь времена для 100 000 объектов (измеренных на Core 2 Duo):
Adding objects = 0.006593 seconds
Removing objects at the end = 0.004674 seconds
Adding objects at the start = 0.003577 seconds
Removing objects at the start = 0.002936 seconds
Adding objects in the middle = 3.057944 seconds
Removing objects in the middle = 3.059942 seconds
Adding objects close to the start = 0.010035 seconds
Removing objects close to the start = 0.007599 seconds
Adding objects close to the end = 0.008005 seconds
Removing objects close to the end = 0.008735 seconds
Adding objects close to the start / end = 0.008795 seconds
Removing objects close to the start / end = 0.008853 seconds
Таким образом, время для каждого добавления/удаления пропорционально расстоянию до начала или конца массива, в зависимости от того, что ближе. Добавление вещей в середине дорого. Вам не нужно работать точно в конце; удаление элементов, близких к началу/концу, также довольно дешево.
Предлагаемая реализация в виде кругового списка исключает важную деталь: существует пустое разность между расположением последнего и первого элемента массива. Когда элементы массива добавляются/удаляются, размер этого пробела изменяется. Необходимо выделить больше памяти, а указатели объектов нужно перемещать, когда пробел исчезает и добавляется больше объектов; массив может быть уменьшен, и указатели объектов необходимо перемещать, когда зазор становится слишком большим. Простое изменение (позволяющее разбить место в любом месте, а не только между последним и первым элементом) позволило бы изменениям в любом месте быть быстрым (при условии, что это одно и то же местоположение) и сделает операции, которые "истощают" "массив быстрее.