Objective-XML 5.3
- Cocotron targets for Windows support.
- XMLRPC support.
- No longer uses 'private' API that was causing AppStore rejections for some iPhone apps using Objective-XML.
- Support for numeric entitites.
Labels: Objective-C, performance, XML
Labels: Objective-C, performance, XML
For the 95% (or more) of code that isn't performance sensitive, it gives you expressiveness very close to Smalltalk, and for the 5% or less that need high performance, it gets you the performance and predictability of C.
Labels: Objective-C, performance
Labels: Objective-C, performance, XML
enum songtags {
item_tag=10, title_tag, category_tag
};
...
[parser setHandler:self forElements:[NSArray arrayWithObjects:@"item",@"title",@"category",nil]
inNamespace:nil prefix:@"" map:nil tagBase:item_tag];
...
-itemElement:(MPWXMLAttributes*)children attributes:(MPWXMLAttributes*)attributes parser:(MPWMAXParser*)p
{
...
[song setTitle:[children objectForTag:title_tag]];
...
...
[parser setHandler:self forElements:[NSArray arrayWithObjects:@"item",@"title",@"category",nil]
inNamespace:nil prefix:@"" map:nil];
...
-itemElement:(MPWXMLAttributes*)children attributes:(MPWXMLAttributes*)attributes parser:(MPWMAXParser*)p
{
...
[song setTitle:[children objectForUniqueKey:@"title"]];
...
Streaming parser like SAX or MAX can be a lot more efficient, but it takes a lot more time and effort until achieving a first useful result. Default methods overcome this hurdle by also delivering an immediately useful generic representation without any extra work. Unlike a DOM, however, this generic representation can be incrementally replaced by more specialized and efficient processing later on.
Labels: Objective-C, performance, XML
Labels: Objective-C, performance, XML
The example pits Cocoa's NSXMLParser against a custom parser based on libxml2, the benchmark is downloading a top 300 list of songs from iTunes.
While my expectations were technically correct for overall performance, I had completely failed to take responsiveness into account. Depending on the network selected, the NSXMLParser sample would appear to hang for 3 to 50 seconds before starting to show results. Needless to say, that is an awful user experience. The libxml example, on the other hand, would start displaying some results almost immediately. While it also was a bit faster in the total time taken, this effect seemed pretty insignificant compared to the fact that results were arriving continually pretty much during the entire time.
The difference, of course, is incremental processing. Whereas NSXMLParser's -initWithContentsOfURL: method apparently downloads the entire document first and then begins processing, the libxml2-based code in the sample downloads the XML in small chunks and processes those chunks immediately.
Alas, going with libxml2 has clear and significant disadvantages, with the code that uses libxml2 being around twice the size of the NSXMLParser-based code, at around 150 lines (non-comment, non-whitespace). If you have worked with NSXMLParser before, you will know that that is already pretty painful, so just imagine that particular brand of joy doubled, with the 150 lines of code giving you the simplest of parsers, with just 5 tags processed. Fortunately, there is a simpler way.
I have to admit that not having incremental processing was a "feature" Objective-XML shared with NSXMLParser until very recently, due to my not taking into account the fact that latency lags bandwidth. This silly oversight has now been fixed, with both MPWMAXParser and MPWSAXParser sporting URL-based parsing methods that do incremental processing.
So that's all there is to it, Objective-XML provides a drop-in replacement for NSXMLParser that has all the performance and responsiveness-benefits of a libxml2-based solution without the coding horror.
-itemElement:(MPWXMLAttributes*)children attributes:(MPWXMLAttributes*)attributes parser:(MPWMAXParser*)p
{
Song *song=[[Song alloc] init];
[song setArtist:[children objectForTag:artist_tag]];
[song setAlbum:[children objectForTag:album_tag]];
[song setTitle:[children objectForTag:title_tag]];
[song setCategory:[children objectForTag:category_tag]];
[song setReleaseDate:[parseFormatter dateFromString:[children objectForTag:releasedate_tag]]];
[self parsedSong:song];
[song release];
return nil;
}
MAX sends the -itemElement:attributes:parser: message to its client whenever it has encountered a complete <item> element, so there is no need for the client to perform string processing on tag names or
manage partial state as in a SAX parser.
The method constructs a song object using data from the <item> element's child elements which it then passes directly to the rest of the app via the parsedSong: message. It does not return an value, so MAX will not build a tree at this level.Artist, album, title and category are the values of nested child elements of the <item> element. The (common) code shared by all these child-elements gets the character content of the respective elements and is shown below:
-defaultElement:children attributes:atrs parser:parser
{
return [[children combinedText] retain];
}
Unlike the <item> processing code, which did not return a value, this method does return a value. MAX uses this return value to build
a DOM-like structure which is then consumed by the next higher-level, in this case the -itemElement:attributes:parser: method shown above. Unlike a traditional DOM, the MAX tree structure is built out of domain-specific objects returned incrementally by the client.These two pieces of sample code demonstrate how MAX can act like both a DOM parser or a SAX parser, controlled simply by wether the processing methods return objects (DOM) or not (SAX). They also demonstrated both element-specific and generic processing.
In the iTunes Song parsing example, I was able to build a MAX parser using about half the code required for the NSXMLParser-based example, a ratio that I have also encountered in larger projects. What about performance? It is slightly better than MPWSAXParser, so also somewhat better than libxml2 and significantly better than NSXMLParser.
While ably demonstrating the performance problems of NSXMLParser, the sample code's solution of using libxml2 is really not a solution, due to the significant increase in code complexity. Objective-XML provides both a drop-in replacement for NSXMLParser with all the performance and latency benefits of the libxml2 solution, as well as a new API that is not just faster, but also much more straightforward than either NSXMLParser or libxml2.
Labels: Objective-C, performance, XML
Sean McGrath claims that Binary XML solves the wrong problem.
Yes and no: it doesn't help much with existing structures and parsing methods, but with the right methods, it can be extremely helpful!
Also: "...how weird is it that we have not moved on from the DOM and SAX in terms of "standard" APIs for XML processing?"
Labels: performance, XML
Now that I've motivated why an MPWObjectCache might be useful, let's go into some more detail as to how it actually works. To follow along, or if you'd rather just read the source code than my ramblings, MPWObjectCache is part of MPWFoundation, which can be downloaded here: http://www.metaobject.com/downloads/Objective-C/.
As I mentioned before, the algorithm for MPWObjectCache is quite simple: there is a circular buffer of object slots. We try to get pre-allocated objects from this circular buffer if possible. If we find an object in the cache and it is available for reuse, we just return it and have just saved the cost of allocation. Two things can prevent this happy state of affairs: (1) we don't have an object yet or (2) we cannot reuse the object because it is still in use. In both cases we will need to allocate a new object, but in the second case we also remove the old object from the cache position.
#if SLOW_SAMPLE_IMPLEMENTATION_WANTED
-getObject
{
id obj;
objIndex++;
if ( objIndex >= cacheSize ) {
objIndex=0;
}
obj=objs[objIndex];
if ( obj==nil || [obj retainCount] > 1 ) {
if ( obj!=nil ) {
[obj release];
}
obj = [[objClass alloc] init];
objs[objIndex]=obj;
}
return [[obj retain] autorelease];
}
#else
This is what a naive implementation looks like. A couple of notes on the code:
-doSomething:target
{
// cache is an ivar
id obj=GETOBJECT(cache);
// target does not have access to cache
[target doSomethingWithObject:obj];
// obj now either has an extra retain or can be reused
}
This pleasant property is a side effect of the decision to turn
the object-cache into an object that can be instantiated and
placed in an instance variable, rather than the typical
object pools that are implemented as class
methods. The class method that maintains such a pool
usually has no information about the lifetime
of objects, so to be safe such an implementation always
has to protect the objects it returns, negating much of
the advantage of caching. Similar caveats apply to
multi-threading and locking.
Those caveats notwithstanding, MPWObjectCache also provides the
CACHING_ALLOC macro for creating class-side allocation methods
backed by an object cache, which is used in the HOM implementation
to reduce the cost of allocating trampolines:
CACHING_ALLOC( quickTrampoline, 5, YES )This creates a +quickTramplone method backed by an object cache with 5 entries. The YES flag allows objects to be returned from the cache without the retain/autorelease despite the fact that it isn't one of the safe "push" patterns described above. However, this use is also safe because the trampoline is used only temporarily to catch and forward the message, all of which is code controlled by the implementation. It is no longer needed once any client code implementing the actual HOM is run. So, this is how and why object-caches can make your (temporary) object allocations much, much faster.
Labels: Objective-C, performance
At its core, Postscript is a stack-oriented, dynamically typed and highly polymorphic interpreted programming language. So implementing Postscript with Objective-C objects is actually not just convenient when you want to get Objective-C objects out, it is also a good match for the semantics of the language.
So all is good, right? Well, we also need to make sure that performance is competitive, otherwise there really isn't much of a point. How do we find out if performance is competitive? Fortunately, we have the gold standard handily available: Adobe's interpreter was not just used in NeXT's DisplayPostscript, but is also available as the PS Normalizer on Mac OS X . So let's test performance with a little Postscript program:
%!
usertime
0 1 1000000 { 4 mul pop } bind for
usertime exch sub dup ==
20 20 moveto /Times-Roman 24 selectfont
100 string cvs show ( ms) show
showpage
The program times a loop that multiplies some numbers one million times. It exercises a good deal of the basic execution machinery in the Postscript language: stack manipulation, procedure invocation, array access (a procedure is just an array with the executable bit set), looping and arithmetic. The loop is timed with the usertime command, which returns CPU time used in milliseconds.This test clocks in at 513 ms (513 ns per iteration) in Preview, which isn't too shabby.
id startcounter=[NSNumber numberWithInt:0];
id endcounter=[NSNumber numberWithInt:1000000];
id counter=startcounter;
id four=[NSNumber numberWithInt:4];
while ( [counter intValue] < [endcounter intValue] ) {
int intResult;
id result;
[stack addObject:counter];
[stack addObject:four];
intResult = [[stack lastObject] intValue] * [[stack objectAtIndex:[stack count]-2] intValue];
result=[NSNumber numberWithInt:intResult];
[stack removeLastObject];
[stack removeLastObject];
[stack addObject:result];
[stack removeLastObject];
counter=[NSNumber numberWithInt:[counter intValue]+1];
}
Sadly, this takes 4.8 µs per iteration, so our 'lower' bound is almost 10 times slower than our target, and that's without accounting for interpretation. Clearly not good enough. What if we get rid of all that silly stack manipulation code and use a plain C loop?
id b=[NSNumber numberWithInt:4];
for (i=0;i < 10000000;i++) {
id a=[NSNumber numberWithInt:i];
id c=[NSNumber numberWithInt:[a intValue] * [b intValue]];
}
id b=[MPWInteger numberWithInt:4];
id a=[MPWInteger numberWithInt:0];
id c=[MPWInteger numberWithInt:0];
for (i=0;i <10000000;i++) {
[a setIntValue:i];
[c setIntValue:[a intValue] * [b intValue]];
}
That's more like it: 50ns per iteration is 100x better than our first attempt and also 10x better than the target we're aiming for. So taking advantage of mutable state makes our basic plan possible, at least in principle. Of course, we now have to reintroduce the stack and add interpretation.
Instead, we need to figure out a way to recycle temporary objects so we can reuse them without spending a lot of time. The common way to do this is to keep a pool of objects from which requests for new MPWInteger instances are satisfied. However, due to the unpredictable nature of the interpreted code, we cannot use the explicit checkin/checkout policy such pools usually require.
Instead we make the pool a circular buffer and use the retain count to verify that an object can be reused. When we get to a position in the pool that has an object, we can reuse that object if the retain count is one, meaning that only the pool has a valid reference. If the retain count of the object is greater than one, someone other than the pool is holding on to the object and it cannot be reused (yet), so we need to get another instance.
This logic is encapsulated in the class MPWObjectCache, which can be used very similarly to a class (factory object) in creating new instances.
MPWObjectCache* intCache=[[MPWObjectCache alloc] initWithCapacity:20
class:[MPWInteger class]];
id b=[MPWInteger integer:5];
for (i=0;i < 1000000;i++) {
id a=GETOBJECT(intCache);
id d=GETOBJECT(intCache);
[a setIntValue:i];
[d setIntValue:[a intValue] * [b intValue]];
This code runs in 100ns per iteration, so we now have a solution that gives us new or safely recycled objects quickly enough to build on with the confidence the end result will perform acceptably.Labels: Objective-C, performance, Postscript