固定点类型无法正确相乘

7
我刚接触Ada,并尝试使用固定点“delta”类型。具体来说,我创建了一个32位的delta类型范围为0.0 .. 1.0。然而,当我尝试对某些值进行平方时,会出现CONSTRAINT_ERROR错误。据我所知,在我的指定范围内不应该发生这种情况。此错误的阈值似乎为sqrt(1/2)。我正在使用MinGW-w64版本4.8.0的GNAT。
测试代码(所有代码都以gnatmake <file>形式编译,无警告/错误):

types.ads:

pragma Ada_2012;

with Ada.Unchecked_Conversion;
with Ada.Text_IO;

package Types is
    type Fixed_Type is delta 1.0 / 2**32 range 0.0 .. 1.0
        with Size => 32;
    type Modular_Type is mod 2**32
        with Size => 32;
    function Fixed_To_Mod is new Ada.Unchecked_Conversion(Fixed_Type, Modular_Type);
    package MIO is new Ada.Text_IO.Modular_IO(Modular_Type);
    package FIO is new Ada.Text_IO.Fixed_IO(Fixed_Type);
end Types;

specifics.adb:

pragma Ada_2012;

with Ada.Text_IO;

with Types; use Types;

procedure Specifics is
    package TIO renames Ada.Text_IO;

    procedure TestValue(val: in Fixed_Type) is
        square : Fixed_Type;
    begin
        square := val * val;
        TIO.Put_Line("Value " & Fixed_Type'Image(val) & " squares properly.");
        TIO.Put_Line("Square: " & Fixed_Type'Image(square));
        TIO.New_Line;
    exception
        when Constraint_Error =>
            TIO.Put_Line("Value " & Fixed_Type'Image(val) & " does not square properly.");
            TIO.Put_Line("Square: " & Fixed_Type'Image(val * val));
            TIO.Put_Line("Not sure how that worked.");
            TIO.New_Line;
    end TestValue;

    function ParseFixed(s: in String; last: in Natural; val: out Fixed_Type) return Boolean is
        l : Natural;
    begin
        FIO.Get(s(s'First..last), val, l);
        return TRUE;
    exception
        when others =>
            TIO.Put_Line("Parsing failed.");
            return FALSE;
    end ParseFixed;

    buffer : String(1..20);
    last : Natural;
    f : Fixed_Type;
begin
    loop
        TIO.Put(">>> ");
        TIO.Get_Line(buffer, last);
        exit when buffer(1..last) = "quit";
        if ParseFixed(buffer, last, f) then
            TestValue(f);
        end if;
    end loop;
end Specifics;

specifics.adb的输出:

>>> 0.1
Value  0.1000000001 squares properly.
Square:  0.0100000000

>>> 0.2
Value  0.2000000000 squares properly.
Square:  0.0399999998

>>> 0.4
Value  0.3999999999 squares properly.
Square:  0.1599999999

>>> 0.6
Value  0.6000000001 squares properly.
Square:  0.3600000001

>>> 0.7
Value  0.7000000000 squares properly.
Square:  0.4899999998

>>> 0.75
Value  0.7500000000 does not square properly.
Square: -0.4375000000
Not sure how that worked.

>>> quit

不知何故,将val乘以自身得到了一个负数,这解释了CONSTRAINT_ERROR...但是不管它,为什么一开始我就得到了一个负数呢?

然后我决定测试平方数字失败的点,所以我写了以下代码片段:

fixedpointtest.adb:

pragma Ada_2012;

with Ada.Text_IO;

with Types; use Types;

procedure FixedPointTest is
    package TIO renames Ada.Text_IO;

    test, square : Fixed_Type := 0.0;
begin
    while test /= Fixed_Type'Last loop
        square := test * test;
        test := test + Fixed_Type'Delta;
    end loop;
exception
    when Constraint_Error =>
        TIO.Put_Line("Last valid value: " & Fixed_Type'Image(test-Fixed_Type'Delta));
        TIO.Put("Hex value: ");
        MIO.Put(Item => Fixed_To_Mod(test-Fixed_Type'Delta), Base => 16);
        TIO.New_Line;
        TIO.Put("Binary value: ");
        MIO.Put(Item => Fixed_To_Mod(test-Fixed_Type'Delta), Base => 2);
        TIO.New_Line;
        TIO.New_Line;
        TIO.Put_Line("First invalid value: " & Fixed_Type'Image(test));
        TIO.Put("Hex value: ");
        MIO.Put(Item => Fixed_To_Mod(test), Base => 16);
        TIO.New_Line;
        TIO.Put("Binary value: ");
        MIO.Put(Item => Fixed_To_Mod(test), Base => 2);
        TIO.New_Line;
        TIO.New_Line;
end FixedPointTest;

并获得以下输出:

Last valid value:  0.7071067810
Hex value: 16#B504F333#
Binary value: 2#10110101000001001111001100110011#

First invalid value:  0.7071067812
Hex value: 16#B504F334#
Binary value: 2#10110101000001001111001100110100#

所以,sqrt(1/2),我们再次见面了。有人能向我解释一下为什么我的代码会这样吗?有办法使其正确地进行乘法运算吗?


1
值得打印出最后一个有效值和第一个无效值的平方的十六进制和二进制值。这种实现方式似乎采用了使用32位(有符号)整数“底层”的捷径,感觉像是一个bug。我倾向于尝试delta = 1.0/231和1.0/233(相同的范围)。后者可能会强制使用更宽的内部类型,或无法编译。 - user1818839
我尝试了那些增量,两个都表现得非常出色(第二个只有在删除 with Size => 32 子句时才能编译通过;否则会出现编译错误)。对于原始的增量,我想知道为什么每当我尝试分配一个值 >= 0.5 时,程序没有引发 CONSTRAINT_ERROR。然而,当我重新使用 -gnato 进行编译时,我发现它实际上不接受这样的值。是否有一种方法可以摆脱符号位,或者我必须使用不同的增量? - ericmaht
1个回答

9

我认为您要求的精度比“机器内部”实际可用的精度还要高出1个比特。

您的声明:

   type Fixed_Type is delta 1.0 / 2**32 range 0.0 .. 1.0
       with Size => 32;

只有因为GNAT使用了一种偏见的表示方式才会被接受;没有空间留给符号位。你可以看到,因为0.7071067810表示为16#B504F333#,最高位被设置了。所以,当你将0.71乘以0.71时,结果具有最高位设置;低级代码认为这必须是符号位,因此我们出现了溢出。

如果您声明 Fixed_Type 如下:

   type Fixed_Type is delta 1.0 / 2**31 range 0.0 .. 1.0
       with Size => 32;

一切都应该是好的。

另外一个要点:在你对输入为0.75的specifics行为的报告中,你引用了结果。

>>> 0.75
Value  0.7500000000 does not square properly.
Square: -0.4375000000
Not sure how that worked.

我使用gnatmake specifics.adb -g -gnato -bargs -E重新构建程序,并且最终结果如下:

>>> 0.75
Value  0.7500000000 does not square properly.

Execution terminated by unhandled exception
Exception name: CONSTRAINT_ERROR
Message: 64-bit arithmetic overflow
Call stack traceback locations:
0x100020b79 0x10000ea80 0x100003520 0x100003912 0x10000143e

并且回溯的解码结果为

system__arith_64__raise_error (in specifics) (s-arit64.adb:364)
__gnat_mulv64 (in specifics) (s-arit64.adb:318)
specifics__testvalue.2581 (in specifics) (specifics.adb:20)        <<<<<<<<<<
_ada_specifics (in specifics) (specifics.adb:45)
main (in specifics) (b~specifics.adb:246)

并且specifics.adb:20

     TIO.Put_Line("Square: " & Fixed_Type'Image(val * val));

在异常处理程序中,又涉及到有问题的算术运算(在异常处理程序中这并不是件好事)。您可以看到,在前一行中打印了值为0.75的内容而且在fixedpointtest.adb中,前面的加法运算得到了最后一个有效值0.7071067810没有出现问题。
我很惊讶发现-gnato也能检测到此错误,因为我认为它只适用于整数运算。但实际上,在GNAT用户指南中有一篇讨论,指出它也适用于定点运算。结果表明,您可以通过使用-gnato3避免约束错误,并获得正确的算术结果。
>>> 0.75
Value  0.7500000000 squares properly.
Square:  0.5625000000

但这只能通过使用任意多精度算术来实现,这对于一个时间受限的系统来说不是一个好主意!


感谢您的快速回复。我想点赞,但我是新手,没有足够的声望。我按照建议更改了增量值,程序按预期工作。我很好奇为什么它会接受任何大于等于0.5的值。我重新编译了-gnato,发现它实际上并不接受这样的值。 - ericmaht
我使用原始的修订版重新编译,并使用了“-gnato”选项。 - ericmaht

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接