【问题标题】:Weird values for math expression数学表达式的奇怪值
【发布时间】:2016-12-21 22:16:28
【问题描述】:

我正在开发一款可在 Windows 和 Android 上运行的游戏,但它有一个我无法解决的问题。基本上我有一个带有一些按钮的 4x5 网格,这些按钮每秒填充一个必须是 2、4 或 8 的随机数。如果您点击两个具有相同数字的按钮,则会计算总和。这是一个火猴项目。

游戏运行良好,但您可以在下面的图片中看到问题。当我在我的 windows 机器上运行游戏时,它会生成 2、4 或 8。在 android 下它会生成 2、4、7 和 8。随机数是这样创建的:

valueToOutput := Trunc(Exp(Ln(2) * (1+Random(3))));

该变量保存要在按钮中显示的数字。为什么我在 windows 和 android 中得到不同的结果?这是两张截图

我确信该函数是正确的,因为我已经绘制了它 (exp(ln(2)*(1+x)) = http://prnt.sc/dmdc3u) 并且当 x 为 0,1 或 2 时(= 当随机数是 0,1 或 2)。这可能是编译器的问题吗?

注意:我已经使用您在下面看到的解决方法解决了这个问题,但起初我使用了您在问题中可以看到的代码,我想了解发生了什么.

valueToOutput := Trunc(Exp(Ln(2) * (1+Random(3))));

//this will always give 2, 4 or 8
if valueToOutput = 7 then
 valueToOutput := valueToOutput + 1;

【问题讨论】:

  • 简单的valueToOutput := 2 shl Random(3) 怎么样?然后你可以跳过所有的浮点垃圾。
  • 是的,当然,这是一个非常好的解决方案(也比乘法更快),但我想知道那个公式。我想知道这是否只是在 android 中的精度损失
  • Android 上的sizeof(Extended) 是什么?在 Win32 上是 10,但在 Win64 上是 8。也许在移动设备上也有类似的精度损失? Embarcadero 的 DocWiki 上没有记录。
  • 相关Is floating point math broken? 但是这并不能解释为什么这个函数不是确定性的。 Random(3) 应该返回 3 个可能的 Integer 值,这意味着没有其他变化的函数应该有 3 个可能的结果。我唯一能想到的可能是在调用之间改变 FPU 设置。这可能会导致内部值的细微差异,其中 pre-Trunc 值略小于 8 注意:Trunc() 丢弃了很多 跨度>
  • 顺便说一句:我怀疑你可能会在不同的 Android 硬件上得到不同的行为。

标签: android delphi firemonkey


【解决方案1】:

对于相同的输入,对于相同的浮点控制状态,浮点计算是可重复的。

有了三个不同的输入,您的表达式因此应该有三个不同的可能输出。因此,唯一的解释是某些东西会改变浮点控制状态,例如程序执行过程中的舍入方式、精度等。

如果浮点算术可以精确地执行计算,则不需要四舍五入为整数。但是如果你必须四舍五入,至少使用Round而不是Trunc四舍五入。

也就是说,这绝对是执行离散任务的错误方法,即随机选择 2、4 和 8 中的一个。这样做:

case Random(3) of
0:
  Result := 2;
1:
  Result := 4;
2:
  Result := 8;
end;

另一种方法是将可能的输出放入一个数组中,然后像这样选择它们:

Result := arr[Random(3)];

当有更多值可供选择时,这变得更具吸引力。

一个黄金法则是,如果您可以避免使用浮点数,请这样做。浮点比整数算术更慢,更难推理。仅在必要时使用。

【讨论】:

  • 关于如何调试和验证的任何指示?我想当控制状态发生变化或者可以从转储中提取控制状态进行验证时,它可能会中断。
  • 对arm编译器不熟悉,不了解详情。
【解决方案2】:

发布这个作为答案,因为它不适合 cmets

我对 Android 几乎一无所知,但我会在 Windows 上使用以下方法来缩小范围。很可能,Android 也存在类似的方法。

  1. 将临时计算保存在全局变量中。这使得从我们将要采用的故障转储中提取值变得更加容易。遇到错误值时引发异常。
  2. 将 procdump 附加到正在运行的进程。 Sysinternals 的 Procdump 允许您为遇到的每个异常创建一个转储文件。命令行类似于procdump -ma -e 1 Project1.exe
  3. 运行计算并等待它引发异常。
  4. 分析转储。可以从内存中提取 tmp 变量的值。

代码

var
  tmpRandom: Integer;
  tmpLn: Extended;
  tmpLnRandom: Extended;
  tmpExp: Extended;
  tmpTrunc: Int64;

procedure TForm1.btn1Click(Sender: TObject);
var
  I: Int64;
begin
  while true do
  begin
      tmpRandom := Random(3);
      tmpLn := Ln(2);
      tmpLnRandom := tmpLn * (1+tmpRandom);
      tmpExp := Exp(tmpLnRandom);
      tmpTrunc := Trunc(tmpExp);
      I := tmpTrunc;
      if (I and 1 = 1) then
        raise Exception.CreateFmt('I = %0:d', [I]);
  end;
end;

变量布局示例

【讨论】:

    【解决方案3】:

    我们没有看到全部。例如,您说 windows 生成 2、4 或 8,但我们在网格中看到 16。您说 Android 生成 7,但我们在网格中看到 14。很明显,您在某处乘以 2。我的分析是,由于 2 * 4 是 8,android 版本 only 生成 2、4 和 7,(不是 8),因为正如大卫所说,你只有 3 个起始状态,所以必须只有三个结束状态。在这种情况下使用 Trunc 是自找麻烦。我认为使用 Round 而不是 Trunc 会缓解这种情况。

    valueToOutput := Round(Exp(Ln(2) * (1+Random(3))));
    

    也就是说,正如大卫所说,这不是正确的做法。

    【讨论】:

    • 不知道为什么你会被否决。问题中的任何内容都无法反驳您的假设,有时开发人员确实错过了显而易见的事情。 (虽然问题确实解释了为什么某些值加倍。)
    • @CraigYoung。谢谢你。在我发布答案的时候,这个答案还没有被接受,我觉得我正在为理论上不可能的事情添加一个潜在的解决方案。我还认为(并且仍然认为)我正在确定他当时不理解的问题的原因,即使有更好的方法,
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-26
    • 2013-08-22
    • 2011-01-02
    • 2014-12-26
    相关资源
    最近更新 更多