AioBuffer.Bytes() cannot be used for coping images from NBD to other
APis because it copies the entire image. Add a new Slice() function,
creating a slice backed by the underling 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 show 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 complied 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_620_aio_buffer_test.go | 41 ++++++++++++++++++++++++++++
2 files changed, 50 insertions(+)
diff --git a/golang/aio_buffer.go b/golang/aio_buffer.go
index dcb036a5..788fe1a8 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 unerlying buffer
// backking 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
// caling Free().
func (b *AioBuffer) Get(i uint) *byte {
return (*byte)(unsafe.Pointer(uintptr(b.P) + uintptr(i)))
}
diff --git a/golang/libnbd_620_aio_buffer_test.go b/golang/libnbd_620_aio_buffer_test.go
index 53d6233b..38c6c74d 100644
--- a/golang/libnbd_620_aio_buffer_test.go
+++ b/golang/libnbd_620_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 underlhing buffer. */
+ s := buf.Slice()
+ if !bytes.Equal(s, zeroes) {
+ t.Fatalf("Expected %v, got %v", zeroes, s)
+ }
+
+ /* Modifing 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 a nother 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