Archive for August, 2013

Rakudo performance

August 2, 2013

Last week I had an amazing adventure in Perl 6. I had a specific goal I needed to accomplish for my day job: determining which face in a STEP model was disappearing when the model was tessellated.

I could determine a face I knew was connected to the face I was looking for, so I decided to try to find all the faces that shared an edge with that face. I have a set of Perl 6 tools for parsing STEP files. It was pretty easy to modify the code I already had to create a script to read & parse the file, walk the topology tree to each edge on the specified face, and then back from those edges to the faces that use them.

This worked well enough when I fed it a small sample file. It did not work so well when I fed it the actual 13M file I needed to work with. I left it running overnight, and it had a fatal error (presumably out of memory) sometime in mid-morning the next day. I think at that point I switched from trying it on Rakudo-Parrot to Rakudo-JVM, with essentially the same results. I added a bunch of say statements, and eventually determined that the line

my %reverse = $step-data.entities.keys.categorize({ $step-data.entities{$_}.map(*.entity_instances) });

was the guilty party. This line built the reverse lookup table, so you could find all the entities that reference a given entity — once built, this lets you quickly walk from an edge to each face that used it. Unfortunately, it seemed to eat up all the memory on my 32G Linux box.

At this point, I figured using categorize was a mistake. I wasn’t sure why it was, mind you. (Though my test program examining the question ended up leading to a separate impressive optimization from jnthn++.) But it occurred to me there was a different approach that avoided it entirely. Instead of walking back from the edges to the faces, just walk from all the faces to their edges, and then look for faces whose set of edges intersected the given face’s set of edges. Promisingly, this approach was about 33% faster on my small test file. And it didn’t use categorize.

However, when I tried this on the big model, this led to the same exact same ballooning memory problem as the first script. Now, though, I had something more concrete to work with, and a few minutes later, I established that the issue was $step-data.entities.kv. On a big hash (this one had 182500 elements), calling .kv or .keys would consume all available memory on my system. You could create the hash, no problem, and if you knew the keys you could access them quite quickly. But asking it to give you the keys killed Rakudo.

With this I had a nice simple golfed test case I could give to jnthn. And before the morning was out, he gave me a patch to fix it. With the patch, the script executed correctly in 11 minutes under Rakudo-JVM. (Note that I did have to change the Java -XmX maximum memory setting in the perl6 script to get it to work.) It also executed correctly in Rakudo-Parrot — but in 7 hours, 52 minutes.

Let me emphasize that. For this real-world task of significant size, Rakudo-JVM was 40 times faster than Rakudo-Parrot.

Note that I don’t know why. But I do know that jnthn++’s fix which was needed to make this work on both Rakudos was in no way JVM-specific. This isn’t an example of optimized Rakudo-JVM versus non-optimized Rakudo-Parrot.

But the important thing here is that this represents a very significant improvement in Rakudo’s performance by switching to JVM. The script is pretty basic core stuff, mostly file I/O, grammar parsing, and hashes. The improvement is much smaller on a small data-set — on my small test file, Rakudo-JVM is not even twice as fast as Rakudo-Parrot. But throw a big task (well, this big task, anyway) at Rakudo, and Rakudo-JVM crushes Rakudo-Parrot.


Follow

Get every new post delivered to your Inbox.