I really do dig programming the iPhone, and the Objective-C language. I understand that working with the language has become much more convenient since its early days. As someone who has been working on the iPhone (and never the Mac, really) for only a couple years, I realized I’m a bit spoiled. But, I came to Objective-C from a scripting background, particularly Matlab and Python, where you could do crazy things like
saveas(gcf(), ‘an_easily_saved_image.png’);
and we got away with it, too. The point of this little post is to share an easy to use class that allows you to save or load your classes with just one line of code.
If you want to skip this post, get the code, and start using it,
This is how you’d use it:
MyObject *myObject = [[MyObject alloc] init]; MyObject.someFloatValue = 3.2; [AWEncoder save:MyObject forKey:@"MyClass"]; ... // At some other point in your code, or even on another // launch of your app, you might want to retrieve that instance // just as you'd saved it before. MyObject *anotherInstance = [[MyClass alloc] init]; [AWEncoder load:anotherInstance forKey:@"MyClass"];
What would you actually use this for? In several of my apps, I have a central preference manager that exists as a singleton, which I access like so:
PreferenceManager *sharedPrefs = [PreferenceManager sharedPreferences]; int someParameterSetByTheUser = sharedPrefs.theImportantParameter;
It’s a pretty convenient way to work, for the most part. However, persistently saving each and every little parameter inside of the preferences class creates a lot of code clutter and unnecessary work, which is how I use to do things. The mess looked a little like this:
- (void)setTheImportantParameter:(int)newValue
{
theImportantParameter = newValue;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setInteger:theImportantParameter forKey:@"theImportantParameter"];
[defaults synchronize];
}That’s completely excluding all of the setup code in the class’s init method that had to pull out each and every value that I’d saved. So, whenever I’d add a new preference, I’d have to add a getter in the init method
self.theImportantParameter = [defaults integerForKey:@"theImportantParameter"];
Well, that’s all profoundly annoying, not to mention difficult to maintain. So, I came up with a little scheme to change that. As I said before, all I have to do now whenever I’d like to persist my preferences class is type
[AWEncoder save:sharedPrefs forKey:@"Preferences"];
And, in the initialization of the SharedPreferences class, I just add one line
- (id)init
{
self = [super init];
if (self) {
[AWEncoder load:self]; // just this one line
return self;
}
return nil;
}That’s it. It works for floats, ints, longs, NSStrings, whatever. It currently does not work for structs, or C arrays of any kind. If you’d like to figure that out, that’d be neat! All of the properties get stored in NSUserDefaults, which persists between launches of the application, and is supposed to be threadsafe. Mind you, though, I haven’t tested this particular method for thread-safeness quite yet. Again, if you’d like to try to break the AWEncoder class in a weird thread-dependent way, I’d love to know how you did it, and how to fix it.
So how does it work? Turns out, the Objective-C runtime will let you get all of the names and types of properties in a class. You can iterate over each property like this:
unsigned int outCount, i;
// Get every available property for the object we've received
objc_property_t *properties = class_copyPropertyList(objectToEncodeClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
... // figure out what to do with the property here
}So, given a property (of type objc_property_t), how do we figure out what type it is, and what its name is?
const char * propertyAttributes = property_getAttributes(property);
The property attributes are returned in a C string that’ll look like “Ti,VnameOfAVariable”. It can look quite a bit hairier, though, with structs and complicated objects. But, for the most part, The syntax is “T{single-character type}, V{name of the variable}”. For more info on possible property types, check out “Declared Properties” in the “Objective-C Runtime Programming Guide”. Anyways, for now, we just care about that second character in the string, along with the property’s name. Here’s how we do that:
char propertyTypeCchar = property_getAttributes(property)[1]; const char * propertyNameCString = property_getName(property); NSString *propertyName = [NSString stringWithCString:propertyNameCString encoding:NSUTF8StringEncoding];
Note that we grab a C string and NSString representation of the property’s name. That’ll come in handy later. So, we’ve got the variable’s type and name, and now, we’ll first cover encoding, then decoding the properties.
So, in order to do any encoding, we have to have first set up a coder. We’ll use the NSKeyedArchiver for encoding (and later, the NSKeyedUnarchiver for decoding). This gets set up outside our for-loop over the properties.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [defaults objectForKey:key];
NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedData];
for (i = ... // the for loop iterating over our properties
// and all our wonderful parsing codeThe coder is what will actually be turning our properties into a format that can be stored on the device’s disk.
So, we’re back inside the for loop, we have a coder ready to serialize our properties. If the property is an Objective-C object, it’s pretty straight-forward.
for (i = ... // we're inside the for loop now
... // here's where we got the property type and name
if (propertyTypeCchar == '@') {
id object_property = [objectToEncode valueForKey:propertyName];
[coder encodeObject:object_property forKey:propertyName];
}That was easy. But what about primitive types, like floats and ints? Those turn out to be a bit harder to grab. But there is a way, and this is it. First, we grab an abstract representation of the property as an “ivar”, or instance variable.
for (i = ...
... // if the variable was an object, we've already dealt with it
else if (propertyTypeCchar == '^') {
// pointer
// we don't deal with that
} else if (propertyTypeCchar == '{}) {
// struct
// we ignore that, too
} else {
// primitives, like floats
// 1.
Ivar ivar = object_getInstanceVariable(objectToEncode, propertyNameCString, NULL);
// 2.
void *pointer_to_value = (void *)(objc_unretainedPointer(objectToEncode) + ivar_getOffset(ivar));
// 3.
if (valuepointer) {
[coder encodeValueOfObjCType:&propertyTypeCchar at:valuepointer];
}
}
} // the end of the for loop here(Remember, ‘@’ means Objective-C object, ‘&’ means a buffer or function pointer, and ‘{’ means struct. We’ve already handled the ObjC types, and we’re going to completely ignore structs, pointers and buffers.)
So what’d we do there?
First, we got a basic representation of our property as an instance variable, or ivar. The ivar doesn’t directly contain our data, but instead holds its name, its type, and its location in memory.
We can get the exact location of the data we care about by taking the address location of our object, and then adding the offset of the property we want. This gives us a memory address.
With our memory address in hand, we tell our coder to grab the data from that location in memory, and encode it.
So, we loop around and around all of our properties, until we’re all done, and then clean up:
for (i = ...
// .. all our saving and encoding stuff
} // the end of the for loop from above
[coder finishEncoding];
[defaults setObject:data forKey:key];
[defaults synchronize];We tell the defaults to save a chunk of data that we’ve encoded into a binary format. That’s it for encoding!
So how do we get those properties back out later? Decoding, dude. The process is mostly similar, with only a few changes. First, the coder is no longer NSKeyedArchiver, but NSKeyedUnarchiver, and we have to initialize it with the data we encoded from before.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSData *encodedData = [defaults objectForKey:key]; NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedData];
Earlier, we encoded two distinct types: objects and primitives. Now, we’ll decode them both. First, the objects.
if (propertyTypeCchar == '@') {
// objects
id object_property = [coder decodeObjectForKey:propertyName];
[objectToDecode setValue:object_property forKey:propertyName];
}Simple, as before. Now, the primitives are again, a little weird.
else {
// primitives
char var_pointer[64]; // more than enough room for any kind of primitive
[coder decodeValueOfObjCType:&propertyTypeCchar at:var_pointer];
object_setInstanceVariable(objectToDecode, propertyNameCString, *(void **)var_pointer);
}What’s that crazy *(void **)var_pointer crap? I’d tell you not to worry about it, but maybe you’d be insistent, and keep asking. Well, if you must now, as far as I can tell, the decodeValueOfObjCType:at: selector wants a memory address, and it sticks the value we want to decode at that pointer location. But, for some reason I don’t thoroughly understand, object_setInstanceVariable() seems to want the value itself, and the only way to wrangle the buffer to not cause a compiler error is to add some serious pointer notation. If it looks hacky, it’s because it is.
Okay, so we’ve gotten our properties out, and back into the class we want. Do we have to do anything with the decoder, or the data from NSUserDefaults? Nope. We’re done. That’s decoding!
You could get really fancy and store the values in the Keychain, but that’s a huge can of worms that I’ve stuck my hand in once. Worms bite, it turns out. Also, as I mentioned, it’s not thoroughly tested, although I use it for my apps now. If you figure out bugs, wanna say “hello!”, or have suggestions, let me know on twitter (I’m @awiltsch). Again, the code can be found as a gist on github.
A simple elementwise kernel, written in both PyOpenCL and PyCUDA, of the form
C(t) = a*A(t) + b*B(t)
Here, the "w/ access" and "w/out access" mean slightly different things than they did before. In both cases, I pull the data off the device, into main memory, but the difference is, the "w/ access" case pulls the data down at every repetition (I redo the computation 100 times for each array size). It looks like PyCUDA suffers from some difficulties in transferring data over the GPU -> memory bus, at least in the particular way I've written the code up. I don't think I'm unfairly penalizing PyCUDA, at least from the perspective of a user of the library, because the two different libraries seem to be designed to be used in very similar ways. There really are only very few modifications necessary to convert from one library to the other.
The issue, though, is that PyCUDA contains some really serious conveniences that PyOpenCL (currently) lacks, like gradient optimization, and sparse matrix multiplication. Still digging in this stuff, and status reports will be forthcoming when interesting things are uncovered.
PREAMBLE:
If you're running Mac OS 10.5, this little walk-through is not for you! Get outta here. Git! If you want a nice tutorial on how to install for 10.5, go to the official tutorial for how to install 10.5.
—————
If you are running Mac OS 10.6, and want some of that gradient optimization and sparse matrix multiplication goodness that PyCUDA offers that (as far as I can tell) ain't in PyOpenCL, you're in for a fun time. Well, I'm being sarcastic, and not you specifically, more like me, I did most of the hair pulling, and I'd like to save you some hair.
Let's go through this step by step.
1) You should have Python on your system and NumPy as well. I'd highly recommend downloading the Enthought Python Distribution (EPD), and forgetting about the trials of getting an optimized NumPy installation working. Just don't worry about it, download the EPD, install it, and come back when you're done.
2) We need to snag all of the drivers and toolkits that NVidia provides that PyCUDA depends upon. You can find them here, at this linky link. Scroll down to the bottom of that page, under "MAC OS X", and download everything that has a "download" link next to it. Go through all the installations of everything you downloaded. Okay. Now...
3) Update your shell. What's that mean? Well, launch Terminal.app, and you'll need to edit your bash profile. You can do this by typing
open ~/.profile
If you're running a different shell, I'll assume you know where your profile file is, and you can open it up on your own. This profile might look like this (or it might not, who knows)
We're going to have to update this to tell the computer where all the fancy things we installed now live. Add these three lines:
export PATH="/usr/local/cuda/bin:${PATH}"
export DYLD_LIBRARY_PATH=/usr/local/cuda/lib:$HOME/pool/lib:${DYLD_LIBRARY_PATH}
export PYTHONPATH=$HOME/lib/python:$PYTHONPATH
So, your bash profile should now look like something like this
Alright, that should do it. We're ready to start rolling.
4) Open up Terminal.app, and issue these commands:
git clone http://git.tiker.net/trees/pycuda.git
python configure.py
What do those lines do? The first one grabs all the code we want to install. The last line writes up some files that'll help us install everything.
3) Okay, that primes the pump, but we're not quite ready to install PyCUDA yet. In terminal, run:
open siteconf.py
That'll open up your favorite text editor. On my system, I use TextMate, and this is what I see:
What's there by default is wrong. We need to add a few lines for our system (otherwise, PyCUDA will complain bitterly that it's not compiling on OS 10.5, and then it'll throw up on itself and crap out). So, we add these lines:# if on Snow Leopard, include these lines:
CXXFLAGS = ["-arch", "x86_64", "-arch", "i386"]
LDFLAGS = ["-arch", "x86_64", "-arch", "i386"]
CXXFLAGS.extend(['-isysroot', '/Developer/SDKs/MacOSX10.6.sdk'])
LDFLAGS.extend(['-isysroot', '/Developer/SDKs/MacOSX10.6.sdk'])
Ta-da!
8 (OPTIONAL) ) Do a jig!
ERROR DISCLAIMER: if this doesn't work for you for some reason, feel free to email me at alex (dot) bw (at) gmail (dot) com. I'd be happy to answer questions. Getting this library installed is kind of a hassle, so anything I can do to make the world a slightly less frustrating place, well, I'd love to do it.
I've taken an interest recently in computer vision, and some of the applications I'd like to try my hand at involve some intense computations ( à la what these folks have been up to ). This means, roughly, an excuse to try out GPU-accelerated computing.
Searching around, getting computations done on the GPU isn't an inherently easy task, but fortunately, there've been efforts made to simplify the process. Namely, the Python language has been used to create a kind of an abstraction, that makes a lot of the setup and tedium almost completely transparent.
I wrote up a little code to test the speed of scaling and adding two arrays of numbers together, and the results are pretty darn cool.
Let me break down what the curves are here. Green is CPU. Red is OpenCL GPU. Crosses (x) denote runs where the data was just left wherever it was stored after the computation, and circles ( o ) are runs where the data was taken out of its resident location at the end of every computation. This penalizes the GPU, because it has to transfer its data from the card, all the way back to memory, whereas a CPU-hosted computation has a much shorter route to RAM, and indeed likely stores the results of its computation in RAM directly.
What's all this mean? CPUs are fast for short little computations, but GPUs really open up and hit full stride once you start working with interesting chunks of data. Now, this is a log scale, so at the far end, there, any operation which doesn't require constant data retrieval is about 100X faster on the GPU.
Keep in mind that this is a pretty trivial application of GPUs, about as simple as it gets. The purpose of this little exercise was just to get my toes wet with these techniques. However, I don't think it's going to be impossibly difficult to apply GPUs to analysis problems in the lab.
Oh, and before I forget, if you want to run this code for yourself, here you go:
The code requires Python (of course), NumPy, matplotlib, and PyOpenCL. I recommend using the Enthought Python Distribution (EPD) to get everything except PyOpenCL. You can grab PyOpenCL using easy_install. Once you've got the EPD installed, open up your command line and type "easy_install pyopencl". Easy as 3.141 fivvvee. Annndd lastly, I did this all on a Mac, running 10.6.7. Find me on the twitters (@awiltsch) if you want to talk about it.
| Search it. | Browse it. | Subscribe it. | Get caught up in it. |
|
|
Go ahead. |