C# 中的 BigFloat 库





5.00/5 (22投票s)
用于大型浮点数的 C# 结构/类库
注意:本文由 ChatGPT 和 Grammarly 共同创作 - 详情请参见 此处。
引言
BigFloat
是一个为处理非常大的浮点数而量身定制的 C# 库。它通过提供可变大小的尾数和大的指数范围,扩展了标准 IEEE 浮点数(如单精度和双精度)的功能。该库结合了精度和灵活性,是科学计算等需要处理大数高精度计算的理想选择。最近,BigFloat
已在 GitHub 上发布,并且现在也在 CodeProject 的这篇文章中详细介绍。
与 IEEE 浮点数的关键区别
BigFloat
在结构上与 IEEE 标准相似,但也引入了一些显著的区别。
- 二进制补码表示:
BigFloat
采用二进制补码作为符号,因为它底层使用的是BigInteger
。二进制补码算术通常更有效率。 - 尺度(Scale)与指数(Exponent):与 IEEE 的左移指数不同,
BigFloat
的“尺度”是从右边最低有效位度量小数点的位置。 - 可变尾数大小:
BigFloat
中的尾数,称为DataBit
s,具有可调的大小,范围可达二十亿位。
数据结构
BigFloat
的架构包含三个核心组件:
- DataBits (类型为
BigInteger
):DataBit
s 代表尾数,存储数字的二进制形式。 - Scale (类型为
int
):尺度决定了小数点的位置,从而实现了可伸缩的精度。正值会将小数点向右移动,增加数字的大小;负值会将小数点向左移动,创建小数部分。零值本质上表示一个整数。 - Size (类型为
int
):缓存的DataBit
s 大小值。此值用于优化频繁访问。'_size
' 等同于函数 'int _size = > ABS(dataBits).GetBitSize();
'
Using the Code
将 BigFloat
集成到您的项目中非常简单。主文件 'BigFloat.cs' 包含所有必需的函数,而可选的 'BigConstants.cs' 文件提供了对扩展数学常量的访问。将这些文件添加到您的项目并进行可选的引用即可完成。
此外,由于使用了一些语言特性,因此需要 C# 11 / .NET 7。
BigConstants.cs 提供了多达 5000 位的十进制数字,但 values 文件夹中一些可选包含的文本文件将此扩展到了 1,000,000 位。
初始化和基本算术示例
关于输出表示法的一个快速说明。在下面,我们将看到类似于 232XXXXXXXX 的输出。当我们看到这种情况时,BigFloat
会告知用户只有 232 部分是精确的。
// Initialize BigFloat numbers
BigFloat a = new("123456789.012345678901234"); // Initialize by String
BigFloat b = new(1234.56789012345678); // Initialize by Double
// Basic arithmetic
BigFloat sum = a + b;
BigFloat difference = a - b;
BigFloat product = a * b;
BigFloat quotient = a / b;
Console.WriteLine($"Sum: {sum}");
// Output: Sum: 123458023.5802358023581
Console.WriteLine($"Difference: {difference}");
// Output: Difference: 123455554.4444555554443
Console.WriteLine($"Product: {product}");
// Output: Product: 152415787532.38838
Console.WriteLine($"Quotient: {quotient}");
// Output: Quotient: 99999.99999999999
使用数学常数
// Access constants like Pi or E from BigConstants
BigFloat.BigConstants bigConstants = new(
requestedAccuracyInBits: 1000,
onInsufficientBitsThenSetToZero: true,
cutOnTrailingZero: true);
BigFloat pi = bigConstants.Pi;
BigFloat e = bigConstants.E;
Console.WriteLine($"e to 1000 binary digits: {e.ToString()}");
// Output:
// e to 1000 binary digits: 2.71828182845904523536028747135266249775724709369995957496696
// 76277240766303535475945713821785251664274274663919320030599218174135966290435729003342
// 95260595630738132328627943490763233829880753195251019011573834187930702154089149934884
// 1675092447614606680822648001684774118537423454424371075390777449920696
// Use Pi in a calculation (Area of a circle with r = 100)
BigFloat radius = new("100.0000000000000000");
BigFloat area = pi * radius * radius;
Console.WriteLine($"Area of the circle: {area}");
// Output: Area of the circle: 31415.92653589793238
精度控制
// Initialize a number with high precision
BigFloat preciseNumber = new("123.45678901234567890123");
BigFloat morePreciseNumber = BigFloat.ExtendPrecision(preciseNumber, bitsToAdd: 50);
Console.WriteLine($"Extend Precision result: {morePreciseNumber}");
// Output: Extend Precision result: 123.45678901234567890122999999999787243
// Initialize an integer with custom precision
BigFloat c = BigFloat.IntWithAccuracy(10, 100);
Console.WriteLine($"Int with specified accuracy: {c}");
// Output: Int with specified accuracy: 10.000000000000000000000000000000
比较数字
// Initialize two BigFloat numbers
BigFloat num1 = new("12345.6790");
BigFloat num2 = new("12345.6789");
// Let's compare the numbers that are not equal...
bool areEqual = num1 == num2;
bool isFirstBigger = num1 > num2;
Console.WriteLine($"Are the numbers equal? {areEqual}");
// Output: Are the numbers equal? False
Console.WriteLine($"Is the first number bigger? {isFirstBigger}");
// Output: Is the first number bigger? True
根据基数,数字可能会向上或向下舍入。在十进制中,12345.67896
会向上舍入到 12345.6790
。然而,在二进制中,它会向下舍入到 11000000111001.1010110111010
。由于 BigFloat
是基于 2 的,这是正确的,但它可能会导致一些奇怪的副作用,例如下面的示例。
BigFloat num3 = new("12345.6789");
BigFloat num4 = new("12345.67896");
areEqual = num3 == num4;
isFirstBigger = num3 > num4;
Console.WriteLine($"Are the numbers equal? {areEqual}");
// Output: Are the numbers equal? True
Console.WriteLine($"Is the first number bigger? {isFirstBigger}");
// Output: Is the first number bigger? False
处理非常大或非常小的指数
// Creating a large number
BigFloat largeNumber = new("1234e+7");
Console.WriteLine($"Large Number: {largeNumber}");
// Output: Large Number: 123XXXXXXXX
// Creating a very large number
BigFloat veryLargeNumber = new("1e+300");
Console.WriteLine($"Very Large Number: {veryLargeNumber}");
// Output: Very Large Number: 1 * 10^300
// Creating a very small number
BigFloat smallNumber = new("1e-300");
Console.WriteLine($"Small Number: {smallNumber}");
// Output: Small Number: 0.00000000000000000000000000000000000000000000000000000000000000
// 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000
// 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000
// 000000000000000000000000000000000000000000000000000000000000000001
BigFloat num5 = new("12121212.1212");
BigFloat num6 = new("1234");
Console.WriteLine($"{num5} * {num6} = {num5 * num6}");
// BigFloat Output: 12121212.1212 * 1234 = 1496XXXXXXX
num5 = new("12121212.1212");
num6 = new("3");
BigFloat result = num5 * num6;
Console.WriteLine($"{num5} * {num6} = {result}");
// BigFloat Output: 12121212.1212 * 3 = 0XXXXXXXX
// Not Perfect, optimal output: 4XXXXXXX
num5 = new("121212.1212");
num6 = new("1234567");
Console.WriteLine($"{num5} * {num6} = {num5 * num6}");
// Output: 121212.1212 * 1234567 = 149644XXXXXX
Console.WriteLine($"GetPrecision: {num6.GetPrecision}");
// Output: GetPrecision: 21
理解“隐藏位”(HiddenBits)
在 BigFloat
中,实际的“数据位”存储在 BigInteger
中。BigFloat
将最低有效 32 位指定为“隐藏位”。这些位通常不被认为是精确的,但对于保持精度至关重要。
隐藏位的作用
为了提高最终结果的精度,BigFloat
保留了一些额外的位,在算术运算期间充当扩展缓冲区。您可以将它们视为部分精确的扩展精度,并保留计算的余数。这可能不是非常显著,但它会导致在进行多次连续数学运算后获得更精确的结果。
示例说明
考虑以下二进制加法,其中竖线字符 '|
' 分隔了精确位和非精确隐藏位。
101.01100|110011001100110011001100110011 (approximately 5.4)
+ 100.01001|100110011001100110011001100110 (approximately +4.3)
==========================================
1001.1011|0011001100110011001100110011001 (approximately 9.7)
如果我们只加精确位,我们的结果将是 `1001.101
`,而忽略了实际结果更接近 `100.110
` 的关键信息。这些额外的位有助于在后续的数学运算中进行更好的舍入和提高精度。
实际影响
通过携带这额外的 32 个隐藏位,BigFloat
可以以更高的精度执行操作。当多个操作串联在一起时,这些“隐藏位”有助于纠正累积的舍入误差,否则这些误差会导致显著的不准确。本质上,它们充当了精度的“安全网”。
十进制到二进制和二进制到十进制转换
本节介绍了一些关于将十进制字符串转换为二进制以及反向转换的基本要点。
转换精度损失
在使用 Parse()
和 ToString()
进行十进制字符串到二进制或反向转换时,可能会发生一些精度损失。这是因为当大多数十进制数转换为二进制时,它们会产生重复的模式。
一些示例
5.4
→101.011001100110011001100... (无限循环)
4.3
→100.01001100110011001100.... (无限循环)
0.25
→0.01 (可以精确转换)
无限循环的二进制数字不容易放入整数中!我们必须截断它或做一些技巧 - 我稍后会讲到。简而言之,大多数十进制数无法准确表示。
隐藏位来拯救!好吧,算是吧。保留一些额外的隐藏位的好处是,我们可以更精确地表示其他重复的位。这些位被认为是超出精度的,但同时,可以存储更多的重复位以提高精度。我们可以存储更多这些重复的二进制数字。当我们说 5.3 升水时,我们指定了两位小数(或大约七位二进制数字,101.0110)。但同时,5.3 可以用更多位来更好地描述,101.0110011001100。
精确表示重复位 - 可能的未来功能
前面我提到过有更好的方法。虽然 BigFloat
中尚未实现,但我还是想提到它,因为它可能是未来的一种添加方式,或者是对其他类的建议。为了存储重复的数字,我们可以引入一个新的属性叫做 '_repeat
'。如果有一个值,那么它表示 DataBits
中重复的最低有效数字的数量。如果为零,则没有重复的数字;因此,此功能未使用。
十进制到二进制 - 选择目标位数
当从真实的十进制数(例如 4.3)转换为二进制数时,我们必须确定它应该编码多少位。每个十进制数字转换为 3.32192809 位这一事实使这个问题变得棘手!我们的 4.3 示例将转换为 6.64 位。我们需要对此进行一些思考。
当以二进制精度查看时,会更清楚,因为二进制是最小的基数。十进制数,如 1、3、4 或 9,都具有一个小数位的精度,但在二进制中,这些数字的精度从 1 到 4 位不等。实际上,我们可以通过找到 Floor(log-base-2(x)+1)
,或以编程方式 (int)Log2(n) + 1
来计算二进制位数。如果我们查看单个数字的一些结果,Floor(Log2(3)+1)
是 2 位,Floor(Log2(9)+1)
是 4 位。我们可以看到数字越大,它拥有的二进制位越多,因此它的二进制精度就越高。19 比 11 具有更显著的二进制数字。所以,表示一个数字所需的位数随着数字在二进制中增长而增长。
然而,可能会出现意想不到的问题。如果我们乘以 3 和 7,我们期望得到 21。在乘法中,输出精度是两个因数中较小的一个。由于 3 只有 2 位,输出也应该有 2 位(加上其移位)。所以我们得到的是一个令人困惑的 18(或 11 x 111 = 11 << 3 => 18),而不是 3 x 7 = 21(或 11 x 111 = 10101)。在这里,隐藏位又来帮忙了。仅用几个额外的隐藏位就可以解决这个奇怪的现象。 (11.000 x 111.000 = 10101. => 21)。隐藏位将避免这种情况,但前提是乘数之间的差异小于 32 位。如果我们取两个大小差异更大的乘数,它将耗尽隐藏位。
输出表示法
在解释提供的示例输出时,您可能会遇到表示为“232XXXXXXXX
”的数字。这种格式用于区分输出中在精度范围内和不在精度范围内部分的数字。具体来说,“232
”部分表示精确且可靠的数字。一系列“X”字符表示超出精度范围的数字,因此不显示,因为其精度无法保证。
对于不精确部分延伸很长的输出,BigFloat
会采用科学计数法来传达这些数字的尺度。例如,一个可能显示为“232XXXXXXXXXXX
”的输出将被呈现为“232 x 10^11
”。这种向科学计数法的转变有助于保持清晰度,尤其是在处理大量数字时,过多的 X 会变得难以阅读。
虽然可以通过尾随零来显示这些数字,例如“232000000000
”,但这可能会误导性地暗示数字直到最后一个零都是精确的。这种表示法与许多基本计算器和计算工具的做法不同,它们可能会在没有明确区分的情况下显示超出精度的数字。更复杂的计算器和工具倾向于使用科学计数法来反映结果的精度,BigFloat
也遵循这一做法。
保持精度 - 核心关注点
BigFloat
最近的一些开发重点是提高舍入精度。最初,BigFloat
会丢弃最低有效位。这也不是什么大问题,因为这些被移除的位甚至位于最低 32 位子精度隐藏位之外。然而,经过数十亿次持续向下舍入的数学运算,这可能会耗尽所有 32 个隐藏位并导致不利的结果。随着项目的演变,舍入这些位的必要性变得显而易见。舍入有助于保持精度,尤其是在连续的数学运算中。
许多函数已更新为舍入最后一个子精度隐藏位,但并非全部。一些数学函数仍需要更新。
BigFloat
中的舍入
- 如何实现:运算后,通过检查移除的最高有效位来应用舍入。如果设置为 1,则加一。这种舍入方法是一种简单而有效的方法。
- 对精度的影响:正确的舍入可以减少精度损失。经过数十亿次运算后,这甚至会耗尽 32 个
HiddenBits
。
虽然这是一个理想情况的例子,但这里有一个例子- 丢弃位时:经过一万亿次(或 240 次)串行加法运算并仅丢弃额外的位,最终结果将比应有的结果低 239。这是因为一半的数学运算会向下舍入,而它们本应向上舍入。这将导致最低 39 位超出精度,耗尽 32 个隐藏位,甚至侵入被认为是精确的位。
- 舍入后:如果我们舍入这些丢弃的位,那么这一万亿次串行运算平均只会影响 18 位,使我们保持在 32 个隐藏位之内,从而保持精度。我们得到 18 位是因为有 75% 的情况舍入是正确的。因此,标准差将是 Sqrt(240 / 4) / 2 => 平均偏差为 262144 或 18 位。
-
未采用银行家舍入法
在浮点运算领域,特别是使用
Float
/Double
数据类型时,银行家舍入在提高精度方面起着至关重要的作用。这种舍入技术通常应用于 IEEE 浮点运算,其中,在遇到超出表示能力范围的额外位时—类似于在必须决定是将以 0.5000... 结尾的数字向上舍入还是向下舍入时—银行家舍入选择舍入到最近的偶数,实际上只有一半的时间向上舍入。这种方法对于只保留少量额外位的 IEEE 浮点数至关重要,因为遇到这种边界舍入决策的情况相对频繁。然而,在
BigFloat
的情况下,不使用银行家舍入法。这种偏离的原因在于BigFloat
能够处理更多的HiddenBits
。考虑到这种增强的位数容量,舍入决策恰好落在中间的可能性非常罕见。因此,IEEE 浮点数需要银行家舍入法的特定条件对于BigFloat
来说不是问题,无需实现。
精度的理论极限
- 使用隐藏位
当向上舍入时,我们的 32 个隐藏位需要一段时间才能用完。大约需要 (232 * 2)2 * 4 或 1.5 x 10^21 次数学运算,我们才会进入我们认为的精确位。这会花一些时间。而且,这是理想情况,可能比这早得多。
舍入示例
101.|11001011101101001000101100110100 (approximately 6)
x 100.|01011001101001011100101110110101 (approximately 4)
============================================================
110.|11001100100110110010011001111111[101100...] (true bits to remove)
110.|11001100100110110010011001111111 (if rounding down or dropping bits)
110.|11001100100110110010011010000000 (if rounded to nearest)
* "|" 是精确位和超出精度的隐藏位之间的分隔符。
** "[ ]" 位甚至超出了隐藏位 - 需要舍入的位
尽管这些位位于隐藏区域且被认为是超出精度的,但舍入有助于解决连续数学运算操作时引起的精度损失。通过截断位(即向下舍入),精度会缓慢降低。然而,如果对某些数学函数正确进行舍入,最低有效位的向上和向下舍入会随着时间的推移而相互抵消。这相当于多次抛硬币计算正面朝上的次数。
这里有一个隐藏位纠正累积舍入误差的例子。
供参考,正确答案...
1000.110100|000000010000000001010110... (精确)
丢弃位...
11.101110|011001110100101011001011
+ 1.010001|011001100110110101100010 (add operation)
====================================
100.111111|110011011011100000101101 (subtotal)
+ 1.010001|011001100110110101100010 (add operation)
====================================
110.010001|001101000010010110001111 (subtotal)
+ 1.010001|011001100110110101100010 (add operation)
====================================
111.100010|100110101001001011110001 (subtotal)
+ 1.010001|011001100110110101100010 (add operation)
====================================
1000.110100|000000010000000001010011 (total is off by 3)
使用舍入...
11.101110|011001110100101011001011
+ 1.010001|01100110011011010110001011 (round and add operation)
====================================
100.111111|110011011011100000101110 (subtotal)
+ 1.010001|01100110011011010110001011 (round and add operation)
====================================
110.010001|001101000010010110010001 (subtotal)
+ 1.010001|01100110011011010110001011 (round and add operation)
====================================
111.100010|100110101001001011110100 (subtotal)
+ 1.010001|01100110011011010110001011 (round and add operation)
====================================
1000.110100|000000010000000001010111 (total - off by 1)
BigFloat 的背景
2020 年,我遇到了一个需要对非常大的非整数进行计算的挑战。为了解决这个问题,我最初依赖 BigInteger
并手动管理小数点的位置。虽然有效,但这种临时解决方案被证明很笨拙,导致代码混乱、管理耗时且容易出错。在 2020 年搜索满足我需求的现有工具后,我不得不创建这个 BigFloat
库。
BigFloat
最初被构想为一个简单的类,其主要功能是准确跟踪基数点的位置——这是一个与“小数点”同义的术语,但适用于任何数字基数。随着时间的推移,该库经历了无数次增强,扩展了其函数库并显著提高了其精度。
这个从管理大规模算术中的基数点的简单实用工具演变成一个全面的 BigFloat
库的过程,体现了一个旨在解决特定需求的工具的演变,通过持续的改进和扩展,它已发展到为广泛的应用提供强大的高精度计算支持。
问答
- BigFloat 是否完整? 虽然功能强大且可用,但
BigFloat
是一个永无止境的项目,持续进行增强和性能优化。 - BigFloat 存在多久了?
BigFloat
于 2020 年 11 月开始作为个人工具,此后不断发展。 - 依赖项:
BigFloat
需要 .NET 7 或更高版本,没有其他依赖项。 - 数据存储: 核心上,
BigFloat
有三项:(1) 用于存储实际DataBits
的BigInteger
。(2) 一个Scale
,显示小数点需要移动多少二进制位。(3) 数据位的尺寸经常被访问。为了方便快速访问,该值(等同于ABS(BigInteger).GetBitCount()
)被缓存。 - 为什么叫 BigFloat?
BigFloat
:这将表示一个带有浮动小数点的二进制数。BigRational
:这将表示数字存储为实际的分数,带有一个分子和一个分母。BigDecimal
:这将表示处理/存储是基于十进制的。然而,这个类是基于二进制的。有些项目使用二进制,但名称是BigDecimal
。
未来愿望清单
- 为有理数添加 `_repeat` 以获得更精确的结果存储。
- 完成 `NthRoot()` 函数。它工作正常,但需要转换为在内部使用
BigInteger
以提高性能。
历史
- 2020 年 11 月 29 日:初始版本
- 2024 年 1 月 6 日:公开发布
- 2024 年 2 月 26 日:发布文章
文章创作过程
本文的开发是人类创造力和人工智能的协同作用。最初,Ryan White 撰写了一份全面的草稿,然后使用 Word 和 Grammarly 进行初步编辑。随后,我们利用 ChatGPT 4 的能力对文章进行了重组和精简。最初,信息减少的程度是一个担忧;然而,我们认识到简洁的价值,因为 ChatGPT 的编辑将这篇文章从可能枯燥的技术叙述转变为引人入胜且简洁的读物。
这个迭代过程涉及手动输入和 AI 建议之间的持续增强和完善。这种协作简化了内容,并确保文章保持活泼且引人入胜的语气。最后的润色包括使用 Grammarly 和 Word 进行细致的校对,这体现了我们对质量的承诺。
此外,文章还包含了由 ChatGPT 设计的 BigFloat
图像,并进行了少量手动调整以加入 Float
一词以提高清晰度。