Skip to content

Commit

Permalink
int: reduce allocation by representing small ints as pointers (#280)
Browse files Browse the repository at this point in the history
* int: consolidate field accessors

This change defines low-level accessors for the small and big
arms of the int union so that the representation can be easily
changed in a follow-up.

Change-Id: I7c4ae279a6d2e7b76e102ba5d01a3cd1c56fb368

* int: improve performance by avoiding allocation

This change defines a new representation for Int on 64-bit machines
running a POSIX operating system, by reserving a 4GB portion of the
address space.

Pointers to addresses in this region represent int32 values,
and are disjoint from all *big.Int pointers,
allowing us to represent the union in a single pointer.
This means the conversion from Int to Value does not allocate.

The gauss benchmark (added in this CL) shows -40% ns, -48% bytes, -63% allocs:
Benchmark/bench_gauss-12  	      84	  13648744 ns/op	 3175816 B/op	  105862 allocs/op (before)
Benchmark/bench_gauss-12  	      55	  24283703 ns/op	 6119844 B/op	  289862 allocs/op (after)

On 32-bit machines, or those running a non-POSIX system,
we continue to use the old representation.
  • Loading branch information
adonovan authored Jun 17, 2020
1 parent fd77f13 commit c6daab6
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 96 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ require (
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c // indirect
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c
)
187 changes: 98 additions & 89 deletions starlark/int.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,21 @@ import (
)

// Int is the type of a Starlark int.
type Int struct {
// We use only the signed 32 bit range of small to ensure
// that small+small and small*small do not overflow.
//
// The zero value is not a legal value; use MakeInt(0).
type Int struct{ impl intImpl }

small int64 // minint32 <= small <= maxint32
big *big.Int // big != nil <=> value is not representable as int32
}

// newBig allocates a new big.Int.
func newBig(x int64) *big.Int {
if 0 <= x && int64(big.Word(x)) == x {
// x is guaranteed to fit into a single big.Word.
// Most starlark ints are small,
// but math/big assumes that since you've chosen to use math/big,
// your big.Ints will probably grow, so it over-allocates.
// Avoid that over-allocation by manually constructing a single-word slice.
// See https://golang.org/cl/150999, which will hopefully land in Go 1.13.
return new(big.Int).SetBits([]big.Word{big.Word(x)})
}
return big.NewInt(x)
}
// --- high-level accessors ---

// MakeInt returns a Starlark int for the specified signed integer.
func MakeInt(x int) Int { return MakeInt64(int64(x)) }

// MakeInt64 returns a Starlark int for the specified int64.
func MakeInt64(x int64) Int {
if math.MinInt32 <= x && x <= math.MaxInt32 {
return Int{small: x}
return makeSmallInt(x)
}
return Int{big: newBig(x)}
return makeBigInt(big.NewInt(x))
}

// MakeUint returns a Starlark int for the specified unsigned integer.
Expand All @@ -53,27 +37,23 @@ func MakeUint(x uint) Int { return MakeUint64(uint64(x)) }
// MakeUint64 returns a Starlark int for the specified uint64.
func MakeUint64(x uint64) Int {
if x <= math.MaxInt32 {
return Int{small: int64(x)}
}
if uint64(big.Word(x)) == x {
// See comment in newBig for an explanation of this optimization.
return Int{big: new(big.Int).SetBits([]big.Word{big.Word(x)})}
return makeSmallInt(int64(x))
}
return Int{big: new(big.Int).SetUint64(x)}
return makeBigInt(new(big.Int).SetUint64(x))
}

// MakeBigInt returns a Starlark int for the specified big.Int.
// The caller must not subsequently modify x.
func MakeBigInt(x *big.Int) Int {
if n := x.BitLen(); n < 32 || n == 32 && x.Int64() == math.MinInt32 {
return Int{small: x.Int64()}
return makeSmallInt(x.Int64())
}
return Int{big: x}
return makeBigInt(x)
}

var (
zero, one = Int{small: 0}, Int{small: 1}
oneBig = newBig(1)
zero, one = makeSmallInt(0), makeSmallInt(1)
oneBig = big.NewInt(1)

_ HasUnary = Int{}
)
Expand All @@ -94,39 +74,42 @@ func (i Int) Unary(op syntax.Token) (Value, error) {
// Int64 returns the value as an int64.
// If it is not exactly representable the result is undefined and ok is false.
func (i Int) Int64() (_ int64, ok bool) {
if i.big != nil {
x, acc := bigintToInt64(i.big)
iSmall, iBig := i.get()
if iBig != nil {
x, acc := bigintToInt64(iBig)
if acc != big.Exact {
return // inexact
}
return x, true
}
return i.small, true
return iSmall, true
}

// BigInt returns the value as a big.Int.
// The returned variable must not be modified by the client.
func (i Int) BigInt() *big.Int {
if i.big != nil {
return i.big
iSmall, iBig := i.get()
if iBig != nil {
return iBig
}
return newBig(i.small)
return big.NewInt(iSmall)
}

// Uint64 returns the value as a uint64.
// If it is not exactly representable the result is undefined and ok is false.
func (i Int) Uint64() (_ uint64, ok bool) {
if i.big != nil {
x, acc := bigintToUint64(i.big)
iSmall, iBig := i.get()
if iBig != nil {
x, acc := bigintToUint64(iBig)
if acc != big.Exact {
return // inexact
}
return x, true
}
if i.small < 0 {
if iSmall < 0 {
return // inexact
}
return uint64(i.small), true
return uint64(iSmall), true
}

// The math/big API should provide this function.
Expand Down Expand Up @@ -163,103 +146,125 @@ var (
)

func (i Int) Format(s fmt.State, ch rune) {
if i.big != nil {
i.big.Format(s, ch)
iSmall, iBig := i.get()
if iBig != nil {
iBig.Format(s, ch)
return
}
newBig(i.small).Format(s, ch)
big.NewInt(iSmall).Format(s, ch)
}
func (i Int) String() string {
if i.big != nil {
return i.big.Text(10)
iSmall, iBig := i.get()
if iBig != nil {
return iBig.Text(10)
}
return strconv.FormatInt(i.small, 10)
return strconv.FormatInt(iSmall, 10)
}
func (i Int) Type() string { return "int" }
func (i Int) Freeze() {} // immutable
func (i Int) Truth() Bool { return i.Sign() != 0 }
func (i Int) Hash() (uint32, error) {
iSmall, iBig := i.get()
var lo big.Word
if i.big != nil {
lo = i.big.Bits()[0]
if iBig != nil {
lo = iBig.Bits()[0]
} else {
lo = big.Word(i.small)
lo = big.Word(iSmall)
}
return 12582917 * uint32(lo+3), nil
}
func (x Int) CompareSameType(op syntax.Token, v Value, depth int) (bool, error) {
y := v.(Int)
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return threeway(op, x.BigInt().Cmp(y.BigInt())), nil
}
return threeway(op, signum64(x.small-y.small)), nil
return threeway(op, signum64(xSmall-ySmall)), nil
}

// Float returns the float value nearest i.
func (i Int) Float() Float {
if i.big != nil {
f, _ := new(big.Float).SetInt(i.big).Float64()
iSmall, iBig := i.get()
if iBig != nil {
f, _ := new(big.Float).SetInt(iBig).Float64()
return Float(f)
}
return Float(i.small)
return Float(iSmall)
}

func (x Int) Sign() int {
if x.big != nil {
return x.big.Sign()
xSmall, xBig := x.get()
if xBig != nil {
return xBig.Sign()
}
return signum64(x.small)
return signum64(xSmall)
}

func (x Int) Add(y Int) Int {
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return MakeBigInt(new(big.Int).Add(x.BigInt(), y.BigInt()))
}
return MakeInt64(x.small + y.small)
return MakeInt64(xSmall + ySmall)
}
func (x Int) Sub(y Int) Int {
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return MakeBigInt(new(big.Int).Sub(x.BigInt(), y.BigInt()))
}
return MakeInt64(x.small - y.small)
return MakeInt64(xSmall - ySmall)
}
func (x Int) Mul(y Int) Int {
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return MakeBigInt(new(big.Int).Mul(x.BigInt(), y.BigInt()))
}
return MakeInt64(x.small * y.small)
return MakeInt64(xSmall * ySmall)
}
func (x Int) Or(y Int) Int {
if x.big != nil || y.big != nil {
return Int{big: new(big.Int).Or(x.BigInt(), y.BigInt())}
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return MakeBigInt(new(big.Int).Or(x.BigInt(), y.BigInt()))
}
return Int{small: x.small | y.small}
return makeSmallInt(xSmall | ySmall)
}
func (x Int) And(y Int) Int {
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return MakeBigInt(new(big.Int).And(x.BigInt(), y.BigInt()))
}
return Int{small: x.small & y.small}
return makeSmallInt(xSmall & ySmall)
}
func (x Int) Xor(y Int) Int {
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
return MakeBigInt(new(big.Int).Xor(x.BigInt(), y.BigInt()))
}
return Int{small: x.small ^ y.small}
return makeSmallInt(xSmall ^ ySmall)
}
func (x Int) Not() Int {
if x.big != nil {
return MakeBigInt(new(big.Int).Not(x.big))
xSmall, xBig := x.get()
if xBig != nil {
return MakeBigInt(new(big.Int).Not(xBig))
}
return Int{small: ^x.small}
return makeSmallInt(^xSmall)
}
func (x Int) Lsh(y uint) Int { return MakeBigInt(new(big.Int).Lsh(x.BigInt(), y)) }
func (x Int) Rsh(y uint) Int { return MakeBigInt(new(big.Int).Rsh(x.BigInt(), y)) }

// Precondition: y is nonzero.
func (x Int) Div(y Int) Int {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
// http://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html
if x.big != nil || y.big != nil {
if xBig != nil || yBig != nil {
xb, yb := x.BigInt(), y.BigInt()

var quo, rem big.Int
Expand All @@ -269,17 +274,19 @@ func (x Int) Div(y Int) Int {
}
return MakeBigInt(&quo)
}
quo := x.small / y.small
rem := x.small % y.small
if (x.small < 0) != (y.small < 0) && rem != 0 {
quo := xSmall / ySmall
rem := xSmall % ySmall
if (xSmall < 0) != (ySmall < 0) && rem != 0 {
quo -= 1
}
return MakeInt64(quo)
}

// Precondition: y is nonzero.
func (x Int) Mod(y Int) Int {
if x.big != nil || y.big != nil {
xSmall, xBig := x.get()
ySmall, yBig := y.get()
if xBig != nil || yBig != nil {
xb, yb := x.BigInt(), y.BigInt()

var quo, rem big.Int
Expand All @@ -289,18 +296,19 @@ func (x Int) Mod(y Int) Int {
}
return MakeBigInt(&rem)
}
rem := x.small % y.small
if (x.small < 0) != (y.small < 0) && rem != 0 {
rem += y.small
rem := xSmall % ySmall
if (xSmall < 0) != (ySmall < 0) && rem != 0 {
rem += ySmall
}
return Int{small: rem}
return makeSmallInt(rem)
}

func (i Int) rational() *big.Rat {
if i.big != nil {
return new(big.Rat).SetInt(i.big)
iSmall, iBig := i.get()
if iBig != nil {
return new(big.Rat).SetInt(iBig)
}
return new(big.Rat).SetInt64(i.small)
return new(big.Rat).SetInt64(iSmall)
}

// AsInt32 returns the value of x if is representable as an int32.
Expand All @@ -309,10 +317,11 @@ func AsInt32(x Value) (int, error) {
if !ok {
return 0, fmt.Errorf("got %s, want int", x.Type())
}
if i.big != nil {
iSmall, iBig := i.get()
if iBig != nil {
return 0, fmt.Errorf("%s out of range", i)
}
return int(i.small), nil
return int(iSmall), nil
}

// NumberToInt converts a number x to an integer value.
Expand Down
33 changes: 33 additions & 0 deletions starlark/int_generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//+build !linux,!darwin !amd64,!arm64,!mips64x,!ppc64x

package starlark

// generic Int implementation as a union

import "math/big"

type intImpl struct {
// We use only the signed 32-bit range of small to ensure
// that small+small and small*small do not overflow.
small_ int64 // minint32 <= small <= maxint32
big_ *big.Int // big != nil <=> value is not representable as int32
}

// --- low-level accessors ---

// get returns the small and big components of the Int.
// small is defined only if big is nil.
// small is sign-extended to 64 bits for ease of subsequent arithmetic.
func (i Int) get() (small int64, big *big.Int) {
return i.small_, i.big_
}

// Precondition: math.MinInt32 <= x && x <= math.MaxInt32
func makeSmallInt(x int64) Int {
return Int{intImpl{small_: x}}
}

// Precondition: x cannot be represented as int32.
func makeBigInt(x *big.Int) Int {
return Int{intImpl{big_: x}}
}
Loading

0 comments on commit c6daab6

Please sign in to comment.