一尘不染

Go如何对常数执行算术运算?

go

我一直在阅读有关Go中的常量的文章,并且试图了解它们如何在内存中存储和使用。您可以在Go中对非常大的常量执行运算,并且只要结果适合内存,就可以将结果强制为类型。例如,10如您所料,此代码显示:

const Huge = 1e1000
fmt.Println(Huge / 1e999)

这是如何工作的?在某个时候,Go必须存储1e1000并存储1e999在内存中,以便对其执行操作。那么常量是如何存储的,Go如何对其进行算术运算呢?


阅读 299

收藏
2020-07-02

共1个答案

一尘不染

简短摘要(TL; DR)在答案的结尾。

无类型的任意精度常量在运行时不存在,常量仅在编译时(在编译期间)存在。话虽如此,Go不必在运行时以任意精度表示常量,而仅在编译应用程序时。

为什么?因为常量不会被编译到可执行二进制文件中。他们不必是。让我们举个例子:

const Huge = 1e1000
fmt.Println(Huge / 1e999)

有一个不变Huge源代码
(并会在包对象),但它不会出现在你的可执行文件。相反,fmt.Println()将记录一个传递给它的值(类型为)来记录对函数的调用float64。因此在可执行文件中,只会记录一个float6410.01e1000可执行文件中没有任何数字的迹象。

float64类型派生自 无类型 常量的 默认
类型。是浮点文字。验证:
__Huge``1e1000

const Huge = 1e1000
x := Huge / 1e999
fmt.Printf("%T", x) // Prints float64

回到任意精度:

规格:常数:

数字常数表示任意精度的精确值,并且不会溢出。

因此,常数表示任意精度的精确值。如我们所见,无需在 运行时 以任意精度表示常量,但编译器仍必须在 编译时 执行某些 操作 。它 确实

显然,“无限”精度无法解决。但是没有必要,因为源代码本身不是“无限的”(源大小是有限的)。但是, 允许
真正任意精度仍然不切实际。因此,规范为此提供了一些自由给编译器:

实现限制:尽管数字常量在语言中具有任意精度,但是编译器可能会使用内部表示形式来实现它们,而精度有限。也就是说,每个实现都必须:

  • 用至少256位表示整数常量。
  • 表示浮点常数,包括复数常数的部分,尾数至少为256位,带符号的指数至少为32位。
  • 如果无法精确表示整数常数,则给出错误。
  • 如果由于溢出而无法表示浮点数或复数常量,则给出错误。
    * 如果由于精度限制而无法表示浮点数或复数常数,则四舍五入到最接近的可表示常数。这些要求既适用于文字常量,也适用于评估常量表达式的结果。

但是,还请注意,当上述所有内容都存在时,标准包为您提供了仍然可以以“任意”精度表示和使用值(常数)的方法,请参见package
go/constant。您可以查看其来源,以了解其实现方式。

实现在中go/constant/value.go。表示此类值的类型:

// A Value represents the value of a Go constant.
type Value interface {
    // Kind returns the value kind.
    Kind() Kind

    // String returns a short, human-readable form of the value.
    // For numeric values, the result may be an approximation;
    // for String values the result may be a shortened string.
    // Use ExactString for a string representing a value exactly.
    String() string

    // ExactString returns an exact, printable form of the value.
    ExactString() string

    // Prevent external implementations.
    implementsValue()
}

type (
    unknownVal struct{}
    boolVal    bool
    stringVal  string
    int64Val   int64                    // Int values representable as an int64
    intVal     struct{ val *big.Int }   // Int values not representable as an int64
    ratVal     struct{ val *big.Rat }   // Float values representable as a fraction
    floatVal   struct{ val *big.Float } // Float values not representable as a fraction
    complexVal struct{ re, im Value }
)

如您所见,该math/big包用于表示无类型的任意精度值。big.Int例如(来自math/big/int.go):

// An Int represents a signed multi-precision integer.
// The zero value for an Int represents the value 0.
type Int struct {
    neg bool // sign
    abs nat  // absolute value of the integer
}

nat(来自math/big/nat.go)在哪里:

// An unsigned integer x of the form
//
//   x = x[n-1]*_B^(n-1) + x[n-2]*_B^(n-2) + ... + x[1]*_B + x[0]
//
// with 0 <= x[i] < _B and 0 <= i < n is stored in a slice of length n,
// with the digits x[i] as the slice elements.
//
// A number is normalized if the slice contains no leading 0 digits.
// During arithmetic operations, denormalized values may occur but are
// always normalized before returning the final result. The normalized
// representation of 0 is the empty or nil slice (length = 0).
//
type nat []Word

最后Word是(来自math/big/arith.go

// A Word represents a single digit of a multi-precision unsigned integer.
type Word uintptr

摘要

在运行时: 预定义
类型提供有限的精度,但是您可以使用某些包(例如math/big和)“模拟”任意精度go/constant。在编译时:常量似乎提供了任意精度,但实际上,编译器可能无法做到这一点(不必这样做);但是规范仍然为所有编译器必须支持的常数提供了最低的精度,例如,整数常数必须至少以256位(32字节)表示(相比之下int64,“仅”
8字节)。

创建可执行二进制文件时,必须转换常量表达式的结果(具有任意精度),并用有限精度类型的值表示-这可能是不可能的,因此可能导致编译时错误。请注意,只有
结果( 而不是中间操作数)必须转换为有限精度,常数运算可以任意精度执行。

规范未定义如何实现这种任意精度或增强精度,math/big例如,将数字的“数字”存储在切片中(其中,数字不是以10为基数表示的数字,而“数字”
uintptr是类似于base的数字)。在4位元架构上显示4294967295,在64位元架构上甚至更大)。

2020-07-02