Skip to content

Commit 4edc66a

Browse files
committed
add noescape annotations
1 parent b90fa65 commit 4edc66a

File tree

2 files changed

+70
-5
lines changed

2 files changed

+70
-5
lines changed

gozstd.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,22 @@ func compress(cctx, cctxDict *cctxWrapper, dst, src []byte, cd *CDict, compressi
147147
return dst
148148
}
149149

150+
// noescape hides a pointer from escape analysis. It is the identity function
151+
// but escape analysis doesn't think the output depends on the input.
152+
// noescape is inlined and currently compiles down to zero instructions.
153+
// This is copied from go's strings.Builder. Allows us to use stack-allocated
154+
// slices.
155+
//go:nosplit
156+
//go:nocheckptr
157+
func noescape(p unsafe.Pointer) unsafe.Pointer {
158+
x := uintptr(p)
159+
return unsafe.Pointer(x ^ 0)
160+
}
161+
150162
func compressInternal(cctx, cctxDict *cctxWrapper, dst, src []byte, cd *CDict, compressionLevel int, mustSucceed bool) C.size_t {
151-
dstHdr := (*reflect.SliceHeader)(unsafe.Pointer(&dst))
152-
srcHdr := (*reflect.SliceHeader)(unsafe.Pointer(&src))
163+
// using noescape will allow this to work with stack-allocated slices
164+
dstHdr := (*reflect.SliceHeader)(noescape(unsafe.Pointer(&dst)))
165+
srcHdr := (*reflect.SliceHeader)(noescape(unsafe.Pointer(&src)))
153166

154167
if cd != nil {
155168
result := C.ZSTD_compress_usingCDict_wrapper(
@@ -180,6 +193,7 @@ func compressInternal(cctx, cctxDict *cctxWrapper, dst, src []byte, cd *CDict, c
180193
if mustSucceed {
181194
ensureNoError("ZSTD_compressCCtx", result)
182195
}
196+
183197
return result
184198
}
185199

@@ -258,7 +272,7 @@ func decompress(dctx, dctxDict *dctxWrapper, dst, src []byte, dd *DDict) ([]byte
258272
}
259273

260274
// Slow path - resize dst to fit decompressed data.
261-
srcHdr := (*reflect.SliceHeader)(unsafe.Pointer(&src))
275+
srcHdr := (*reflect.SliceHeader)(noescape(unsafe.Pointer(&src)))
262276
contentSize := C.ZSTD_getFrameContentSize_wrapper(unsafe.Pointer(srcHdr.Data), C.size_t(len(src)))
263277
switch {
264278
case contentSize == C.ZSTD_CONTENTSIZE_UNKNOWN || contentSize > maxFrameContentSize:
@@ -290,8 +304,8 @@ func decompress(dctx, dctxDict *dctxWrapper, dst, src []byte, dd *DDict) ([]byte
290304

291305
func decompressInternal(dctx, dctxDict *dctxWrapper, dst, src []byte, dd *DDict) C.size_t {
292306
var (
293-
dstHdr = (*reflect.SliceHeader)(unsafe.Pointer(&dst))
294-
srcHdr = (*reflect.SliceHeader)(unsafe.Pointer(&src))
307+
dstHdr = (*reflect.SliceHeader)(noescape(unsafe.Pointer(&dst)))
308+
srcHdr = (*reflect.SliceHeader)(noescape(unsafe.Pointer(&src)))
295309
n C.size_t
296310
)
297311
if dd != nil {

gozstd_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/hex"
66
"fmt"
7+
"io"
78
"math/rand"
89
"runtime"
910
"strings"
@@ -54,6 +55,14 @@ func TestDecompressSmallBlockWithoutSingleSegmentFlag(t *testing.T) {
5455
})
5556
}
5657

58+
func TestCompressEmpty(t *testing.T) {
59+
var dst [64]byte
60+
res := Compress(dst[:0], nil)
61+
if len(res) > 0 {
62+
t.Fatalf("unexpected non-empty compressed frame: %X", res)
63+
}
64+
}
65+
5766
func TestDecompressTooLarge(t *testing.T) {
5867
src := []byte{40, 181, 47, 253, 228, 122, 118, 105, 67, 140, 234, 85, 20, 159, 67}
5968
_, err := Decompress(nil, src)
@@ -70,6 +79,48 @@ func mustUnhex(dataHex string) []byte {
7079
return data
7180
}
7281

82+
func TestCompressWithStackMove(t *testing.T) {
83+
var srcBuf [96]byte
84+
85+
n, err := io.ReadFull(rand.New(rand.NewSource(time.Now().Unix())), srcBuf[:])
86+
if err != nil {
87+
t.Fatalf("cannot fill srcBuf with random data: %s", err)
88+
}
89+
90+
// We're running this twice, because the first run will allocate
91+
// objects in sync.Pool, calls to which extend the stack, and the second
92+
// run can skip those allocations and extend the stack right before
93+
// the CGO call.
94+
// Note that this test might require some go:nosplit annotations
95+
// to force the stack move to happen exactly before the CGO call.
96+
for i := 0; i < 2; i++ {
97+
ch := make(chan struct{})
98+
go func() {
99+
defer close(ch)
100+
101+
var dstBuf [1416]byte
102+
103+
res := Compress(dstBuf[:0], srcBuf[:n])
104+
105+
// make a copy of the result, so the original can remain on the stack
106+
compressedCpy := make([]byte, len(res))
107+
copy(compressedCpy, res)
108+
109+
orig, err := Decompress(nil, compressedCpy)
110+
if err != nil {
111+
panic(fmt.Errorf("cannot decompress: %s", err))
112+
}
113+
if !bytes.Equal(orig, srcBuf[:n]) {
114+
panic(fmt.Errorf("unexpected decompressed data; got %q; want %q", orig, srcBuf[:n]))
115+
}
116+
}()
117+
// wait for the goroutine to finish
118+
<-ch
119+
}
120+
121+
runtime.GC()
122+
}
123+
73124
func TestCompressDecompressDistinctConcurrentDicts(t *testing.T) {
74125
// Build multiple distinct dicts.
75126
var cdicts []*CDict

0 commit comments

Comments
 (0)