Skip to content

Commit

Permalink
internal: create package for unsafe bytes convertion (#8733)
Browse files Browse the repository at this point in the history
Co-authored-by: Alessio Treglia <[email protected]>
  • Loading branch information
robert-zaremba and Alessio Treglia authored Mar 1, 2021
1 parent 0792db7 commit 5f2b90c
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 56 deletions.
2 changes: 2 additions & 0 deletions internal/conv/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package conv provides internal functions for convertions and data manipulation
package conv
23 changes: 23 additions & 0 deletions internal/conv/string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package conv

import (
"reflect"
"unsafe"
)

// UnsafeStrToBytes uses unsafe to convert string into byte array. Returned bytes
// must not be altered after this function is called as it will cause a segmentation fault.
func UnsafeStrToBytes(s string) []byte {
var buf = *(*[]byte)(unsafe.Pointer(&s))
(*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(s)
return buf
}

// UnsafeBytesToStr is meant to make a zero allocation conversion
// from []byte -> string to speed up operations, it is not meant
// to be used generally, but for a specific pattern to delete keys
// from a map.
func UnsafeBytesToStr(b []byte) string {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&b))
return *(*string)(unsafe.Pointer(hdr))
}
47 changes: 47 additions & 0 deletions internal/conv/string_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package conv

import (
"runtime"
"testing"
"time"

"github.com/stretchr/testify/suite"
)

func TestStringSuite(t *testing.T) {
suite.Run(t, new(StringSuite))
}

type StringSuite struct{ suite.Suite }

func unsafeConvertStr() []byte {
return UnsafeStrToBytes("abc")
}

func (s *StringSuite) TestUnsafeStrToBytes() {
// we convert in other function to trigger GC. We want to check that
// the underlying array in []bytes is accessible after GC will finish swapping.
for i := 0; i < 5; i++ {
b := unsafeConvertStr()
runtime.GC()
<-time.NewTimer(2 * time.Millisecond).C
b2 := append(b, 'd')
s.Equal("abc", string(b))
s.Equal("abcd", string(b2))
}
}

func unsafeConvertBytes() string {
return UnsafeBytesToStr([]byte("abc"))
}

func (s *StringSuite) TestUnsafeBytesToStr() {
// we convert in other function to trigger GC. We want to check that
// the underlying array in []bytes is accessible after GC will finish swapping.
for i := 0; i < 5; i++ {
str := unsafeConvertBytes()
runtime.GC()
<-time.NewTimer(2 * time.Millisecond).C
s.Equal("abc", str)
}
}
29 changes: 3 additions & 26 deletions store/cachekv/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import (
"bytes"
"container/list"
"io"
"reflect"
"sort"
"sync"
"time"
"unsafe"

dbm "github.com/tendermint/tm-db"

"github.com/cosmos/cosmos-sdk/internal/conv"
"github.com/cosmos/cosmos-sdk/store/tracekv"
"github.com/cosmos/cosmos-sdk/store/types"
"github.com/cosmos/cosmos-sdk/telemetry"
Expand Down Expand Up @@ -179,35 +178,13 @@ func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator {
return newCacheMergeIterator(parent, cache, ascending)
}

// strToByte is meant to make a zero allocation conversion
// from string -> []byte to speed up operations, it is not meant
// to be used generally, but for a specific pattern to check for available
// keys within a domain.
func strToByte(s string) []byte {
var b []byte
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
hdr.Cap = len(s)
hdr.Len = len(s)
hdr.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
return b
}

// byteSliceToStr is meant to make a zero allocation conversion
// from []byte -> string to speed up operations, it is not meant
// to be used generally, but for a specific pattern to delete keys
// from a map.
func byteSliceToStr(b []byte) string {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&b))
return *(*string)(unsafe.Pointer(hdr))
}

// Constructs a slice of dirty items, to use w/ memIterator.
func (store *Store) dirtyItems(start, end []byte) {
unsorted := make([]*kv.Pair, 0)

n := len(store.unsortedCache)
for key := range store.unsortedCache {
if dbm.IsKeyInDomain(strToByte(key), start, end) {
if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) {
cacheValue := store.cache[key]
unsorted = append(unsorted, &kv.Pair{Key: []byte(key), Value: cacheValue.value})
}
Expand All @@ -219,7 +196,7 @@ func (store *Store) dirtyItems(start, end []byte) {
}
} else { // Otherwise, normally delete the unsorted keys from the map.
for _, kv := range unsorted {
delete(store.unsortedCache, byteSliceToStr(kv.Key))
delete(store.unsortedCache, conv.UnsafeBytesToStr(kv.Key))
}
}

Expand Down
13 changes: 2 additions & 11 deletions types/address/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"bytes"
"crypto/sha256"
"fmt"
"reflect"
"sort"
"unsafe"

"github.com/cosmos/cosmos-sdk/internal/conv"
"github.com/cosmos/cosmos-sdk/types/errors"
)

Expand All @@ -21,7 +20,7 @@ type Addressable interface {
// Hash creates a new address from address type and key
func Hash(typ string, key []byte) []byte {
hasher := sha256.New()
hasher.Write(unsafeStrToByteArray(typ))
hasher.Write(conv.UnsafeStrToBytes(typ))
th := hasher.Sum(nil)

hasher.Reset()
Expand Down Expand Up @@ -63,11 +62,3 @@ func Module(moduleName string, key []byte) []byte {
mKey := append([]byte(moduleName), 0)
return Hash("module", append(mKey, key...))
}

// unsafeStrToByteArray uses unsafe to convert string into byte array. Returned bytes
// must not be altered after this function is called as it will cause a segmentation fault.
func unsafeStrToByteArray(s string) []byte {
var buf = *(*[]byte)(unsafe.Pointer(&s))
(*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(s)
return buf
}
19 changes: 0 additions & 19 deletions types/address/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package address

import (
"crypto/sha256"
"runtime"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -77,23 +75,6 @@ func (suite *AddressSuite) TestModule() {
assert.NotEqual(addr2, addr3, "changing key must change address")
}

func unsafeConvertABC() []byte {
return unsafeStrToByteArray("abc")
}

func (suite *AddressSuite) TestUnsafeStrToBytes() {
// we convert in other function to trigger GC. We want to check that
// the underlying array in []bytes is accessible after GC will finish swapping.
for i := 0; i < 5; i++ {
b := unsafeConvertABC()
runtime.GC()
<-time.NewTimer(2 * time.Millisecond).C
b2 := append(b, 'd')
suite.Equal("abc", string(b))
suite.Equal("abcd", string(b2))
}
}

type addrMock struct {
Addr []byte
}
Expand Down

0 comments on commit 5f2b90c

Please sign in to comment.