How does Go do arithmetic on constants?

I read this post in constants in Go , and I'm trying to figure out how they are stored and used in memory. You can perform operations with very large constants in Go, and as long as the result fits into memory, you can force the result to type. For example, this code prints 10 , as you would expect:

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

How does it work under the hood? At some point, Go must store 1e1000 and 1e999 in memory in order to perform operations on them. So, how are constants stored and how does Go do arithmetic on them?

+2
source share
2 answers

A brief summary (TL; DR) is at the end of the answer.

Untyped constants of arbitrary precision do not live at run time, constants live only at compile time (at compile time). At the same time, Go does not have to represent constants with arbitrary precision at runtime, only when compiling the application.

Why? Because constants are not compiled into executable files. They should not be. Let's take your example:

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

There is a Huge constant in the source code (and it will be in the package object), but it will not appear in your executable file. Instead, the function call in fmt.Println() will be written with the value passed to it, whose type will be float64 . Thus, only the float64 value, which is 10.0 , will be written to the executable file. There is no indication that the executable contains 1e1000 .

This type of float64 is derived from the default type for the untyped Huge constant. 1e1000 is a floating point literal . To check this:

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

Back to arbitrary precision:

Spec: Constants:

Numeric constants are exact values ​​of arbitrary precision and do not overflow.

Thus, constants are exact values ​​of arbitrary accuracy. As we have seen, it is not necessary to represent constants with arbitrary precision at runtime, but the compiler must still do something at compile time. And so it is!

Obviously, it is impossible to cope with "infinite" accuracy. But this is not necessary, since the source code itself is not "infinite" (the size of the source is finite). However, it is not practical to allow truly arbitrary accuracy. Thus, the specification gives some freedom to compilers regarding this:

Implementation limitation: although numerical constants have arbitrary precision in the language, the compiler can implement them using an internal representation with limited precision. However, each implementation should:

  • Represent integer constants of at least 256 bits.
  • They represent floating point constants, including parts of a complex constant, with a mantissa of at least 256 bits and an exponent of at least 32 bits.
  • Give an error if you cannot accurately represent an integer constant.
  • Throws an error if it cannot represent a floating point or a complex constant due to overflow.
  • Round to the nearest representable constant if you cannot represent a floating point or complex constant due to accuracy limitations. These requirements apply to both literal constants and the result of evaluating constant expressions .

However, also note that when all of the above says, the standard package provides you with tools for representing and working with values ​​(constants) with "arbitrary" precision, see the go/constant package. You can look at its source to understand how it is implemented.

Implementation in go/constant/value.go . Types representing such values:

 // 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 } ) 

As you can see, the math/big package is used to represent untyped values ​​of arbitrary precision. big.Int , for example (from 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 } 

Where nat (from 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 

And finally, Word (from math/big/arith.go )

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

Summary

At run time: predefined types provide limited precision, but you can "simulate" arbitrary precision with specific packages such as math/big and go/constant . At compile time: constants seem to provide arbitrary precision, but in reality the compiler may not match this (not required); but nevertheless, the specification provides minimal accuracy for the constants that the entire compiler must support, for example, integer constants must be represented by at least 256 bits, which is 32 bytes (compared to int64 , which is "only" 8 bytes).

When an executable binary file is created, the results of constant expressions (with arbitrary precision) must be converted and represented by type values ​​with finite precision - which may not be possible and, therefore, may lead to errors during compilation. Please note that only the results - not intermediate operands - must be converted to finite precision, constant operations are performed with arbitrary precision.

How this arbitrary or improved precision is implemented is not determined by the specification, for example, math/big stores the β€œdigits” of a number in a slice (where the digits are not digits of the 10th base representation, and the β€œdigit” is uintptr , which is similar to the base representation 4294967295 in 32-bit architectures and even more on 64-bit architectures).

+5
source

Go constants are not allocated to memory. They are used in context by the compiler. The blog post you are linking to gives an example of Pi:

 Pi = 3.14159265358979323846264338327950288419716939937510582097494459 

If you assign Pi to float32 , it will lose accuracy to match, but if you assign it to float64 , it will lose less accuracy, but the compiler will determine which type to use.

0
source

Source: https://habr.com/ru/post/1264058/


All Articles