I am somewhat scared by the recent "full blocks" discussion. I looks
like I'm making heavy use of inefficient design patterns without knowning about the consequences. Just to mention a few examples, I always choose the more "elegant" expression (A), but now seriously wonder if the less elegant one (B) is more recommendable wrt performance: Guard clauses: (A) anArgument ifNil:[ ^self ]. (B) anArgument isNil ifTrue:[ ^self ]. Default values: (A) ^(self anyExpression) ifNil:[ defaultValue ] (B) | answer | ^(answer := self anyExpression) isNil ifTrue:[ defaultValue ] ifFalse:[ answer ] Cached values: (A) ^instVar ifNil:[ instVar := self computeInstVar ]. (B) instVar isNil ifTrue:[ instVar := self computeInstVar ]. ^instVar Nested guarded expressions (my absolue specialty) (A) ^(definitionsCache at: instName ifAbsent:[^nil]) getParameter: anAspect (B) | d | ^(d := definitionsCache at: instName ifAbsent: nil) notNil ifTrue:[ d getParameter: anAspect ] ifFalse:[ nil ] I often used these LISP-inspired expressions with even deeper nesting to avoid temporary assignments. As my current application is about rendering complex nested objects (very similar to a GUI), most code is based on cached lazy evaluation, stream-oriented enumeration and functional-style expressions. This proved to be very robust and easy to maintain, because I need not care about computation order. In a way, everything "feels" like a tree and organzies itself properly. Flushing objects that became invalid is a lot easier to handle in a complex environment, as opposed to doing the necessary things in the correct order. Hence, if the above patterns could be improved, my app would largely benefit from it. Any suggestions? Andre |
Andre Schnoor schrieb am 13.05.2007 17:07:
> I am somewhat scared by the recent "full blocks" discussion. I looks > like I'm making heavy use of inefficient design patterns without > knowning about the consequences. Just to mention a few examples, I > always choose the more "elegant" expression (A), but now seriously > wonder if the less elegant one (B) is more recommendable wrt performance: I wouldn't rewrite the code unless it really makes a difference. As Eliot Miranda already wrote in this thread, the "full blocks" check in Smalllint comes from a time when using full blocks could make a large difference in performance. In VisualWorks 7, the VM handles full blocks much more efficiently than in VW 2.5 and 3.x. I usually ignore the full block warnings. There's only one way to see if your code is efficient enough: measuring. If you suspect that a performance problem might be caused by full blocks, measure the execution time of the action, then rewrite the code such that it doesn't use full blocks, and measure again. Here's an experiment I just did with VW 7.5 on Windows: Test>>testGuardFullBlockWith: anArgument anArgument ifNil: [ ^self ]. ^nil Test>>testGuardWith: anArgument anArgument isNil ifTrue: [ ^self ]. ^nil Time millisecondsToRun: [| test | test := Smalltalk.Test new. 1000000 timesRepeat: [test testGuardFullBlockWith: nil]] 388 301 336 330 322 Time millisecondsToRun: [| test | test := Smalltalk.Test new. 1000000 timesRepeat: [test testGuardWith: nil]] 17 36 41 34 36 Time millisecondsToRun: [| test | test := Smalltalk.Test new. 1000000 timesRepeat: [test testGuardFullBlockWith: 1]] 172 169 176 168 168 Time millisecondsToRun: [| test | test := Smalltalk.Test new. 1000000 timesRepeat: [test testGuardWith: 1]] 34 33 33 34 37 You might say "Wow, a factor of 5 to 10! Let's eliminate full blocks!" But this benchmark is not representative. Usually, your method will do something more interesting than returning nil after the guarding clause. E.g., "^(Timestamp now addSeconds: 15) printString". Using this return value for non-nil arguments, the execution times are: testGuardFullBlockWith: 53014 50949 testGuardWith: 52102 51419 There's no measurable difference anymore, as execution time is dominated by something else. I haven't tried, but I guess that full blocks won't make much difference in your other examples, as dictionary lookups and other computations will dominate. Also, the time difference of 300ms with nil arguments was for 1 million repetitions, i.e. one execution accounts for about 0.0000003 seconds. You'll have to do this really often to notice a difference. HTH, Joachim Geidel |
To be exact,
you are comparing here full block with no block at all because ifTrue: [] is not coded as a block by compiler. This is the extreme case. If you test full block vs clean block, the ratio is less spectacular. Nicolas Test>>testGuardFullBlock: aDictionary with: anArgument aDictionary at: anArgument ifAbsent: [^self]. ^nil Test>>testGuardFull: aDictionary with: anArgument | tmp | tmp := aDictionary at: anArgument ifAbsent: [nil]. tmp isNil ifTrue: [^self]. ^nil Time millisecondsToRun: [| test d | test := Smalltalk.Test new. d := Dictionary new. 1000000 timesRepeat: [test testGuardFullBlock: d with: 1]]. 831 1017 924 Time millisecondsToRun: [| test d | test := Smalltalk.Test new. d := Dictionary new. 1000000 timesRepeat: [test testGuard: d with: 1]]. 282 283 284 Time millisecondsToRun: [| test d | test := Smalltalk.Test new. (d := Dictionary new) at: 1 put: String new. 1000000 timesRepeat: [test testGuardFullBlock: d with: 1]]. 779 738 744 Time millisecondsToRun: [| test d | test := Smalltalk.Test new. (d := Dictionary new) at: 1 put: String new. 1000000 timesRepeat: [test testGuard: d with: 1]]. 310 309 312 Joachim Geidel a écrit : > Andre Schnoor schrieb am 13.05.2007 17:07: >> I am somewhat scared by the recent "full blocks" discussion. I looks >> like I'm making heavy use of inefficient design patterns without >> knowning about the consequences. Just to mention a few examples, I >> always choose the more "elegant" expression (A), but now seriously >> wonder if the less elegant one (B) is more recommendable wrt performance: > > I wouldn't rewrite the code unless it really makes a difference. As > Eliot Miranda already wrote in this thread, the "full blocks" check in > Smalllint comes from a time when using full blocks could make a large > difference in performance. In VisualWorks 7, the VM handles full blocks > much more efficiently than in VW 2.5 and 3.x. I usually ignore the full > block warnings. > > There's only one way to see if your code is efficient enough: measuring. > If you suspect that a performance problem might be caused by full > blocks, measure the execution time of the action, then rewrite the code > such that it doesn't use full blocks, and measure again. > > Here's an experiment I just did with VW 7.5 on Windows: > > Test>>testGuardFullBlockWith: anArgument > anArgument ifNil: [ ^self ]. > ^nil > > Test>>testGuardWith: anArgument > anArgument isNil ifTrue: [ ^self ]. > ^nil > > Time millisecondsToRun: > [| test | > test := Smalltalk.Test new. > 1000000 timesRepeat: [test testGuardFullBlockWith: nil]] > 388 301 336 330 322 > Time millisecondsToRun: > [| test | > test := Smalltalk.Test new. > 1000000 timesRepeat: [test testGuardWith: nil]] > 17 36 41 34 36 > Time millisecondsToRun: > [| test | > test := Smalltalk.Test new. > 1000000 timesRepeat: [test testGuardFullBlockWith: 1]] > 172 169 176 168 168 > Time millisecondsToRun: > [| test | > test := Smalltalk.Test new. > 1000000 timesRepeat: [test testGuardWith: 1]] > 34 33 33 34 37 > > You might say "Wow, a factor of 5 to 10! Let's eliminate full blocks!" > But this benchmark is not representative. Usually, your method will do > something more interesting than returning nil after the guarding clause. > E.g., "^(Timestamp now addSeconds: 15) printString". Using this return > value for non-nil arguments, the execution times are: > > testGuardFullBlockWith: 53014 50949 > testGuardWith: 52102 51419 > > There's no measurable difference anymore, as execution time is dominated > by something else. I haven't tried, but I guess that full blocks won't > make much difference in your other examples, as dictionary lookups and > other computations will dominate. > > Also, the time difference of 300ms with nil arguments was for 1 million > repetitions, i.e. one execution accounts for about 0.0000003 seconds. > You'll have to do this really often to notice a difference. > > HTH, > Joachim Geidel > > |
In reply to this post by Andre Schnoor
Don't worry about performance, if you do find a problem, then fix it, but not a minute sooner. Oh and feel free to ignore that full blocks warning :) |
In reply to this post by Joachim Geidel
Joachim Geidel schrieb: Andre Schnoor schrieb am 13.05.2007 17:07:I am somewhat scared by the recent "full blocks" discussion. I looks like I'm making heavy use of inefficient design patterns without knowning about the consequences. Just to mention a few examples, I always choose the more "elegant" expression (A), but now seriously wonder if the less elegant one (B) is more recommendable wrt performance:I wouldn't rewrite the code unless it really makes a difference. [...] Thanks Joachim, for the measurements. Really interesting. Well, I agree that minor optimizations don't make a big difference in many cases. However, I just didn't want to get used to bad habits. In the inner loops of heavily recursive algorithms, even tiny optimizations can sum up. I've got some deterministic pattern recognition stuff that runs for minutes. Andre |
In reply to this post by Andre Schnoor
agreed, the difference in performance between the two is in most operational contexts inconsequential.
-Charles
-- |
In reply to this post by Andre Schnoor
it probably makes more sense to spend the time writing an "in-liner" than make the code less pretty i.e. given that it makes a diff. The in-liner would be part of a packaging process but now I'm getting geeky :)
-- |
Free forum by Nabble | Edit this page |