mdbx: добавление rkl с итераторами.

RKL — сортированный набор txnid, использующий внутри комбинацию
непрерывного интервала и списка. Обеспечивает хранение id записей при
переработке, очистку и обновлении GC, включая возврат остатков
переработанных страниц.

Итератор для RKL — обеспечивает изоляцию внутреннего устройства rkl от
остального кода, чем существенно его упрощает. Фактически именно
использованием rkl с итераторами ликвидируется "ребус" исторически
образовавшийся в gc-update.

--

При переработке GC записи преимущественно выбираются последовательно, но
это не гарантируется. В LIFO-режиме переработка и добавление записей в
rkl происходит преимущественно в обратном порядке, но из-за завершения
читающих транзакций могут быть «скачки» в прямом направлении. В
FIFO-режиме записи GC перерабатываются в прямом порядке и при этом
линейно, но не обязательно строго последовательно, при этом
гарантируется что между добавляемыми в rkl идентификаторами в GC нет
записей, т.е. между первой (минимальный id) и последней (максимальный
id) в GC нет записей и весь интервал может быть использован для возврата
остатков страниц в GC.

Таким образом, комбинация линейного интервала и списка (отсортированного
в порядке возрастания элементов) является рациональным решением, близким
к теоретически оптимальному пределу.

Реализация rkl достаточно проста/прозрачная, если не считать неочевидную
«магию» обмена непрерывного интервала и образующихся в списке
последовательностей. Однако, именно этот автоматически выполняемый без
лишних операций обмен оправдывает все накладные расходы.
This commit is contained in:
Леонид Юрьев (Leonid Yuriev) 2025-01-30 18:43:30 +03:00
parent 9568209ee4
commit de968541cc
6 changed files with 533 additions and 0 deletions

View File

@ -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"

View File

@ -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' \

View File

@ -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"

View File

@ -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)

462
src/rkl.c Normal file
View File

@ -0,0 +1,462 @@
/// \copyright SPDX-License-Identifier: Apache-2.0
/// \author Леонид Юрьев aka Leonid Yuriev <leo@yuriev.ru> \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;
}

64
src/rkl.h Normal file
View File

@ -0,0 +1,64 @@
/// \copyright SPDX-License-Identifier: Apache-2.0
/// \author Леонид Юрьев aka Leonid Yuriev <leo@yuriev.ru> \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);