有必要单独指定“小型”和“增量”吗?

4

编译器选择'Small的演示

作为Ada中固定点类型的新手,听到'Small的默认值是小于或等于指定delta的2的幂次方时,我感到惊讶。以下是一个简短的片段来介绍这个问题:

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
   type foo is delta 0.1 range 0.0..1.0;
   x : foo := foo'delta;
begin
   Put (x'Image);
   while true loop
      x := x + foo'delta;
      Put (x'Image);
   end loop;
end Main;

输出结果表明,“Small确实是小于0.1的最大2的幂,因为一些打印出来的值出现了两次:”
 0.1 0.1 0.2 0.3 0.3 0.4 0.4 0.5 0.6 0.6 0.7 0.8 0.8 0.9 0.9 1.0

raised CONSTRAINT_ERROR : main.adb:9 range check failed

解决方案:将它们指定为相同的值

如果我们真的想要0.1作为差值,我们可以这样说:

   real_delta : constant := 0.1;
   type foo is delta real_delta range 0.0..1.0
      with Small => real_delta;

问题:指定两个不同的值是否有用?

如果优化是唯一的用例,则可以将其作为布尔属性,或者只是一个警告“选择的增量不是2的幂(建议使用2 ** -4)”。是否有任何原因指定两个不同的值,例如:

   type foo is delta 0.1 range 0.0..1.0
      with Small => 0.07;
   x : foo := 0.4 + 0.4;  -- equals 0.7 rather than 0.8

这似乎只会让后来遇到这个问题的读者更加困惑。以下示例摘自John Barnes的《Ada 2012编程》第17.5节第434页。他没有解释为什么增量(delta)是一个远大于实际“Small used”值的值,而不是它的倍数。
π : constant := Ada.numerics.π;
type angle is delta 0.1 range -4*π .. 4 *π;
for Angle'Small use π * 2.0**(-13);

我看到的唯一区别是“图像现在只打印一位数字精度。那是唯一的区别吗?

此外,为什么我的编译器会拒绝for foo'Small use foo'Delta

我遇到了直接执行上述操作而不使用常量的代码:

type foo is delta 0.1 range 0.0..1.0;
for foo'Small use foo'Delta;

但 GNAT 在声明 foo 后立即报告其被冻结:

main.adb:6:04: representation item appears too late

这个在Ada的某个版本中改变了吗?它应该是有效的Ada2012吗?


3
因为你要求编译器将值 foo'Delta 分配给 foo'Small,所以表示子句被拒绝。因此,编译器会冻结类型 foo(包括类型的 small 部分)以计算(固定)foo'Delta 的值,但是一旦确定了值 foo'Delta,由于 foo 已经被冻结,赋值就不再可能了。 - DeeDee
1
我想不出有什么理由需要不同的small和delta。需要同时指定两者很烦人,而Barnes的例子很奇怪。 - Simon Wright
@SimonWright,这样做的原因是为了防止下溢,如果您感兴趣,以下有更多详细信息。 - Raffles
1
@SimonWright,也就是说,通常情况下你不应该同时指定两个相同的值,而应该像John Barnes一样指定'Small'来使用所有可用的位。希望这可以帮到你。谢谢。 - Raffles
有趣的是,你失败的代表条款代码与Barnes在Raffles参考下一页上给出的代码相同。我会声明一个常量Foo_Delta := 0.1;(当然,0.1是一个非常大的值)。 - Simon Wright
2个回答

4
免责声明:固定点算术是一个相当特殊的主题。关于这个主题,以下是我所理解的内容,但我必须在此发出警告:我可能对下面的写作有误。因此,对于每个阅读者,请纠正我如果我错了。

在Ada中,实数类型是通过它们的精度来定义的。这与大多数其他语言定义实数类型的方式(即在硬件上表示)相反。使用精度属性而不是表示方面来定义实数类型的选择符合语言哲学:精度作为一个概念与正确性密切相关;这是语言的目标。从精度方面来定义实数类型也更自然,因为您让编译器根据您对精度的要求选择最优类型(在计算机上,所有值都是近似值,您必须以某种方式处理这个事实)。

Delta属性定义了与基础固定点类型相关联的绝对误差界限(精度)的要求(另请参见Ada 83 Rationale, section 5.1.3)。其优点有两个:

  • 程序员使用需求指定数字类型,并将硬件上最佳表示的选择委托给编译器。

  • 绝对误差界限通常用于数值分析,以分析和预测算术运算对准确性的影响,并直接在类型定义中说明。数值分析(准确性和范围分析)是实现计算算法的重要方面,特别是在使用定点类型时。

2020年10月24日更新:这些先前的段落应该在Ada 83的原始语言规范的背景下阅读。此外,Ada 83语言有一个第二个重要目标,似乎影响了使用精度定义数字实数类型的选择:分离原则。请参见Ada 83 Rationale,第15章,以了解其含义。然而,在开发Ada 95时,逻辑类型属性(如精度)和机器表示之间的分离(至少对于定点类型)进行了审查,并发现实际上并不像人们所希望的那样有用(请参见Ada 95 Rationale,G.4.2)。因此,从Ada 95开始,Delta属性的作用已经减弱,而Small属性已被用于制定定点类型和操作的工作方式(例如,请参见RM G.2.3)。
作为一个例子,考虑下面的示例程序。该程序定义了一种数字类型,并指定"真实"值与基础表示之间的绝对差不得超过0.07:
type Fix is delta 0.07 range 0.0 .. 10.0;     --  0.07 is just a random value here

换句话说,当给定的“真实”值被转换为类型 Fix 时,它将获得+/- 0.07的不确定性。 因此,下面程序中的三个命名常量 X , Y 和 Z ,当转换为类型 Fix 时,变为:
X : constant := 5.6;       --  Becomes 5.6 +/- 0.07 when casted to type Fix.
Y : constant := 0.3;       --  Becomes 0.3 +/- 0.07 when casted to type Fix.
Z : constant := 2.5;       --  Becomes 2.5 +/- 0.07 when casted to type Fix.

鉴于这些不确定性,人们可以计算某个算术操作序列的结果的不确定度(也可参见this SO上的优秀答案)。这实际上在程序中得到了证明。

更新 24-10-2020:回过头来看,这似乎是不正确的,并且存在复杂性。程序中的不确定度计算没有考虑到可能在计算和最终赋值过程中发生的数字中间和最终转换(量化)。因此,计算出的不确定度不正确且过于乐观(即它们应该更大)。尽管如此,我不会删除示例程序,因为它确实提供了对 Delta 属性最初意图的直观理解。

三个计算都使用了 Long_FloatFlt)和自定义的定点类型 Fix。使用 Long_Float 进行计算的结果当然也是一个近似值,但为了演示起见,我们可以假设它是精确的。然而,定点计算的结果具有(非常)有限的精度,因为我们为类型 Fix 指定了相当大的误差界限。另一方面,定点值需要更少的空间(这里:每个值只有 8 位),并且算术运算不需要专门的浮点硬件。
你可以调整 Small 属性的事实只是为了让程序员控制由固定点类型定义的集合中可用的模型数。将 Small 表示方面始终设置为 Delta 属性可能很诱人,但是使它们相等通常不会改变使用定点数进行算术运算时需要执行一些数值(误差)分析的要求。 2020年10月24日更新:我认为这个声明只部分正确。 Small 属性确实允许程序员控制模型数字(即数据类型可以准确表示的数字),但不仅仅是如此。自 Ada 95 以来,Small 属性在固定点算术应该如何工作方面扮演了重要角色(RM G.2.3),此外,大多数固定点算法文档和软件分析(例如here)假定已知类型在硬件中的实际表示方法;他们对主题的处理并不从绝对误差界限出发,而是总是从固定点值的表示出发。

最后,这一切都是关于使用资源(内存、浮点硬件)换取数值精度。

更新于2020年10月24日:此声明还需要一条注释:在Ada中执行定点运算不需要浮点运算取决于上下文。特别是乘法和除法的定点运算,如果操作数和操作结果的类型对于Small具有特定值,则可以仅使用整数运算来完成。这里放太多细节了,但是一些有趣的信息实际上可以在GNAT本身的源代码中找到,例如exp_fixd.adb

2020年10月24日更新: 总的来说,鉴于Ada 95的变化以及用于执行定点分析的工具的当前状态,似乎没有充分的理由选择不同的DeltaSmall值。 Delta属性仍代表绝对误差界限,但其值并非像最初认为的那样有用。正如您已经提到的那样,它的主要用途似乎只在固定点数据类型的I/O中(RM 3.5.10 (5)Ada.Text_IO.Fixed_IO)。

main.adb

pragma Warnings (Off, "static fixed-point value is not a multiple of Small");
pragma Warnings (Off, "high bound adjusted down by delta (RM 3.5.9(13))");


with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

   type Flt is new Long_Float;
   type Fix is delta 0.07 range 0.0 .. 10.0;

   ---------
   -- Put --
   ---------

   procedure Put (Nominal, Uncertainty : Flt; Result : Fix) is

      package Fix_IO is new Fixed_IO (Fix);
      use Fix_IO;

      package Flt_IO is new Float_IO (Flt);
      use Flt_IO;

   begin
      Put ("   Result will be within     : ");
      Put (Nominal, Fore => 2, Aft => 4, Exp => 0);
      Put (" +/-");
      Put (Uncertainty, Fore => 2, Aft => 4, Exp => 0);
      New_Line;

      Put ("   Actual fixed-point result : ");
      Put (Result, Fore => 2);
      New_Line (2);

   end Put;

   X : constant := 5.6;
   Y : constant := 0.3;
   Z : constant := 2.5;

   D : constant Flt := Fix'Delta;

begin

   Put_Line ("Size  of fixed-point type : " & Fix'Size'Image);
   Put_Line ("Small of fixed-point type : " & Fix'Small'Image);
   New_Line;

   --  Update 24-10-2020: Uncertainty computation is too optimistic. It omits
   --                     the effect of quantization in intermediate and final
   --                     variable assignments.

   Put_Line ("X + Y = ");
   Put (Nominal     => Flt (X) + Flt (Y),
        Uncertainty => D + D,
        Result      => Fix (X) + Fix (Y));

   Put_Line ("X * Y = ");
   Put (Nominal     => Flt (X) * Flt (Y),
        Uncertainty => (D / X + D / Y) * X * Y,
        Result      => Fix (X) * Fix (Y));

   Put_Line ("X * Y + Z = ");
   Put (Nominal     => Flt (X) * Flt (Y) + Flt (Z),
        Uncertainty => (D / X + D / Y) * X * Y + D,
        Result      => Fix (X) * Fix (Y) + Fix (Z));

end Main;

输出

Size  of fixed-point type :  8
Small of fixed-point type :  6.25000000000000000E-02

X + Y = 
   Result will be within     :  5.9000 +/- 0.1400
   Actual fixed-point result :  5.81

X * Y = 
   Result will be within     :  1.6800 +/- 0.4130
   Actual fixed-point result :  1.38

X * Y + Z = 
   Result will be within     :  4.1800 +/- 0.4830
   Actual fixed-point result :  3.88

但错误实际上是由“Small”而不是“Delta”限定的,这个限制比你的代码给出的更小,但是正确的,尽管我无法证明是否存在失败的情况。我的问题的一部分是:除了“Put()”默认给出的表示之外,在指定Delta和Small时是否有任何区别? - TamaMcGlinn
我感谢并赞扬您的努力,尽管这个答案包含了很多关于误差界限的有用解释,但它并没有回答我的问题。答案应该是列出指定 delta、small 或两者都使用的用例列表。 - TamaMcGlinn
你提到了浮点硬件,但这正确吗?我认为指定一个非2的幂次方的“小数”会导致额外的整数乘法指令进行缩放,但从不涉及浮点指令。 - TamaMcGlinn
@TamaMcGlinn 所以我承认:在这个答案中,我走进了一个兔子洞。由于其历史,Ada中的定点算术主题相当复杂。我更新了我的答案,并加上了一些额外的评论和某种结论,希望能够在一定程度上回答您的问题。 - DeeDee
1
@TamaMcGlinn 是的,实际的误差界限是“小”,但最初的意图是程序员将使用基于某种一次性分析的Delta指定所需的绝对误差界限。编译器将选择一个值为Small(因此是实际的绝对误差界限),给定可用的硬件。将要求与实际表示分离将允许算法在数值精度方面具有某种可移植性。然而,正如我在更新的答案中所述,这并没有像预期的那样有效。 - DeeDee
通过浮点硬件(我猜你指的是最后一句话),我指的是使用浮点算术和相关硬件进行计算,通常可以获得良好的精度,而不使用它,而是使用固定点算术进行计算,从而导致某些通常较低的精度。 固定点操作可以仅使用整数操作来实现,但由于Ada中固定点类型的灵活性(请参见更新的答案),这可能并非总是如此。 - DeeDee

2
是的。为了避免下溢和/或不浪费比特。
考虑一下,如果你把0.2乘以0.2,然后在某个时间点上除以0.1。
正确答案是0.4。然而,如果你的'Small'与'Delta'相同(即0.1),当0.2被平方时,真实值0.04将下溢,因此将被计算为零。当你然后除以0.1时,你会得到零作为答案,而不是0.4。
另一方面,如果你将你的'Small'指定为0.01,则答案将被正确计算。如果你尝试访问它,第一部分计算将被正确报告为零,因为0.04最接近于零,并且这是最接近0.1的正确表示,但是当该值然后被除以0.1时,正确的值出现了,即0.4。
考虑一下,如果你有一个16位值,并且范围如你的示例中所指定的那样'range 0.0..1.0',那么只有11个可能的值。这些可以用4位表示,但这将使其他12位完全浪费。为什么不利用它们来保持额外的精度,以便在有任何算术运算时答案是准确的。我注意到很多工程师在这些问题上与自己斗争,除非他们能看到可能需要计算的原因,否则他们很难指定额外的精度。然而,我认为这是错误的问题。一个更好的问题是你要浪费其他位吗?利用它们是不需要成本的,并且可以预防任何可能发生在类型上的未预见的计算,并避免出现错误。
这就是错误发生的方式。工程师A认为:“我看不到这个测量值会涉及到任何计算,它只是被记录并报告,我将'Small'设置为'Delta'。”不幸的是,工程师A只考虑了当前项目。5年后,工程师A已经离开公司,工程师B被要求添加一个功能/重用现有代码在一个新项目/将其变成一个产品,然后工程师C最终必须对其进行一些算术运算......没有意识到这个16位值实际上只有4位精度。砰。Barnes先生显然非常清楚这些问题!
一点补充 - 回到你最初的调查 - 干得好。是的,这就是正确的方式。事实上,由于这些问题(即非常不直观的默认行为),Ada现在有了十进制定点类型,因此您可以执行例如type Foo2 is delta 0.1 digits 2;,它指定了一个具有实际增量为0.1(而不是小于其的二进制分数)的定点类型,因此行为更加直观。指定2个数字范围为-9.9至+9.0,指定digits 1范围为-0.9至+0.9等。增量可以是任何10的幂次。希望这对您有所帮助。

工程师D可能会稍后加入,尝试对100个值进行求和,然后再除以总和 - 这将需要使用额外的位数,与我看到的相反。 - TamaMcGlinn
那么,在Barnes的例子中,π * 2.0**(-13)是指“32位最大精度”或类似的东西吗?你是怎么算出来的? - TamaMcGlinn
嗨@TamaMcGlinn,关于你的第一点,这是通过类型范围来指定的,而不是“小”,所以我没有提到它,但你绝对是正确的。通常情况下,您必须将范围设置为原因充分。但是,如果没有 - 是的,您应该考虑在两个方向上最好使用位。请记住,您将失去编译时检查和运行时约束错误的安全性。Ada没有等效的保护措施来防止下溢,因此,如果您只是扩展精度而不是范围,则不会失去任何东西。 - Raffles
1
关于Barnes的例子,我认为Barnes先生可能借鉴了您的思路-明显的范围应该是+/-2pi,但+/-4pi允许进行加法。我没有仔细阅读,但在我看来,它看起来像一个范围为8,即二进制点上方需要3位,二进制点下方需要13位-总共16位,这当然是许多常用嵌入式处理器和微控制器的字长,Ada经常用于这种平台。但是我只是在字里行间读懂了一些东西-您必须询问Barnes先生才能确定。 - Raffles
1
Barnes第237页的浮点代码范围为0.0 .. 2.0 * pi,我认为第434页的示例错误地记为-2.0 * pi .. 2.0 * pi。在使用定点数(或任何情况下),我最初会将范围指定为-pi .. pi,然后意识到加法问题并将其加倍;这来自于底层硬件相关的观点。我现在意识到,当我说小应该等于delta时,我想表达的是delta应该等于小——测量的精度无疑会低于精度,但类型系统能否用于处理此问题? - Simon Wright
@Raffles 虽然 Ada 没有防止下溢的保护,但是 SPARK 有。即使没有修改 Ada 代码(例如添加合同),运行 gnatprove 也会告诉您任何可能的下溢。 - TamaMcGlinn

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