Which happens first?

Carlos Alberto Reis Ribeiro cribeiro at mail.inet.com.br
Sat Apr 7 21:42:24 EDT 2001


At 19:41 06/04/01 +0500, you wrote:
>I know that this may be a dumb questio, yet I am new to programming and I
>am courious in a mathmatical expression such as  ( 16 * ( 15j + ( 4.2 -
>3.9 ) + 20  ) / -2 ) % 5 which happens first is the () or is it the
>conversion to the complex number?? I would think it would be the ()

<WARNING> I you are *really* new to programming, the following advice may 
look like overkill. But it does answer your question, while at the same 
time raising others...</WARNING>

Having learned recently about the dis module (thanks to Robin :-) I set out 
to find how this expression was compiled. I am using Python 2.0, and I 
believe that a lot of optimizations are being included by the Python gurus. 
Anyway, what I discovered worried me little bit. This is the code for the 
expression above:

 >>> p = compile('( 16 * ( 15j + ( 4.2 - 3.9 ) + 20  ) / -2 ) % 5', 
'<string>', 'single')
 >>> dis.disassemble(p)
           0 SET_LINENO               0
           3 SET_LINENO               1
           6 LOAD_CONST               0 (16)
           9 LOAD_CONST               1 (15j)
          12 LOAD_CONST               2 (4.2000000000000002)
          15 LOAD_CONST               3 (3.8999999999999999)
          18 BINARY_SUBTRACT
          19 BINARY_ADD
          20 LOAD_CONST               4 (20)
          23 BINARY_ADD
          24 BINARY_MULTIPLY
          25 LOAD_CONST               5 (2)
          28 UNARY_NEGATIVE
          29 BINARY_DIVIDE
          30 LOAD_CONST               6 (5)
          33 BINARY_MODULO
          34 PRINT_EXPR
          35 LOAD_CONST               7 (None)
          38 RETURN_VALUE

1) At [15], we have the widely known floating point representation problem. 
3.9 is not exactly repreentable in binary floating point notation, so the 
3.899... (we all know, it's not a Python problem. Just to shown that it 
happens).

2) The complex constant is recognized by the compiler. There is no 
intermediate step - no call to make a number "complex". This is good.

3) The complex number is only used at the BINARY_ADD call at line 19. There 
is only one operation being evaluated before [23] (namely, [18], that is 
the subtraction between parenthesis). So the expression is calculated as a 
complex number from this point on.

4) Now the ugly part. At [25], we load a constant (with the value 2), and 
then do a unary minus operation. The question is, does it really need to be 
so? There is any good reason not to optimize this, making the conversion to 
the negative value in the compiler?

5) Other strange thing: at least in this example, the compiler did not made 
any optimization on constant expressions. This is evident by the fact that 
the entire expression is shown step by step. I sincerely don't know if the 
compiler's behavior would be different if I was loading, let us say, a 
source file or a module; however, I think that this is not going to make 
difference. Only a careful study of the parser/compiler implementation can 
show the truth.

My personal conclusions are as follows. Please feel free to disagree and/or 
clarify:

1) Avoid any unnecessary calculation, specially inside tight loops. I tend 
to use lots of explicit calculations, in order to make code easier to 
understand. Things like adding/subtracting one, or calculating an offset 
(for a struct, for instance) are common applications of this idiom, for 
example:

 >>> p = a[(3*4+2)-1]   # get the second byte of the third DWORD

This kind of construct should be avoided.

2) If your expression involves value of mixed types, check *carefully* the 
sequence of the calculation. It is possible to optimize the evaluation a 
little bit by moving the more complex (and slower) types to the end of the 
evaluation.

3) The opposite may also be true; in some cases, the cost of the coercion 
may be higher than forcing the right types in the expression:

 >>> q = compile('1*2.0', '<string>', 'single')
 >>> dis.disassemble(q)
           0 SET_LINENO               0
           3 SET_LINENO               1
           6 LOAD_CONST               0 (1)
           9 LOAD_CONST               1 (2.0)
          12 BINARY_MULTIPLY
          13 PRINT_EXPR
          14 LOAD_CONST               2 (None)
          17 RETURN_VALUE
 >>>

There will be a intermediate step while executing step 12 
(BINARY_MULTIPLY), because the first parameter is an integer that needs to 
be converted to a float. It is going to be more efficient to write the 
expression as 1.0*2.0, so this conversion is not needed.

I think that this may be turned into a recipe...


Carlos Ribeiro






More information about the Python-list mailing list