lib/bytesutil: split Resize() into ResizeNoCopy() and ResizeWithCopy() functions

Previously bytesutil.Resize() was copying the original byte slice contents to a newly allocated slice.
This wasted CPU cycles and memory bandwidth in some places, where the original slice contents wasn't needed
after slize resizing. Switch such places to bytesutil.ResizeNoCopy().

Rename the original bytesutil.Resize() function to bytesutil.ResizeWithCopy() for the sake of improved readability.

Additionally, allocate new slice with `make()` instead of `append()`. This guarantees that the capacity of the allocated slice
exactly matches the requested size. The `append()` could return a slice with bigger capacity as an optimization for further `append()` calls.
This could result in excess memory usage when the returned byte slice was cached (for instance, in lib/blockcache).

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2007
This commit is contained in:
Aliaksandr Valialkin 2022-01-25 15:16:24 +02:00
parent 91f2af2d7a
commit f4989edd96
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
18 changed files with 101 additions and 42 deletions

View file

@ -149,7 +149,7 @@ func (tbf *tmpBlocksFile) MustReadBlockRefAt(partRef storage.PartRef, addr tmpBl
} else {
bb := tmpBufPool.Get()
defer tmpBufPool.Put(bb)
bb.B = bytesutil.Resize(bb.B, addr.size)
bb.B = bytesutil.ResizeNoCopy(bb.B, addr.size)
tbf.r.MustReadAt(bb.B, int64(addr.offset))
buf = bb.B
}

View file

@ -98,7 +98,7 @@ func marshalTimeseriesFast(dst []byte, tss []*timeseries, maxSize int, step int6
// Allocate the buffer for the marshaled tss before its' marshaling.
// This should reduce memory fragmentation and memory usage.
dst = bytesutil.Resize(dst, size)
dst = bytesutil.ResizeNoCopy(dst, size)
dst = marshalFastTimestamps(dst[:0], tss[0].Timestamps)
for _, ts := range tss {
dst = ts.marshalFastNoTimestamps(dst)

View file

@ -53,7 +53,7 @@ func (bb *ByteBuffer) MustReadAt(p []byte, offset int64) {
func (bb *ByteBuffer) ReadFrom(r io.Reader) (int64, error) {
b := bb.B
bLen := len(b)
b = Resize(b, 4*1024)
b = ResizeWithCopy(b, 4*1024)
b = b[:cap(b)]
offset := bLen
for {

View file

@ -5,12 +5,29 @@ import (
"unsafe"
)
// Resize resizes b to n bytes and returns b (which may be newly allocated).
func Resize(b []byte, n int) []byte {
if nn := n - cap(b); nn > 0 {
b = append(b[:cap(b)], make([]byte, nn)...)
// ResizeWithCopy resizes b to n bytes and returns the resized buffer (which may be newly allocated).
//
// If newly allocated buffer is returned then b contents is copied to it.
func ResizeWithCopy(b []byte, n int) []byte {
if n <= cap(b) {
return b[:n]
}
return b[:n]
// Allocate the exact number of bytes instead of using `b = append(b[:cap(b)], make([]byte, nn)...)`,
// since `append()` may allocate more than the requested bytes for additional capacity.
// Using make() instead of append() should save RAM when the resized slice is cached somewhere.
bNew := make([]byte, n)
copy(bNew, b)
return bNew
}
// ResizeNoCopy resizes b to n bytes and returns the resized buffer (which may be newly allocated).
//
// If newly allocated buffer is returned then b contents isn't copied to it.
func ResizeNoCopy(b []byte, n int) []byte {
if n <= cap(b) {
return b[:n]
}
return make([]byte, n)
}
// ToUnsafeString converts b to string without memory allocations.

View file

@ -5,19 +5,62 @@ import (
"testing"
)
func TestResize(t *testing.T) {
func TestResizeNoCopy(t *testing.T) {
for i := 0; i < 1000; i++ {
b := Resize(nil, i)
b := ResizeNoCopy(nil, i)
if len(b) != i {
t.Fatalf("invalid b size; got %d; expecting %d", len(b), i)
}
b1 := Resize(b, i)
b1 := ResizeNoCopy(b, i)
if len(b1) != len(b) || (len(b) > 0 && &b1[0] != &b[0]) {
t.Fatalf("invalid b1; got %x; expecting %x", b1, b)
t.Fatalf("invalid b1; got %x; expecting %x", &b1[0], &b[0])
}
b2 := Resize(b[:0], i)
b2 := ResizeNoCopy(b[:0], i)
if len(b2) != len(b) || (len(b) > 0 && &b2[0] != &b[0]) {
t.Fatalf("invalid b2; got %x; expecting %x", b2, b)
t.Fatalf("invalid b2; got %x; expecting %x", &b2[0], &b[0])
}
if i > 0 {
b[0] = 123
b3 := ResizeNoCopy(b, i+1)
if len(b3) != i+1 {
t.Fatalf("invalid b3 len; got %d; want %d", len(b3), i+1)
}
if &b3[0] == &b[0] {
t.Fatalf("b3 must be newly allocated")
}
if b3[0] != 0 {
t.Fatalf("b3[0] must be zeroed; got %d", b3[0])
}
}
}
}
func TestResizeWithCopy(t *testing.T) {
for i := 0; i < 1000; i++ {
b := ResizeWithCopy(nil, i)
if len(b) != i {
t.Fatalf("invalid b size; got %d; expecting %d", len(b), i)
}
b1 := ResizeWithCopy(b, i)
if len(b1) != len(b) || (len(b) > 0 && &b1[0] != &b[0]) {
t.Fatalf("invalid b1; got %x; expecting %x", &b1[0], &b[0])
}
b2 := ResizeWithCopy(b[:0], i)
if len(b2) != len(b) || (len(b) > 0 && &b2[0] != &b[0]) {
t.Fatalf("invalid b2; got %x; expecting %x", &b2[0], &b[0])
}
if i > 0 {
b[0] = 123
b3 := ResizeWithCopy(b, i+1)
if len(b3) != i+1 {
t.Fatalf("invalid b3 len; got %d; want %d", len(b3), i+1)
}
if &b3[0] == &b[0] {
t.Fatalf("b3 must be newly allocated for i=%d", i)
}
if b3[0] != b[0] || b3[0] != 123 {
t.Fatalf("b3[0] must equal b[0]; got %d; want %d", b3[0], b[0])
}
}
}
}

View file

@ -135,7 +135,7 @@ func (s *Server) serveUDP(insertHandler func(r io.Reader) error) {
go func() {
defer wg.Done()
var bb bytesutil.ByteBuffer
bb.B = bytesutil.Resize(bb.B, 64*1024)
bb.B = bytesutil.ResizeNoCopy(bb.B, 64*1024)
for {
bb.Reset()
bb.B = bb.B[:cap(bb.B)]

View file

@ -135,7 +135,7 @@ func (s *Server) serveUDP(insertHandler func(r io.Reader) error) {
go func() {
defer wg.Done()
var bb bytesutil.ByteBuffer
bb.B = bytesutil.Resize(bb.B, 64*1024)
bb.B = bytesutil.ResizeNoCopy(bb.B, 64*1024)
for {
bb.Reset()
bb.B = bb.B[:cap(bb.B)]

View file

@ -153,7 +153,7 @@ func (s *Server) serveUDP(insertHandler func(r io.Reader) error) {
go func() {
defer wg.Done()
var bb bytesutil.ByteBuffer
bb.B = bytesutil.Resize(bb.B, 64*1024)
bb.B = bytesutil.ResizeNoCopy(bb.B, 64*1024)
for {
bb.Reset()
bb.B = bb.B[:cap(bb.B)]

View file

@ -211,13 +211,13 @@ func (bsr *blockStreamReader) Next() bool {
bsr.bh = &bsr.bhs[bsr.bhIdx]
bsr.bhIdx++
bsr.sb.itemsData = bytesutil.Resize(bsr.sb.itemsData, int(bsr.bh.itemsBlockSize))
bsr.sb.itemsData = bytesutil.ResizeNoCopy(bsr.sb.itemsData, int(bsr.bh.itemsBlockSize))
if err := fs.ReadFullData(bsr.itemsReader, bsr.sb.itemsData); err != nil {
bsr.err = fmt.Errorf("cannot read compressed items block with size %d: %w", bsr.bh.itemsBlockSize, err)
return false
}
bsr.sb.lensData = bytesutil.Resize(bsr.sb.lensData, int(bsr.bh.lensBlockSize))
bsr.sb.lensData = bytesutil.ResizeNoCopy(bsr.sb.lensData, int(bsr.bh.lensBlockSize))
if err := fs.ReadFullData(bsr.lensReader, bsr.sb.lensData); err != nil {
bsr.err = fmt.Errorf("cannot read compressed lens block with size %d: %w", bsr.bh.lensBlockSize, err)
return false
@ -260,7 +260,7 @@ func (bsr *blockStreamReader) readNextBHS() error {
bsr.mrIdx++
// Read compressed index block.
bsr.packedBuf = bytesutil.Resize(bsr.packedBuf, int(mr.indexBlockSize))
bsr.packedBuf = bytesutil.ResizeNoCopy(bsr.packedBuf, int(mr.indexBlockSize))
if err := fs.ReadFullData(bsr.indexReader, bsr.packedBuf); err != nil {
return fmt.Errorf("cannot read compressed index block with size %d: %w", mr.indexBlockSize, err)
}

View file

@ -119,7 +119,7 @@ func (ib *inmemoryBlock) Add(x []byte) bool {
}
if cap(data) < maxInmemoryBlockSize {
dataLen := len(data)
data = bytesutil.Resize(data, maxInmemoryBlockSize)[:dataLen]
data = bytesutil.ResizeWithCopy(data, maxInmemoryBlockSize)[:dataLen]
}
dataLen := len(data)
data = append(data, x...)
@ -141,7 +141,7 @@ func (ib *inmemoryBlock) sort() {
data := ib.data
items := ib.items
bb := bbPool.Get()
b := bytesutil.Resize(bb.B, len(data))
b := bytesutil.ResizeNoCopy(bb.B, len(data))
b = b[:0]
for i, it := range items {
bLen := len(b)
@ -394,7 +394,7 @@ func (ib *inmemoryBlock) UnmarshalData(sb *storageBlock, firstItem, commonPrefix
// Resize ib.data to dataLen instead of maxInmemoryBlockSize,
// since the data isn't going to be resized after unmarshaling.
// This may save memory for caching the unmarshaled block.
data := bytesutil.Resize(ib.data, dataLen)
data := bytesutil.ResizeNoCopy(ib.data, dataLen)
if n := int(itemsCount) - cap(ib.items); n > 0 {
ib.items = append(ib.items[:cap(ib.items)], make([]Item, n)...)
}
@ -492,7 +492,7 @@ func (ib *inmemoryBlock) unmarshalDataPlain(sb *storageBlock, firstItem []byte,
// Unmarshal items data.
data := ib.data
items := ib.items
data = bytesutil.Resize(data, len(firstItem)+len(sb.itemsData)+len(commonPrefix)*int(itemsCount))
data = bytesutil.ResizeNoCopy(data, len(firstItem)+len(sb.itemsData)+len(commonPrefix)*int(itemsCount))
data = append(data[:0], firstItem...)
items = append(items[:0], Item{
Start: 0,

View file

@ -274,7 +274,7 @@ func (ps *partSearch) nextBHS() error {
}
func (ps *partSearch) readIndexBlock(mr *metaindexRow) (*indexBlock, error) {
ps.compressedIndexBuf = bytesutil.Resize(ps.compressedIndexBuf, int(mr.indexBlockSize))
ps.compressedIndexBuf = bytesutil.ResizeNoCopy(ps.compressedIndexBuf, int(mr.indexBlockSize))
ps.p.indexFile.MustReadAt(ps.compressedIndexBuf, int64(mr.indexBlockOffset))
var err error
@ -311,10 +311,10 @@ func (ps *partSearch) getInmemoryBlock(bh *blockHeader) (*inmemoryBlock, error)
func (ps *partSearch) readInmemoryBlock(bh *blockHeader) (*inmemoryBlock, error) {
ps.sb.Reset()
ps.sb.itemsData = bytesutil.Resize(ps.sb.itemsData, int(bh.itemsBlockSize))
ps.sb.itemsData = bytesutil.ResizeNoCopy(ps.sb.itemsData, int(bh.itemsBlockSize))
ps.p.itemsFile.MustReadAt(ps.sb.itemsData, int64(bh.itemsBlockOffset))
ps.sb.lensData = bytesutil.Resize(ps.sb.lensData, int(bh.lensBlockSize))
ps.sb.lensData = bytesutil.ResizeNoCopy(ps.sb.lensData, int(bh.lensBlockSize))
ps.p.lensFile.MustReadAt(ps.sb.lensData, int64(bh.lensBlockOffset))
ib := getInmemoryBlock()

View file

@ -499,7 +499,7 @@ func (q *queue) readBlock(dst []byte) ([]byte, error) {
again:
// Read block len.
header := headerBufPool.Get()
header.B = bytesutil.Resize(header.B, 8)
header.B = bytesutil.ResizeNoCopy(header.B, 8)
err := q.readFull(header.B)
blockLen := encoding.UnmarshalUint64(header.B)
headerBufPool.Put(header)
@ -520,7 +520,7 @@ again:
// Read block contents.
dstLen := len(dst)
dst = bytesutil.Resize(dst, dstLen+int(blockLen))
dst = bytesutil.ResizeWithCopy(dst, dstLen+int(blockLen))
if err := q.readFull(dst[dstLen:]); err != nil {
logger.Errorf("skipping corrupted %q, since contents with size %d bytes cannot be read from it: %s", q.readerPath, blockLen, err)
if err := q.skipBrokenChunkFile(); err != nil {

View file

@ -40,7 +40,7 @@ func ReadLinesBlock(r io.Reader, dstBuf, tailBuf []byte) ([]byte, []byte, error)
func ReadLinesBlockExt(r io.Reader, dstBuf, tailBuf []byte, maxLineLen int) ([]byte, []byte, error) {
startTime := time.Now()
if cap(dstBuf) < defaultBlockSize {
dstBuf = bytesutil.Resize(dstBuf, defaultBlockSize)
dstBuf = bytesutil.ResizeNoCopy(dstBuf, defaultBlockSize)
}
dstBuf = append(dstBuf[:0], tailBuf...)
tailBuf = tailBuf[:0]
@ -79,7 +79,7 @@ again:
if cap(dstBuf) < 2*len(dstBuf) {
// Increase dsbBuf capacity, so more data could be read into it.
dstBufLen := len(dstBuf)
dstBuf = bytesutil.Resize(dstBuf, 2*cap(dstBuf))
dstBuf = bytesutil.ResizeWithCopy(dstBuf, 2*cap(dstBuf))
dstBuf = dstBuf[:dstBufLen]
}
goto again

View file

@ -84,7 +84,7 @@ func ParseStream(req *http.Request, callback func(block *Block) error) error {
wg.Wait()
return fmt.Errorf("too big metricName size; got %d; shouldn't exceed %d", bufSize, 1024*1024)
}
uw.metricNameBuf = bytesutil.Resize(uw.metricNameBuf, int(bufSize))
uw.metricNameBuf = bytesutil.ResizeNoCopy(uw.metricNameBuf, int(bufSize))
if _, err := io.ReadFull(br, uw.metricNameBuf); err != nil {
readErrors.Inc()
wg.Wait()
@ -105,7 +105,7 @@ func ParseStream(req *http.Request, callback func(block *Block) error) error {
wg.Wait()
return fmt.Errorf("too big native block size; got %d; shouldn't exceed %d", bufSize, 1024*1024)
}
uw.blockBuf = bytesutil.Resize(uw.blockBuf, int(bufSize))
uw.blockBuf = bytesutil.ResizeNoCopy(uw.blockBuf, int(bufSize))
if _, err := io.ReadFull(br, uw.blockBuf); err != nil {
readErrors.Inc()
wg.Wait()

View file

@ -303,7 +303,7 @@ func (bsr *blockStreamReader) readBlock() error {
if usePrevTimestamps {
bsr.Block.timestampsData = append(bsr.Block.timestampsData[:0], bsr.prevTimestampsData...)
} else {
bsr.Block.timestampsData = bytesutil.Resize(bsr.Block.timestampsData, int(bsr.Block.bh.TimestampsBlockSize))
bsr.Block.timestampsData = bytesutil.ResizeNoCopy(bsr.Block.timestampsData, int(bsr.Block.bh.TimestampsBlockSize))
if err := fs.ReadFullData(bsr.timestampsReader, bsr.Block.timestampsData); err != nil {
return fmt.Errorf("cannot read timestamps block at offset %d: %w", bsr.timestampsBlockOffset, err)
}
@ -312,7 +312,7 @@ func (bsr *blockStreamReader) readBlock() error {
}
// Read values data.
bsr.Block.valuesData = bytesutil.Resize(bsr.Block.valuesData, int(bsr.Block.bh.ValuesBlockSize))
bsr.Block.valuesData = bytesutil.ResizeNoCopy(bsr.Block.valuesData, int(bsr.Block.bh.ValuesBlockSize))
if err := fs.ReadFullData(bsr.valuesReader, bsr.Block.valuesData); err != nil {
return fmt.Errorf("cannot read values block at offset %d: %w", bsr.valuesBlockOffset, err)
}
@ -347,7 +347,7 @@ func (bsr *blockStreamReader) readIndexBlock() error {
}
// Read index block.
bsr.compressedIndexData = bytesutil.Resize(bsr.compressedIndexData, int(bsr.mr.IndexBlockSize))
bsr.compressedIndexData = bytesutil.ResizeNoCopy(bsr.compressedIndexData, int(bsr.mr.IndexBlockSize))
if err := fs.ReadFullData(bsr.indexReader, bsr.compressedIndexData); err != nil {
return fmt.Errorf("cannot read index block from index data at offset %d: %w", bsr.indexBlockOffset, err)
}

View file

@ -370,8 +370,7 @@ func (mn *MetricName) Marshal(dst []byte) []byte {
tag := &mn.Tags[i]
requiredSize += len(tag.Key) + len(tag.Value) + 2
}
dst = bytesutil.Resize(dst, requiredSize)
dst = dst[:dstLen]
dst = bytesutil.ResizeWithCopy(dst, requiredSize)[:dstLen]
// Marshal MetricGroup
dst = marshalTagValue(dst, mn.MetricGroup)
@ -478,7 +477,7 @@ func MarshalMetricNameRaw(dst []byte, labels []prompb.Label) []byte {
dstSize += len(label.Value)
dstSize += 4
}
dst = bytesutil.Resize(dst, dstSize)[:dstLen]
dst = bytesutil.ResizeWithCopy(dst, dstSize)[:dstLen]
// Marshal labels to dst.
for i := range labels {

View file

@ -211,7 +211,7 @@ func skipSmallMetaindexRows(metaindex []metaindexRow, tsid *TSID) []metaindexRow
}
func (ps *partSearch) readIndexBlock(mr *metaindexRow) (*indexBlock, error) {
ps.compressedIndexBuf = bytesutil.Resize(ps.compressedIndexBuf[:0], int(mr.IndexBlockSize))
ps.compressedIndexBuf = bytesutil.ResizeNoCopy(ps.compressedIndexBuf, int(mr.IndexBlockSize))
ps.p.indexFile.MustReadAt(ps.compressedIndexBuf, int64(mr.IndexBlockOffset))
var err error

View file

@ -75,10 +75,10 @@ func (br *BlockRef) MustReadBlock(dst *Block, fetchData bool) {
return
}
dst.timestampsData = bytesutil.Resize(dst.timestampsData[:0], int(br.bh.TimestampsBlockSize))
dst.timestampsData = bytesutil.ResizeNoCopy(dst.timestampsData, int(br.bh.TimestampsBlockSize))
br.p.timestampsFile.MustReadAt(dst.timestampsData, int64(br.bh.TimestampsBlockOffset))
dst.valuesData = bytesutil.Resize(dst.valuesData[:0], int(br.bh.ValuesBlockSize))
dst.valuesData = bytesutil.ResizeNoCopy(dst.valuesData, int(br.bh.ValuesBlockSize))
br.p.valuesFile.MustReadAt(dst.valuesData, int64(br.bh.ValuesBlockOffset))
}