В Mac OS X 10.5,
CoreData фреймворк перешел от использования метода
valueForKey:, как рекомендованного способа доступа к атрибутам CoreData, к методу с автоматически генерируемыми методами доступа. Этот новый подход хорош для быстрого получения значений переменных, но проигрывает
NSKeyValueCoding с его возможностью объединения значений, извлеченных из каждого объекта со связями "to-many", путем вызова одного метода.
В этой статье я рассмотрю возможность замены методов NSKeyValueCoding по обходу и объединению NSSet на вызовы автоматически сгенированных методов доступа к полям, чтобы сравнить производительность нового и старого методов.
Доступ к атрибутам и связям NSManagedObject
Давайте рассмотрим вопрос производительности в приложениях CoreData, используя следующую модель:

Если вы не знакомы с диаграммами CoreData, то важным моментом здесь является то, что каждая
Company может иметь несколько
Project и каждый
Project может иметь несколько
Employee.
С учетом этой модели, если у меня есть указатель,
aCompany, который указывает на один из объектов
Company, получить название компании довольно просто:
NSString *companyName = aCompany.name;
* This source code was highlighted with Source Code Highlighter.
Доступ к
name здесь осуществляется с помощь автоматически генерируемых методов доступа, которые
NSManagedObject предоставляет для нас.
До Mac OS X 10.5, единственным способом доступа к значениям полей в Core Data было использование метода
Key Value Coding "ключ-значение" (key-value):
NSString *companyName = [aCompany valueForKey:@"name"];
* This source code was highlighted with Source Code Highlighter.
Так почему же решили уйти от использования метода "ключ-значение"? Основной причиной является производительность (хотя улучшился и синтаксис, и безопасность типов). Извлечение переменной миллион раз при использовании метода "ключ-значение" занимает 0.284016 секунд, а использование автоматически генерируемых методов доступа занимает 0.109017 секунд, что в 2,6 раза быстрее.
Обход значений
Но метод "ключ-значение" (старый метод) имеет одно важное преимущество перед автоматически сгенерированными методами: он очень быстр при обходе набора данных со связями "to-many".
Например, если я хочу получить полный набор имен
Project используемых
aCompany, то с Key Value Coding я могу сделать это очень легко:
NSSet *projectNames = [aCompany valueForKeyPath:@"projects.name"];
* This source code was highlighted with Source Code Highlighter.
Это сработает, потому что реализация
NSSet из
NSKeyValueCoding умеет автоматически обходить себя, чтобы получить имена для каждого объекта
Project, который она содержит.
Эквивалентом с использованием методов доступа было бы:
NSMutableSet *result = [NSMutableSet set];
for (Project *project in aCompany.projects)
{
NSString *name = project.name;
if (value)
{
[result addObject:value];
}
}
* This source code was highlighted with Source Code Highlighter.
Нам потребовалось не только написать больше кода, чем при использовании метода "ключ-значение", но и этот метод действительно медленнее. Для 10000 объектов
Company, каждый из которых содержит 100 объектов
Project, обход с использованием метода "ключ-значение" займет 0,25692 секунды, а подход, с использованием автоматически сгенерированных методово доступа, занимает 0,52873 секунды.
Новый и усовершенствованный подход прошел свой путь от "в 2,6 раза быстрее", в случае с доступом к полю, к "2 раза медленнее", в случае с обходом множества полей приведенным выше.
Исправление проблем со скоростью
Старый метод все еще быстрее
Прежде чем я объясню, почему новый метод медленнее, важно понять, что метод "ключ-значение" на самом деле быстрее при использовании обхода набора данных. Несмотря на дополнительную работу, связанную с обходом полей
Company в
Project и помещением значений в
NSSet, метод "ключ-значение" потратил лишь 0,25692 секунды, что извлечь один миллион имен из
Project, по сравнению с 0,284016 секунд затраченных на извлечение одного миллиона имен
Company.
Это не глюк, и, несмотря на больший объем работы, при использовании метода "ключ-значение" производительность заметно увеличивается при множестве внутренних итераций (в рамках пути по ключу), а не извне (как я делал при переборе более одного миллиона объектов
Company).
Несмотря на заметные улучшения, мы должны таки побить
Key Value Coding, используя наш подход с автоматически генерируемыми методами доступа, но разница в производительности будет незначительно менее эффективной, чем это было при итерации
aCompany.name...
Исправление проблем с новым методом
Простой профайлинг быстро обнаруживает, что имеющиеся здесь проблемы имеют мало общего с методами доступа к объектам. Медленная скорость в первую очередь объясняется вызовом
addObject:.
Посмотрев на вызов private методов стека в профайлере, стало ясно, что причиной всех проблем является перераспределение данных. Каждый раз, когда
NSMutableSet необходимо увеличится в размерах, происходит перераспределение памяти, что и приводит к низкой производительности.
Мы можем предварительно выделить память для всего набора данных на основе наибольшего размера (подразумевая, что все имена
Project уникальны). Наш код становится таким:
NSSet *projects = aCompany.projects;
NSMutableSet *result = [NSMutableSet setWithCapacity:[projects count]];
for (Project *project in projects)
{
NSString *name = project.name;
if (value)
{
[result addObject:value];
}
}
* This source code was highlighted with Source Code Highlighter.
Победа! Эта версия работает за 0,19104 секунд (по сравнению с полученными ранее 0,52873 секунд), что на 25% быстрее, чем подход с использованием метода "ключ-значение".
Уже не в 2,6 раза быстрее, но реализация Key Value Coding в NSSet имеет ряд преимуществ: поскольку имеется внутренний доступ к хранилищу, NSSet может оптимизировать итерации со связами "to-many" и, соответственно, может создавать новые наборы данных быстрее, чем мы.
Реализация категории
(
Category (категория). Objective-C позволяет очень просто расширять функциональность имеющихся классов. Он поддерживает так называемые категории, которые позволяют модифицировать существующие классы «на месте». С помощью категорий можно добавить требуемую функциональность, не внося в них изменений и даже вообще не имея доступа к исходному коду существующих классов).
Для повторного использования этого подхода в будущем, мы можем реализовать категории для NSSet.
Там будет два метода:
* objectValuesForProperty:
* coalescedValuesForProperty:
Первый будет осуществлять упомянутое в примере выше (где NSSet содержит основные объекты).
Второй будет реплицировать оператор Key Value Coding
@distinctUnionOfSets (обрабатывать случай, когда NSSet содержит
NSSet и нужно объединять объекты внутри множества).
Примером второго метода будет получение всех объектов
Employee в
Company. В случае использования Key Value Coding мы должны написать:
NSSet *allEmployees = [aCompany valueForKeyPath:@"projects.@distinctUnionOfSets.employees"];
* This source code was highlighted with Source Code Highlighter.
С методом coalescedValuesForProperty:, мы можем написать:
NSSet *allEmployees = [aCompany.projects coalescedValuesForProperty:@selector(employees)];
* This source code was highlighted with Source Code Highlighter.
Реализация:
#import <objc/message.h>
@implementation NSSet (PropertyCoalescing)
- (NSSet *)objectValuesForProperty:(SEL)propertySelector
{
NSMutableSet *result = [NSMutableSet setWithCapacity:[self count]];
for (id object in self)
{
id value = objc_msgSend(object, propertySelector);
if (value)
{
[result addObject:value];
}
}
return result;
}
- (NSSet *)coalescedValuesForProperty:(SEL)propertySelector
{
NSInteger count = 0;
for (id object in self)
{
count += [objc_msgSend(object, propertySelector) count];
}
NSMutableSet *result = [NSMutableSet setWithCapacity:count];
for (id object in self)
{
id value = objc_msgSend(object, propertySelector);
if (value)
{
[result unionSet:value];
}
}
return result;
}
@end
* This source code was highlighted with Source Code Highlighter.
С методом coalescedValuesForProperty: мы перебираем набор данных дважды, чтобы получить размер, но это по-прежнему самый быстрый вариант - на самом деле, этот метод примерно на 35% быстрее, чем при использовании Key Value Coding. В сравнении с objectValuesForProperty: прирост производительности составит 25%.
Заключение
По просьбе читателей, вот код, который я использовал в тестировании:
PropertyAccessors.zip (32kB). Это наспех написанный код для этой заметки, так что код не очень хорошо написан, но он есть, если он вам нужен.
Я написал этот код и провел тесты производительности, потому что у меня много кода, который использует Key Value Coding для обхода данных с отношениями "to-many". Я был обеспокоен тем, что CoreData предлагает использовать автоматически генерируемые методы доступа из соображений производительности и мои методы с использованием "ключ-значение" будут значительно медленнее в этих случаях, чем следовало бы.
Результатом является то, что производительность Key Value Coding для обхода набора данных в CoreData дает прирост производительности лишь 25-35%, а не на 260% в сравнении с заменой Key Value Coding на использование индивидуального доступа к свойствам объекта. Key Value Coding достаточно эффективен при работе с наборами данных - безусловно, более эффективен, чем доступ к уникальным полям.
Безусловно, улучшение производительности на 35% будет полезным в критически важных участках кода.
Что касается непосредственно реализации: не стоит недооценивать влияния на производительность фактора перераспределения памяти. Постоянный растущий с помощью
addObject: NSSet работает в 3 раза медленнее, чем если бы вы выделили память на весь набор данных сразу.
Удобно заранее выделить память для
NSMutableSet, чтобы вместить все объекты, но, если не все объекты будут уникальными, то вы выделите больше памяти, чем требуется. Если лишняя трата памяти имеет для вас значение, вы можете скопировать множество в момент его создания. Копия будет такого размера, как вам требуется и вы сможете удалить из памяти оригинал. Недостатком такого меода является то, что процесс копирования добавит еще 10-15% ко времени исполнения.