AioBuffer.Bytes() cannot be used for copying images from NBD to other
APis because it copies the entire image. Add a new Slice() function,
creating a slice backed by the underlying buffer.
Using Slice() is efficient, but less safe, like Get(). The returned
slice must be used only before calling Free(). This should not be an
issue with typical code.
Testing shows that Slice() is much faster than Bytes() for typical 256k
buffer:
BenchmarkAioBufferBytes-12 86616 16529 ns/op
BenchmarkAioBufferSlice-12 1000000000 0.4630 ns/op
I modified the aio_copy example to use AioBuffer and compiled 2
versions, one using Bytes() and one using Slice(). When copying 6g fully
allocated image, the version using Slice() is 1.6 times faster.
$ hyperfine -r3 "./aio_copy-bytes $SRC >/dev/null" "./aio_copy-slice
$SRC >/dev/null"
Benchmark 1: ./aio_copy-bytes nbd+unix:///?socket=/tmp/src.sock >/dev/null
Time (mean ± σ): 3.357 s ± 0.039 s [User: 2.656 s, System: 1.162 s]
Range (min … max): 3.313 s … 3.387 s 3 runs
Benchmark 2: ./aio_copy-slice nbd+unix:///?socket=/tmp/src.sock >/dev/null
Time (mean ± σ): 2.046 s ± 0.009 s [User: 0.423 s, System: 0.892 s]
Range (min … max): 2.037 s … 2.055 s 3 runs
Summary
'./aio_copy-slice nbd+unix:///?socket=/tmp/src.sock >/dev/null' ran
1.64 ± 0.02 times faster than './aio_copy-bytes nbd+unix:///?socket=/tmp/src.sock
>/dev/null'
When copying a 6g empty image (qemu-nbd sends one hole chunk for every
read), the version using Slice() is 2.6 times faster.
$ hyperfine -r3 "./aio_copy-bytes $SRC >/dev/null" "./aio_copy-slice
$SRC >/dev/null"
Benchmark 1: ./aio_copy-bytes nbd+unix:///?socket=/tmp/src.sock >/dev/null
Time (mean ± σ): 1.210 s ± 0.023 s [User: 1.428 s, System: 0.345 s]
Range (min … max): 1.191 s … 1.235 s 3 runs
Benchmark 2: ./aio_copy-slice nbd+unix:///?socket=/tmp/src.sock >/dev/null
Time (mean ± σ): 461.4 ms ± 13.1 ms [User: 394.2 ms, System: 76.6 ms]
Range (min … max): 450.6 ms … 476.0 ms 3 runs
Summary
'./aio_copy-slice nbd+unix:///?socket=/tmp/src.sock >/dev/null' ran
2.62 ± 0.09 times faster than './aio_copy-bytes nbd+unix:///?socket=/tmp/src.sock
>/dev/null'
Signed-off-by: Nir Soffer <nsoffer(a)redhat.com>
---
golang/aio_buffer.go | 9 ++++++
golang/libnbd_020_aio_buffer_test.go | 41 ++++++++++++++++++++++++++++
2 files changed, 50 insertions(+)
diff --git a/golang/aio_buffer.go b/golang/aio_buffer.go
index d2e6e350..008d9ae0 100644
--- a/golang/aio_buffer.go
+++ b/golang/aio_buffer.go
@@ -71,16 +71,25 @@ func (b *AioBuffer) Free() {
}
}
// Bytes copies the underlying C array to Go allocated memory and return a
// slice. Modifying the returned slice does not modify the underlying buffer
// backing array.
func (b *AioBuffer) Bytes() []byte {
return C.GoBytes(b.P, C.int(b.Size))
}
+// Slice creates a slice backed by the underlying C array. The slice can be
+// used to access or modify the contents of the underlying array. The slice
+// must not be used after caling Free().
+func (b *AioBuffer) Slice() []byte {
+ // See
https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices
+ // TODO: Use unsafe.Slice() when we require Go 1.17.
+ return (*[1<<30]byte)(b.P)[:b.Size:b.Size]
+}
+
// Get returns a pointer to a byte in the underlying C array. The pointer can
// be used to modify the underlying array. The pointer must not be used after
// calling Free().
func (b *AioBuffer) Get(i uint) *byte {
return (*byte)(unsafe.Pointer(uintptr(b.P) + uintptr(i)))
}
diff --git a/golang/libnbd_020_aio_buffer_test.go b/golang/libnbd_020_aio_buffer_test.go
index b3a2a8d9..4b1c5f93 100644
--- a/golang/libnbd_020_aio_buffer_test.go
+++ b/golang/libnbd_020_aio_buffer_test.go
@@ -44,20 +44,35 @@ func TestAioBuffer(t *testing.T) {
/* Modifying returned slice does not modify the buffer. */
for i := 0; i < len(b); i++ {
b[i] = 42
}
/* Bytes() still returns zeroes. */
if !bytes.Equal(buf.Bytes(), zeroes) {
t.Fatalf("Expected %v, got %v", zeroes, buf.Bytes())
}
+ /* Creating a slice without copying the underlying buffer. */
+ s := buf.Slice()
+ if !bytes.Equal(s, zeroes) {
+ t.Fatalf("Expected %v, got %v", zeroes, s)
+ }
+
+ /* Modifying the slice modifies the underlying buffer. */
+ for i := 0; i < len(s); i++ {
+ s[i] = 42
+ }
+
+ if !bytes.Equal(buf.Slice(), s) {
+ t.Fatalf("Expected %v, got %v", s, buf.Slice())
+ }
+
/* Create another buffer from Go slice. */
buf2 := FromBytes(zeroes)
defer buf2.Free()
if !bytes.Equal(buf2.Bytes(), zeroes) {
t.Fatalf("Expected %v, got %v", zeroes, buf2.Bytes())
}
/* Crated a zeroed buffer. */
buf3 := MakeAioBufferZero(uint(32))
@@ -89,20 +104,34 @@ func TestAioBufferBytesAfterFree(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatal("Did not recover from panic calling Bytes() after Free()")
}
}()
buf.Bytes()
}
+func TestAioBufferSliceAfterFree(t *testing.T) {
+ buf := MakeAioBuffer(uint(32))
+ buf.Free()
+
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatal("Did not recover from panic calling Bytes() after Free()")
+ }
+ }()
+
+ s := buf.Slice()
+ s[0] = 42
+}
+
func TestAioBufferGetAfterFree(t *testing.T) {
buf := MakeAioBuffer(uint(32))
buf.Free()
defer func() {
if r := recover(); r == nil {
t.Fatal("Did not recover from panic calling Get() after Free()")
}
}()
@@ -152,10 +181,22 @@ func BenchmarkFromBytes(b *testing.B) {
func BenchmarkAioBufferBytes(b *testing.B) {
buf := MakeAioBuffer(bufferSize)
defer buf.Free()
var r int
b.ResetTimer()
for i := 0; i < b.N; i++ {
r += len(buf.Bytes())
}
}
+
+// Benchmark creating a slice without copying the underling buffer.
+func BenchmarkAioBufferSlice(b *testing.B) {
+ buf := MakeAioBuffer(bufferSize)
+ defer buf.Free()
+ var r int
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ r += len(buf.Slice())
+ }
+}
--
2.34.1