Sunday, May 16, 2010

Groovy: duck-typing not always the best choice?

Working through Grails In Action chapters 3 & 4, I noticed something. The integration tests written to test domain classes and controllers use Groovy defs, presumably for simplicity.

The test cases look a lot like this:

void testFirstSaveEver() {
def user = new User(userId:'joe', password:'secret')
assertNotNull user.save()
assertNotNull user.id
def foundUser = User.get(user.id)
assertEquals 'joe', foundUser.userId
}

I'm not going to go too deeply into the whole dynamic versus static typing debate, though I do have some verbose opinions on the matter. I was particularly intrigued with Groovy-- after learning Python and PHP and coming from a C++/Java/.NET background-- because it seems to be at the sweet spot of statically typed and dynamically typed. Being able to do both is a powerful thing.

For one, the type can serve as a form of documentation. Anyone looking at the code can tell what the original developer is expecting to receive when the type is declared, which may at times be easier/clearer/more telling than evaluating the expression on the right.

Additionally, I would expect that declaring the type at compile time would enable the Groovy interpreter to do some fancy things. Interpreted languages with dynamic typing get a bad wrap because they're slow. This is for a reason, as the duck typing resolution is not a free operation. I figure that if you can help the process along by telling the interpreter what the type is, when you know what the type should be, it should speed up the interpretation.

Curious, I decided to start writing the tests presented in the book, and tests that had statically typed variables instead of def. Like so:



void testFollowing(){
def before = System.currentTimeMillis()
def glen = new User(userId:'glen', password:'password').save()
def peter = new User(userId:'peter', password:'password').save()
def sven = new User(userId:'sven', password:'password').save()

glen.addToFollowing(peter)
glen.addToFollowing(sven)
assertEquals 2, glen.following.size()
sven.addToFollowing(peter)
assertEquals 1, sven.following.size()
def after = System.currentTimeMillis()
println "User following using duck typing: " + (after-before)
}

void testTypedFollowing(){
long before = System.currentTimeMillis()
User glen = new User(userId:'glen', password:'password').save()
User peter = new User(userId:'peter', password:'password').save()
User sven = new User(userId:'sven', password:'password').save()

glen.addToFollowing(peter)
glen.addToFollowing(sven)
assertEquals 2, glen.following.size()
sven.addToFollowing(peter)
assertEquals 1, sven.following.size()
long after = System.currentTimeMillis()
println "User following using static typing: " + (after-before)
}

Through the end of chapter 4, I've written quite a few of these tests. The output is shown below


Unit Test Results.


Designed for use with JUnit and Ant.



All Tests


NameTime(s)
PostIntegrationTeststestFirstPost0.188
PostIntegrationTeststestAccessingPosts0.078
PostIntegrationTeststestPostWithTags0.069
PostIntegrationTeststestTypedPostWithTags0.050
QueryIntegrationTeststestBasicDynamicFinders0.248
QueryIntegrationTeststestTypedBasicDynamicFinders0.051
QueryIntegrationTeststestQueryByExample0.052
QueryIntegrationTeststestTypedQueryByExample0.040
TagIntegrationTeststestSomething0.016
UserIntegrationTeststestBlankUserName1.518
UserIntegrationTeststestFollowing0.055
UserIntegrationTeststestTypedFollowing0.033
UserIntegrationTeststestFirstSaveEver0.022
UserIntegrationTeststestSaveAndUpdate0.022
UserIntegrationTeststestSaveThenDelete0.101
UserIntegrationTeststestEvilSave0.046
UserIntegrationTeststestSaveEvilCorrected0.029


In every comparison of typed versus def, the typed version has been faster. To verify, I also look at what I printed out to console:



--Output from testBasicDynamicFinders--
Duck typing testing dynamic finders: 230}
--Output from testTypedBasicDynamicFinders--
Typed testing dynamic finders: 41}
--Output from testQueryByExample--
Duck typing testing query by example: 42}
--Output from testTypedQueryByExample--
Static typing testing query by example: 30}

I guess that's kind of pointless. I just verify that JUnit tells time. :) The point being, however, that there seems to strong evidence that using the type when possible dramatically improves performance. The testBasicDynamicFinders method saw an almost 5x reduction in time of execution. This seems to completely contradict John Wilson's assertions that "Knowing the type of a parameter makes the call slower!" Of course, it was 4 years ago, but it looks like there was a good reason for blackdrag to investigate!


The question becomes: are there times when declaring the type is slower than def? Are these results valid, or is my system just nuts? If they are, should *hint hint* these portions of the book be rewritten in the next edition to spread best practice?

1 comment:

  1. Turns out it doesn't matter, as Peter Ledbrook rightfully asked whether the order of the test makes a difference. After switching the order in the UserIntegrationTests, the typed ones ran slower.

    So the real factor of difference here was grails' query caching. It might be worth investigation in more trivial Groovy code examples.

    ReplyDelete