Skip to content

Commit 44d20b2

Browse files
committed
Merge pull request #100619 from Ivorforce/cowdata-resize-direct
Optimize / refactor `CowData`, combining resize and fork to avoid unnecessary reallocations.
2 parents fc1ffeb + 015a3b0 commit 44d20b2

File tree

1 file changed

+109
-101
lines changed

1 file changed

+109
-101
lines changed

core/templates/cowdata.h

Lines changed: 109 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,6 @@ class CowData {
8585

8686
// internal helpers
8787

88-
static _FORCE_INLINE_ SafeNumeric<USize> *_get_refcount_ptr(uint8_t *p_ptr) {
89-
return (SafeNumeric<USize> *)(p_ptr + REF_COUNT_OFFSET);
90-
}
91-
92-
static _FORCE_INLINE_ USize *_get_size_ptr(uint8_t *p_ptr) {
93-
return (USize *)(p_ptr + SIZE_OFFSET);
94-
}
95-
9688
static _FORCE_INLINE_ T *_get_data_ptr(uint8_t *p_ptr) {
9789
return (T *)(p_ptr + DATA_OFFSET);
9890
}
@@ -146,8 +138,23 @@ class CowData {
146138
void _unref();
147139
void _ref(const CowData *p_from);
148140
void _ref(const CowData &p_from);
149-
USize _copy_on_write();
150-
Error _realloc(Size p_alloc_size);
141+
142+
// Ensures that the backing buffer is at least p_size wide, and that this CowData instance is
143+
// the only reference to it. The buffer is populated with as many element copies from the old
144+
// array as possible.
145+
// It is the responsibility of the caller to populate newly allocated space up to p_size.
146+
Error _fork_allocate(USize p_size);
147+
Error _copy_on_write() { return _fork_allocate(size()); }
148+
149+
// Allocates a backing array of the given capacity. The reference count is initialized to 1.
150+
// It is the responsibility of the caller to populate the array and the new size property.
151+
Error _alloc(USize p_alloc_size);
152+
153+
// Re-allocates the backing array to the given capacity. The reference count is initialized to 1.
154+
// It is the responsibility of the caller to populate the array and the new size property.
155+
// The caller must also make sure there are no other references to the data, as pointers may
156+
// be invalidated.
157+
Error _realloc(USize p_alloc_size);
151158

152159
public:
153160
void operator=(const CowData<T> &p_from) { _ref(p_from); }
@@ -179,7 +186,7 @@ class CowData {
179186
}
180187
}
181188

182-
_FORCE_INLINE_ void clear() { resize(0); }
189+
_FORCE_INLINE_ void clear() { _unref(); }
183190
_FORCE_INLINE_ bool is_empty() const { return _ptr == nullptr; }
184191

185192
_FORCE_INLINE_ void set(Size p_index, const T &p_elem) {
@@ -253,7 +260,8 @@ void CowData<T>::_unref() {
253260
_ptr = nullptr;
254261
return;
255262
}
256-
// Clean up.
263+
// We had the only reference; destroy the data.
264+
257265
// First, invalidate our own reference.
258266
// NOTE: It is required to do so immediately because it must not be observable outside of this
259267
// function after refcount has already been reduced to 0.
@@ -271,136 +279,136 @@ void CowData<T>::_unref() {
271279
}
272280
}
273281

274-
// free mem
282+
// Free memory.
275283
Memory::free_static((uint8_t *)prev_ptr - DATA_OFFSET, false);
276284
}
277285

278286
template <typename T>
279-
typename CowData<T>::USize CowData<T>::_copy_on_write() {
280-
if (!_ptr) {
281-
return 0;
287+
Error CowData<T>::_fork_allocate(USize p_size) {
288+
if (p_size == 0) {
289+
// Wants to clean up.
290+
_unref();
291+
return OK;
282292
}
283293

284-
SafeNumeric<USize> *refc = _get_refcount();
285-
286-
USize rc = refc->get();
287-
if (unlikely(rc > 1)) {
288-
/* in use by more than me */
289-
USize current_size = *_get_size();
294+
USize alloc_size;
295+
ERR_FAIL_COND_V(!_get_alloc_size_checked(p_size, &alloc_size), ERR_OUT_OF_MEMORY);
290296

291-
uint8_t *mem_new = (uint8_t *)Memory::alloc_static(_get_alloc_size(current_size) + DATA_OFFSET, false);
292-
ERR_FAIL_NULL_V(mem_new, 0);
297+
const USize prev_size = size();
293298

294-
SafeNumeric<USize> *_refc_ptr = _get_refcount_ptr(mem_new);
295-
USize *_size_ptr = _get_size_ptr(mem_new);
296-
T *_data_ptr = _get_data_ptr(mem_new);
299+
if (!_ptr) {
300+
// We had no data before; just allocate a new array.
301+
const Error error = _alloc(alloc_size);
302+
if (error) {
303+
return error;
304+
}
305+
} else if (_get_refcount()->get() == 1) {
306+
// Resize in-place.
307+
// NOTE: This case is not just an optimization, but required, as some callers depend on
308+
// `_copy_on_write()` calls not changing the pointer after the first fork
309+
// (e.g. mutable iterators).
310+
if (p_size == prev_size) {
311+
// We can shortcut here; we don't need to do anything.
312+
return OK;
313+
}
297314

298-
new (_refc_ptr) SafeNumeric<USize>(1); //refcount
299-
*(_size_ptr) = current_size; //size
315+
// Destroy extraneous elements.
316+
if constexpr (!std::is_trivially_destructible_v<T>) {
317+
for (USize i = prev_size; i > p_size; i--) {
318+
_ptr[i - 1].~T();
319+
}
320+
}
300321

301-
// initialize new elements
302-
if constexpr (std::is_trivially_copyable_v<T>) {
303-
memcpy((uint8_t *)_data_ptr, _ptr, current_size * sizeof(T));
304-
} else {
305-
for (USize i = 0; i < current_size; i++) {
306-
memnew_placement(&_data_ptr[i], T(_ptr[i]));
322+
if (alloc_size != _get_alloc_size(prev_size)) {
323+
const Error error = _realloc(alloc_size);
324+
if (error) {
325+
// Out of memory; the current array is still valid though.
326+
return error;
307327
}
308328
}
329+
} else {
330+
// Resize by forking.
331+
332+
// Create a temporary CowData to hold ownership over our _ptr.
333+
// It will be used to copy elements from the old buffer over to our new buffer.
334+
// At the end of the block, it will be automatically destructed by going out of scope.
335+
const CowData prev_data;
336+
prev_data._ptr = _ptr;
337+
_ptr = nullptr;
309338

310-
_unref();
311-
_ptr = _data_ptr;
339+
const Error error = _alloc(alloc_size);
340+
if (error) {
341+
// On failure to allocate, just give up the old data and return.
342+
// We could recover our old pointer from prev_data, but by just dropping our data, we
343+
// consciously invite early failure for the case that the caller does not handle this
344+
// case gracefully.
345+
return error;
346+
}
312347

313-
rc = 1;
348+
// Copy over elements.
349+
const USize copied_element_count = MIN(prev_size, p_size);
350+
if (copied_element_count > 0) {
351+
if constexpr (std::is_trivially_copyable_v<T>) {
352+
memcpy((uint8_t *)_ptr, (uint8_t *)prev_data._ptr, copied_element_count * sizeof(T));
353+
} else {
354+
for (USize i = 0; i < copied_element_count; i++) {
355+
memnew_placement(&_ptr[i], T(prev_data._ptr[i]));
356+
}
357+
}
358+
}
314359
}
315-
return rc;
360+
361+
// Set our new size.
362+
*_get_size() = p_size;
363+
364+
return OK;
316365
}
317366

318367
template <typename T>
319368
template <bool p_ensure_zero>
320369
Error CowData<T>::resize(Size p_size) {
321370
ERR_FAIL_COND_V(p_size < 0, ERR_INVALID_PARAMETER);
322371

323-
Size current_size = size();
324-
325-
if (p_size == current_size) {
372+
const Size prev_size = size();
373+
if (p_size == prev_size) {
326374
return OK;
327375
}
328376

329-
if (p_size == 0) {
330-
// Wants to clean up.
331-
_unref(); // Resets _ptr to nullptr.
332-
return OK;
377+
const Error error = _fork_allocate(p_size);
378+
if (error) {
379+
return error;
333380
}
334381

335-
// possibly changing size, copy on write
336-
_copy_on_write();
337-
338-
USize current_alloc_size = _get_alloc_size(current_size);
339-
USize alloc_size;
340-
ERR_FAIL_COND_V(!_get_alloc_size_checked(p_size, &alloc_size), ERR_OUT_OF_MEMORY);
341-
342-
if (p_size > current_size) {
343-
if (alloc_size != current_alloc_size) {
344-
if (current_size == 0) {
345-
// alloc from scratch
346-
uint8_t *mem_new = (uint8_t *)Memory::alloc_static(alloc_size + DATA_OFFSET, false);
347-
ERR_FAIL_NULL_V(mem_new, ERR_OUT_OF_MEMORY);
348-
349-
SafeNumeric<USize> *_refc_ptr = _get_refcount_ptr(mem_new);
350-
USize *_size_ptr = _get_size_ptr(mem_new);
351-
T *_data_ptr = _get_data_ptr(mem_new);
352-
353-
new (_refc_ptr) SafeNumeric<USize>(1); //refcount
354-
*(_size_ptr) = 0; //size, currently none
355-
356-
_ptr = _data_ptr;
357-
358-
} else {
359-
const Error error = _realloc(alloc_size);
360-
if (error) {
361-
return error;
362-
}
363-
}
364-
}
365-
366-
// construct the newly created elements
367-
memnew_arr_placement<p_ensure_zero>(_ptr + current_size, p_size - current_size);
382+
if (p_size > prev_size) {
383+
memnew_arr_placement<p_ensure_zero>(_ptr + prev_size, p_size - prev_size);
384+
}
368385

369-
*_get_size() = p_size;
386+
return OK;
387+
}
370388

371-
} else if (p_size < current_size) {
372-
if constexpr (!std::is_trivially_destructible_v<T>) {
373-
// deinitialize no longer needed elements
374-
for (USize i = p_size; i < *_get_size(); i++) {
375-
T *t = &_ptr[i];
376-
t->~T();
377-
}
378-
}
389+
template <typename T>
390+
Error CowData<T>::_alloc(USize p_alloc_size) {
391+
uint8_t *mem_new = (uint8_t *)Memory::alloc_static(p_alloc_size + DATA_OFFSET, false);
392+
ERR_FAIL_NULL_V(mem_new, ERR_OUT_OF_MEMORY);
379393

380-
if (alloc_size != current_alloc_size) {
381-
const Error error = _realloc(alloc_size);
382-
if (error) {
383-
return error;
384-
}
385-
}
394+
_ptr = _get_data_ptr(mem_new);
386395

387-
*_get_size() = p_size;
388-
}
396+
// If we alloc, we're guaranteed to be the only reference.
397+
new (_get_refcount()) SafeNumeric<USize>(1);
389398

390399
return OK;
391400
}
392401

393402
template <typename T>
394-
Error CowData<T>::_realloc(Size p_alloc_size) {
403+
Error CowData<T>::_realloc(USize p_alloc_size) {
395404
uint8_t *mem_new = (uint8_t *)Memory::realloc_static(((uint8_t *)_ptr) - DATA_OFFSET, p_alloc_size + DATA_OFFSET, false);
396405
ERR_FAIL_NULL_V(mem_new, ERR_OUT_OF_MEMORY);
397406

398-
SafeNumeric<USize> *_refc_ptr = _get_refcount_ptr(mem_new);
399-
T *_data_ptr = _get_data_ptr(mem_new);
407+
_ptr = _get_data_ptr(mem_new);
400408

401409
// If we realloc, we're guaranteed to be the only reference.
402-
new (_refc_ptr) SafeNumeric<USize>(1);
403-
_ptr = _data_ptr;
410+
// So the reference was 1 and was copied to be 1 again.
411+
DEV_ASSERT(_get_refcount()->get() == 1);
404412

405413
return OK;
406414
}

0 commit comments

Comments
 (0)