duanshan1977 2016-08-16 18:43
浏览 80
已采纳

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

I've been reading this post on constants in Go, and I'm trying to understand how they are stored and used in memory. You can perform operations on very large constants in Go, and as long as the result fits in memory, you can coerce that result to a type. For example, this code prints 10, as you would expect:

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

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

  • 写回答

2条回答 默认 最新

  • duanming0494 2016-08-16 19:21
    关注

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

    Untyped arbitrary-precision constants don't live at runtime, constants live only at compile time (during the compilation). That being said, Go does not have to represent constants with arbitrary precision at runtime, only when compiling your application.

    Why? Because constants do not get compiled into the executable binaries. They don't have to be. Let's take your example:

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

    There is a constant Huge in the source code (and will be in the package object), but it won't appear in your executable. Instead a function call to fmt.Println() will be recorded with a value passed to it, whose type will be float64. So in the executable only a float64 value being 10.0 will be recorded. There is no sign of any number being 1e1000 in the executable.

    This float64 type is derived from the default type of the untyped constant Huge. 1e1000 is a floating-point literal. To verify it:

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

    Back to the arbitrary precision:

    Spec: Constants:

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

    So constants represent exact values of arbitrary precision. As we saw, there is no need to represent constants with arbitrary precision at runtime, but the compiler still has to do something at compile time. And it does!

    Obviously "infinite" precision cannot be dealt with. But there is no need, as the source code itself is not "infinite" (size of the source is finite). Still, it's not practical to allow truly arbitrary precision. So the spec gives some freedom to compilers regarding to this:

    Implementation restriction: Although numeric constants have arbitrary precision in the language, a compiler may implement them using an internal representation with limited precision. That said, every implementation must:

    • Represent integer constants with at least 256 bits.
    • Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed exponent of at least 32 bits.
    • Give an error if unable to represent an integer constant precisely.
    • Give an error if unable to represent a floating-point or complex constant due to overflow.
    • Round to the nearest representable constant if unable to represent a floating-point or complex constant due to limits on precision. These requirements apply both to literal constants and to the result of evaluating constant expressions.

    However, also note that when all the above said, the standard package provides you the means to still represent and work with values (constants) with "arbitrary" precision, see package go/constant. You may look into its source to get an idea how it's implemented.

    Implementation is 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 arbitrary precision values. big.Int is 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 is (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 is (from math/big/arith.go)

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

    Summary

    At runtime: predefined types provide limited precision, but you can "mimic" arbitrary precision with certain packages, such as math/big and go/constant. At compile time: constants seemingly provide arbitrary precision, but in reality a compiler may not live up to this (doesn't have to); but still the spec provides minimal precision for constants that all compiler must support, e.g. integer constants must be represented with at least 256 bits which is 32 bytes (compared to int64 which is "only" 8 bytes).

    When an executable binary is created, results of constant expressions (with arbitrary precision) have to be converted and represented with values of finite precision types – which may not be possible and thus may result in compile-time errors. Note that only results –not intermediate operands– have to be converted to finite precision, constant operations are carried out with arbitrary precision.

    How this arbitrary or enhanced precision is implemented is not defined by the spec, math/big for example stores "digits" of the number in a slice (where digits is not a digit of the base 10 representation, but "digit" is an uintptr which is like base 4294967295 representation on 32-bit architectures, and even bigger on 64-bit architectures).

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥15 C++ 头文件/宏冲突问题解决
  • ¥15 用comsol模拟大气湍流通过底部加热(温度不同)的腔体
  • ¥50 安卓adb backup备份子用户应用数据失败
  • ¥20 有人能用聚类分析帮我分析一下文本内容嘛
  • ¥15 请问Lammps做复合材料拉伸模拟,应力应变曲线问题
  • ¥30 python代码,帮调试
  • ¥15 #MATLAB仿真#车辆换道路径规划
  • ¥15 java 操作 elasticsearch 8.1 实现 索引的重建
  • ¥15 数据可视化Python
  • ¥15 要给毕业设计添加扫码登录的功能!!有偿