Elegance vs. Performance

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

Elegance vs. Performance

Andre Schnoor
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

Reply | Threaded
Open this post in threaded view
|

Re: Elegance vs. Performance

Joachim Geidel
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

Reply | Threaded
Open this post in threaded view
|

Re: Elegance vs. Performance

Nicolas Cellier-3
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
>
>

Reply | Threaded
Open this post in threaded view
|

Re: Elegance vs. Performance

Boris Popov, DeepCove Labs (SNN)
In reply to this post by Andre Schnoor
Re: Elegance vs. Performance

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 :)

Cheers!

-Boris
(Sent from a BlackBerry)

----- Original Message -----
From: [hidden email] <[hidden email]>
To: vwnc-list <[hidden email]>
Sent: Sun May 13 08:07:20 2007
Subject: Elegance vs. Performance

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

Reply | Threaded
Open this post in threaded view
|

Re: Elegance vs. Performance

Andre Schnoor
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

Reply | Threaded
Open this post in threaded view
|

Re: Elegance vs. Performance

Charles A. Monteiro-2
In reply to this post by Andre Schnoor
agreed, the difference in performance between the two is in most operational contexts inconsequential.

-Charles


 

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 :)

Cheers!

-Boris
(Sent from a BlackBerry)

----- Original Message -----
From: [hidden email] <[hidden email]>
To: vwnc-list <[hidden email]>
Sent: Sun May 13 08:07:20 2007
Subject: Elegance vs. Performance

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





--
Charles A. Monteiro
http://monteirofusion.blogspot.com

Reply | Threaded
Open this post in threaded view
|

Re: Elegance vs. Performance

Charles A. Monteiro-2
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 :)

 

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





--
Charles A. Monteiro
http://monteirofusion.blogspot.com