The Artima Developer Community
Sponsored Link

Agile Buzz Forum
Pragmatic solution to Extensible but Efficient Case-Like Statements

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
James Robertson

Posts: 29924
Nickname: jarober61
Registered: Jun, 2003

David Buck, Smalltalker at large
Pragmatic solution to Extensible but Efficient Case-Like Statements Posted: Nov 5, 2008 12:53 AM
Reply to this message Reply

This post originated from an RSS feed registered with Agile Buzz by James Robertson.
Original Post: Pragmatic solution to Extensible but Efficient Case-Like Statements
Feed Title: Travis Griggs - Blog
Feed URL: http://www.cincomsmalltalk.com/rssBlog/travis-rss.xml
Feed Description: This TAG Line is Extra
Latest Agile Buzz Posts
Latest Agile Buzz Posts by James Robertson
Latest Posts From Travis Griggs - Blog

Advertisement

If I only was at that Smalltalk Superpowers thing again. Here's an interesting "pattern" I played with earlier today, and profiled this evening.

The Use Case

Somewhere, in a piece of code, someone writes some code which looks kind of like a case statement. A sequence of computations, the first of which returns a positive result, results in the computation of some sort of result object. For testing/example purposes, I put together an arbitrary example:

caseStatement

	

	(input between: 0 and: 0.2) ifTrue: [^1].

	(input between: 0.1 and: 0.3) ifTrue: [^2].

	(input between: 0.2 and: 0.4) ifTrue: [^3].

	(input between: 0.3 and: 0.5) ifTrue: [^4].

	(input between: 0.4 and: 0.6) ifTrue: [^5].

	(input between: 0.5 and: 0.7) ifTrue: [^6].

	(input between: 0.6 and: 0.8) ifTrue: [^7].

	(input between: 0.7 and: 0.9) ifTrue: [^8].

	(input between: 0.8 and: 1.0) ifTrue: [^9].

	^-1

The tests are all numeric ranges, and the return values are simple literals, to keep things simple. It could be any variety of tests though. I have a couple in mind, but I'll let you use your own imagination.

The Problem

When this becomes a problem, is when you suddenly need this to be more pluggable. Perhaps this is sort of tool, and someone needs to insert an

	(input between: 0.35 and: 0.55) ifTrue: [^(9/2)]

To do this, they have to patch the original method. But if the base developers make any changes to the original method later, those updates are lost when ever the patch is loaded. And heaven forbid if it's a popular place to patch and add one's own hooks into.

First Cut Solution

One way to solve this, is to break these individual test/return values up into separate methods. Using method tags, we can register methods that participate in this, and then we can write an aggregrated check thing. The individual methods look something like

case6

	

	<case>

	^(input between: 0.5 and: 0.7)

		ifTrue: [6]

		ifFalse: [nil]

And then we have a method that finds them all, and executes them in a specified order looking for the first match

slowCase

	

	| methods |

	methods := Pragma

		allNamed: #case

		in: self class

		sortedUsing: [:a :b | a selector < b selector].

	methods

		do: [:each | (self perform: each selector) ifNotNil: [:result | ^result]].

	self shouldNotImplement

This works like a charm. New modules can add new <case> tagged methods, and everyone is happy.

The only problem, is if you're original code needed to run at all quick. I tested these on a pretty fast machine (~4K Bogomips Linux box). At 1,000,000 reps, the original runs in just a second (~1060ms). Our flexible thing, runs in about 150s; I didn't leave out the 'm'. It's about 150 times slower.

If make it work, make it right, is enough, you don't need to keep going. It doesn't matter. But if the method was something that was getting pounded a lot and you're not excited about having something that was running at an acceptable speed, jump up by 150+ times it's old length, you start scratching your head.

Inlining

So one way we can make things go faster, is recognize that the handy Pragma services are expensive. It's allocating a new collection each time. Sorting them. Searching the whole method dictionary parametrically. We could exploit our knowledge of the Smalltalk execution objects and go a little faster. Something like

streamlinedDynamicCase

	

	self class getMethodDictionary

		keysAndValuesDo:

			[:selector :method | 

			| attributes |

			(attributes := method attributes) == nil

				ifFalse:

					[(attributes includesKey: #case)

						ifTrue:

							[| result |

							(result := self perform: selector) == nil ifFalse: [^result]]]].

	self shouldNotImplement
This brings us down to ~7 seconds! I'm cheating in that I know the attributes are an IdentityDictionary. That all the methods are local (none inherited). And I've totally cheated by knowing that the linear storage of the MethodDictionary is going to have my case1, case2, case3, already sorted for me.

Taking a Hint from the VM

The above is a real speed jump. But it's mean and fast, still 7x slower, and I'd like to get back to the high level reflection afforded by Pragma object. One thing the VM is good about doing, is taking an "it's ok to do it once slow, if you do it faster after that" approach.

Using the method tags, gives us a nice entry point to know any time any of our <case> tagged methods change, get added to, or are removed. Depending on whether its class side or instance side, our object will receive a classMethodsChanged/instanceMethodsChanged any time one of these <case> tagged methods is added/removed/modified.

The thing to do is build up an object that removes any of the parts that don't need to be dynamic, and execute them as quickly as possible. A method like the following can do this for us


updateDynamicCaseBlock

	

	| ws caseMethods |

	ws := String new writeStream.

	ws nextPutAll: '[:object | | result | '.

	caseMethods := Pragma

		allNamed: #case

		in: self class

		sortedUsing: [:a :b | a selector < b selector].

	caseMethods

		do:

			[:each | ws nextPutAll: '(result := object ' , each selector , ') == nil ifTrue: ['].

	ws nextPutAll: 'self shouldNotImplement'.

	caseMethods size + 1 timesRepeat: [ws nextPut: $]].

	DynamicCaseBlock := Compiler evaluate: ws contents

We add a new class shared variable (DynamicCaseBlock) and then add our access point

blockDynamicCase

	^DynamicCaseBlock value: self

Does it pay off? It executes in 1.2 seconds. So we end up with a pattern that pays a 20% speed penalty for having a pluggable mechanism for adding new query/result pairs. Not bad. If you need it. And if it's at a point where your browser lists are pounding at it for every display... you just might this kind of technique.

Read: Pragmatic solution to Extensible but Efficient Case-Like Statements

Topic: Smalltalk Daily 11/3/08: The ActionButton Widget Previous Topic   Next Topic Topic: GStreamer for Squeak - Audio

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use