【问题标题】:Different results between c++ and c# sin function with large valuesc++和c#大值sin函数结果不同
【发布时间】:2021-05-07 20:15:55
【问题描述】:

当我使用大数时,我在 C# 中遇到了 Math.Sin 函数的这种奇怪行为;例如:

C#:.Net 4.7.2:Math.Sin(6.2831853071795856E+45) = 6.2831853071795856E+45

C++: sin(6.2831853071795856E+45) = -0.089650623841643268

任何想法如何获得与 C++ 相同的结果?

C# 示例:

double value = 6.2831853071795856E+45;    
Console.WriteLine(Math.Sin(value));

C++ 示例:

double x = 6.2831853071795856E+45;
double result;
    
result = sin(x);
cout << "sin(x) = " << result << endl;

【问题讨论】:

  • 你确定你的数字是对的吗?一方面,sin 函数的返回值必须介于 0 和 1 之间...
  • 好的,如果我可以将我的观察添加到这个有趣的主题:C++ with -std=c++20 -O0: 0.824816C# with .net 5.0: 0.8248163906169679C# with .net 4.7.2: 6.28318530717959E+45,所以,嗯,编译器错误?为了完整起见:我的手机计算器:-0.6427876096。我想我再也不会碰浮点数学了。
  • 依赖于double的分辨率,1E45数量级的粒度巨大,比pi大很多,计算无意义。
  • DuckDuckGo 显示的官方 .net 文档中缓存的部分结果说“可接受的值范围从大约 -9223372036854775295 到大约 9223372036854775295。对于此范围之外的值,Sin 方法返回不变的值而不是抛出异常。”。不幸的是,这句话似乎已从官方网站本身中清除,所以我不知道它可能指的是哪个版本。
  • @Bathsheba 这是您评论上方指出的范围错误

标签: c# c++ .net floating-point .net-4.7.2


【解决方案1】:

这两个答案都大错特错——但您提出的问题可能会给您带来麻烦。

sin(6.2831853071795856 × 10⁴⁵)的真值约为0.09683996046341126;这个近似值与真实值的差异小于真实值的 10⁻¹⁶ 部分。 (我使用具有 165 位中间精度的 Sollya 计算了这个近似值。)

但是,您不会通过询问带有签名 public static double Sin (double a) 的 C# 函数或带有签名 double sin(double) 的 C++ 函数来得到这个答案。为什么? 6.2831853071795856 × 10⁴⁵ 不是 IEEE 754 binary64 或“双”浮点数,所以充其量您将了解附近浮点数的罪过是什么。在最近 em>的浮点数,和你通常会通过打字6.2831853071795856E+45进入一个程序,是6283185307179585571582855233194226059181031424,从6.2831853071795856×10⁴⁵相差28417144766805773940818968576≈2.84×10²⁸。 P>

IEEE 754 binary64 浮点数 6283185307179585571582855233194226059181031424 很好地近似于 6.2831853071795856 × 10⁴⁵ 相对误差 0,⁻⁵ 的部分,但小于 1绝对误差 ~2.84 × 10²⁸远远超过罪的周期 2?(并且远不接近 ? 的整数倍)。所以你通过询问双函数得到的答案将不支持与问题相似似乎询问:您在哪里可以在@ 987654331,而不是SIN(628318530717958560000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000真值)。

这不是浮点的错。浮点算术可以很好地保持相对误差较小——一个好的数学库可以很容易地使用 binary64 浮点为您提出的问题计算出一个好的答案。如果你的尺子没有超过 10⁴⁵ 的刻度,你输入 sin 的错误也可能来自一个小的测量误差。该错误可能来自某种近似错误,例如通过使用截断序列来评估任何给您 sin 输入的函数,无论您使用哪种算法来计算该输入。 问题是您要求在一个小的相对误差对应于一个远远超过功能。


因此,如果您发现自己试图回答 6.2831853071795856 × 10⁴⁵ 的原罪是什么的问题,那么您可能做错了——天真地使用双浮点数学库例程并不能帮助您回答您的问题题。但是使这个问题更加复杂的是,您的 C# 和 C++ 实现都无法返回接近 sin(6283185307179585571582855233194226059181031424) 真实值的任何值:

  • C# documentation for Math.Sin 宣传该域可能存在与机器相关的限制。

    最有可能的是,您使用的是 Intel CPU,而您的 C# 实现只是执行 Intel x87 fsin 指令,根据 Intel manual,该指令仅限于域 [−2⁶³, 2⁶³] 中的输入,而你的超过 2¹⁵²。正如您所观察到的,此域之外的输入会逐字返回,即使它们对于正弦函数来说是完全无意义的值。

    将输入放入一个肯定有效的范围内的一种快速而肮脏的方法是编写:

    Math.Sin(Math.IEEERemainder(6.2831853071795856E+45, 2*Math.PI))
    

    这样,您就不会误用 Math.Sin 库例程,因此答案至少应该在 [−1,1] 中,就像正弦一样。您可以使用sin(fmod(6.2831853071795856E+45, 2*M_PI)) 安排在 C/C++ 中获得相同或接近的结果。但是您可能会得到接近 0.35680453559729486 的结果,这也是错误的——请参阅下面的参数缩减。

  • 但是,您正在使用的 sin 的 C++ 实现完全被破坏了; C++ 标准中对域没有这样的限制,并且使用广泛可用的高质量软件来计算参数约简模?并在约简域上计算 sin,没有任何借口可以搞砸这个(即使这不是好问题要问!)。

    我不知道错误是什么,只是通过观察输出,但很可能是在参数减少步骤中:因为 sin(? + 2?) = sin(?) 和 sin(−?) = −sin (?参数约简是计算给定?的?的任务。

    典型的基于 x87 的 sin 实现使用 fldpi 指令以 64 位精度以二进制 80 格式加载 ? 的近似值,然后使用 fprem1 将该近似值模减少为 ?。这种近似不是很好:在内部,英特尔架构近似于π通过0x0.c90fdaa22168c234cp + 2 = 3.1415926535897932384585988507819109827323700301349163055419921875与精度66位,并且fldpi然后其四舍五入为binary80浮点数0x0.c90fdaa22168c235p + 2 = 3.14159265358979323851280895940618620443274267017841339111328125与只有 64 位精度。

    相比之下,典型的数学库,例如古老的 fdlibm,通常使用精度超过 100 位的近似值来减少模?,这就是为什么 fdlibm 导数能够非常准确地计算 sin(6283185307179585571582855233194226059181031424)。

    但是,使用fldpi/fprem1/fsin 进行的明显 x87 计算得出大约 -0.8053589558881794,并且将 x87 单元设置为 binary64 算术(53 位精度)而不是 binary80 算术(64-位精度),或者首先使用 ? 的二进制 64 近似值,得到大约 0.35680453559729486。所以很明显,你的 C++ 数学库正在做其他事情来为一个糟糕的问题提供错误的答案!


从您输入的数字来看,我猜您可能一直在尝试查看当您尝试评估 2? 的大倍数时会发生什么:6.2831853071795856 × 10⁴⁵ 与 2? × 10⁴⁵ 有一个小的相对误差。当然,这样的 sin 总是为零,但也许您更一般地想要计算函数 sin(2??),其中 ? 是一个浮点数。

浮点运算标准 IEEE 754-2019 推荐(但不强制)运算 sinPi、cosPi、tanPi 等,其中 sinPi(?) = sin(?⋅?)。如果您的数学库支持它们,您可以使用 sinPi(2*t) 来获得 sin(2??) 的近似值。在这种情况下,因为 ? 是一个整数(实际上对于任何二进制 64 浮点数,幅度至少为 2⁵²,它们都是整数),你会得到正好 0。

很遗憾,.NET does not yet have these functions(截至 2021 年 2 月 4 日),C 或 C++ 标准数学库也不包含它们,尽管您可以轻松找到 example code for sinPi and cosPi floating around

当然,您的问题仍然存在一个问题,即即使在非常输入时评估 sinPi 函数也会自找麻烦,因为即使是很小的相对误差(比如 10⁻¹⁷)仍然意味着绝对误差远远超出函数的周期 2。但是这个问题不会因为对一个超越数取模的坏参数归约而被放大:计算一个浮点数除以 2 后的余数很容易精确计算。

【讨论】:

  • 感谢您发布正确答案!欢迎来到堆栈溢出!我希望您有更多这种品质的内容与我们分享。
  • 值得注意的是,即使按照记录的限制调用 x87 FSIN 指令,它也不一定会给你正确的答案。为了避免灾难性的抵消,您需要将输入范围减小到 +/- pi/2。但即使这样还不够,因为您要处理 pi 表示的有限精度。如需更多信息,请参阅Bruce Dawson's excellent blog post on the subject
【解决方案2】:

6.2831853071795856E+45 等大数的sin;没有意义。请记住,Pi 大约是 3.14,而您的浮点数可能是 IEEE 754(更多信息请参见 http://floating-point-gui.de/

计算出来的数字可能完全错误。

考虑在您的 C 或 C++ 代码上使用静态分析工具,例如 Fluctuat。或者像CADNA这样的动态工具

根据经验,sincostan 等三角函数不应与 数字一起使用(例如,PI ℼ = 3.14 in 的数千倍以上)绝对值)。

否则,请使用像 GMPlib 这样的 bignum 库。它会使计算速度减慢数千倍,但它可以为sin (pi * 1.e+45) 提供有意义的结果

请阅读有关Taylor series 的内容(它们与三角函数有关)

也尝试使用GNUplot 来“可视化”sin 函数(对于合理的数字)。

在数学上,每个有限实数的sin 都在 -1 和 1 之间。所以 Math.Sin(6.2831853071795856E+45) = 6.2831853071795856E+45 是错误的。

还可以阅读 this onethat one 等会议论文。

【讨论】:

  • 这是否意味着每个具有极大数字和“正常”数字的计算都是毫无意义的?我怀疑。
  • @HimBromBeere 对于参数 n 和 n+epsilon,其结果在整个结果范围内随机变化的每个计算都是毫无意义的。
  • @HimBromBeere 其他计算,例如非常大的 n 的 n+1 仅仅是有问题的。
  • @HimBromBeere 问题在于像6.2831853071795856E+45 这样的数字的“精度”非常小,远低于sin 所需的“精度”6.28(在6.28 中,您可以一个完整的“轮”罪)。 6.2831853071795856E+45 之后的下一个数字有一个“距离”> 1E30
  • @xanatos 现在答案很有意义。
【解决方案3】:

作为旁注,.NET 计算 sin/cos/tan 的方式发生了变化。它发生在 2016 年 6 月 2 日,this commit 在 .NET Core 上。正如作者在floatdouble.cpp中所写:

AMD64 Windows 上的 Sin、Cos 和 Tan 之前在 vm\amd64\JitHelpers_Fast.asm 中实现 通过调用 x87 浮点代码(fsin、fcos、fptan),因为 CRT 助手太慢了。这 不再是这种情况,所有平台都使用 CRT 调用。

请注意,CRT 是 C 语言运行时。这解释了为什么较新版本的 .NET Core在同一平台上使用时会提供与 C++ 相同的结果。它们使用的 CRT 与其他 C++ 程序使用的相同。

“旧”代码的最新版本供感兴趣的人使用:12

尚不清楚 .NET Framework 是否/何时继承了这些更改。从一些测试看来,

.NET Core >= 2.0(未测试过以前的版本):Math.Sin(6.2831853071795856E+45) == 0.824816390616968

.NET Framework 4.8(32 位和 64 位):Math.Sin(6.2831853071795856E+45) == 6.28318530717959E+45

所以它没有继承它们。

有趣的附录

做了一些检查,Math.Sin(6.2831853071795856E+45) == 6.28318530717959E+45x87 程序集的fsin 操作码的“官方”答案,所以它是英特尔(大约 1980 年)关于罪过多少的“官方”答案6.2831853071795856E+45,所以如果你使用英特尔,你必须相信Math.Sin(6.2831853071795856E+45) == 6.28318530717959E+45,否则你就是叛徒! ???

如果你想仔细检查:

public static class TrigAsm
{
    [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
    private static extern IntPtr VirtualAlloc(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, uint flProtect);

    [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool VirtualProtect(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, out uint lpflOldProtect);

    [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool VirtualFree(IntPtr lpAddress, IntPtr dwSize, uint dwFreeType);

    private const uint PAGE_READWRITE = 0x04;
    private const uint PAGE_EXECUTE = 0x10;
    private const uint MEM_COMMIT = 0x1000;
    private const uint MEM_RELEASE = 0x8000;

    [SuppressUnmanagedCodeSecurity]
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    public delegate double Double2Double(double d);

    public static readonly Double2Double Sin;

    static TrigAsm()
    {
        // Opcoes generated with https://defuse.ca/online-x86-assembler.htm
        byte[] body = Environment.Is64BitProcess ?
            new byte[] 
            { 
                0xF2, 0x0F, 0x11, 0x44, 0x24, 0x08, // movsd  QWORD PTR [rsp+0x8],xmm0
                0xDD, 0x44, 0x24, 0x08,             // fld    QWORD PTR [rsp+0x8] 
                0xD9, 0xFE,                         // fsin
                0xDD, 0x5C, 0x24, 0x08,             // fstp   QWORD PTR [rsp+0x8]
                0xF2, 0x0F, 0x10, 0x44, 0x24, 0x08, // movsd  xmm0,QWORD PTR [rsp+0x8]
                0xC3,                               // ret
            } :
            new byte[] 
            { 
                0xDD, 0x44, 0x24, 0x04, // fld    QWORD PTR [esp+0x4]
                0xD9, 0xFE,             // fsin
                0xC2, 0x08, 0x00,       // ret    0x8
            };

        IntPtr buf = IntPtr.Zero;

        try
        {
            // We VirtualAlloc body.Length bytes, with R/W access
            // Note that from what I've read, MEM_RESERVE is useless
            // if the first parameter is IntPtr.Zero
            buf = VirtualAlloc(IntPtr.Zero, (IntPtr)body.Length, MEM_COMMIT, PAGE_READWRITE);

            if (buf == IntPtr.Zero)
            {
                throw new Win32Exception();
            }

            // Copy our instructions in the buf
            Marshal.Copy(body, 0, buf, body.Length);

            // Change the access of the allocated memory from R/W to Execute
            uint oldProtection;
            bool result = VirtualProtect(buf, (IntPtr)body.Length, PAGE_EXECUTE, out oldProtection);

            if (!result)
            {
                throw new Win32Exception();
            }

            // Create a delegate to the "function"
            Sin = (Double2Double)Marshal.GetDelegateForFunctionPointer(buf, typeof(Double2Double));

            buf = IntPtr.Zero;
        }
        finally
        {
            // There was an error!
            if (buf != IntPtr.Zero)
            {
                // Free the allocated memory
                bool result = VirtualFree(buf, IntPtr.Zero, MEM_RELEASE);

                if (!result)
                {
                    throw new Win32Exception();
                }
            }
        }
    }
}

在 .NET 中不能有内联汇编(Visual Studio 的内联汇编只有 32 位)。我解决了将汇编操作码放在一块内存中并将内存标记为可执行文件的问题。

后记:请注意,没有人再使用 x87 指令集,因为它很慢。

【讨论】:

  • x87 FSIN 指令要求源操作数以弧度给出,范围在 -2^63 到 +2^63 之间。超出该范围的任何内容都是指令集级别的未定义行为。因此,它实际上并没有产生“错误”的结果;当您输入错误时,不会出现“错误”结果。规范化源操作数是调用者的责任。生成FSIN 指令的库函数和/或编译器有责任执行此操作。不要责怪 x87 或英特尔!
  • 此外,您在后记中声称“x87 指令集......很慢”是非常可疑的。当然在某些情况下 SSE2 指令会更快,但 x87 提供了 SSE2 仍然无法实现的扩展精度,并且性能差异通常很小,除非您实际上可以对代码进行矢量化。编译器根本不再使用 x87 指令,因为他们不必这样做,而且编写基于堆栈的最优 x87 代码很困难。
  • @CodyGray 你说的都是真的,但是......我不是在看“正确”或“错误”。我正在解释为什么结果会在不同的“平台”上发生变化。关于“缓慢”,这是我在各个地方发现的参考。关于 x87 的附加精度,是的,x87 支持 80 位双精度,但例如它们甚至在 .NET 中不受支持(这是速度与精度的权衡)。关于 FSIN 的限制:没有注意到查看 Intel 表,但重读它们清楚地写了。
【解决方案4】:

f(x) = sin(x) 在正负 1 之间振荡,周期为 2 * pi,即 sin(x) = sin(N * 2 * pi + x)。

暂时忘掉编程语言,忘掉 sin(),想想科学计数法 1.23E+45 中的值是什么意思。它可以表示从 1225000...(41 个零)到 1234999...(42 个九)的任何值,即 1E43 个可能的整数(如果允许小数,则可以表示无限数量的值)。

我在这里用十进制术语表示严格的定义,并将有效数字与精确度等同起来。在此处查找链接以了解更多信息:https://en.wikipedia.org/wiki/Precision。我也希望我的指数算术正确,但无论如何,含义应该很清楚。

回到 sin()、C# 和 C++:因为纯数学函数输出每 2 * pi (~6.28) 重复一次,而 C# 和 C++ 中的双精度浮点数(IEEE 754 64 位双精度数 https://en.wikipedia.org/wiki/IEEE_754)只有 15 -17 个有效十进制数字,sin() 函数的输出可能没有意义,因为输入值“缺失”(不精确)至少 30 个有效/相关十进制数字

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多