neo-go/pkg/encoding/bigint/bigint.go
Roman Khimov 9a06995460 bigint: don't allocate in ToPreallocatedBytes
Turns out, it's almost always allocating because we're mostly dealing with
small integers while the buffer size is calculated in 8-byte chunks here, so
preallocated buffer is always insufficient.

name                   old time/op    new time/op    delta
ToPreallocatedBytes-8    28.5ns ± 7%    19.7ns ± 5%   -30.72%  (p=0.000 n=10+10)

name                   old alloc/op   new alloc/op   delta
ToPreallocatedBytes-8     16.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)

name                   old allocs/op  new allocs/op  delta
ToPreallocatedBytes-8      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)

Fix StorageItem reuse at the same time. We don't copy when getting values from
the storage, but we don when we're putting them, so buffer reuse could corrupt
old values.
2022-06-02 15:38:39 +03:00

139 lines
2.8 KiB
Go

package bigint
import (
"math/big"
"math/bits"
"github.com/nspcc-dev/neo-go/pkg/util/slice"
)
const (
// MaxBytesLen is the maximum length of a serialized integer suitable for Neo VM.
MaxBytesLen = 32 // 256-bit signed integer
// wordSizeBytes is a size of a big.Word (uint) in bytes.
wordSizeBytes = bits.UintSize / 8
)
var bigOne = big.NewInt(1)
// FromBytesUnsigned converts data in little-endian format to an unsigned integer.
func FromBytesUnsigned(data []byte) *big.Int {
bs := slice.CopyReverse(data)
return new(big.Int).SetBytes(bs)
}
// FromBytes converts data in little-endian format to
// an integer.
func FromBytes(data []byte) *big.Int {
n := new(big.Int)
size := len(data)
if size == 0 {
if data == nil {
panic("nil slice provided to `FromBytes`")
}
return big.NewInt(0)
}
isNeg := data[size-1]&0x80 != 0
size = getEffectiveSize(data, isNeg)
if size == 0 {
if isNeg {
return big.NewInt(-1)
}
return big.NewInt(0)
}
lw := size / wordSizeBytes
ws := make([]big.Word, lw+1)
for i := 0; i < lw; i++ {
base := i * wordSizeBytes
for j := base + 7; j >= base; j-- {
ws[i] <<= 8
ws[i] ^= big.Word(data[j])
}
}
for i := size - 1; i >= lw*wordSizeBytes; i-- {
ws[lw] <<= 8
ws[lw] ^= big.Word(data[i])
}
if isNeg {
for i := 0; i <= lw; i++ {
ws[i] = ^ws[i]
}
shift := byte(wordSizeBytes-size%wordSizeBytes) * 8
ws[lw] = ws[lw] & (^big.Word(0) >> shift)
n.SetBits(ws)
n.Neg(n)
return n.Sub(n, bigOne)
}
return n.SetBits(ws)
}
// getEffectiveSize returns the minimal number of bytes required
// to represent a number (two's complement for negatives).
func getEffectiveSize(buf []byte, isNeg bool) int {
var b byte
if isNeg {
b = 0xFF
}
size := len(buf)
for ; size > 0; size-- {
if buf[size-1] != b {
break
}
}
return size
}
// ToBytes converts an integer to a slice in little-endian format.
// Note: NEO3 serialization differs from default C# BigInteger.ToByteArray()
// when n == 0. For zero is equal to empty slice in NEO3.
// https://github.com/neo-project/neo-vm/blob/master/src/neo-vm/Types/Integer.cs#L16
func ToBytes(n *big.Int) []byte {
return ToPreallocatedBytes(n, []byte{})
}
// ToPreallocatedBytes converts an integer to a slice in little-endian format using the given
// byte array for conversion result.
func ToPreallocatedBytes(n *big.Int, data []byte) []byte {
sign := n.Sign()
if sign == 0 {
return data[:0]
}
if sign < 0 {
n.Add(n, bigOne)
defer func() { n.Sub(n, bigOne) }()
if n.Sign() == 0 { // n == -1
return append(data[:0], 0xFF)
}
}
lb := n.BitLen()/8 + 1
if c := cap(data); c < lb {
data = make([]byte, lb)
} else {
data = data[:lb]
}
_ = n.FillBytes(data)
slice.Reverse(data)
if sign == -1 {
for i := range data {
data[i] = ^data[i]
}
}
return data
}