6

I just tried to optimize some communication stack. I am using Qt 5.3.2 / VS2013.

The stack uses a QByteArray as a data buffer. I intended to use the capacity() and reserve() methods to reduce unnecessary internal buffer reallocations while the data size grows. However the behaviour of QByteArray turned out to be inconsistent. The reserved space sometimes seems to be squeezed implicitly.

I could extract the following demo applying a string append, a string assignment and a character append to three buffers. These single operations seem to preserve the internal buffers size (obtained using capacity()). However when applying each of these three operations to the same QByteArray the reserved size changes. The behaviour looks random to me:

QByteArray x1; x1.reserve(1000);
x1.append("test");
qDebug() << "x1" << x1.capacity() << x1;

QByteArray x2; x2.reserve(1000);
x2 = "test";
qDebug() << "x2" << x2.capacity() << x2;

QByteArray x3; x3.reserve(1000);
x3.append('t');
qDebug() << "x3" << x3.capacity() << x3;

QByteArray x4; x4.reserve(1000);
x4.append("test");
x4.append('t');
x4 = "test";
qDebug() << "x4" << x4.capacity() << x4;

The expected output would be:

x1 1000 "test"
x2 1000 "test"
x3 1000 "t"
x4 1000 "test"

But the actual output is:

x1 1000 "test"
x2 1000 "test"
x3 1000 "t"
x4 4 "test"

Does anyone have an explanation for that strange behaviour?

UPDATE: Looks like clear() also discards reservation.

Silicomancer
  • 8,604
  • 10
  • 63
  • 130
  • Please look again... x2 uses the same assignment. It is not the assignment... somehow it is the combination of operations that causes the problem. – Silicomancer Nov 08 '14 at 19:42
  • Right. Sorry about that. x2 assigns to an allocated, empty buffer, x4 resets the buffer.... – Mat Nov 08 '14 at 19:54
  • Do you think this actually is a desired behaviour? – Silicomancer Nov 08 '14 at 20:03
  • 1
    I was looking at the code for `operator=` in QByteArray and... I don't really understand the logic. It does not fit your use-case, the reserved capacity will not be maintained in a lot of conditions. (Looking at the other resize/truncate things, doesn't look better.) – Mat Nov 08 '14 at 20:13
  • (Also if you've actually measured that those allocations are costing you, the overhead of the implicitly-shared model of QByteArray might itself be too much overhead for your use-case.) – Mat Nov 08 '14 at 20:15
  • If the size isn't preserved, doesn't this render reserve() pretty useless? – Silicomancer Nov 08 '14 at 20:16
  • It's maintained in `append`. But from a cursory look, that's about it. – Mat Nov 08 '14 at 20:20
  • It looks like it loses capacity when you use assignment operator after there was some other content that gets discarded. – dtech Nov 08 '14 at 21:17
  • Like @Mat says I would reconsider if `QByteArray` is really the data structure you want to use. I generally find the Qt data structures to be less useful than their equivalents in the standard library, especially with all the "surprising" behaviour Copy On Write gives you. – sjdowling Nov 08 '14 at 23:19

3 Answers3

3

Ok. I think I got the information I need.

Obviously the reservation isn't maintained beyond all methods. Especially clear() and operator=() seem to cancel the reservation. In case of operator=() it actually will be impossible to preserve the reservation due to implicit sharing of data that is used by operator=(QByteArray).

This also implies that reservation mechanism of QByteArray is made for a different use case. Trying to make the reservation persist during the entire life of the QByteArray object is difficult.

For my use case there seems to be a workaround using truncate(0) instead of clear() or operator=():

QByteArray buffer;
buffer.reserve(1000);
buffer.append("foo");
qDebug() << "buffer" << buffer.capacity() << buffer;

buffer.truncate(0);
buffer.append("bar");
qDebug() << "buffer" << buffer.capacity() << buffer;

This prints:

buffer 1000 "foo"
buffer 1000 "bar"

(Thanks Alejandro)

Still the more stable approach will be to do an reserve() call before every data collection/appending sequence. This does not reduce the reallocation to one through the entire life of the QByteArray but at least it uses exactly one reallocation per data sequence where it would need many reallocations otherwise. I think this is an acceptable workaround.

Anyway before using reserve() on a Qt container one should test the behaviour in detail because otherwise it may happen that the container behaves much different than expected. This also is important since those essential implementation details are not documented and could change without further notice in future Qt versions.

Silicomancer
  • 8,604
  • 10
  • 63
  • 130
2

operator= in QByteArray contains next code

int len = qstrlen(str);
if (d->ref != 1 || len > d->alloc || (len < d->size && len < d->alloc >> 1))
realloc(len);

It reallocated memory, if new data length more then allocated or if new data length less then current size and less then (allocated >> 1)

Meefte
  • 6,469
  • 1
  • 39
  • 42
  • Is there any sense in this condition I am missing? What sense does reserve() make if reserved space is discarded that easy? – Silicomancer Nov 08 '14 at 20:29
  • 1
    @Silicomancer I don't really understand why `operator=` do it. `append()` realloc memory only if `(d->ref != 1 || d->size + len > d->alloc)`. So if you use `append()`, without `operator=`, `reserve()` work as expected – Meefte Nov 08 '14 at 20:37
1

There is no straightforward way to take advantage of reserve() method in QByteArray class. I tried doing below operations:

QByteArray buffer;
buffer.reserve(1000);
buffer.append("foo");
cout << "Capacity " << buffer.capacity() << " Buffer " << buffer;

buffer.truncate(0);
buffer.append("bar");
cout << "Capacity " << buffer.capacity() << " Buffer " << buffer;

And the output is:

Capacity 1000 Buffer foo
Capacity 8 Buffer bar

It happens because, calling truncate on buffer calls "resize" method which actually deallocates the reserved memory. To make it work, there is no straightforward way by using documented QByteArray APIs. I could figure out a way, which can be tried out.

QByteArray buffer;
buffer.reserve(1000);
buffer.append("foo");
cout << "Capacity " << buffer.capacity() << " Buffer " << buffer;

buffer.fill('\0');
buffer.data_ptr()->size = 0;
buffer.append("bar");
cout << "Capacity " << buffer.capacity() << " Buffer " << buffer;

And the output is:

Capacity 1000 Buffer foo
Capacity 1000 Buffer bar

Line:

buffer.fill('\0');

Sets the content of existing buffer to NULL. It's optional to use.

Line:

buffer.data_ptr()->size = 0;

data_ptr() method returns a pointer of an internal structure Data, then by modifying its size variable to 0, makes the buffer empty. Therefore, further calls to appends are legitimate.

nik_kgp
  • 1,112
  • 1
  • 9
  • 17