A quicker JSON support for iOS/macOS, Part 6: Eliminating KVC from the Equation

In Last time, we achieved a performance boost by directly converting JSON to objects, bypassing the intermediate plist representation. This reduced processing time from 1.1 seconds to 600 milliseconds.

This improvement involved utilizing the [setValue:forKey:](https://developer.apple.com/documentation/objectivec/nsobject/1415969-setvalue?language=objc) method of Key Value Coding to directly map JSON data to object attributes. This approach outperformed Swift’s JSONDecoder() by a factor of 7, which is unexpected considering KVC’s reputation for inefficiency, as discussed in first article.

Understanding KVC and its Performance Bottlenecks

Introduced in 1994 as part of NeXT’s Enterprise Object Framework, Key Value Coding enables indirect object property access using keys instead of directly accessing fields or invoking accessors. This mechanism, extensively used in Enterprise Objects, facilitates relationship traversal via keypaths.

Key-value coding provides a unified method for accessing object data, abstracting the need for knowledge about accessor methods, their names, or data accessibility through fields.

(Excerpt from Key Concepts of Object-Oriented Programming, NeXT/Apple EOF Overview)

KVC remains a potent technique for crafting generic algorithms applicable to any object property. It underpins technologies like CoreData, AppleScript support, Key Value Observing, and Bindings, despite performance concerns raised in The Siren Call of KVO and (Cocoa) Bindings, and inspired Polymorphic Identifiers.

The foundation of KVC lies in the valueForKey: and setValue:forKey: messages, inherently implemented in NSObject. These implementations dynamically derive accessor messages from NSString keys, executing them to retrieve or set values. Non-object types are automatically wrapped and unwrapped as needed.

This dynamic approach introduces overhead. Each KVC method call potentially requires string manipulation and selector conversion, which NSString, designed for human-readable text, handles inefficiently for low-level operations.

Apple mitigates some overhead by caching intermediate results in global hash tables, indexed by class and property/key. However, these lookups, along with necessary locking mechanisms, still introduce latency compared to direct property access.

Introducing ValueAccessor

We can bypass KVC’s overhead with a custom object like MPWValueAccessor (.h .m). Unlike MPWStringtable, MPWValueAccessor is experimental but functional for this purpose.

At its core is the AccessPathComponent struct:


` ``` typedef struct { Class targetClass; int targetOffset; SEL getSelector,putSelector; IMP0 getIMP; IMP1 putIMP; id additionalArg; char objcType; } AccessPathComponent;

``` `


This struct offers various ways to get/set data:

  1. Direct access to the instance variable’s memory offset.
  2. Selectors for getter and setter methods.
  3. Function pointers to the resolved getter and setter implementations.
  4. An additional argument, typically the key for keyed access.

Initialized with objc_msgSend(), getIMP and putImp ensure universal applicability. Binding the ValueAccessor to a class resolves these pointers to the actual methods. Additionally, objcType stores the instance variable’s type for automatic conversions like KVC.

Crucially, the string processing and lookups inherent in KVC are performed once during initialization. Subsequent access involves only direct method calls or function pointer invocations.

Integrating ValueAccessor

Integrating MPWValueAccessor into MPWObjectBuilder (.h .m) proved surprisingly straightforward:


` ``` @property (nonatomic, strong) MPWSmallStringTable *accessorTable;

-(void)setupAcceessors:(Class)theClass { NSArray *ivars=[theClass ivarNames]; ivars=[[ivars collect] substringFromIndex:1]; NSMutableArray *accessors=[NSMutableArray arrayWithCapacity:ivars.count]; for (NSString *ivar in ivars) { MPWValueAccessor *accessor=[MPWValueAccessor valueForName:ivar]; [accessor bindToClass:theClass]; [accessors addObject:accessor]; } MPWSmallStringTable *table=[[[MPWSmallStringTable alloc] initWithKeys:ivars values:accessors] autorelease]; self.accessorTable=table; }

-(void)writeObject:anObject forKey:aKey { MPWValueAccessor *accesssor=[self.accessorTable objectForKey:aKey]; [accesssor setValue:anObject forTarget:*tos]; }

``` `


The primary change is the -setupAccessors: method, which retrieves a class’s instance variables, creates corresponding value accessors, binds them to the class, and stores them in a lookup table.

The -writeObject:forKey: method now utilizes the lookup table to retrieve and use the appropriate value accessor instead of relying on KVC.

Performance Gains

Testing with the same 44 MB JSON file yielded a parsing time of 441 ms, achieving nearly 100 MB/s processing speed, 10 times faster than Swift’s JSONDecoder, and nearing the performance of raw NSJSONSerialization.

Future Optimizations

Analyzing the performance profile () reveals beginDictionary as the most time-consuming operation, indicating progress. However, optimization opportunities remain.

For instance, while SmallStringTable lookups are relatively fast, the -objectForKey: method incurs overhead from NSString wrapping and unwrapping. Passing raw char* pointers and lengths could eliminate this redundancy.

Similar inefficiencies exist with integer handling, where conversions to and from NSNumbers introduce unnecessary overhead.

Additionally, objc_msgSend() calls contribute noticeably to the runtime. Implementing IMP caching and streamlining method dispatch could yield further improvements.

This highlights that achieving optimal performance often involves meticulous optimization of seemingly minor details, going beyond initial gains.

Contact

For assistance with performance optimization and agile coaching, contact info at metaobject.com.

Table of Contents

Somewhat Less Lethargic JSON Support for iOS/macOS, Part 1: The Status Quo
Somewhat Less Lethargic JSON Support for iOS/macOS, Part 2: Analysis
Somewhat Less Lethargic JSON Support for iOS/macOS, Part 3: Dematerialization
Equally Lethargic JSON Support for iOS/macOS, Part 4: Our Keys are Small but Legion
Less Lethargic JSON Support for iOS/macOS, Part 5: Cutting out the Middleman
Somewhat Faster JSON Support for iOS/macOS, Part 6: Cutting KVC out of the Loop
Faster JSON Support for iOS/macOS, Part 7: Polishing the Parser
Faster JSON Support for iOS/macOS, Part 8: Dematerialize All the Things!
Beyond Faster JSON Support for iOS/macOS, Part 9: CSV and SQLite

CFStringGetCStringPtr()

small-ish seeming detail

Licensed under CC BY-NC-SA 4.0
Last updated on Jul 24, 2022 06:34 +0100