From de968541cc003aed14ba26c3dba01b4e0d8a9726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=BE=D0=BD=D0=B8=D0=B4=20=D0=AE=D1=80=D1=8C?= =?UTF-8?q?=D0=B5=D0=B2=20=28Leonid=20Yuriev=29?= Date: Thu, 30 Jan 2025 18:43:30 +0300 Subject: [PATCH] =?UTF-8?q?mdbx:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20rkl=20=D1=81=20=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RKL — сортированный набор txnid, использующий внутри комбинацию непрерывного интервала и списка. Обеспечивает хранение id записей при переработке, очистку и обновлении GC, включая возврат остатков переработанных страниц. Итератор для RKL — обеспечивает изоляцию внутреннего устройства rkl от остального кода, чем существенно его упрощает. Фактически именно использованием rkl с итераторами ликвидируется "ребус" исторически образовавшийся в gc-update. -- При переработке GC записи преимущественно выбираются последовательно, но это не гарантируется. В LIFO-режиме переработка и добавление записей в rkl происходит преимущественно в обратном порядке, но из-за завершения читающих транзакций могут быть «скачки» в прямом направлении. В FIFO-режиме записи GC перерабатываются в прямом порядке и при этом линейно, но не обязательно строго последовательно, при этом гарантируется что между добавляемыми в rkl идентификаторами в GC нет записей, т.е. между первой (минимальный id) и последней (максимальный id) в GC нет записей и весь интервал может быть использован для возврата остатков страниц в GC. Таким образом, комбинация линейного интервала и списка (отсортированного в порядке возрастания элементов) является рациональным решением, близким к теоретически оптимальному пределу. Реализация rkl достаточно проста/прозрачная, если не считать неочевидную «магию» обмена непрерывного интервала и образующихся в списке последовательностей. Однако, именно этот автоматически выполняемый без лишних операций обмен оправдывает все накладные расходы. --- CMakeLists.txt | 4 + GNUmakefile | 1 + src/alloy.c | 1 + src/internals.h | 1 + src/rkl.c | 462 ++++++++++++++++++++++++++++++++++++++++++++++++ src/rkl.h | 64 +++++++ 6 files changed, 533 insertions(+) create mode 100644 src/rkl.c create mode 100644 src/rkl.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ea34172..1b7e9835 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,8 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/preface.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/proto.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/refund.c" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/rkl.c" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/rkl.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/sort.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/spill.c" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/spill.h" @@ -832,6 +834,8 @@ else() "${MDBX_SOURCE_DIR}/preface.h" "${MDBX_SOURCE_DIR}/proto.h" "${MDBX_SOURCE_DIR}/refund.c" + "${MDBX_SOURCE_DIR}/rkl.c" + "${MDBX_SOURCE_DIR}/rkl.h" "${MDBX_SOURCE_DIR}/sort.h" "${MDBX_SOURCE_DIR}/spill.c" "${MDBX_SOURCE_DIR}/spill.h" diff --git a/GNUmakefile b/GNUmakefile index e8da9a35..c541f5e8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -710,6 +710,7 @@ $(DIST_DIR)/@tmp-internals.inc: $(DIST_DIR)/@tmp-essentials.inc src/version.c $( -e '/#include "essentials.h"/d' \ -e '/#include "atomics-ops.h"/r src/atomics-ops.h' \ -e '/#include "proto.h"/r src/proto.h' \ + -e '/#include "rkl.h"/r src/rkl.h' \ -e '/#include "txl.h"/r src/txl.h' \ -e '/#include "unaligned.h"/r src/unaligned.h' \ -e '/#include "cogs.h"/r src/cogs.h' \ diff --git a/src/alloy.c b/src/alloy.c index 4b5cf057..9c3cab5e 100644 --- a/src/alloy.c +++ b/src/alloy.c @@ -41,6 +41,7 @@ #include "page-ops.c" #include "pnl.c" #include "refund.c" +#include "rkl.c" #include "spill.c" #include "table.c" #include "tls.c" diff --git a/src/internals.h b/src/internals.h index 98f89ee1..0a142bbc 100644 --- a/src/internals.h +++ b/src/internals.h @@ -46,6 +46,7 @@ typedef struct bind_reader_slot_result { #include "atomics-ops.h" #include "proto.h" +#include "rkl.h" #include "txl.h" #include "unaligned.h" #if defined(_WIN32) || defined(_WIN64) diff --git a/src/rkl.c b/src/rkl.c new file mode 100644 index 00000000..5ffc0991 --- /dev/null +++ b/src/rkl.c @@ -0,0 +1,462 @@ +/// \copyright SPDX-License-Identifier: Apache-2.0 +/// \author Леонид Юрьев aka Leonid Yuriev \date 2025 + +#include "rkl.h" +#include "internals.h" + +static inline size_t rkl_size2bytes(const size_t size) { + assert(size > 0 && size <= txl_max * 2); + size_t bytes = ceil_powerof2(MDBX_ASSUME_MALLOC_OVERHEAD + sizeof(txnid_t) * size, txl_granulate * sizeof(txnid_t)) - + MDBX_ASSUME_MALLOC_OVERHEAD; + return bytes; +} + +static inline size_t rkl_bytes2size(const size_t bytes) { + size_t size = bytes / sizeof(txnid_t); + assert(size > 0 && size <= txl_max * 2); + return size; +} + +void rkl_init(rkl_t *rkl) { + rkl->list_limit = ARRAY_LENGTH(rkl->inplace); + rkl->list = rkl->inplace; + rkl_clear(rkl); +} + +void rkl_clear(rkl_t *rkl) { + rkl->solid_begin = UINT64_MAX; + rkl->solid_end = 0; + rkl->list_length = 0; +} + +void rkl_destroy(rkl_t *rkl) { + void *ptr = rkl->list; + rkl->list = nullptr; + if (ptr != rkl->inplace) + osal_free(rkl->list); +} + +static inline bool solid_empty(const rkl_t *rkl) { return !(rkl->solid_begin < rkl->solid_end); } + +#define RKL_ORDERED(first, last) ((first) < (last)) + +SEARCH_IMPL(rkl_bsearch, txnid_t, txnid_t, RKL_ORDERED) + +void rkl_destructive_move(rkl_t *src, rkl_t *dst) { + assert(rkl_check(src)); + dst->solid_begin = src->solid_begin; + dst->solid_end = src->solid_end; + dst->list_length = src->list_length; + if (dst->list != dst->inplace) + osal_free(dst->list); + if (src->list_length > ARRAY_LENGTH(dst->inplace)) { + assert(src->list != src->inplace); + dst->list = src->list; + dst->list_limit = src->list_limit; + } else { + dst->list = dst->inplace; + dst->list_limit = ARRAY_LENGTH(src->inplace); + memcpy(dst->inplace, src->list, sizeof(dst->inplace)); + if (src->list != src->inplace) + osal_free(src->list); + } + rkl_init(src); +} + +static int rkl_resize(rkl_t *rkl, size_t size) { + assert(size > rkl->list_length); + assert(rkl_check(rkl)); + STATIC_ASSERT(txl_max < INT_MAX / sizeof(txnid_t)); + if (unlikely(size > txl_max)) { + ERROR("rkl too long (%zu >= %zu)", size, (size_t)txl_max); + return MDBX_TXN_FULL; + } + if (unlikely(size < rkl->list_length)) { + ERROR("unable shrink rkl to %zu since length is %u", size, rkl->list_length); + return MDBX_TXN_FULL; + } + if (unlikely(size <= ARRAY_LENGTH(rkl->inplace))) { + if (rkl->list != rkl->inplace) { + memcpy(rkl->inplace, rkl->list, sizeof(txnid_t) * rkl->list_length); + rkl->list_limit = ARRAY_LENGTH(rkl->inplace); + osal_free(rkl->list); + rkl->list = rkl->inplace; + } else { + assert(rkl->list_limit == ARRAY_LENGTH(rkl->inplace)); + } + } else if (size != rkl->list_limit) { + size_t bytes = rkl_size2bytes(size); + void *const ptr = (rkl->list == rkl->inplace) ? osal_malloc(bytes) : osal_realloc(rkl->list, bytes); + if (unlikely(!ptr)) + return MDBX_ENOMEM; +#ifdef osal_malloc_usable_size + bytes = osal_malloc_usable_size(ptr); +#endif /* osal_malloc_usable_size */ + rkl->list_limit = rkl_bytes2size(bytes); + if (rkl->list == rkl->inplace) + memcpy(ptr, rkl->inplace, sizeof(rkl->inplace)); + rkl->list = ptr; + } + return MDBX_SUCCESS; +} + +int rkl_copy(const rkl_t *src, rkl_t *dst) { + assert(rkl_check(src)); + rkl_init(dst); + if (!rkl_empty(src)) { + if (dst->list_limit < src->list_length) { + int err = rkl_resize(dst, src->list_limit); + if (unlikely(err != MDBX_SUCCESS)) + return err; + } + memcpy(dst->list, src->list, sizeof(txnid_t) * src->list_length); + dst->list_length = src->list_length; + dst->solid_begin = src->solid_begin; + dst->solid_end = src->solid_end; + } + return MDBX_SUCCESS; +} + +size_t rkl_len(const rkl_t *rkl) { return rkl_empty(rkl) ? 0 : rkl->solid_end - rkl->solid_begin + rkl->list_length; } + +__hot bool rkl_contain(const rkl_t *rkl, txnid_t id) { + assert(rkl_check(rkl)); + if (!rkl_empty(rkl)) { + if (id >= rkl->solid_begin && id < rkl->solid_end) + return true; + + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, id); + const txnid_t *const end = rkl->list + rkl->list_length; + assert(it >= rkl->list && it <= end); + if (it != rkl->list) + assert(RKL_ORDERED(it[-1], id)); + if (it != end) { + assert(!RKL_ORDERED(it[0], id)); + return *it == id; + } + } + return false; +} + +static inline txnid_t list_remove_first(rkl_t *rkl) { + assert(rkl->list_length > 0); + const txnid_t first = rkl->list[0]; + if (--rkl->list_length) { + /* TODO: Можно подумать о том, чтобы для избавления от memove() добавить headroom или вместо длины и + * указателя на список использовать три поля: list_begin, list_end и list_buffer. */ + size_t i = 0; + do + rkl->list[i] = rkl->list[i + 1]; + while (++i <= rkl->list_length); + } + return first; +} + +static inline txnid_t after_cut(rkl_t *rkl, const txnid_t out) { + if (rkl->list_length == 0 && rkl->solid_begin == rkl->solid_end) { + rkl->solid_end = 0; + rkl->solid_begin = UINT64_MAX; + } + return out; +} + +static int extend_solid(rkl_t *rkl, txnid_t solid_begin, txnid_t solid_end, const txnid_t id) { + if (rkl->list_length) { + const txnid_t *i = rkl_bsearch(rkl->list, rkl->list_length, id); + const txnid_t *const end = rkl->list + rkl->list_length; + /* если начало или конец списка примыкает к непрерывному интервалу, + * то переносим эти элементы из списка в непрерывный интервал */ + txnid_t *f = (txnid_t *)i; + while (f > rkl->list && f[-1] >= solid_begin - 1) { + f -= 1; + solid_begin -= 1; + if (unlikely(*f != solid_begin)) + return MDBX_RESULT_TRUE; + } + txnid_t *t = (txnid_t *)i; + while (t < end && *t <= solid_end) { + if (unlikely(*t != solid_end)) + return MDBX_RESULT_TRUE; + solid_end += 1; + t += 1; + } + if (f < t) { + rkl->list_length -= t - f; + while (t < end) + *f++ = *t++; + } + } + + rkl->solid_begin = solid_begin; + rkl->solid_end = solid_end; + assert(rkl_check(rkl)); + return MDBX_SUCCESS; +} + +int rkl_push(rkl_t *rkl, const txnid_t id, const bool known_continuous) { + assert(id >= MIN_TXNID && id < INVALID_TXNID); + assert(rkl_check(rkl)); + + if (rkl->solid_begin >= rkl->solid_end) { + /* непрерывный интервал пуст */ + return extend_solid(rkl, id, id + 1, id); + } else if (id < rkl->solid_begin) { + if (known_continuous || id + 1 == rkl->solid_begin) + /* id примыкает к solid_begin */ + return extend_solid(rkl, id, rkl->solid_end, id); + } else if (id >= rkl->solid_end) { + if (known_continuous || id == rkl->solid_end) + /* id примыкает к solid_end */ + return extend_solid(rkl, rkl->solid_begin, id + 1, id); + } else { + /* id входит в интервал между solid_begin и solid_end, т.е. подан дубликат */ + return MDBX_RESULT_TRUE; + } + + if (rkl->list_length == 1 && rkl->solid_end == rkl->solid_begin + 1 && + (rkl->list[0] == id + 1 || rkl->list[0] == id - 1)) { + /* В списке один элемент и добавляемый id примыкает к нему, при этом в непрерывном интервале тоже один элемент. + * Лучше поменять элементы списка и непрерывного интервала. */ + const txnid_t couple = (rkl->list[0] == id - 1) ? id - 1 : id; + rkl->list[0] = rkl->solid_begin; + rkl->solid_begin = couple; + rkl->solid_end = couple + 2; + assert(rkl_check(rkl)); + return MDBX_SUCCESS; + } + + if (unlikely(rkl->list_length == rkl->list_limit)) { + /* удваиваем размер буфера если закончилось место */ + size_t x2 = (rkl->list_limit + 1) << 1; + x2 = (x2 > 62) ? x2 : 62; + x2 = (x2 < txl_max) ? x2 : txl_max; + x2 = (x2 > rkl->list_length) ? x2 : rkl->list_length + 42; + int err = rkl_resize(rkl, x2); + if (unlikely(err != MDBX_SUCCESS)) + return err; + assert(rkl->list_limit > rkl->list_length); + } + + size_t i = rkl->list_length; + /* ищем место для вставки двигаясь от конца к началу списка, сразу переставляя/раздвигая элементы */ + while (i > 0) { + if (RKL_ORDERED(id, rkl->list[i - 1])) { + rkl->list[i] = rkl->list[i - 1]; + i -= 1; + continue; + } + if (unlikely(id == rkl->list[i - 1])) { + while (++i < rkl->list_length) + rkl->list[i - 1] = rkl->list[i]; + return MDBX_RESULT_TRUE; + } + break; + } + + rkl->list[i] = id; + rkl->list_length++; + assert(rkl_check(rkl)); + + /* После добавления id в списке могла образоваться длинная последовательность, + * которую (возможно) стоит обменять с непрерывным интервалом. */ + if (rkl->list_length > (MDBX_DEBUG ? 2 : 16) && + ((i > 0 && rkl->list[i - 1] == id - 1) || (i + 1 < rkl->list_length && rkl->list[i + 1] == id + 1))) { + txnid_t new_solid_begin = id; + size_t from = i; + while (from > 0 && rkl->list[from - 1] == new_solid_begin - 1) { + from -= 1; + new_solid_begin -= 1; + } + txnid_t new_solid_end = id + 1; + size_t to = i + 1; + while (to < rkl->list_length && rkl->list[to] == new_solid_end) { + to += 1; + new_solid_end += 1; + } + + const size_t new_solid_len = to - from; + if (new_solid_len > 3) { + const size_t old_solid_len = rkl->solid_end - rkl->solid_begin; + if (new_solid_len > old_solid_len) { + /* Новая непрерывная последовательность длиннее текущей. + * Считаем обмен выгодным, если он дешевле пути развития событий с добавлением следующего элемента в список. */ + const size_t old_solid_pos = rkl_bsearch(rkl->list, rkl->list_length, rkl->solid_begin) - rkl->list; + const size_t swap_cost = + /* количество элементов списка после изымаемой из списка последовательности, + * которые нужно переместить */ + rkl->list_length - to + + /* количество элементов списка после позиции добавляемой в список последовательности, + * которые нужно переместить */ + ((from > old_solid_pos) ? from - old_solid_pos : 0) + /* количество элементов списка добавляемой последовательности, которые нужно добавить */ + + old_solid_len; + /* количество элементов списка, которые нужно переместить для вставки еще-одного/следующего элемента */ + const size_t new_insert_cost = rkl->list_length - i; + if (unlikely(swap_cost < new_insert_cost) || MDBX_DEBUG) { + /* Изымаемая последовательность длиннее добавляемой, поэтому: + * - список станет короче; + * - перемещать хвост нужно всегда к началу; + * - если начальные элементы потребуется раздвигать, + * то места хватит и остающиеся элементы в конце не будут перезаписаны. */ + size_t moved = 0; + if (from > old_solid_pos) { + /* добавляемая последовательность ближе к началу, нужно раздвинуть элементы в голове для вставки. */ + moved = from - old_solid_pos; + do { + from -= 1; + rkl->list[from + old_solid_len] = rkl->list[from]; + } while (from > old_solid_pos); + } else if (from + new_solid_len < old_solid_pos) { + /* добавляемая последовательность дальше от начала, + * перемещаем часть элементов из хвоста после изымаемой последовательности */ + do + rkl->list[from++] = rkl->list[to++]; + while (from < old_solid_pos - new_solid_len); + } + + /* вставляем последовательноть */ + i = 0; + do + rkl->list[from++] = rkl->solid_begin + i++; + while (i != old_solid_len); + + /* сдвигаем оставшийся хвост */ + while (to < rkl->list_length) + rkl->list[moved + from++] = rkl->list[to++]; + + rkl->list_length = rkl->list_length - new_solid_len + old_solid_len; + rkl->solid_begin = new_solid_begin; + rkl->solid_end = new_solid_end; + assert(rkl_check(rkl)); + } + } + } + } + return MDBX_SUCCESS; +} + +txnid_t rkl_pop(rkl_t *rkl, const bool highest_not_lowest) { + assert(!rkl_empty(rkl)); + assert(rkl_check(rkl)); + + if (rkl->list_length) { + assert(rkl->solid_begin <= rkl->solid_end); + if (highest_not_lowest && (solid_empty(rkl) || rkl->solid_end < rkl->list[rkl->list_length - 1])) + return after_cut(rkl, rkl->list[rkl->list_length -= 1]); + if (!highest_not_lowest && (solid_empty(rkl) || rkl->solid_begin > rkl->list[0])) + return after_cut(rkl, list_remove_first(rkl)); + } + + if (!solid_empty(rkl)) + return after_cut(rkl, highest_not_lowest ? --rkl->solid_end : rkl->solid_begin++); + + assert(rkl_empty(rkl)); + return 0; +} + +txnid_t rkl_lowest(const rkl_t *rkl) { + if (rkl->list_length) + return (solid_empty(rkl) || rkl->list[0] < rkl->solid_begin) ? rkl->list[0] : rkl->solid_begin; + + return !solid_empty(rkl) ? rkl->solid_begin : INVALID_TXNID; +} + +txnid_t rkl_highest(const rkl_t *rkl) { + if (rkl->list_length) + return (solid_empty(rkl) || rkl->list[rkl->list_length - 1] >= rkl->solid_end) ? rkl->list[rkl->list_length - 1] + : rkl->solid_end - 1; + + return !solid_empty(rkl) ? rkl->solid_end - 1 : 0; +} + +void rkl_iterator_init(rkl_iterator_t *iter, const rkl_t *rkl, const bool reverse) { + iter->pos = reverse ? rkl_len(rkl) : 0; + iter->solid_offset = 0; + if (!solid_empty(rkl) && rkl->list_length && rkl->solid_begin > rkl->list[0] && + rkl->solid_end < rkl->list[rkl->list_length - 1]) { + /* непрерывный интервал "плавает" внутри списка, т.е. находится между какими-то соседними значениями */ + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, rkl->solid_begin); + const txnid_t *const end = rkl->list + rkl->list_length; + assert(it > rkl->list && it < end && *it > rkl->solid_begin); + iter->solid_offset = it - rkl->list; + } +} + +txnid_t rkl_iterator_turn(rkl_iterator_t *iter, const rkl_t *rkl, const bool reverse) { + size_t pos = iter->pos - !!reverse; + if (pos >= rkl_len(rkl)) + return 0; + + iter->pos = pos + !reverse; + assert(iter->pos <= rkl_len(rkl)); + + const size_t solid_len = rkl->solid_end - rkl->solid_begin; + if (rkl->list_length) { + if (solid_len) { + if (rkl->solid_end < rkl->list[0]) { + if (pos < solid_len) + return rkl->solid_begin + pos; + pos -= solid_len; + assert(pos < rkl->list_length); + return rkl->list[pos]; + } else if (rkl->solid_begin > rkl->list[rkl->list_length - 1]) { + if (pos < rkl->list_length) + return rkl->list[pos]; + pos -= rkl->list_length; + assert(pos < solid_len); + return rkl->solid_begin + pos; + } else { + assert(rkl->solid_begin > rkl->list[0] && rkl->solid_end < rkl->list[rkl->list_length - 1]); + assert(iter->solid_offset > 0 && iter->solid_offset < rkl->list_length); + /* непрерывный интервал "плавает" внутри списка, т.е. находится между какими-то соседними значениями */ + if (pos < iter->solid_offset) + return rkl->list[pos]; + else if (pos < iter->solid_offset + solid_len) + return rkl->solid_begin + pos - iter->solid_offset; + else + return rkl->list[pos - solid_len]; + } + } + assert(pos < rkl->list_length); + return rkl->list[pos]; + } + assert(pos < solid_len); + return rkl->solid_begin + pos; +} + +size_t rkl_iterator_left(rkl_iterator_t *iter, const rkl_t *rkl, const bool reverse) { + assert(iter->pos <= rkl_len(rkl)); + return reverse ? iter->pos : rkl_len(rkl) - iter->pos; +} + +bool rkl_check(const rkl_t *rkl) { + if (!rkl) + return false; + if (rkl->list == rkl->inplace && unlikely(rkl->list_limit != ARRAY_LENGTH(rkl->inplace))) + return false; + if (unlikely(rkl->list_limit < ARRAY_LENGTH(rkl->inplace))) + return false; + + if (rkl_empty(rkl)) + return rkl->list_length == 0 && solid_empty(rkl); + + if (rkl->list_length) { + for (size_t i = 1; i < rkl->list_length; ++i) + if (unlikely(!RKL_ORDERED(rkl->list[i - 1], rkl->list[i]))) + return false; + if (!solid_empty(rkl) && rkl->solid_begin - 1 <= rkl->list[rkl->list_length - 1] && + rkl->solid_end >= rkl->list[0]) { + /* непрерывный интервал "плавает" внутри списка, т.е. находится между какими-то соседними значениями */ + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, rkl->solid_begin); + const txnid_t *const end = rkl->list + rkl->list_length; + if (it < rkl->list || it > end) + return false; + if (it > rkl->list && it[-1] >= rkl->solid_begin) + return false; + if (it < end && it[0] <= rkl->solid_end) + return false; + } + } + + return true; +} diff --git a/src/rkl.h b/src/rkl.h new file mode 100644 index 00000000..27f843a0 --- /dev/null +++ b/src/rkl.h @@ -0,0 +1,64 @@ +/// \copyright SPDX-License-Identifier: Apache-2.0 +/// \author Леонид Юрьев aka Leonid Yuriev \date 2025 + +#pragma once + +#include "essentials.h" + +/* Сортированный набор txnid, использующий внутри комбинацию непрерывного интервала и списка. + * Обеспечивает хранение id записей при переработке, очистку и обновлении GC, включая возврат остатков переработанных + * страниц. + * + * При переработке GC записи преимущественно выбираются последовательно, но это не гарантируется. В LIFO-режиме + * переработка и добавление записей в rkl происходит преимущественно в обратном порядке, но из-за завершения читающих + * транзакций могут быть «скачки» в прямом направлении. В FIFO-режиме записи GC перерабатываются в прямом порядке и при + * этом линейно, но не обязательно строго последовательно, при этом гарантируется что между добавляемыми в rkl + * идентификаторами в GC нет записей, т.е. между первой (минимальный id) и последней (максимальный id) в GC нет записей + * и весь интервал может быть использован для возврата остатков страниц в GC. + * + * Таким образом, комбинация линейного интервала и списка (отсортированного в порядке возрастания элементов) является + * рациональным решением, близким к теоретически оптимальному пределу. + * + * Реализация rkl достаточно проста/прозрачная, если не считать неочевидную «магию» обмена непрерывного интервала и + * образующихся в списке последовательностей. Однако, именно этот автоматически выполняемый без лишних операций обмен + * оправдывает все накладные расходы. */ +typedef struct MDBX_rkl { + txnid_t solid_begin, solid_end; /* начало и конец непрерывной последовательности solid_begin ... solid_end-1. */ + unsigned list_length; /* текущая длина списка. */ + unsigned list_limit; /* размер буфера выделенного под список, равен ARRAY_LENGTH(inplace) когда list == inplace. */ + txnid_t *list; /* список отдельных элементов в порядке возрастания (наименьший в начале). */ + txnid_t inplace[4 + 8]; /* статический массив для коротких списков, чтобы избавиться от выделения/освобождения памяти + * в большинстве случаев. */ +} rkl_t; + +MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_init(rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_clear(rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_destroy(rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_destructive_move(rkl_t *dst, rkl_t *src); +MDBX_MAYBE_UNUSED MDBX_INTERNAL int __must_check_result rkl_copy(const rkl_t *src, rkl_t *dst); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION static inline bool rkl_empty(const rkl_t *rkl) { + return rkl->solid_begin > rkl->solid_end; +} +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL bool rkl_check(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL size_t rkl_len(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL bool rkl_contain(const rkl_t *rkl, txnid_t id); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL txnid_t rkl_lowest(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL txnid_t rkl_highest(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL int __must_check_result rkl_push(rkl_t *rkl, const txnid_t id, + const bool known_continuous); +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result txnid_t rkl_pop(rkl_t *rkl, const bool highest_not_lowest); + +/* Итератор для rkl. + * Обеспечивает изоляцию внутреннего устройства rkl от остального кода, чем существенно его упрощает. + * Фактически именно использованием rkl с итераторами ликвидируется "ребус" исторически образовавшийся в gc-update. */ +typedef struct MDBX_rkl_iter { + unsigned pos; + unsigned solid_offset; +} rkl_iterator_t; + +MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_iterator_init(rkl_iterator_t *iter, const rkl_t *rkl, const bool reverse); +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result txnid_t rkl_iterator_turn(rkl_iterator_t *iter, const rkl_t *rkl, + const bool reverse); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL size_t rkl_iterator_left(rkl_iterator_t *iter, + const rkl_t *rkl, + const bool reverse);