libmdbx/src/gc-get.c

1461 lines
59 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// \copyright SPDX-License-Identifier: Apache-2.0
/// \author Леонид Юрьев aka Leonid Yuriev <leo@yuriev.ru> \date 2015-2024
#include "internals.h"
#if MDBX_ENABLE_MINCORE
/*------------------------------------------------------------------------------
* Проверка размещения/расположения отображенных страниц БД в ОЗУ (mem-in-core),
* с кешированием этой информации. */
static inline bool bit_tas(uint64_t *field, char bit) {
const uint64_t m = UINT64_C(1) << bit;
const bool r = (*field & m) != 0;
*field |= m;
return r;
}
static bool mincore_fetch(MDBX_env *const env, const size_t unit_begin) {
lck_t *const lck = env->lck;
for (size_t i = 1; i < ARRAY_LENGTH(lck->mincore_cache.begin); ++i) {
const ptrdiff_t dist = unit_begin - lck->mincore_cache.begin[i];
if (likely(dist >= 0 && dist < 64)) {
const pgno_t tmp_begin = lck->mincore_cache.begin[i];
const uint64_t tmp_mask = lck->mincore_cache.mask[i];
do {
lck->mincore_cache.begin[i] = lck->mincore_cache.begin[i - 1];
lck->mincore_cache.mask[i] = lck->mincore_cache.mask[i - 1];
} while (--i);
lck->mincore_cache.begin[0] = tmp_begin;
lck->mincore_cache.mask[0] = tmp_mask;
return bit_tas(lck->mincore_cache.mask, (char)dist);
}
}
size_t pages = 64;
unsigned unit_log = globals.sys_pagesize_ln2;
unsigned shift = 0;
if (env->ps > globals.sys_pagesize) {
unit_log = env->ps2ln;
shift = env->ps2ln - globals.sys_pagesize_ln2;
pages <<= shift;
}
const size_t offset = unit_begin << unit_log;
size_t length = pages << globals.sys_pagesize_ln2;
if (offset + length > env->dxb_mmap.current) {
length = env->dxb_mmap.current - offset;
pages = length >> globals.sys_pagesize_ln2;
}
#if MDBX_ENABLE_PGOP_STAT
env->lck->pgops.mincore.weak += 1;
#endif /* MDBX_ENABLE_PGOP_STAT */
uint8_t *const vector = alloca(pages);
if (unlikely(mincore(ptr_disp(env->dxb_mmap.base, offset), length,
(void *)vector))) {
NOTICE("mincore(+%zu, %zu), err %d", offset, length, errno);
return false;
}
for (size_t i = 1; i < ARRAY_LENGTH(lck->mincore_cache.begin); ++i) {
lck->mincore_cache.begin[i] = lck->mincore_cache.begin[i - 1];
lck->mincore_cache.mask[i] = lck->mincore_cache.mask[i - 1];
}
lck->mincore_cache.begin[0] = unit_begin;
uint64_t mask = 0;
#ifdef MINCORE_INCORE
STATIC_ASSERT(MINCORE_INCORE == 1);
#endif
for (size_t i = 0; i < pages; ++i) {
uint64_t bit = (vector[i] & 1) == 0;
bit <<= i >> shift;
mask |= bit;
}
lck->mincore_cache.mask[0] = ~mask;
return bit_tas(lck->mincore_cache.mask, 0);
}
#endif /* MDBX_ENABLE_MINCORE */
MDBX_MAYBE_UNUSED static inline bool mincore_probe(MDBX_env *const env,
const pgno_t pgno) {
#if MDBX_ENABLE_MINCORE
const size_t offset_aligned =
floor_powerof2(pgno2bytes(env, pgno), globals.sys_pagesize);
const unsigned unit_log2 = (env->ps2ln > globals.sys_pagesize_ln2)
? env->ps2ln
: globals.sys_pagesize_ln2;
const size_t unit_begin = offset_aligned >> unit_log2;
eASSERT(env, (unit_begin << unit_log2) == offset_aligned);
const ptrdiff_t dist = unit_begin - env->lck->mincore_cache.begin[0];
if (likely(dist >= 0 && dist < 64))
return bit_tas(env->lck->mincore_cache.mask, (char)dist);
return mincore_fetch(env, unit_begin);
#else
(void)env;
(void)pgno;
return false;
#endif /* MDBX_ENABLE_MINCORE */
}
/*----------------------------------------------------------------------------*/
MDBX_MAYBE_UNUSED __hot static pgno_t *
scan4seq_fallback(pgno_t *range, const size_t len, const size_t seq) {
assert(seq > 0 && len > seq);
#if MDBX_PNL_ASCENDING
assert(range[-1] == len);
const pgno_t *const detent = range + len - seq;
const ptrdiff_t offset = (ptrdiff_t)seq;
const pgno_t target = (pgno_t)offset;
if (likely(len > seq + 3)) {
do {
const pgno_t diff0 = range[offset + 0] - range[0];
const pgno_t diff1 = range[offset + 1] - range[1];
const pgno_t diff2 = range[offset + 2] - range[2];
const pgno_t diff3 = range[offset + 3] - range[3];
if (diff0 == target)
return range + 0;
if (diff1 == target)
return range + 1;
if (diff2 == target)
return range + 2;
if (diff3 == target)
return range + 3;
range += 4;
} while (range + 3 < detent);
if (range == detent)
return nullptr;
}
do
if (range[offset] - *range == target)
return range;
while (++range < detent);
#else
assert(range[-(ptrdiff_t)len] == len);
const pgno_t *const detent = range - len + seq;
const ptrdiff_t offset = -(ptrdiff_t)seq;
const pgno_t target = (pgno_t)offset;
if (likely(len > seq + 3)) {
do {
const pgno_t diff0 = range[-0] - range[offset - 0];
const pgno_t diff1 = range[-1] - range[offset - 1];
const pgno_t diff2 = range[-2] - range[offset - 2];
const pgno_t diff3 = range[-3] - range[offset - 3];
/* Смысл вычислений до ветвлений в том, чтобы позволить компилятору
* загружать и вычислять все значения параллельно. */
if (diff0 == target)
return range - 0;
if (diff1 == target)
return range - 1;
if (diff2 == target)
return range - 2;
if (diff3 == target)
return range - 3;
range -= 4;
} while (range > detent + 3);
if (range == detent)
return nullptr;
}
do
if (*range - range[offset] == target)
return range;
while (--range > detent);
#endif /* pnl_t sort-order */
return nullptr;
}
MDBX_MAYBE_UNUSED static const pgno_t *scan4range_checker(const pnl_t pnl,
const size_t seq) {
size_t begin = MDBX_PNL_ASCENDING ? 1 : MDBX_PNL_GETSIZE(pnl);
#if MDBX_PNL_ASCENDING
while (seq <= MDBX_PNL_GETSIZE(pnl) - begin) {
if (pnl[begin + seq] - pnl[begin] == seq)
return pnl + begin;
++begin;
}
#else
while (begin > seq) {
if (pnl[begin - seq] - pnl[begin] == seq)
return pnl + begin;
--begin;
}
#endif /* pnl_t sort-order */
return nullptr;
}
#if defined(_MSC_VER) && !defined(__builtin_clz) && \
!__has_builtin(__builtin_clz)
MDBX_MAYBE_UNUSED static __always_inline size_t __builtin_clz(uint32_t value) {
unsigned long index;
_BitScanReverse(&index, value);
return 31 - index;
}
#endif /* _MSC_VER */
#if defined(_MSC_VER) && !defined(__builtin_clzl) && \
!__has_builtin(__builtin_clzl)
MDBX_MAYBE_UNUSED static __always_inline size_t __builtin_clzl(size_t value) {
unsigned long index;
#ifdef _WIN64
assert(sizeof(value) == 8);
_BitScanReverse64(&index, value);
return 63 - index;
#else
assert(sizeof(value) == 4);
_BitScanReverse(&index, value);
return 31 - index;
#endif
}
#endif /* _MSC_VER */
#if !MDBX_PNL_ASCENDING
#if !defined(MDBX_ATTRIBUTE_TARGET) && \
(__has_attribute(__target__) || __GNUC_PREREQ(5, 0))
#define MDBX_ATTRIBUTE_TARGET(target) __attribute__((__target__(target)))
#endif /* MDBX_ATTRIBUTE_TARGET */
#ifndef MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND
/* Workaround for GCC's bug with `-m32 -march=i686 -Ofast`
* gcc/i686-buildroot-linux-gnu/12.2.0/include/xmmintrin.h:814:1:
* error: inlining failed in call to 'always_inline' '_mm_movemask_ps':
* target specific option mismatch */
#if !defined(__FAST_MATH__) || !__FAST_MATH__ || !defined(__GNUC__) || \
defined(__e2k__) || defined(__clang__) || defined(__amd64__) || \
defined(__SSE2__)
#define MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND 0
#else
#define MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND 1
#endif
#endif /* MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND */
#if defined(__SSE2__) && defined(__SSE__)
#define MDBX_ATTRIBUTE_TARGET_SSE2 /* nope */
#elif (defined(_M_IX86_FP) && _M_IX86_FP >= 2) || defined(__amd64__)
#define __SSE2__
#define MDBX_ATTRIBUTE_TARGET_SSE2 /* nope */
#elif defined(MDBX_ATTRIBUTE_TARGET) && defined(__ia32__) && \
!MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND
#define MDBX_ATTRIBUTE_TARGET_SSE2 MDBX_ATTRIBUTE_TARGET("sse,sse2")
#endif /* __SSE2__ */
#if defined(__AVX2__)
#define MDBX_ATTRIBUTE_TARGET_AVX2 /* nope */
#elif defined(MDBX_ATTRIBUTE_TARGET) && defined(__ia32__) && \
!MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND
#define MDBX_ATTRIBUTE_TARGET_AVX2 MDBX_ATTRIBUTE_TARGET("sse,sse2,avx,avx2")
#endif /* __AVX2__ */
#if defined(MDBX_ATTRIBUTE_TARGET_AVX2)
#if defined(__AVX512BW__)
#define MDBX_ATTRIBUTE_TARGET_AVX512BW /* nope */
#elif defined(MDBX_ATTRIBUTE_TARGET) && defined(__ia32__) && \
!MDBX_GCC_FASTMATH_i686_SIMD_WORKAROUND && \
(__GNUC_PREREQ(6, 0) || __CLANG_PREREQ(5, 0))
#define MDBX_ATTRIBUTE_TARGET_AVX512BW \
MDBX_ATTRIBUTE_TARGET("sse,sse2,avx,avx2,avx512bw")
#endif /* __AVX512BW__ */
#endif /* MDBX_ATTRIBUTE_TARGET_AVX2 for MDBX_ATTRIBUTE_TARGET_AVX512BW */
#ifdef MDBX_ATTRIBUTE_TARGET_SSE2
MDBX_ATTRIBUTE_TARGET_SSE2 static __always_inline unsigned
diffcmp2mask_sse2(const pgno_t *const ptr, const ptrdiff_t offset,
const __m128i pattern) {
const __m128i f = _mm_loadu_si128((const __m128i *)ptr);
const __m128i l = _mm_loadu_si128((const __m128i *)(ptr + offset));
const __m128i cmp = _mm_cmpeq_epi32(_mm_sub_epi32(f, l), pattern);
return _mm_movemask_ps(*(const __m128 *)&cmp);
}
MDBX_MAYBE_UNUSED __hot MDBX_ATTRIBUTE_TARGET_SSE2 static pgno_t *
scan4seq_sse2(pgno_t *range, const size_t len, const size_t seq) {
assert(seq > 0 && len > seq);
#if MDBX_PNL_ASCENDING
#error "FIXME: Not implemented"
#endif /* MDBX_PNL_ASCENDING */
assert(range[-(ptrdiff_t)len] == len);
pgno_t *const detent = range - len + seq;
const ptrdiff_t offset = -(ptrdiff_t)seq;
const pgno_t target = (pgno_t)offset;
const __m128i pattern = _mm_set1_epi32(target);
uint8_t mask;
if (likely(len > seq + 3)) {
do {
mask = (uint8_t)diffcmp2mask_sse2(range - 3, offset, pattern);
if (mask) {
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
found:
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
return range + 28 - __builtin_clz(mask);
}
range -= 4;
} while (range > detent + 3);
if (range == detent)
return nullptr;
}
/* Далее происходит чтение от 4 до 12 лишних байт, которые могут быть не
* только за пределами региона выделенного под PNL, но и пересекать границу
* страницы памяти. Что может приводить как к ошибкам ASAN, так и к падению.
* Поэтому проверяем смещение на странице, а с ASAN всегда страхуемся. */
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
const unsigned on_page_safe_mask = 0xff0 /* enough for '-15' bytes offset */;
if (likely(on_page_safe_mask & (uintptr_t)(range + offset)) &&
!RUNNING_ON_VALGRIND) {
const unsigned extra = (unsigned)(detent + 4 - range);
assert(extra > 0 && extra < 4);
mask = 0xF << extra;
mask &= diffcmp2mask_sse2(range - 3, offset, pattern);
if (mask)
goto found;
return nullptr;
}
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
do
if (*range - range[offset] == target)
return range;
while (--range != detent);
return nullptr;
}
#endif /* MDBX_ATTRIBUTE_TARGET_SSE2 */
#ifdef MDBX_ATTRIBUTE_TARGET_AVX2
MDBX_ATTRIBUTE_TARGET_AVX2 static __always_inline unsigned
diffcmp2mask_avx2(const pgno_t *const ptr, const ptrdiff_t offset,
const __m256i pattern) {
const __m256i f = _mm256_loadu_si256((const __m256i *)ptr);
const __m256i l = _mm256_loadu_si256((const __m256i *)(ptr + offset));
const __m256i cmp = _mm256_cmpeq_epi32(_mm256_sub_epi32(f, l), pattern);
return _mm256_movemask_ps(*(const __m256 *)&cmp);
}
MDBX_ATTRIBUTE_TARGET_AVX2 static __always_inline unsigned
diffcmp2mask_sse2avx(const pgno_t *const ptr, const ptrdiff_t offset,
const __m128i pattern) {
const __m128i f = _mm_loadu_si128((const __m128i *)ptr);
const __m128i l = _mm_loadu_si128((const __m128i *)(ptr + offset));
const __m128i cmp = _mm_cmpeq_epi32(_mm_sub_epi32(f, l), pattern);
return _mm_movemask_ps(*(const __m128 *)&cmp);
}
MDBX_MAYBE_UNUSED __hot MDBX_ATTRIBUTE_TARGET_AVX2 static pgno_t *
scan4seq_avx2(pgno_t *range, const size_t len, const size_t seq) {
assert(seq > 0 && len > seq);
#if MDBX_PNL_ASCENDING
#error "FIXME: Not implemented"
#endif /* MDBX_PNL_ASCENDING */
assert(range[-(ptrdiff_t)len] == len);
pgno_t *const detent = range - len + seq;
const ptrdiff_t offset = -(ptrdiff_t)seq;
const pgno_t target = (pgno_t)offset;
const __m256i pattern = _mm256_set1_epi32(target);
uint8_t mask;
if (likely(len > seq + 7)) {
do {
mask = (uint8_t)diffcmp2mask_avx2(range - 7, offset, pattern);
if (mask) {
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
found:
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
return range + 24 - __builtin_clz(mask);
}
range -= 8;
} while (range > detent + 7);
if (range == detent)
return nullptr;
}
/* Далее происходит чтение от 4 до 28 лишних байт, которые могут быть не
* только за пределами региона выделенного под PNL, но и пересекать границу
* страницы памяти. Что может приводить как к ошибкам ASAN, так и к падению.
* Поэтому проверяем смещение на странице, а с ASAN всегда страхуемся. */
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
const unsigned on_page_safe_mask = 0xfe0 /* enough for '-31' bytes offset */;
if (likely(on_page_safe_mask & (uintptr_t)(range + offset)) &&
!RUNNING_ON_VALGRIND) {
const unsigned extra = (unsigned)(detent + 8 - range);
assert(extra > 0 && extra < 8);
mask = 0xFF << extra;
mask &= diffcmp2mask_avx2(range - 7, offset, pattern);
if (mask)
goto found;
return nullptr;
}
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
if (range - 3 > detent) {
mask = diffcmp2mask_sse2avx(range - 3, offset, *(const __m128i *)&pattern);
if (mask)
return range + 28 - __builtin_clz(mask);
range -= 4;
}
while (range > detent) {
if (*range - range[offset] == target)
return range;
--range;
}
return nullptr;
}
#endif /* MDBX_ATTRIBUTE_TARGET_AVX2 */
#ifdef MDBX_ATTRIBUTE_TARGET_AVX512BW
MDBX_ATTRIBUTE_TARGET_AVX512BW static __always_inline unsigned
diffcmp2mask_avx512bw(const pgno_t *const ptr, const ptrdiff_t offset,
const __m512i pattern) {
const __m512i f = _mm512_loadu_si512((const __m512i *)ptr);
const __m512i l = _mm512_loadu_si512((const __m512i *)(ptr + offset));
return _mm512_cmpeq_epi32_mask(_mm512_sub_epi32(f, l), pattern);
}
MDBX_MAYBE_UNUSED __hot MDBX_ATTRIBUTE_TARGET_AVX512BW static pgno_t *
scan4seq_avx512bw(pgno_t *range, const size_t len, const size_t seq) {
assert(seq > 0 && len > seq);
#if MDBX_PNL_ASCENDING
#error "FIXME: Not implemented"
#endif /* MDBX_PNL_ASCENDING */
assert(range[-(ptrdiff_t)len] == len);
pgno_t *const detent = range - len + seq;
const ptrdiff_t offset = -(ptrdiff_t)seq;
const pgno_t target = (pgno_t)offset;
const __m512i pattern = _mm512_set1_epi32(target);
unsigned mask;
if (likely(len > seq + 15)) {
do {
mask = diffcmp2mask_avx512bw(range - 15, offset, pattern);
if (mask) {
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
found:
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
return range + 16 - __builtin_clz(mask);
}
range -= 16;
} while (range > detent + 15);
if (range == detent)
return nullptr;
}
/* Далее происходит чтение от 4 до 60 лишних байт, которые могут быть не
* только за пределами региона выделенного под PNL, но и пересекать границу
* страницы памяти. Что может приводить как к ошибкам ASAN, так и к падению.
* Поэтому проверяем смещение на странице, а с ASAN всегда страхуемся. */
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
const unsigned on_page_safe_mask = 0xfc0 /* enough for '-63' bytes offset */;
if (likely(on_page_safe_mask & (uintptr_t)(range + offset)) &&
!RUNNING_ON_VALGRIND) {
const unsigned extra = (unsigned)(detent + 16 - range);
assert(extra > 0 && extra < 16);
mask = 0xFFFF << extra;
mask &= diffcmp2mask_avx512bw(range - 15, offset, pattern);
if (mask)
goto found;
return nullptr;
}
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
if (range - 7 > detent) {
mask = diffcmp2mask_avx2(range - 7, offset, *(const __m256i *)&pattern);
if (mask)
return range + 24 - __builtin_clz(mask);
range -= 8;
}
if (range - 3 > detent) {
mask = diffcmp2mask_sse2avx(range - 3, offset, *(const __m128i *)&pattern);
if (mask)
return range + 28 - __builtin_clz(mask);
range -= 4;
}
while (range > detent) {
if (*range - range[offset] == target)
return range;
--range;
}
return nullptr;
}
#endif /* MDBX_ATTRIBUTE_TARGET_AVX512BW */
#if (defined(__ARM_NEON) || defined(__ARM_NEON__)) && \
(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
static __always_inline size_t diffcmp2mask_neon(const pgno_t *const ptr,
const ptrdiff_t offset,
const uint32x4_t pattern) {
const uint32x4_t f = vld1q_u32(ptr);
const uint32x4_t l = vld1q_u32(ptr + offset);
const uint16x4_t cmp = vmovn_u32(vceqq_u32(vsubq_u32(f, l), pattern));
if (sizeof(size_t) > 7)
return vget_lane_u64(vreinterpret_u64_u16(cmp), 0);
else
return vget_lane_u32(vreinterpret_u32_u8(vmovn_u16(vcombine_u16(cmp, cmp))),
0);
}
__hot static pgno_t *scan4seq_neon(pgno_t *range, const size_t len,
const size_t seq) {
assert(seq > 0 && len > seq);
#if MDBX_PNL_ASCENDING
#error "FIXME: Not implemented"
#endif /* MDBX_PNL_ASCENDING */
assert(range[-(ptrdiff_t)len] == len);
pgno_t *const detent = range - len + seq;
const ptrdiff_t offset = -(ptrdiff_t)seq;
const pgno_t target = (pgno_t)offset;
const uint32x4_t pattern = vmovq_n_u32(target);
size_t mask;
if (likely(len > seq + 3)) {
do {
mask = diffcmp2mask_neon(range - 3, offset, pattern);
if (mask) {
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
found:
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
return ptr_disp(range, -(__builtin_clzl(mask) >> sizeof(size_t) / 4));
}
range -= 4;
} while (range > detent + 3);
if (range == detent)
return nullptr;
}
/* Далее происходит чтение от 4 до 12 лишних байт, которые могут быть не
* только за пределами региона выделенного под PNL, но и пересекать границу
* страницы памяти. Что может приводить как к ошибкам ASAN, так и к падению.
* Поэтому проверяем смещение на странице, а с ASAN всегда страхуемся. */
#if !defined(ENABLE_MEMCHECK) && !defined(__SANITIZE_ADDRESS__)
const unsigned on_page_safe_mask = 0xff0 /* enough for '-15' bytes offset */;
if (likely(on_page_safe_mask & (uintptr_t)(range + offset)) &&
!RUNNING_ON_VALGRIND) {
const unsigned extra = (unsigned)(detent + 4 - range);
assert(extra > 0 && extra < 4);
mask = (~(size_t)0) << (extra * sizeof(size_t) * 2);
mask &= diffcmp2mask_neon(range - 3, offset, pattern);
if (mask)
goto found;
return nullptr;
}
#endif /* !ENABLE_MEMCHECK && !__SANITIZE_ADDRESS__ */
do
if (*range - range[offset] == target)
return range;
while (--range != detent);
return nullptr;
}
#endif /* __ARM_NEON || __ARM_NEON__ */
#if defined(__AVX512BW__) && defined(MDBX_ATTRIBUTE_TARGET_AVX512BW)
#define scan4seq_default scan4seq_avx512bw
#define scan4seq_impl scan4seq_default
#elif defined(__AVX2__) && defined(MDBX_ATTRIBUTE_TARGET_AVX2)
#define scan4seq_default scan4seq_avx2
#elif defined(__SSE2__) && defined(MDBX_ATTRIBUTE_TARGET_SSE2)
#define scan4seq_default scan4seq_sse2
#elif (defined(__ARM_NEON) || defined(__ARM_NEON__)) && \
(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
#define scan4seq_default scan4seq_neon
/* Choosing of another variants should be added here. */
#endif /* scan4seq_default */
#endif /* MDBX_PNL_ASCENDING */
#ifndef scan4seq_default
#define scan4seq_default scan4seq_fallback
#endif /* scan4seq_default */
#ifdef scan4seq_impl
/* The scan4seq_impl() is the best or no alternatives */
#elif !MDBX_HAVE_BUILTIN_CPU_SUPPORTS
/* The scan4seq_default() will be used since no cpu-features detection support
* from compiler. Please don't ask to implement cpuid-based detection and don't
* make such PRs. */
#define scan4seq_impl scan4seq_default
#else
/* Selecting the most appropriate implementation at runtime,
* depending on the available CPU features. */
static pgno_t *scan4seq_resolver(pgno_t *range, const size_t len,
const size_t seq);
static pgno_t *(*scan4seq_impl)(pgno_t *range, const size_t len,
const size_t seq) = scan4seq_resolver;
static pgno_t *scan4seq_resolver(pgno_t *range, const size_t len,
const size_t seq) {
pgno_t *(*choice)(pgno_t *range, const size_t len, const size_t seq) =
nullptr;
#if __has_builtin(__builtin_cpu_init) || defined(__BUILTIN_CPU_INIT__) || \
__GNUC_PREREQ(4, 8)
__builtin_cpu_init();
#endif /* __builtin_cpu_init() */
#ifdef MDBX_ATTRIBUTE_TARGET_SSE2
if (__builtin_cpu_supports("sse2"))
choice = scan4seq_sse2;
#endif /* MDBX_ATTRIBUTE_TARGET_SSE2 */
#ifdef MDBX_ATTRIBUTE_TARGET_AVX2
if (__builtin_cpu_supports("avx2"))
choice = scan4seq_avx2;
#endif /* MDBX_ATTRIBUTE_TARGET_AVX2 */
#ifdef MDBX_ATTRIBUTE_TARGET_AVX512BW
if (__builtin_cpu_supports("avx512bw"))
choice = scan4seq_avx512bw;
#endif /* MDBX_ATTRIBUTE_TARGET_AVX512BW */
/* Choosing of another variants should be added here. */
scan4seq_impl = choice ? choice : scan4seq_default;
return scan4seq_impl(range, len, seq);
}
#endif /* scan4seq_impl */
/*----------------------------------------------------------------------------*/
#define ALLOC_COALESCE 4 /* внутреннее состояние */
#define ALLOC_SHOULD_SCAN 8 /* внутреннее состояние */
#define ALLOC_LIFO 16 /* внутреннее состояние */
static inline bool is_gc_usable(MDBX_txn *txn, const MDBX_cursor *mc,
const uint8_t flags) {
/* If txn is updating the GC, then the retired-list cannot play catch-up with
* itself by growing while trying to save it. */
if (mc->tree == &txn->dbs[FREE_DBI] && !(flags & ALLOC_RESERVE) &&
!(mc->flags & z_gcu_preparation))
return false;
/* avoid search inside empty tree and while tree is updating,
https://libmdbx.dqdkfa.ru/dead-github/issues/31 */
if (unlikely(txn->dbs[FREE_DBI].items == 0)) {
txn->flags |= txn_gc_drained;
return false;
}
return true;
}
__hot static bool is_already_reclaimed(const MDBX_txn *txn, txnid_t id) {
const size_t len = MDBX_PNL_GETSIZE(txn->tw.gc.reclaimed);
for (size_t i = 1; i <= len; ++i)
if (txn->tw.gc.reclaimed[i] == id)
return true;
return false;
}
__hot static pgno_t relist_get_single(MDBX_txn *txn) {
const size_t len = MDBX_PNL_GETSIZE(txn->tw.relist);
assert(len > 0);
pgno_t *target = MDBX_PNL_EDGE(txn->tw.relist);
const ptrdiff_t dir = MDBX_PNL_ASCENDING ? 1 : -1;
/* Есть ТРИ потенциально выигрышные, но противо-направленные тактики:
*
* 1. Стараться использовать страницы с наименьшими номерами. Так обмен с
* диском будет более кучным, а у страниц ближе к концу БД будет больше шансов
* попасть под авто-компактификацию. Частично эта тактика уже реализована, но
* для её эффективности требуется явно приоритезировать выделение страниц:
* - поддерживать для relist, для ближних и для дальних страниц;
* - использовать страницы из дальнего списка, если первый пуст,
* а второй слишком большой, либо при пустой GC.
*
* 2. Стараться выделять страницы последовательно. Так записываемые на диск
* регионы будут линейными, что принципиально ускоряет запись на HDD.
* Одновременно, в среднем это не повлияет на чтение, точнее говоря, если
* порядок чтения не совпадает с порядком изменения (иначе говоря, если
* чтение не коррклирует с обновлениями и/или вставками) то не повлияет, иначе
* может ускорить. Однако, последовательности в среднем достаточно редки.
* Поэтому для эффективности требуется аккумулировать и поддерживать в ОЗУ
* огромные списки страниц, а затем сохранять их обратно в БД. Текущий формат
* БД (без битовых карт) для этого крайне не удачен. Поэтому эта тактика не
* имеет шансов быть успешной без смены формата БД (Mithril).
*
* 3. Стараться экономить последовательности страниц. Это позволяет избегать
* лишнего чтения/поиска в GC при более-менее постоянном размещении и/или
* обновлении данных требующих более одной страницы. Проблема в том, что без
* информации от приложения библиотека не может знать насколько
* востребованными будут последовательности в ближайшей перспективе, а
* экономия последовательностей "на всякий случай" не только затратна
* сама-по-себе, но и работает во вред.
*
* Поэтому:
* - в TODO добавляется разделение relist на «ближние» и «дальние» страницы,
* с последующей реализацией первой тактики;
* - преимущественное использование последовательностей отправляется
* в MithrilDB как составляющая "HDD frendly" feature;
* - реализованная в 3757eb72f7c6b46862f8f17881ac88e8cecc1979 экономия
* последовательностей отключается через MDBX_ENABLE_SAVING_SEQUENCES=0.
*
* В качестве альтернативы для безусловной «экономии» последовательностей,
* в следующих версиях libmdbx, вероятно, будет предложено
* API для взаимодействия с GC:
* - получение размера GC, включая гистограммы размеров последовательностей
* и близости к концу БД;
* - включение формирования "линейного запаса" для последующего использования
* в рамках текущей транзакции;
* - намеренная загрузка GC в память для коагуляции и "выпрямления";
* - намеренное копирование данных из страниц в конце БД для последующего
* из освобождения, т.е. контролируемая компактификация по запросу. */
#ifndef MDBX_ENABLE_SAVING_SEQUENCES
#define MDBX_ENABLE_SAVING_SEQUENCES 0
#endif
if (MDBX_ENABLE_SAVING_SEQUENCES && unlikely(target[dir] == *target + 1) &&
len > 2) {
/* Пытаемся пропускать последовательности при наличии одиночных элементов.
* TODO: необходимо кэшировать пропускаемые последовательности
* чтобы не сканировать список сначала при каждом выделении. */
pgno_t *scan = target + dir + dir;
size_t left = len;
do {
if (likely(scan[-dir] != *scan - 1 && *scan + 1 != scan[dir])) {
#if MDBX_PNL_ASCENDING
target = scan;
break;
#else
/* вырезаем элемент с перемещением хвоста */
const pgno_t pgno = *scan;
MDBX_PNL_SETSIZE(txn->tw.relist, len - 1);
while (++scan <= target)
scan[-1] = *scan;
return pgno;
#endif
}
scan += dir;
} while (--left > 2);
}
const pgno_t pgno = *target;
#if MDBX_PNL_ASCENDING
/* вырезаем элемент с перемещением хвоста */
MDBX_PNL_SETSIZE(txn->tw.relist, len - 1);
for (const pgno_t *const end = txn->tw.relist + len - 1; target <= end;
++target)
*target = target[1];
#else
/* перемещать хвост не нужно, просто усекам список */
MDBX_PNL_SETSIZE(txn->tw.relist, len - 1);
#endif
return pgno;
}
__hot static pgno_t relist_get_sequence(MDBX_txn *txn, const size_t num,
uint8_t flags) {
const size_t len = MDBX_PNL_GETSIZE(txn->tw.relist);
pgno_t *edge = MDBX_PNL_EDGE(txn->tw.relist);
assert(len >= num && num > 1);
const size_t seq = num - 1;
#if !MDBX_PNL_ASCENDING
if (edge[-(ptrdiff_t)seq] - *edge == seq) {
if (unlikely(flags & ALLOC_RESERVE))
return P_INVALID;
assert(edge == scan4range_checker(txn->tw.relist, seq));
/* перемещать хвост не нужно, просто усекам список */
MDBX_PNL_SETSIZE(txn->tw.relist, len - num);
return *edge;
}
#endif
pgno_t *target = scan4seq_impl(edge, len, seq);
assert(target == scan4range_checker(txn->tw.relist, seq));
if (target) {
if (unlikely(flags & ALLOC_RESERVE))
return P_INVALID;
const pgno_t pgno = *target;
/* вырезаем найденную последовательность с перемещением хвоста */
MDBX_PNL_SETSIZE(txn->tw.relist, len - num);
#if MDBX_PNL_ASCENDING
for (const pgno_t *const end = txn->tw.relist + len - num; target <= end;
++target)
*target = target[num];
#else
for (const pgno_t *const end = txn->tw.relist + len; ++target <= end;)
target[-(ptrdiff_t)num] = *target;
#endif
return pgno;
}
return 0;
}
static inline pgr_t page_alloc_finalize(MDBX_env *const env,
MDBX_txn *const txn,
const MDBX_cursor *const mc,
const pgno_t pgno, const size_t num) {
#if MDBX_ENABLE_PROFGC
size_t majflt_before;
const uint64_t cputime_before = osal_cputime(&majflt_before);
gc_prof_stat_t *const prof = (mc->mc_dbi == FREE_DBI)
? &env->lck->pgops.gc_prof.self
: &env->lck->pgops.gc_prof.work;
#else
(void)mc;
#endif /* MDBX_ENABLE_PROFGC */
ENSURE(env, pgno >= NUM_METAS);
pgr_t ret;
bool need_clean = (env->flags & MDBX_PAGEPERTURB) != 0;
if (env->flags & MDBX_WRITEMAP) {
ret.page = pgno2page(env, pgno);
MDBX_ASAN_UNPOISON_MEMORY_REGION(ret.page, pgno2bytes(env, num));
VALGRIND_MAKE_MEM_UNDEFINED(ret.page, pgno2bytes(env, num));
/* Содержимое выделенной страницы не нужно, но если страница отсутствует
* в ОЗУ (что весьма вероятно), то любое обращение к ней приведет
* к page-fault:
* - прерыванию по отсутствию страницы;
* - переключение контекста в режим ядра с засыпанием процесса;
* - чтение страницы с диска;
* - обновление PTE и пробуждением процесса;
* - переключение контекста по доступности ЦПУ.
*
* Пытаемся минимизировать накладные расходы записывая страницу, что при
* наличии unified page cache приведет к появлению страницы в ОЗУ без чтения
* с диска. При этом запись на диск должна быть отложена адекватным ядром,
* так как страница отображена в память в режиме чтения-записи и следом в
* неё пишет ЦПУ. */
/* В случае если страница в памяти процесса, то излишняя запись может быть
* достаточно дорогой. Кроме системного вызова и копирования данных, в особо
* одаренных ОС при этом могут включаться файловая система, выделяться
* временная страница, пополняться очереди асинхронного выполнения,
* обновляться PTE с последующей генерацией page-fault и чтением данных из
* грязной I/O очереди. Из-за этого штраф за лишнюю запись может быть
* сравним с избегаемым ненужным чтением. */
if (env->prefault_write_activated) {
void *const pattern =
ptr_disp(env->page_auxbuf, need_clean ? env->ps : env->ps * 2);
size_t file_offset = pgno2bytes(env, pgno);
if (likely(num == 1)) {
if (!mincore_probe(env, pgno)) {
osal_pwrite(env->lazy_fd, pattern, env->ps, file_offset);
#if MDBX_ENABLE_PGOP_STAT
env->lck->pgops.prefault.weak += 1;
#endif /* MDBX_ENABLE_PGOP_STAT */
need_clean = false;
}
} else {
struct iovec iov[MDBX_AUXILARY_IOV_MAX];
size_t n = 0, cleared = 0;
for (size_t i = 0; i < num; ++i) {
if (!mincore_probe(env, pgno + (pgno_t)i)) {
++cleared;
iov[n].iov_len = env->ps;
iov[n].iov_base = pattern;
if (unlikely(++n == MDBX_AUXILARY_IOV_MAX)) {
osal_pwritev(env->lazy_fd, iov, MDBX_AUXILARY_IOV_MAX,
file_offset);
#if MDBX_ENABLE_PGOP_STAT
env->lck->pgops.prefault.weak += 1;
#endif /* MDBX_ENABLE_PGOP_STAT */
file_offset += pgno2bytes(env, MDBX_AUXILARY_IOV_MAX);
n = 0;
}
}
}
if (likely(n > 0)) {
osal_pwritev(env->lazy_fd, iov, n, file_offset);
#if MDBX_ENABLE_PGOP_STAT
env->lck->pgops.prefault.weak += 1;
#endif /* MDBX_ENABLE_PGOP_STAT */
}
if (cleared == num)
need_clean = false;
}
}
} else {
ret.page = page_shadow_alloc(txn, num);
if (unlikely(!ret.page)) {
ret.err = MDBX_ENOMEM;
goto bailout;
}
}
if (unlikely(need_clean))
memset(ret.page, -1, pgno2bytes(env, num));
VALGRIND_MAKE_MEM_UNDEFINED(ret.page, pgno2bytes(env, num));
ret.page->pgno = pgno;
ret.page->dupfix_ksize = 0;
ret.page->flags = 0;
if ((ASSERT_ENABLED() || AUDIT_ENABLED()) && num > 1) {
ret.page->pages = (pgno_t)num;
ret.page->flags = P_LARGE;
}
ret.err = page_dirty(txn, ret.page, (pgno_t)num);
bailout:
tASSERT(txn, pnl_check_allocated(txn->tw.relist, txn->geo.first_unallocated -
MDBX_ENABLE_REFUND));
#if MDBX_ENABLE_PROFGC
size_t majflt_after;
prof->xtime_cpu += osal_cputime(&majflt_after) - cputime_before;
prof->majflt += (uint32_t)(majflt_after - majflt_before);
#endif /* MDBX_ENABLE_PROFGC */
return ret;
}
pgr_t gc_alloc_ex(const MDBX_cursor *const mc, const size_t num,
uint8_t flags) {
pgr_t ret;
MDBX_txn *const txn = mc->txn;
MDBX_env *const env = txn->env;
#if MDBX_ENABLE_PROFGC
gc_prof_stat_t *const prof = (mc->mc_dbi == FREE_DBI)
? &env->lck->pgops.gc_prof.self
: &env->lck->pgops.gc_prof.work;
prof->spe_counter += 1;
#endif /* MDBX_ENABLE_PROFGC */
eASSERT(env, num > 0 || (flags & ALLOC_RESERVE));
eASSERT(env, pnl_check_allocated(txn->tw.relist, txn->geo.first_unallocated -
MDBX_ENABLE_REFUND));
size_t newnext;
const uint64_t monotime_begin =
(MDBX_ENABLE_PROFGC || (num > 1 && env->options.gc_time_limit))
? osal_monotime()
: 0;
struct monotime_cache now_cache;
now_cache.expire_countdown =
1 /* старт с 1 позволяет избавиться как от лишних системных вызовов когда
лимит времени задан нулевой или уже исчерпан, так и от подсчета
времени при не-достижении rp_augment_limit */
;
now_cache.value = monotime_begin;
pgno_t pgno = 0;
if (num > 1) {
#if MDBX_ENABLE_PROFGC
prof->xpages += 1;
#endif /* MDBX_ENABLE_PROFGC */
if (MDBX_PNL_GETSIZE(txn->tw.relist) >= num) {
eASSERT(env,
MDBX_PNL_LAST(txn->tw.relist) < txn->geo.first_unallocated &&
MDBX_PNL_FIRST(txn->tw.relist) < txn->geo.first_unallocated);
pgno = relist_get_sequence(txn, num, flags);
if (likely(pgno))
goto done;
}
} else {
eASSERT(env, num == 0 || MDBX_PNL_GETSIZE(txn->tw.relist) == 0);
eASSERT(env, !(flags & ALLOC_RESERVE) || num == 0);
}
//---------------------------------------------------------------------------
if (unlikely(!is_gc_usable(txn, mc, flags))) {
eASSERT(env, (txn->flags & txn_gc_drained) || num > 1);
goto no_gc;
}
eASSERT(env,
(flags & (ALLOC_COALESCE | ALLOC_LIFO | ALLOC_SHOULD_SCAN)) == 0);
flags += (env->flags & MDBX_LIFORECLAIM) ? ALLOC_LIFO : 0;
if (/* Не коагулируем записи при подготовке резерва для обновления GC.
* Иначе попытка увеличить резерв может приводить к необходимости ещё
* большего резерва из-за увеличения списка переработанных страниц. */
(flags & ALLOC_RESERVE) == 0) {
if (txn->dbs[FREE_DBI].branch_pages &&
MDBX_PNL_GETSIZE(txn->tw.relist) < env->maxgc_large1page / 2)
flags += ALLOC_COALESCE;
}
MDBX_cursor *const gc = ptr_disp(env->basal_txn, sizeof(MDBX_txn));
eASSERT(env, mc != gc && gc->next == gc);
gc->txn = txn;
gc->dbi_state = txn->dbi_state;
gc->top_and_flags = z_fresh_mark;
env->prefault_write_activated = env->options.prefault_write;
if (env->prefault_write_activated) {
/* Проверка посредством minicore() существенно снижает затраты, но в
* простейших случаях (тривиальный бенчмарк) интегральная производительность
* становится вдвое меньше. А на платформах без mincore() и с проблемной
* подсистемой виртуальной памяти ситуация может быть многократно хуже.
* Поэтому избегаем затрат в ситуациях когда prefault-write скорее всего не
* нужна. */
const bool readahead_enabled = env->lck->readahead_anchor & 1;
const pgno_t readahead_edge = env->lck->readahead_anchor >> 1;
if (/* Не суетимся если GC почти пустая и БД маленькая */
(txn->dbs[FREE_DBI].branch_pages == 0 && txn->geo.now < 1234) ||
/* Не суетимся если страница в зоне включенного упреждающего чтения */
(readahead_enabled && pgno + num < readahead_edge))
env->prefault_write_activated = false;
}
retry_gc_refresh_oldest:;
txnid_t oldest = txn_snapshot_oldest(txn);
retry_gc_have_oldest:
if (unlikely(oldest >= txn->txnid)) {
ERROR("unexpected/invalid oldest-readed txnid %" PRIaTXN
" for current-txnid %" PRIaTXN,
oldest, txn->txnid);
ret.err = MDBX_PROBLEM;
goto fail;
}
const txnid_t detent = oldest + 1;
txnid_t id = 0;
MDBX_cursor_op op = MDBX_FIRST;
if (flags & ALLOC_LIFO) {
if (!txn->tw.gc.reclaimed) {
txn->tw.gc.reclaimed = txl_alloc();
if (unlikely(!txn->tw.gc.reclaimed)) {
ret.err = MDBX_ENOMEM;
goto fail;
}
}
/* Begin lookup backward from oldest reader */
id = detent - 1;
op = MDBX_SET_RANGE;
} else if (txn->tw.gc.last_reclaimed) {
/* Continue lookup forward from last-reclaimed */
id = txn->tw.gc.last_reclaimed + 1;
if (id >= detent)
goto depleted_gc;
op = MDBX_SET_RANGE;
}
next_gc:;
MDBX_val key;
key.iov_base = &id;
key.iov_len = sizeof(id);
#if MDBX_ENABLE_PROFGC
prof->rsteps += 1;
#endif /* MDBX_ENABLE_PROFGC */
/* Seek first/next GC record */
ret.err = cursor_ops(gc, &key, nullptr, op);
if (unlikely(ret.err != MDBX_SUCCESS)) {
if (unlikely(ret.err != MDBX_NOTFOUND))
goto fail;
if ((flags & ALLOC_LIFO) && op == MDBX_SET_RANGE) {
op = MDBX_PREV;
goto next_gc;
}
goto depleted_gc;
}
if (unlikely(key.iov_len != sizeof(txnid_t))) {
ERROR("%s/%d: %s", "MDBX_CORRUPTED", MDBX_CORRUPTED,
"invalid GC key-length");
ret.err = MDBX_CORRUPTED;
goto fail;
}
id = unaligned_peek_u64(4, key.iov_base);
if (flags & ALLOC_LIFO) {
op = MDBX_PREV;
if (id >= detent || is_already_reclaimed(txn, id))
goto next_gc;
} else {
op = MDBX_NEXT;
if (unlikely(id >= detent))
goto depleted_gc;
}
txn->flags &= ~txn_gc_drained;
/* Reading next GC record */
MDBX_val data;
page_t *const mp = gc->pg[gc->top];
if (unlikely((ret.err = node_read(gc, page_node(mp, gc->ki[gc->top]), &data,
mp)) != MDBX_SUCCESS))
goto fail;
pgno_t *gc_pnl = (pgno_t *)data.iov_base;
if (unlikely(data.iov_len % sizeof(pgno_t) ||
data.iov_len < MDBX_PNL_SIZEOF(gc_pnl) ||
!pnl_check(gc_pnl, txn->geo.first_unallocated))) {
ERROR("%s/%d: %s", "MDBX_CORRUPTED", MDBX_CORRUPTED,
"invalid GC value-length");
ret.err = MDBX_CORRUPTED;
goto fail;
}
const size_t gc_len = MDBX_PNL_GETSIZE(gc_pnl);
TRACE("gc-read: id #%" PRIaTXN " len %zu, re-list will %zu ", id, gc_len,
gc_len + MDBX_PNL_GETSIZE(txn->tw.relist));
if (unlikely(gc_len + MDBX_PNL_GETSIZE(txn->tw.relist) >=
env->maxgc_large1page)) {
/* Don't try to coalesce too much. */
if (flags & ALLOC_SHOULD_SCAN) {
eASSERT(env, flags & ALLOC_COALESCE);
eASSERT(env, !(flags & ALLOC_RESERVE));
eASSERT(env, num > 0);
#if MDBX_ENABLE_PROFGC
env->lck->pgops.gc_prof.coalescences += 1;
#endif /* MDBX_ENABLE_PROFGC */
TRACE("clear %s %s", "ALLOC_COALESCE", "since got threshold");
if (MDBX_PNL_GETSIZE(txn->tw.relist) >= num) {
eASSERT(env,
MDBX_PNL_LAST(txn->tw.relist) < txn->geo.first_unallocated &&
MDBX_PNL_FIRST(txn->tw.relist) <
txn->geo.first_unallocated);
if (likely(num == 1)) {
pgno = relist_get_single(txn);
goto done;
}
pgno = relist_get_sequence(txn, num, flags);
if (likely(pgno))
goto done;
}
flags -= ALLOC_COALESCE | ALLOC_SHOULD_SCAN;
}
if (unlikely(/* list is too long already */ MDBX_PNL_GETSIZE(
txn->tw.relist) >= env->options.rp_augment_limit) &&
((/* not a slot-request from gc-update */ num &&
/* have enough unallocated space */ txn->geo.upper >=
txn->geo.first_unallocated + num &&
monotime_since_cached(monotime_begin, &now_cache) +
txn->tw.gc.time_acc >=
env->options.gc_time_limit) ||
gc_len + MDBX_PNL_GETSIZE(txn->tw.relist) >= PAGELIST_LIMIT)) {
/* Stop reclaiming to avoid large/overflow the page list. This is a rare
* case while search for a continuously multi-page region in a
* large database, see https://libmdbx.dqdkfa.ru/dead-github/issues/123 */
NOTICE("stop reclaiming %s: %zu (current) + %zu "
"(chunk) -> %zu, rp_augment_limit %u",
likely(gc_len + MDBX_PNL_GETSIZE(txn->tw.relist) < PAGELIST_LIMIT)
? "since rp_augment_limit was reached"
: "to avoid PNL overflow",
MDBX_PNL_GETSIZE(txn->tw.relist), gc_len,
gc_len + MDBX_PNL_GETSIZE(txn->tw.relist),
env->options.rp_augment_limit);
goto depleted_gc;
}
}
/* Remember ID of readed GC record */
txn->tw.gc.last_reclaimed = id;
if (flags & ALLOC_LIFO) {
ret.err = txl_append(&txn->tw.gc.reclaimed, id);
if (unlikely(ret.err != MDBX_SUCCESS))
goto fail;
}
/* Append PNL from GC record to tw.relist */
ret.err = pnl_need(&txn->tw.relist, gc_len);
if (unlikely(ret.err != MDBX_SUCCESS))
goto fail;
if (LOG_ENABLED(MDBX_LOG_EXTRA)) {
DEBUG_EXTRA("readed GC-pnl txn %" PRIaTXN " root %" PRIaPGNO
" len %zu, PNL",
id, txn->dbs[FREE_DBI].root, gc_len);
for (size_t i = gc_len; i; i--)
DEBUG_EXTRA_PRINT(" %" PRIaPGNO, gc_pnl[i]);
DEBUG_EXTRA_PRINT(", first_unallocated %u\n", txn->geo.first_unallocated);
}
/* Merge in descending sorted order */
pnl_merge(txn->tw.relist, gc_pnl);
flags |= ALLOC_SHOULD_SCAN;
if (AUDIT_ENABLED()) {
if (unlikely(!pnl_check(txn->tw.relist, txn->geo.first_unallocated))) {
ERROR("%s/%d: %s", "MDBX_CORRUPTED", MDBX_CORRUPTED,
"invalid txn retired-list");
ret.err = MDBX_CORRUPTED;
goto fail;
}
} else {
eASSERT(env,
pnl_check_allocated(txn->tw.relist, txn->geo.first_unallocated));
}
eASSERT(env, dpl_check(txn));
eASSERT(env, MDBX_PNL_GETSIZE(txn->tw.relist) == 0 ||
MDBX_PNL_MOST(txn->tw.relist) < txn->geo.first_unallocated);
if (MDBX_ENABLE_REFUND && MDBX_PNL_GETSIZE(txn->tw.relist) &&
unlikely(MDBX_PNL_MOST(txn->tw.relist) ==
txn->geo.first_unallocated - 1)) {
/* Refund suitable pages into "unallocated" space */
txn_refund(txn);
}
eASSERT(env, pnl_check_allocated(txn->tw.relist, txn->geo.first_unallocated -
MDBX_ENABLE_REFUND));
/* Done for a kick-reclaim mode, actually no page needed */
if (unlikely(num == 0)) {
eASSERT(env, ret.err == MDBX_SUCCESS);
TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "early-exit for slot", id,
MDBX_PNL_GETSIZE(txn->tw.relist));
goto early_exit;
}
/* TODO: delete reclaimed records */
eASSERT(env, op == MDBX_PREV || op == MDBX_NEXT);
if (flags & ALLOC_COALESCE) {
TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "coalesce-continue", id,
MDBX_PNL_GETSIZE(txn->tw.relist));
goto next_gc;
}
scan:
eASSERT(env, flags & ALLOC_SHOULD_SCAN);
eASSERT(env, num > 0);
if (MDBX_PNL_GETSIZE(txn->tw.relist) >= num) {
eASSERT(env,
MDBX_PNL_LAST(txn->tw.relist) < txn->geo.first_unallocated &&
MDBX_PNL_FIRST(txn->tw.relist) < txn->geo.first_unallocated);
if (likely(num == 1)) {
eASSERT(env, !(flags & ALLOC_RESERVE));
pgno = relist_get_single(txn);
goto done;
}
pgno = relist_get_sequence(txn, num, flags);
if (likely(pgno))
goto done;
}
flags -= ALLOC_SHOULD_SCAN;
if (ret.err == MDBX_SUCCESS) {
TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "continue-search", id,
MDBX_PNL_GETSIZE(txn->tw.relist));
goto next_gc;
}
depleted_gc:
TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "gc-depleted", id,
MDBX_PNL_GETSIZE(txn->tw.relist));
ret.err = MDBX_NOTFOUND;
if (flags & ALLOC_SHOULD_SCAN)
goto scan;
txn->flags |= txn_gc_drained;
//-------------------------------------------------------------------------
/* There is no suitable pages in the GC and to be able to allocate
* we should CHOICE one of:
* - make a new steady checkpoint if reclaiming was stopped by
* the last steady-sync, or wipe it in the MDBX_UTTERLY_NOSYNC mode;
* - kick lagging reader(s) if reclaiming was stopped by ones of it.
* - extend the database file. */
/* Will use new pages from the map if nothing is suitable in the GC. */
newnext = txn->geo.first_unallocated + num;
/* Does reclaiming stopped at the last steady point? */
const meta_ptr_t recent = meta_recent(env, &txn->tw.troika);
const meta_ptr_t prefer_steady = meta_prefer_steady(env, &txn->tw.troika);
if (recent.ptr_c != prefer_steady.ptr_c && prefer_steady.is_steady &&
detent == prefer_steady.txnid + 1) {
DEBUG("gc-kick-steady: recent %" PRIaTXN "-%s, steady %" PRIaTXN
"-%s, detent %" PRIaTXN,
recent.txnid, durable_caption(recent.ptr_c), prefer_steady.txnid,
durable_caption(prefer_steady.ptr_c), detent);
const pgno_t autosync_threshold =
atomic_load32(&env->lck->autosync_threshold, mo_Relaxed);
const uint64_t autosync_period =
atomic_load64(&env->lck->autosync_period, mo_Relaxed);
uint64_t eoos_timestamp;
/* wipe the last steady-point if one of:
* - UTTERLY_NOSYNC mode AND auto-sync threshold is NOT specified
* - UTTERLY_NOSYNC mode AND free space at steady-point is exhausted
* otherwise, make a new steady-point if one of:
* - auto-sync threshold is specified and reached;
* - upper limit of database size is reached;
* - database is full (with the current file size)
* AND auto-sync threshold it NOT specified */
if (F_ISSET(env->flags, MDBX_UTTERLY_NOSYNC) &&
((autosync_threshold | autosync_period) == 0 ||
newnext >= prefer_steady.ptr_c->geometry.now)) {
/* wipe steady checkpoint in MDBX_UTTERLY_NOSYNC mode
* without any auto-sync threshold(s). */
#if MDBX_ENABLE_PROFGC
env->lck->pgops.gc_prof.wipes += 1;
#endif /* MDBX_ENABLE_PROFGC */
ret.err = meta_wipe_steady(env, detent);
DEBUG("gc-wipe-steady, rc %d", ret.err);
if (unlikely(ret.err != MDBX_SUCCESS))
goto fail;
eASSERT(env, prefer_steady.ptr_c !=
meta_prefer_steady(env, &txn->tw.troika).ptr_c);
goto retry_gc_refresh_oldest;
}
if ((autosync_threshold &&
atomic_load64(&env->lck->unsynced_pages, mo_Relaxed) >=
autosync_threshold) ||
(autosync_period &&
(eoos_timestamp =
atomic_load64(&env->lck->eoos_timestamp, mo_Relaxed)) &&
osal_monotime() - eoos_timestamp >= autosync_period) ||
newnext >= txn->geo.upper ||
((num == 0 || newnext >= txn->geo.end_pgno) &&
(autosync_threshold | autosync_period) == 0)) {
/* make steady checkpoint. */
#if MDBX_ENABLE_PROFGC
env->lck->pgops.gc_prof.flushes += 1;
#endif /* MDBX_ENABLE_PROFGC */
meta_t meta = *recent.ptr_c;
ret.err = dxb_sync_locked(env, env->flags & MDBX_WRITEMAP, &meta,
&txn->tw.troika);
DEBUG("gc-make-steady, rc %d", ret.err);
eASSERT(env, ret.err != MDBX_RESULT_TRUE);
if (unlikely(ret.err != MDBX_SUCCESS))
goto fail;
eASSERT(env, prefer_steady.ptr_c !=
meta_prefer_steady(env, &txn->tw.troika).ptr_c);
goto retry_gc_refresh_oldest;
}
}
if (unlikely(true ==
atomic_load32(&env->lck->rdt_refresh_flag, mo_AcquireRelease))) {
oldest = txn_snapshot_oldest(txn);
if (oldest >= detent)
goto retry_gc_have_oldest;
}
/* Avoid kick lagging reader(s) if is enough unallocated space
* at the end of database file. */
if (!(flags & ALLOC_RESERVE) && newnext <= txn->geo.end_pgno) {
eASSERT(env, pgno == 0);
goto done;
}
if (oldest < txn->txnid - xMDBX_TXNID_STEP) {
oldest = mvcc_kick_laggards(env, oldest);
if (oldest >= detent)
goto retry_gc_have_oldest;
}
//---------------------------------------------------------------------------
no_gc:
eASSERT(env, pgno == 0);
#ifndef MDBX_ENABLE_BACKLOG_DEPLETED
#define MDBX_ENABLE_BACKLOG_DEPLETED 0
#endif /* MDBX_ENABLE_BACKLOG_DEPLETED*/
if (MDBX_ENABLE_BACKLOG_DEPLETED &&
unlikely(!(txn->flags & txn_gc_drained))) {
ret.err = MDBX_BACKLOG_DEPLETED;
goto fail;
}
if (flags & ALLOC_RESERVE) {
ret.err = MDBX_NOTFOUND;
goto fail;
}
/* Will use new pages from the map if nothing is suitable in the GC. */
newnext = txn->geo.first_unallocated + num;
if (newnext <= txn->geo.end_pgno)
goto done;
if (newnext > txn->geo.upper || !txn->geo.grow_pv) {
NOTICE("gc-alloc: next %zu > upper %" PRIaPGNO, newnext, txn->geo.upper);
ret.err = MDBX_MAP_FULL;
goto fail;
}
eASSERT(env, newnext > txn->geo.end_pgno);
const size_t grow_step = pv2pages(txn->geo.grow_pv);
size_t aligned = pgno_align2os_pgno(
env, (pgno_t)(newnext + grow_step - newnext % grow_step));
if (aligned > txn->geo.upper)
aligned = txn->geo.upper;
eASSERT(env, aligned >= newnext);
VERBOSE("try growth datafile to %zu pages (+%zu)", aligned,
aligned - txn->geo.end_pgno);
ret.err = dxb_resize(env, txn->geo.first_unallocated, (pgno_t)aligned,
txn->geo.upper, implicit_grow);
if (ret.err != MDBX_SUCCESS) {
ERROR("unable growth datafile to %zu pages (+%zu), errcode %d", aligned,
aligned - txn->geo.end_pgno, ret.err);
goto fail;
}
env->txn->geo.end_pgno = (pgno_t)aligned;
eASSERT(env, pgno == 0);
//---------------------------------------------------------------------------
done:
ret.err = MDBX_SUCCESS;
if (likely((flags & ALLOC_RESERVE) == 0)) {
if (pgno) {
eASSERT(env,
pgno + num <= txn->geo.first_unallocated && pgno >= NUM_METAS);
eASSERT(env,
pnl_check_allocated(txn->tw.relist, txn->geo.first_unallocated -
MDBX_ENABLE_REFUND));
} else {
pgno = txn->geo.first_unallocated;
txn->geo.first_unallocated += (pgno_t)num;
eASSERT(env, txn->geo.first_unallocated <= txn->geo.end_pgno);
eASSERT(env,
pgno >= NUM_METAS && pgno + num <= txn->geo.first_unallocated);
}
ret = page_alloc_finalize(env, txn, mc, pgno, num);
if (unlikely(ret.err != MDBX_SUCCESS)) {
fail:
eASSERT(env, ret.err != MDBX_SUCCESS);
eASSERT(env,
pnl_check_allocated(txn->tw.relist, txn->geo.first_unallocated -
MDBX_ENABLE_REFUND));
int level;
const char *what;
if (flags & ALLOC_RESERVE) {
level = (flags & ALLOC_UNIMPORTANT) ? MDBX_LOG_DEBUG : MDBX_LOG_NOTICE;
what = num ? "reserve-pages" : "fetch-slot";
} else {
txn->flags |= MDBX_TXN_ERROR;
level = MDBX_LOG_ERROR;
what = "pages";
}
if (LOG_ENABLED(level))
debug_log(level, __func__, __LINE__,
"unable alloc %zu %s, alloc-flags 0x%x, err %d, txn-flags "
"0x%x, re-list-len %zu, loose-count %zu, gc: height %u, "
"branch %zu, leaf %zu, large %zu, entries %zu\n",
num, what, flags, ret.err, txn->flags,
MDBX_PNL_GETSIZE(txn->tw.relist), txn->tw.loose_count,
txn->dbs[FREE_DBI].height,
(size_t)txn->dbs[FREE_DBI].branch_pages,
(size_t)txn->dbs[FREE_DBI].leaf_pages,
(size_t)txn->dbs[FREE_DBI].large_pages,
(size_t)txn->dbs[FREE_DBI].items);
ret.page = nullptr;
}
if (num > 1)
txn->tw.gc.time_acc += monotime_since_cached(monotime_begin, &now_cache);
} else {
early_exit:
DEBUG("return nullptr for %zu pages for ALLOC_%s, rc %d", num,
num ? "RESERVE" : "SLOT", ret.err);
ret.page = nullptr;
}
#if MDBX_ENABLE_PROFGC
prof->rtime_monotonic += osal_monotime() - monotime_begin;
#endif /* MDBX_ENABLE_PROFGC */
return ret;
}
__hot pgr_t gc_alloc_single(const MDBX_cursor *const mc) {
MDBX_txn *const txn = mc->txn;
tASSERT(txn, mc->txn->flags & MDBX_TXN_DIRTY);
tASSERT(txn,
F_ISSET(*cursor_dbi_state(mc), DBI_LINDO | DBI_VALID | DBI_DIRTY));
/* If there are any loose pages, just use them */
while (likely(txn->tw.loose_pages)) {
#if MDBX_ENABLE_REFUND
if (unlikely(txn->tw.loose_refund_wl > txn->geo.first_unallocated)) {
txn_refund(txn);
if (!txn->tw.loose_pages)
break;
}
#endif /* MDBX_ENABLE_REFUND */
page_t *lp = txn->tw.loose_pages;
MDBX_ASAN_UNPOISON_MEMORY_REGION(lp, txn->env->ps);
VALGRIND_MAKE_MEM_DEFINED(&page_next(lp), sizeof(page_t *));
txn->tw.loose_pages = page_next(lp);
txn->tw.loose_count--;
DEBUG_EXTRA("db %d use loose page %" PRIaPGNO, cursor_dbi_dbg(mc),
lp->pgno);
tASSERT(txn, lp->pgno < txn->geo.first_unallocated);
tASSERT(txn, lp->pgno >= NUM_METAS);
VALGRIND_MAKE_MEM_UNDEFINED(page_data(lp), page_space(txn->env));
lp->txnid = txn->front_txnid;
pgr_t ret = {lp, MDBX_SUCCESS};
return ret;
}
if (likely(MDBX_PNL_GETSIZE(txn->tw.relist) > 0))
return page_alloc_finalize(txn->env, txn, mc, relist_get_single(txn), 1);
return gc_alloc_ex(mc, 1, ALLOC_DEFAULT);
}