C# 中的浮点数学一致吗?可以吗?

发布于 2024-11-24 03:25:15 字数 1559 浏览 1 评论 0 原文

不,这不是另一个“为什么是 (1/3.0)*3 != 1”问题。

我最近读了很多关于浮点的文章;具体来说,在不同的架构或优化设置上相同的计算可能会产生不同的结果

对于存储重播或点对点网络的视频游戏来说,这是一个问题(与服务器-客户端相反),它依赖于所有客户端每次运行程序时都会生成完全相同的结果 - 一个浮点计算中的微小差异可能会导致不同机器上截然不同的游戏状态(甚至<一个href="http://www.parashift.com/c%2B%2B-faq-lite/newbie.html#faq-29.18" rel="noreferrer">在同一台机器上!)

这种情况甚至会发生在“遵循”IEEE-754 的处理器中,主要是因为某些处理器(即x86) 使用双倍扩展精度。也就是说,它们使用 80 位寄存器进行所有计算,然后截断为 64 位或 32 位,从而导致舍入结果与使用 64 位或 32 位进行计算的机器不同。

我在网上看到了这个问题的几种解决方案,但都是针对 C++,而不是 C#:


那么,这在 C# 中也是一个问题吗?如果我只想支持 Windows(而不是 Mono)怎么办?

如果是,有没有办法强制我的程序以正常双精度运行?

如果不是,是否有任何库可以帮助保持浮点计算的一致性?

No, this is not another "Why is (1/3.0)*3 != 1" question.

I've been reading about floating-points a lot lately; specifically, how the same calculation might give different results on different architectures or optimization settings.

This is a problem for video games which store replays, or are peer-to-peer networked (as opposed to server-client), which rely on all clients generating exactly the same results every time they run the program - a small discrepancy in one floating-point calculation can lead to a drastically different game-state on different machines (or even on the same machine!)

This happens even amongst processors that "follow" IEEE-754, primarily because some processors (namely x86) use double extended precision. That is, they use 80-bit registers to do all the calculations, then truncate to 64- or 32-bits, leading to different rounding results than machines which use 64- or 32- bits for the calculations.

I've seen several solutions to this problem online, but all for C++, not C#:

  • Disable double extended-precision mode (so that all double calculations use IEEE-754 64-bits) using _controlfp_s (Windows), _FPU_SETCW (Linux?), or fpsetprec (BSD).
  • Always run the same compiler with the same optimization settings, and require all users to have the same CPU architecture (no cross-platform play). Because my "compiler" is actually the JIT, which may optimize differently every time the program is run, I don't think this is possible.
  • Use fixed-point arithmetic, and avoid float and double altogether. decimal would work for this purpose, but would be much slower, and none of the System.Math library functions support it.

So, is this even a problem in C#? What if I only intend to support Windows (not Mono)?

If it is, is there any way to force my program to run at normal double-precision?

If not, are there any libraries that would help keep floating-point calculations consistent?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

眉黛浅 2024-12-01 03:25:15

我知道没有办法让.net中的普通浮点具有确定性。 JITter 可以创建在不同平台(或不同版本的 .net 之间)表现不同的代码。因此,在确定性 .net 代码中使用普通的 float 是不可能的。

我考虑的解决方法:

  1. 在 C# 中实现 FixPoint32。虽然这并不太难(我已经完成了一半的实现),但非常小的值范围使其使用起来很烦人。您必须始终小心,以免溢出或失去太多精度。最后我发现这并不比直接使用整数容易。
  2. 在 C# 中实现 FixPoint64。我发现这很难做到。对于某些操作,128 位的中间整数会很有用。但.net 不提供这样的类型。
  3. 实现自定义 32 位浮点。缺乏 BitScanReverse 内在函数会在实现时带来一些麻烦。但目前我认为这是最有前途的一条路。
  4. 使用本机代码进行数学运算。每个数学运算都会产生委托调用的开销。

我刚刚开始 32 位浮点数学的软件实现。在我的 2.66GHz i3 上,它每秒可以执行大约 7000 万次加法/乘法。
https://github.com/CodesInChaos/SoftFloat 。显然它仍然非常不完整并且有缺陷。

I know of no way to way to make normal floating points deterministic in .net. The JITter is allowed to create code that behaves differently on different platforms(or between different versions of .net). So using normal floats in deterministic .net code is not possible.

The workarounds I considered:

  1. Implement FixedPoint32 in C#. While this is not too hard(I have a half finished implementation) the very small range of values makes it annoying to use. You have to be careful at all times so you neither overflow, nor lose too much precision. In the end I found this not easier than using integers directly.
  2. Implement FixedPoint64 in C#. I found this rather hard to do. For some operations intermediate integers of 128bit would be useful. But .net doesn't offer such a type.
  3. Implement a custom 32 bit floatingpoint. The lack of a BitScanReverse intrinsic causes a few annoyances when implementing this. But currently I think this is the most promising path.
  4. Use native code for the math operations. Incurs the overhead of a delegate call on every math operation.

I've just started a software implementation of 32 bit floating point math. It can do about 70million additions/multiplications per second on my 2.66GHz i3.
https://github.com/CodesInChaos/SoftFloat . Obviously it's still very incomplete and buggy.

放手` 2024-12-01 03:25:15

C# 规范(第 4.1.6 节浮点类型)特别允许使用高于结果的精度来完成浮点计算。所以,不,我认为你不能直接在 .Net 中使这些计算具有确定性。其他人提出了各种解决方法,因此您可以尝试一下。

The C# specification (§4.1.6 Floating point types) specifically allows floating point computations to be done using precision higher than that of the result. So, no, I don't think you can make those calculations deterministic directly in .Net. Others suggested various workarounds, so you could try them.

寂寞清仓 2024-12-01 03:25:15

如果您需要此类操作的绝对可移植性,以下页面可能会很有用。它讨论了用于测试 IEEE 754 标准实现的软件,包括用于模拟浮点运算的软件。然而,大多数信息可能特定于 C 或 C++。

http://www.math.utah.edu/~beebe/software/ieee /

关于定点的说明

二进制定点数也可以很好地替代浮点,这从四个基本算术运算中可以明显看出:

  • 加法和减法是微不足道的。它们的工作方式与整数相同。只需添加或减去即可!
  • 要乘两个定点数,请将两个数字相乘,然后右移定义的小数位数。
  • 要除两个定点数,请将被除数向左移动定义的小数位数,然后除以除数。
  • Hattangady (2007) 的第四章提供了有关实现二进制定点数的附加指导(SK Hattangady,“

二进制定点数可以在任何整数数据类型(例如 int、long 和 BigInteger)以及不符合 CLS 的类型 uint 和 ulong 上实现。

正如另一个答案中所建议的,您可以使用查找表,其中表中的每个元素都是二进制定点数,以帮助实现复杂的函数,例如正弦、余弦、平方根等。如果查找表的粒度小于定点数,建议通过将查找表粒度的一半添加到输入来对输入进行舍入:

// Assume each number has a 12 bit fractional part. (1/4096)
// Each entry in the lookup table corresponds to a fixed point number
//  with an 8-bit fractional part (1/256)
input+=(1<<3); // Add 2^3 for rounding purposes
input>>=4; // Shift right by 4 (to get 8-bit fractional part)
// --- clamp or restrict input here --
// Look up value.
return lookupTable[input];

The following page may be useful in the case where you need absolute portability of such operations. It discusses software for testing implementations of the IEEE 754 standard, including software for emulating floating point operations. Most information is probably specific to C or C++, however.

http://www.math.utah.edu/~beebe/software/ieee/

A note on fixed point

Binary fixed point numbers can also work well as a substitute for floating point, as is evident from the four basic arithmetic operations:

  • Addition and subtraction are trivial. They work the same way as integers. Just add or subtract!
  • To multiply two fixed point numbers, multiply the two numbers then shift right the defined number of fractional bits.
  • To divide two fixed point numbers, shift the dividend left the defined number of fractional bits, then divide by the divisor.
  • Chapter four of Hattangady (2007) has additional guidance on implementing binary fixed point numbers (S.K. Hattangady, "Development of a Block Floating Point Interval ALU for DSP and Control Applications", Master's thesis, North Carolina State University, 2007).

Binary fixed point numbers can be implemented on any integer data type such as int, long, and BigInteger, and the non-CLS-compliant types uint and ulong.

As suggested in another answer, you can use lookup tables, where each element in the table is a binary fixed point number, to help implement complex functions such as sine, cosine, square root, and so on. If the lookup table is less granular than the fixed point number, it is suggested to round the input by adding one half of the granularity of the lookup table to the input:

// Assume each number has a 12 bit fractional part. (1/4096)
// Each entry in the lookup table corresponds to a fixed point number
//  with an 8-bit fractional part (1/256)
input+=(1<<3); // Add 2^3 for rounding purposes
input>>=4; // Shift right by 4 (to get 8-bit fractional part)
// --- clamp or restrict input here --
// Look up value.
return lookupTable[input];
紫﹏色ふ单纯 2024-12-01 03:25:15

这对于 C# 来说是一个问题吗?

是的。不同的架构是您最不用担心的,不同的帧速率等可能会由于浮点表示的不准确而导致偏差 - 即使它们是相同的不准确(例如相同的架构,除了一台机器上的 GPU 速度较慢) )。

我可以使用 System.Decimal 吗?

没有理由不能,但它太慢了。

有没有办法强制我的程序以双精度运行?

是的。 自行托管 CLR 运行时;并在调用 CorBindToRuntimeEx 之前将所有必要的调用/标志(更改浮点运算的行为)编译到 C++ 应用程序中。

是否有任何库可以帮助保持浮点计算的一致性?

据我所知没有。

还有其他方法可以解决这个问题吗?

我之前已经解决过这个问题,想法是使用 QNumber。它们是定点实数的一种形式;但不是以 10 为底(十进制)的固定点 - 而是以 2 为底(二进制);因此,它们上的数学原语(add、sub、mul、div)比简单的以 10 为基数的定点要快得多;特别是如果两个值的 n 相同(在您的情况下是相同的)。此外,由于它们是不可或缺的,因此它们在每个平台上都有明确定义的结果。

请记住,帧速率仍然会影响这些,但它并没有那么糟糕,并且可以使用同步点轻松纠正。

我可以在 QNumbers 中使用更多数学函数吗?

是的,可以通过往返小数来实现此目的。此外,您确实应该使用 查找表< /a> 用于三角(sin、cos)函数;因为它们在不同平台上确实会给出不同的结果 - 如果您正确编码它们,它们可以直接使用 QNumbers。

Is this a problem for C#?

Yes. Different architectures are the least of your worries, different framerates etc. can lead to deviations due to inaccuracies in float representations - even if they are the same inaccuracies (e.g. same architecture, except a slower GPU on one machine).

Can I use System.Decimal?

There is no reason you can't, however it's dog slow.

Is there a way to force my program to run in double precision?

Yes. Host the CLR runtime yourself; and compile in all the nessecary calls/flags (that change the behaviour of floating point arithmetic) into the C++ application before calling CorBindToRuntimeEx.

Are there any libraries that would help keep floating point calculations consistent?

Not that I know of.

Is there another way to solve this?

I have tackled this problem before, the idea is to use QNumbers. They are a form of reals that are fixed-point; but not fixed point in base-10 (decimal) - rather base-2 (binary); because of this the mathematical primitives on them (add, sub, mul, div) are much faster than the naive base-10 fixed points; especially if n is the same for both values (which in your case it would be). Furthermore because they are integral they have well-defined results on every platform.

Keep in mind that framerate can still affect these, but it is not as bad and is easily rectified using syncronisation points.

Can I use more mathematical functions with QNumbers?

Yes, round-trip a decimal to do this. Furthermore, you should really be using lookup tables for the trig (sin, cos) functions; as those can really give different results on different platforms - and if you code them correctly they can use QNumbers directly.

零時差 2024-12-01 03:25:15

根据这个稍微旧的 MSDN 博客条目,JIT 不会使用 SSE /SSE2 表示浮点,都是 x87。因此,正如您提到的,您必须担心模式和标志,而在 C# 中这是无法控制的。因此,使用普通的浮点运算并不能保证程序在每台机器上得到完全相同的结果。

为了获得双精度的精确再现性,您必须进行软件浮点(或定点)仿真。我不知道 C# 库可以做到这一点。

根据您需要的操作,您也许可以使用单精度。想法如下:

  • 以单精度存储您关心的所有值
  • 以执行操作:
    • 将输入扩展为双精度
    • 以双精度进行运算
    • 将结果转换回单精度

x87 的一个大问题是,计算可能以 53 位或 64 位精度完成,具体取决于精度标志以及寄存器是否溢出记忆。但对于许多运算,以高精度执行运算并舍入到较低精度将保证正确的答案,这意味着答案将保证在所有系统上都相同。是否获得额外的精度并不重要,因为无论哪种情况,您都有足够的精度来保证正确的答案。

在此方案中应适用的运算:加法、减法、乘法、除法、开方。像 sin、exp 等这样的东西不会起作用(结果通常会匹配,但不能保证)。 “什么时候双舍入无害?” ACM 参考(付费注册要求)

希望这会有所帮助!

According to this slightly old MSDN blog entry the JIT will not use SSE/SSE2 for floating point, it's all x87. Because of that, as you mentioned you have to worry about modes and flags, and in C# that's not possible to control. So using normal floating point operations will not guarantee the exact same result on every machine for your program.

To get precise reproducibility of double precision you are going to have to do software floating point (or fixed point) emulation. I don't know of C# libraries to do this.

Depending on the operations you need, you might be able to get away with single precision. Here's the idea:

  • store all values you care about in single precision
  • to perform an operation:
    • expand inputs to double precision
    • do operation in double precision
    • convert result back to single precision

The big issue with x87 is that calculations might be done in 53-bit or 64-bit accuracy depending on the precision flag and whether the register spilled to memory. But for many operations, performing the operation in high precision and rounding back to lower precision will guarantee the correct answer, which implies that the answer will be guaranteed to be the same on all systems. Whether you get the extra precision won't matter, since you have enough precision to guarantee the right answer in either case.

Operations that should work in this scheme: addition, subtraction, multiplication, division, sqrt. Things like sin, exp, etc. won't work (results will usually match but there is no guarantee). "When is double rounding innocuous?" ACM Reference (paid reg. req.)

Hope this helps!

梦旅人picnic 2024-12-01 03:25:15

正如其他答案已经指出的:
是的,这是 C# 中的一个问题 - 即使保持纯粹的 Windows。

至于解决方案:
如果您使用内置的 BigInteger 类并通过对任何计算/存储使用公分母将所有计算缩放到定义的精度,则可以减少(并且需要付出一些努力/性能损失)完全避免问题这样的数字。

根据 OP 的要求 - 关于性能:

System.Decimal 表示带有 1 位符号、96 位整数和“标度”(表示小数点所在位置)的数字。对于您进行的所有计算,都必须在此数据结构上进行操作,并且不能使用 CPU 内置的任何浮点指令。

BigInteger“解决方案”执行类似的操作 - 只是您可以定义需要/想要多少位......也许您只需要 80 位或 240 位精度。

缓慢的原因始终是必须通过纯整数指令模拟这些数字的所有运算,而不使用 CPU/FPU 内置指令,这反过来又导致每个数学运算需要更多指令。

为了减轻性能损失,有几种策略 - 例如 QNumbers(请参阅 Jonathan Dickinson 的回答 - 浮点数学在 C# 中一致吗?可以吗?)和/或缓存(例如三角计算...)等。

As already stated by other answers:
Yes, this is a problem in C# - even when staying pure Windows.

As for a solution:
You can reduce (and with some effort/performance hit) avoid the problem completely if you use built-in BigInteger class and scaling all calculations to a defined precision by using a common denominator for any calculation/storage of such numbers.

As requested by OP - regarding performance:

System.Decimal represents number with 1 bit for a sign and 96 bit Integer and a "scale" (representing where the decimal point is). For all calculations you make it must operate on this data structure and can't use any floating point instructions built into the CPU.

The BigInteger "solution" does something similar - only that you can define how much digits you need/want... perhaps you want only 80 bits or 240 bits of precision.

The slowness comes always from having to simulate all operations on these number via integer-only instructions without using the CPU/FPU-built-in instructions which in turn leads to much more instructions per mathematical operation.

To lessen the performance hit there are several strategies - like QNumbers (see answer from Jonathan Dickinson - Is floating-point math consistent in C#? Can it be?) and/or caching (for example trig calculations...) etc.

埖埖迣鎅 2024-12-01 03:25:15

好吧,这是我关于如何执行此操作的第一次尝试:

  1. 创建一个 ATL.dll 项目,其中包含一个用于关键浮点运算的简单对象。确保使用禁止使用任何非 xx87 硬件进行浮点运算的标志进行编译。
  2. 创建调用浮点运算并返回结果的函数;从简单开始,如果它对您有用,您可以随时增加复杂性以满足您以后的性能需求(如有必要)。
  3. 将 control_fp 调用放在实际的数学中,以确保它在所有机器上都以相同的方式完成。
  4. 引用您的新库并进行测试以确保它按预期工作。

(我相信您可以编译为 32 位 .dll,然后将其与 x86 或 AnyCpu 一起使用 [或者可能仅针对 64 位系统上的 x86;请参阅下面的评论]。)

然后,假设它有效,您应该想要使用 Mono 我想你应该能够以类似的方式在其他 x86 平台上复制该库(当然不是 COM;不过,也许可以使用 wine?一旦我们去那里,就有点超出我的范围了......) 。

假设您可以使其工作,您应该能够设置可以同时执行多个操作的自定义函数来解决任何性能问题,并且您将拥有浮点数学,可以让您以最少的量在跨平台上获得一致的结果用 C++ 编写的代码,并将其余代码保留在 C# 中。

Well, here would be my first attempt on how to do this:

  1. Create an ATL.dll project that has a simple object in it to be used for your critical floating point operations. make sure to compile it with flags that disable using any non xx87 hardware to do floating point.
  2. Create functions that call floating point operations and return the results; start simple and then if it's working for you, you can always increase the complexity to meet your performance needs later if necessary.
  3. Put the control_fp calls around the actual math to ensure that it's done the same way on all machines.
  4. Reference your new library and test to make sure it works as expected.

(I believe you can just compile to a 32-bit .dll and then use it with either x86 or AnyCpu [or likely only targeting x86 on a 64-bit system; see comment below].)

Then, assuming it works, should you want to use Mono I imagine you should be able to replicate the library on other x86 platforms in a similar manner (not COM of course; although, perhaps, with wine? a little out of my area once we go there though...).

Assuming you can make it work, you should be able to set up custom functions that can do multiple operations at once to fix any performance issues, and you'll have floating point math that allows you to have consistent results across platforms with a minimal amount of code written in C++, and leaving the rest of your code in C#.

若能看破又如何 2024-12-01 03:25:15

我不是游戏开发人员,尽管我确实对计算难题有很多经验……所以,我会尽力而为。

我将采用的策略本质上是这样的:

  • 使用较慢的(如果有必要;如果有更快的方法,那就太好了!),但可预测的方法来获得可重现的结果
  • 使用 double 来处理其他所有事情(例如,渲染

)就是:你需要找到一个平衡点。如果您花费 30 毫秒渲染(约 33 fps),并且仅花费 1 毫秒进行碰撞检测(或插入一些其他高度敏感的操作)——即使您将执行关键算术所需的时间增加三倍,它对帧速率的影响也是如此你从 33.3fps 下降到 30.3fps。

我建议您分析所有内容,计算每项明显昂贵的计算花费了多少时间,然后使用一种或多种解决此问题的方法重复测量,并查看影响是什么。

I'm not a game developer, though I do have a lot of experience with computationally difficult problems ... so, I'll do my best.

The strategy I would adopt is essentially this:

  • Use a slower (if necessary; if there's a faster way, great!), but predictable method to get reproducible results
  • Use double for everything else (eg, rendering)

The short'n long of this is: you need to find a balance. If you're spending 30ms rendering (~33fps) and only 1ms doing collision detection (or insert some other highly sensitive operation) -- even if you triple the time it takes to do the critical arithmetic, the impact it has on your framerate is you drop from 33.3fps to 30.3fps.

I suggest you profile everything, account for how much time is spent doing each of the noticeably expensive calculations, then repeat the measurements with 1 or more methods of resolving this problem and see what the impact is.

白芷 2024-12-01 03:25:15

检查其他答案中的链接可以清楚地表明您永远无法保证浮点是否“正确”实现,或者您是否总是会获得给定计算的一定精度,但也许您可以通过以下方式尽最大努力(1) 将所有计算截断到共同的最小值(例如,如果不同的实现将为您提供 32 到 80 位的精度,则始终将每个操作截断为 30 或 31 位),(2) 在启动时有一个包含一些测试用例的表(加、减、乘、除、平方、余弦等的边界情况),如果实现计算出与表匹配的值,则无需进行任何调整。

Checking the links in the other answers make it clear you'll never have a guarantee of whether floating point is "correctly" implemented or whether you'll always receive a certain precision for a given calculation, but perhaps you could make a best effort by (1) truncating all calculations to a common minimum (eg, if different implementations will give you 32 to 80 bits of precision, always truncating every operation to 30 or 31 bits), (2) have a table of a few test cases at startup (borderline cases of add, subtract, multiply, divide, sqrt, cosine, etc.) and if the implementation calculates values matching the table then not bother making any adjustments.

谜兔 2024-12-01 03:25:15

你的问题非常困难和技术性的东西O_o。不过我可能有一个想法。

您肯定知道 CPU 在执行任何浮点操作后都会进行一些调整。
CPU提供了多种不同的指令来进行不同的舍入操作。

因此,对于表达式,编译器将选择一组指令来得出结果。但任何其他指令工作流程,即使它们打算计算相同的表达式,也可以提供不同的结果。

舍入调整所造成的“错误”将在每次进一步指示时增加。

举个例子,我们可以说,在汇编级别:a * b * c 不等于a * c * b。

我不完全确定这一点,你需要询问比我更了解 CPU 架构的人:p

但是要回答你的问题:在 C 或 C++ 中你可以解决你的问题,因为你对机器有一定的控制权代码由你的编译器生成,但是在 .NET 中你没有任何代码。因此,只要您的机器代码可能不同,您就永远无法确定确切的结果。

我很好奇这会以何种方式成为问题,因为变化似乎很小,但如果您需要真正精确的操作,我能想到的唯一解决方案是增加浮动寄存器的大小。如果可以的话,使用双精度甚至长双精度(不确定使用 CLI 是否可行)。

我希望我已经说得足够清楚了,我的英语并不完美(...一点也不:s)

Your question in quite difficult and technical stuff O_o. However I may have an idea.

You sure know that the CPU makes some adjustment after any floating operations.
And CPU offer several different instructions which make different rounding operation.

So for an expression, your compiler will choose a set of instructions which lead you to a result. But any other instruction workflow, even if they intend to compute the same expression, can provide another result.

The 'mistakes' made by a rounding adjustment will grow at each further instructions.

As an exemple we can say that at an assembly level: a * b * c is not equivalent to a * c * b.

I'm not entirely sure of that, you will need to ask for someone who know CPU architecture a lot more than me : p

However to answer your question: in C or C++ you can solve your problem because you have some control on the machine code generate by your compiler, however in .NET you don't have any. So as long as your machine code can be different, you'll never be sure about the exact result.

I'm curious in which way this can be a problem because variation seems very minimal, but if you need really accurate operation the only solution I can think about will be to increase the size of your floating registers. Use double precision or even long double if you can (not sure that's possible using CLI).

I hope I've been clear enough, I'm not perfect in English (...at all : s)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文