Signed zeros: is this a bug?

M

Mark Dickinson

I get the following behaviour on Python 2.5 (OS X 10.4.8 on PowerPC,
in case it's relevant.)
(-0.0, -0.0)

I would have expected y to be -0.0 in the first case, and 0.0 in the
second. Should the above be considered a bug, or is Python not
expected to honour signs of zeros? I'm working in a situation
involving complex arithmetic where branch cuts, and hence signed
zeros, are important, and it would be handy if the above code could be
relied upon to do the right thing.

Mark
 
J

jim-on-linux

I get the following behaviour on Python 2.5 (OS
X 10.4.8 on PowerPC, in case it's relevant.)


(0.0, 0.0)


(-0.0, -0.0)

I would have expected y to be -0.0 in the first
case, and 0.0 in the second. Should the above
be considered a bug, or is Python not expected
to honour signs of zeros? I'm working in a
situation involving complex arithmetic where
branch cuts, and hence signed zeros, are
important, and it would be handy if the above
code could be relied upon to do the right
thing.

Mark



This works for some reason
instead of x,y = -0.0, 0.0
clumpy but the results are right.

x = -0.0
y= 0.0

x,y
(-0.0, 0.0)




jim-on-linux
http:\\inqvista.com
 
D

Dan Bishop

I get the following behaviour on Python 2.5 (OS X 10.4.8 on PowerPC,
in case it's relevant.)


(-0.0, -0.0)

I would have expected y to be -0.0 in the first case, and 0.0 in the
second. Should the above be considered a bug, or is Python not
expected to honour signs of zeros? I'm working in a situation
involving complex arithmetic where branch cuts, and hence signed
zeros, are important, and it would be handy if the above code could be
relied upon to do the right thing.

IIRC, float.__repr__ just does whatever libc does. Have you tried
using printf("%g, %g", 0.0, -0.0) in a C program?
 
T

Terry Reedy

| > I get the following behaviour on Python 2.5 (OS X 10.4.8 on PowerPC,
| > in case it's relevant.)
| >
| > >>> x, y = 0.0, -0.0
| > >>> x, y
| > (0.0, 0.0)
| > >>> x, y = -0.0, 0.0
| > >>> x, y
| >
| > (-0.0, -0.0)
|| IIRC, float.__repr__ just does whatever libc does. Have you tried
| using printf("%g, %g", 0.0, -0.0) in a C program?

Detailed FP behavior like this is system (and yes, libc) dependent. On
WinXP
IDLE 1.1.30.0

Terry Jan Reedy
 
M

Mark Dickinson

| > I get the following behaviour on Python 2.5 (OS X 10.4.8 on PowerPC,
| > in case it's relevant.)
| >
| > >>> x, y = 0.0, -0.0
| > >>> x, y
| > (0.0, 0.0)
| > >>> x, y = -0.0, 0.0
| > >>> x, y
| >
| > (-0.0, -0.0)
|| IIRC, float.__repr__ just does whatever libc does. Have you tried
| using printf("%g, %g", 0.0, -0.0) in a C program?

Detailed FP behavior like this is system (and yes, libc) dependent. On
WinXP
IDLE 1.1.3>>> x,y = 0.0, -0.0

0.0

Terry Jan Reedy

Interesting. So on Windows there's probably no hope of what I want to
do working.
But on a platform where the C library does the right thing with signed
zeros, this
behaviour is still a little surprising. I guess what's happening is
that there's
some optimization that avoids creating two separate float objects for
a float literal
that appears twice, and that optimization doesn't see the difference
between 0. and -0.
True

jim-on-linux's solution works in the interpreter, but not in a script,
presumably because we've got file-wide optimization rather than
line-by-line optimization.

#test.py
x = -0.0
y = 0.0
print x, y
#end test.py
-0.0 -0.0

Mark
 
D

Duncan Booth

Mark Dickinson said:
I guess what's happening is that there's some optimization that avoids
creating two separate float objects for a float literal that appears
twice, and that optimization doesn't see the difference between 0. and
-0.

Don't guess. Test.
x = 0.0
y = -0.0
return x, y
2 0 LOAD_CONST 1 (0.0)
3 STORE_FAST 0 (x)

3 6 LOAD_CONST 1 (0.0)
9 STORE_FAST 1 (y)

4 12 LOAD_FAST 0 (x)
15 LOAD_FAST 1 (y)
18 BUILD_TUPLE 2
21 RETURN_VALUE

Yes. Just the one constant there.


Tim Peters wrote in
http://blog.gmane.org/gmane.comp.python.devel/day=20050409:
 
A

Alex Martelli

Mark Dickinson said:
I get the following behaviour on Python 2.5 (OS X 10.4.8 on PowerPC,
in case it's relevant.)

(-0.0, -0.0)

I would have expected y to be -0.0 in the first case, and 0.0 in the
second. Should the above be considered a bug, or is Python not
expected to honour signs of zeros? I'm working in a situation
involving complex arithmetic where branch cuts, and hence signed
zeros, are important, and it would be handy if the above code could be
relied upon to do the right thing.

Looks to me like you've found a bug with the parser/compiler:
1 0 LOAD_CONST 1 ((-0.0, -0.0))
3 RETURN_VALUE

Similar problems appear in parsing display forms of lists and dicts and
calls to functions with constant arguments (among which a constant 0.0
and a constant -0.0) -- and more generally when both constants -0.0 and
0.0 appear within the same "unit of compilation" (expression, function,
....) as for example in:
.... a = -0.0
.... return a, 0.0
....
Why I think it has specifically to do with parsing/compiling, e.g.:

so there appears to be no problem once the 0.0 and -0.0 values come from
variables or the like -- only if they're both from constants within the
same "unit of compilation". Indeed, if you put the code above in a
function, rather than in separate entries to the toplevel
interpreter...:
.... x = 0.0
.... y = -0.0
.... return {23:x, 45:y}
....
....the bug surfaces again. Looks a bit like the result of a slightly
errant "peephole optimizer" pass which tries to roll multiple
occurrences of "the same constant" within the same codeobject into a
single one, and consider two immutables a "multiple occurrence" if
they're == to each other (as, indeed, 0.0 and -0.0 must be).

The Python-coded compiler package does NOT give the problem:
1 0 LOAD_CONST 1 (0.0)
3 UNARY_NEGATIVE
4 LOAD_CONST 1 (0.0)
7 BUILD_TUPLE 2
10 RETURN_VALUE

....presumably because it doesn't do peephole optimization (nor any other
kind of optimization).

Unfortunately I've never really looked in depth into these parts of
Python so I can't help much; however, if you open a bug at
<http://sourceforge.net/tracker/?group_id=5470> I think you stand a good
chance of eventually seeing it fixed in 2.5.x for some x.
Python/peephole.c does appear to be taking some care in constant
folding:

192 case UNARY_NEGATIVE:
193 /* Preserve the sign of -0.0 */
194 if (PyObject_IsTrue(v) == 1)
195 newconst = PyNumber_Negative(v);

so I suspect the problem is elsewhere, but I can't find it yet.


Alex


In the meantime, I hope that some available workarounds for the bug are
clear from this discussion: avoid using multiple constants in a single
compilation unit where one is 0.0 and another is -0.0, or, if you really
can't avoid that, perhaps use compiler.compile to explicitly build the
bytecode you need.
 
M

Mark Dickinson

Tim Peters wrote inhttp://blog.gmane.org/gmane.comp.python.devel/day=20050409:

Understood. Platform dependent is fine. But does this really excuse
results
like the following?
3.14159265359

Mark
 
A

Alex Martelli

Duncan Booth said:
Don't guess. Test.

x = 0.0
y = -0.0
return x, y

2 0 LOAD_CONST 1 (0.0)
3 STORE_FAST 0 (x)

3 6 LOAD_CONST 1 (0.0)
9 STORE_FAST 1 (y)

4 12 LOAD_FAST 0 (x)
15 LOAD_FAST 1 (y)
18 BUILD_TUPLE 2
21 RETURN_VALUE

Yes. Just the one constant there.

And yet, as I wrote in a parallel post (the result of quite some
exploration), Python/peephole.c takes specific precautions against
improperly "constant folding" for the unary-minus of 0.0 -- the
collapsing of 0.0 and -0.0 into the "same" constant must happen
elsewhere (I haven't found out where, yet) and doesn't happen in the
Python-coded compiler module. So (on platforms where the underlying C
libraries do the right thing) I think this specific "overzealous
constant folding" must be considered a small bug -- and should be easy
to fix (by specialcasing -0.0 like Python/peephole.c does) if I could
but find out where in the Python sources the folding is happening...


Alex
 
M

Mark Dickinson

[Long analysis of probable cause of the problem]

Thank you for this. I was suspecting something along these lines,
but I don't yet know my way around the source well enough to figure
out where the problem was coming from.
In the meantime, I hope that some available workarounds for the bug are
clear from this discussion: avoid using multiple constants in a single
compilation unit where one is 0.0 and another is -0.0, or, if you really
can't avoid that, perhaps use compiler.compile to explicitly build the
bytecode you need.

Yup: the workaround seems to be as simple as replacing all occurrences
of -0.0 with -(0.0). I'm embarrassed that I didn't figure this out
sooner.
(-0.0, 0.0)

Mark
 
J

jim-on-linux

#########
Interesting. So on Windows there's probably no
hope of what I want to do working.
But on a platform where the C library does the
right thing with signed zeros, this
behaviour is still a little surprising. I
guess what's happening is that there's
some optimization that avoids creating two
separate float objects for a float literal
that appears twice, and that optimization
doesn't see the difference between 0. and -0.


True

jim-on-linux's solution works in the
interpreter, but not in a script, presumably
because we've got file-wide optimization rather
than line-by-line optimization.

#test.py
x = -0.0
y = 0.0
print x, y
#end test.py


-0.0 -0.0

Mark


This is the only way I could make this work in a
script.

from decimal import Decimal

x = Decimal( "-0.0")
y= Decimal("0.0")
print x,y


x = Decimal( "0.0")
y= Decimal("-0.0")
print x,y



jim-on-linux
http:\\www.inqvista.com
 
A

Alex Martelli

Mark Dickinson said:
[Long analysis of probable cause of the problem]

Thank you for this. I was suspecting something along these lines,
but I don't yet know my way around the source well enough to figure
out where the problem was coming from.

The parser/compiler/etc are unfortunately some of the hardest parts of
the sources -- I'm not all that familiar with that part myself, which is
why it took me quite some digging.

Yup: the workaround seems to be as simple as replacing all occurrences
of -0.0 with -(0.0). I'm embarrassed that I didn't figure this out
sooner.

(-0.0, 0.0)

Glad it works for you, but it's the kind of workaround that could break
with any minor tweak/optimization to the compiler... very fragile:-(.

I think i found the cause of the bug, BTW. The collection of constants
in a code object is built in Python/compile.c and it's built as a
dictionary, field u_consts in struct compiler_unit. The "visitor" for
an expression that is a number is (in a case statement)

case Num_kind:
ADDOP_O(c, LOAD_CONST, e->v.Num.n, consts);
break;

(line 2947 of compile.c in Python's current sources from svn). ADDOP_O
just calls compiler_addop_o, which in turn does compiler_add_o before
adding the opcode (LOAD_CONST)

compiler_add_o (at lines 903-933) is used for all of the temporary
dictionaries in compiler_unit; a Python equivalent, basically, would be:

def eqv_cao(somedict, someobj):
# make sure types aren't coerced, e.g. int and long
t = someobj, type(someobj)
if t in somedict:
return somedict[t]
somedict[t] = index = len(somedict)
return index

a simple and fast way to provide a distinct numeric index (0 and up) to
each of a bunch of (hashable) objects.

Alas, here is the problem...: 0.0 and -0.0 are NOT separate as dict
keys! They are == to each other. So are 0, 0L, and 0+j0, but the
compiler smartly distinguishes these cases by using (obj, type) as the
key (the *types* are distinguished, even though the *values*) are; this
doesn't help with 0.0 and -0.0 since both have type float.

So, the first occurrence of either 0.0 or -0.0 in the compilation unit
ends up in the table of constants, and every other occurrence of either
value later in the unit is mapped to that one constant value:-(

This is not trivial to fix cleanly...:-(. compiler_add_o would have to
test "is the object I'm storing -0.0" (I don't even know how to do that
in portable C...) and then do some kludge -- e.g. use as the key into
the dict (-0.0, 0) instead of (-0.0, float) for this one special case.
(I think the table of constants would still be emitted OK, since the
type part of the key is elided anyway in that table as placed in the
bytecode). Or maybe we should give up ever storing -0.0 in the tables
of constant and ALWAYS have "0.0, unary-minus" wherever it appears (that
would presumably require working on the AST-to-bytecode visitors that
currently work ever-so-slightly-differently for this specific
troublespot in the C-coded version vs the Python-coded one...).

If you know the proper way to test for -0.0 in portable C code (or some
feature macro to use in a #if to protect nonportable code) I could try
proposing the first of these two solutions as a patch (I'm not going to
keep delving into that AST and visitors much longer...:), but I suspect
it would be rejected as "too tricky [and minisculely slowing down every
compilation] for something that's too much of special case [and Python
does not undertake to support in general anyway]". Still, we can't be
sure unless we try (and maybe somebody can think of a cleaner
workaround...).


Alex
 
A

Alex Martelli

Alex Martelli said:
Glad it works for you, but it's the kind of workaround that could break
with any minor tweak/optimization to the compiler... very fragile:-(.

OTOH, Python 2.4 works just fine...:

Python 2.4.3 (#1, Apr 7 2006, 10:54:33)
[GCC 4.0.1 (Apple Computer, Inc. build 5250)] on darwin
Type "help", "copyright", "credits" or "license" for more information.(-0.0, 0.0)

so it seems to be very specifically a 2.5 problem.


Alex
 
S

Scott David Daniels

Alex said:
Alas, here is the problem...: 0.0 and -0.0 are NOT separate as dict
keys! They are == to each other. So are 0, 0L, and 0+j0, but the
compiler smartly distinguishes these cases by using (obj, type) as the
key (the *types* are distinguished, even though the *values*) are; this
doesn't help with 0.0 and -0.0 since both have type float....
If you know the proper way to test for -0.0 in portable C code (or some
feature macro to use in a #if to protect nonportable code) ....

Perhaps you could add a type "NegativeFloat" -- a subtype of float,
and distinguish negative zero from zero that way. Not saying I know how
in portable C, but ....

--Scott David Daniels
(e-mail address removed)
 
M

Mark Dickinson

[...]
OTOH, Python 2.4 works just fine...:

Python 2.4.3 (#1, Apr 7 2006, 10:54:33)
[GCC 4.0.1 (Apple Computer, Inc. build 5250)] on darwin
Type "help", "copyright", "credits" or "license" for more information.>>> 0.0,-0.0
(0.0, -0.0)
(-0.0, 0.0)

so it seems to be very specifically a 2.5 problem.

I've filed a bug report (bug #1678380) and got an impressively
quick response from MvL. It looks like this is a `won't fix'.
Oh well.

Thanks for all your help.

Mark
 
G

Gabriel Genellina

Or maybe we should give up ever storing -0.0 in the tables
of constant and ALWAYS have "0.0, unary-minus" wherever it appears (that
would presumably require working on the AST-to-bytecode visitors that
currently work ever-so-slightly-differently for this specific
troublespot in the C-coded version vs the Python-coded one...).

I think that way is the less intrusive, doesn't rely on a particular FP
implementation, and the more likely to be accepted.
Looking at ast.c, the culprit is some optimization in ast_for_factor, line
1506
/* If the unary - operator is applied to a constant, don't generate
a UNARY_NEGATIVE opcode. Just store the approriate value as a
constant. The peephole optimizer already does something like
this but it doesn't handle the case where the constant is
(sys.maxint - 1). In that case, we want a PyIntObject, not a
PyLongObject.
*/

After the long "if", I would use parsenumber(STR(pnum)) and check the type
and value of the resulting object; if it's a float and 0.0, skip the
optimization and continue below as a normal case.
Unfortunately I'm not able to compile and test the code right now, so I
can't provide an actual patch. (At least, not until tuesday). But I hope
this simple comment is useful anyway...

(I cannot find peephole.c on the source distribution for Python 2.5, but
you menctioned it on a previous message, and the comment above refers to
the peephole optimizer... where is it?)
 
T

Tim Peters

[attribution lost]
....
[Alex Martelli]
[also Alex]
OTOH, Python 2.4 works just fine...:

Python 2.4.3 (#1, Apr 7 2006, 10:54:33)
[GCC 4.0.1 (Apple Computer, Inc. build 5250)] on darwin
Type "help", "copyright", "credits" or "license" for more information.(-0.0, 0.0)

so it seems to be very specifically a 2.5 problem.

It's a bug that keeps resurfacing, probably because there's no portable
way to test that it stays fixed :-( (it's not an accident that the OP
relied on atan2 to distinguish +0.0 from -0.0! they act the same in
/almost/ all contexts).

Python's grammar doesn't have negative numeric literals. Instead

"-" CONSTANT_LITERAL

looks like unary minus applied to the non-negative CONSTANT_LITERAL.
All early versions of Python worked that way too.

The first time the +/- 0.0 bug occurred is when the front end was
changed to act as if

"-" CONSTANT_LITERAL

were a literal in its own right, and then that +0.0 == -0.0 caused the
first instance of either in a compilation unit to be used as the value
for all instances of both throughout the compilation unit. That was
fixed by refusing to apply the optimimization if the value of
CONSTANT_LITERAL was a float that compared equal to 0.0.

2.5 introduced a new front end and more ambitious constant-folding, and
I expect the bug showed up again due to one of those.

Note: printing the value of a float 0 may not reveal its sign. IIRC,
glibc's float-to-string routines do display the sign of 0, but
Microsoft's do not. However, atan2() is sensitive to the sign under
both glibm and Microsoft's libm equivalent.
 
D

Dennis Lee Bieber

Interesting. So on Windows there's probably no hope of what I want to
do working.
But on a platform where the C library does the right thing with signed
zeros, this

Pardon? What is "the right thing with signed zeros"... In the last
30 years I've been on machines that normalize floating zero into a true
zero (all bits are zero: mantissa, exponent, and sign). This is the
first time I've even seen a variable output as a negative 0.0!
--
Wulfraed Dennis Lee Bieber KD6MOG
(e-mail address removed) (e-mail address removed)
HTTP://wlfraed.home.netcom.com/
(Bestiaria Support Staff: (e-mail address removed))
HTTP://www.bestiaria.com/
 
P

Paul Rubin

Tim Peters said:
2.5 introduced a new front end and more ambitious constant-folding, and
I expect the bug showed up again due to one of those.

I hope it does get fixed. Not having referential transparency in a
basic math function like atan2 is pretty disturbing in a modern
computer language.
 
P

Paul Rubin

Dennis Lee Bieber said:
Pardon? What is "the right thing with signed zeros"... In the last
30 years I've been on machines that normalize floating zero into a true
zero (all bits are zero: mantissa, exponent, and sign). This is the
first time I've even seen a variable output as a negative 0.0!

Most machines these days use IEEE 754 which supports negative zero.

http://en.wikipedia.org/wiki/Negative_zero
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,598
Members
45,144
Latest member
KetoBaseReviews
Top