"причина ошибки: выделение через IntervalND::opera...

Creato il: 21 aprile 2026

Risposto usando GPT-5.4 Thinking da Chat01

Domanda

"причина ошибки: выделение через IntervalND::operator new (slab), а освобождение через IntervalND::operator delete (который, судя по стеку, пытается использовать другой механизм освобождения или не находит блок в slab). Это приводит к SLAB_FREE_IGNORED и повреждению служебных структур slab (видно по дампам SLAB_DUMP с некорректными head/tail).

🚩 Самые показательные строки для отправки копилоту
Отправьте копилоту несколько полных блоков (по 3–4 строки подряд), содержащих пару SLAB_ALLOC → SLAB_FREE_IGNORED для одного и того же указателя, плюс ближайший SLAB_DUMP для того же указателя. Вот конкретные примеры из вашего лога:

Пара 1 (ptr = 0000024D56789880)
text
2026-04-21 21:50:26 [TID=8040] SLAB_ALLOC ptr=0000024D56789880 size=64 bt=00007FFCB83CFC17[agp_log_event+0x223 (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:65)],00007FFCB83BAC7B[IntervalND::operator new+0x83 (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:502)],00007FFCB83D5B2D[agp_run_branch_mpi+0x5649 (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:1152)],00007FFCB83F3AE1[AGP_Manip2D+0xd25 (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:1309)],...
2026-04-21 21:50:26 [TID=8040] SLAB_FREE_IGNORED ptr=0000024D56789880 size=0 bt=00007FFCB83CFC17[agp_log_event+0x223 (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:65)],00007FFCB83BADCF[IntervalND::operator delete+0x7b (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:520)],00007FFCB83D6589[agp_run_branch_mpi+0x60a5 (E:\TEST_AGP\TEST_FUNC\TEST_FUNC.cpp:11a1)],...
2026-04-21 21:50:26 [TID=8040] SLAB_DUMP head=8707003f8b07003f5acfef41e5d21142 tail=8b07800000000000a9562c3e0b000202 ptr=0000024D56789880 size=0 bt=...
Пара 2 (ptr = 0000024D567898C0)
text
2026-04-21 21:50:26 [TID=8040] SLAB_ALLOC ptr=0000024D567898C0 size=64 bt=... IntervalND::operator new+0x83 (TEST_FUNC.cpp:502) ...
2026-04-21 21:50:26 [TID=8040] SLAB_FREE_IGNORED ptr=0000024D567898C0 size=0 bt=... IntervalND::operator delete+0x7b (TEST_FUNC.cpp:520) ...
2026-04-21 21:50:26 [TID=8040] SLAB_DUMP head=8b07003f8f07003fe5d21142b7638643 tail=8f07800000000000a9562c3e0b000202 ptr=0000024D567898C0 ...
Пара 3 (ptr = 0000024D56789980)
text
2026-04-21 21:50:26 [TID=8040] SLAB_ALLOC ptr=0000024D56789980 size=64 bt=... IntervalND::operator new+0x83 ...
2026-04-21 21:50:26 [TID=8040] SLAB_FREE_IGNORED ptr=0000024D56789980 size=0 bt=... IntervalND::operator delete+0x7b ...
2026-04-21 21:50:26 [TID=8040] SLAB_DUMP head=8b07003f8d07003fe5d211427681df42 tail=8d07800000000000f813173e0b000202 ptr=0000024D56789980 ...
Пара 4 (ptr = 0000024D56789440 – первая в логе)
text
2026-04-21 21:50:26 [TID=8040] SLAB_FREE_IGNORED ptr=0000024D56789440 size=0 bt=... IntervalND::operator delete+0x7b ...
2026-04-21 21:50:26 [TID=8040] SLAB_DUMP head=fc07003fff07003f65ee0b440b4f1c43 tail=ff07800000000000f813173e0b000202 ptr=0000024D56789440 ...
(Хотя здесь нет предшествующего SLAB_ALLOC в этом фрагменте, он явно был ранее выделен через slab, и его освобождение тоже не распознано.)

🔍 Что ещё важно отметить для копилота
Все операции в одном потоке (TID=8040), значит проблема не в cross‑thread free, а в логической ошибке кода.

Класс IntervalND:

Выделение через operator new (строка 502 в TEST_FUNC.cpp) – вероятно, перегруженный оператор, использующий slab‑аллокатор.

Освобождение через operator delete (строка 520) – этот оператор, судя по стеку, не может найти блок в slab (поэтому size=0 и дамп показывает испорченные заголовки).

Стеки STORE_Q: между выделениями и освобождениями идут записи STORE_Q (копирование состояний), что может указывать на копирование указателей без правильной семантики владения.

CO_TASK_ALLOC в конце – это выделение через CoTaskMemAlloc, которое может происходить уже после того, как куча повреждена предыдущими некорректными освобождениями.

Отправьте эти строки копилоту – по ним он точно определит, что IntervalND::operator delete вызывает неправильную функцию освобождения (вероятно, глобальный ::operator delete или free вместо специального освобождения slab).

" - актуальные логи о проблеме, и: "struct Slab final {
char* const base;
char* current;
char* const end;

text
__forceinline Slab(void* memory, size_t usable) noexcept : base(static_cast<char*>(memory)), current(base), end(base + (usable & ~static_cast<size_t>(63u))) { } __forceinline void reset() noexcept { current = base; }

};

static constexpr size_t AGP_INTERVAL_SLAB_BYTES = 67108864u;

static tbb::enumerable_thread_specific<Slab*> tls( noexcept {
void* memory = _aligned_malloc(AGP_INTERVAL_SLAB_BYTES, 64u);
Slab* slab = static_cast<Slab*>(_aligned_malloc(sizeof(Slab), 64u));
new (slab) Slab(memory, AGP_INTERVAL_SLAB_BYTES);

text
char* p = slab->base; while (p < slab->end) { *p = 0; p += 4096u; } return slab; });

static __forceinline void AgpResetIntervalSlabs() noexcept {
for (auto it = tls.begin(); it != tls.end(); ++it) {
Slab* s = *it;
if (s != nullptr) s->reset();
}
}

static __declspec(noalias) __forceinline
float agp_nextup01(float x) noexcept
{
unsigned u = reinterpret_cast<unsigned>(&x);
if (u == 0x3f800000u) return x;
if ((u & 0x7fffffffU) == 0u) {
const unsigned one = 1u;
return reinterpret_cast<const float>(&one);
}
++u;
return reinterpret_cast<float>(&u);
}

static __declspec(noalias) __forceinline
float agp_nextdown01(float x) noexcept
{
unsigned u = reinterpret_cast<unsigned>(&x);
if ((u & 0x7fffffffU) == 0u) return x;
--u;
return reinterpret_cast<float>(&u);
}

static __declspec(noalias) __forceinline
bool agp_force_strict_interior(float x1, float x2, float& t) noexcept
{
const float lo = agp_nextup01(x1);
const float hi = agp_nextdown01(x2);

text
if (!(lo < hi)) return false; if (!(t > lo)) t = lo; else if (!(t < hi)) t = hi; return true;

}

struct IntervalND final {
float x1, x2, y1, y2, delta_y, ordinate_factor, N_factor, quadratic_term, M, R;
unsigned long long i1, i2;
float diam;

text
union { struct { unsigned short span_level; unsigned char idx1; unsigned char idx2; }; unsigned span_pack; }; static __declspec(noalias) __forceinline void* operator new(size_t sz) noexcept { Slab* s = tls.local(); const size_t chunk = (sz + 63u) & ~static_cast<size_t>(63u); if (s->current + chunk <= s->end) { char* r = s->current; s->current += chunk; return r; } return _aligned_malloc(chunk, 64u); } static __declspec(noalias) __forceinline void operator delete(void* p) noexcept { if (!p) return; Slab* s = tls.local(); const char* pc = static_cast<const char*>(p); if (pc < s->base || pc >= s->end) { _aligned_free(p); } } __declspec(noalias) __forceinline IntervalND(float _x1, float _x2, float _y1, float _y2, unsigned _idx1, unsigned _idx2) noexcept : x1(_x1) , x2(_x2) , y1(_y1) , y2(_y2) , delta_y(fmaf(_y2, 1.0f, -_y1)) , ordinate_factor(fmaf(fmaf(-y1, 1.0f, -y2), 2.0f, 0.0f)) , N_factor(0.0f) , quadratic_term(0.0f) , M(0.0f) , R(0.0f) , i1(0ull) , i2(0ull) , diam(0.0f) , span_pack((static_cast<unsigned>(_idx2 & 0xFFu) << 24) | (static_cast<unsigned>(_idx1 & 0xFFu) << 16)) { } __declspec(noalias) __forceinline void compute_span_level(const struct MortonND& map) noexcept; __declspec(noalias) __forceinline void set_metric(float d_alpha) noexcept { N_factor = d_alpha; if (idx1 == idx2) { const float inv = 1.0f / N_factor; quadratic_term = fmaf(inv, fmaf(delta_y, delta_y, 0.0f), 0.0f); M = fmaf(inv, fabsf(delta_y), 0.0f); } else { quadratic_term = 0.0f; M = 0.0f; } } __declspec(noalias) __forceinline void ChangeCharacteristic(float _m) noexcept { if (idx1 == idx2) R = fmaf(1.0f / _m, quadratic_term, fmaf(_m, N_factor, ordinate_factor)); else if (idx1 < idx2) R = fmaf(2.0f * _m, N_factor, -4.0f * y2); else R = fmaf(2.0f * _m, N_factor, -4.0f * y1); }

};

static_assert(sizeof(IntervalND) == 64);

using IntervalPtrVec =
std::vector<IntervalND*, boost::alignment::aligned_allocator<IntervalND*, 32u>>;

using IntervalHeap =
std::vector<IntervalND*, boost::alignment::aligned_allocator<IntervalND*, 32u>>;

static __declspec(noalias) __forceinline
bool ComparePtrND(const IntervalND* a, const IntervalND* b) noexcept
{
return a->R < b->R;
}

static __declspec(noalias) __forceinline
void heap_sift_up(IntervalHeap& H, size_t pos) noexcept
{
IntervalND* v = H[pos];
while (pos > 0u) {
const size_t parent = (pos - 1u) >> 1u;
if (!ComparePtrND(H[parent], v)) break;
H[pos] = H[parent];
pos = parent;
}
H[pos] = v;
}

static __declspec(noalias) __forceinline
void heap_sift_down(IntervalHeap& H, size_t pos) noexcept
{
const size_t n = H.size();
IntervalND* v = H[pos];
size_t child = pos * 2u + 1u;

text
while (child < n) { size_t best = child; const size_t right = child + 1u; if (right < n && ComparePtrND(H[best], H[right])) best = right; if (!ComparePtrND(v, H[best])) break; H[pos] = H[best]; pos = best; child = pos * 2u + 1u; } H[pos] = v;

}

static __declspec(noalias) __forceinline
void heap_fix_at(IntervalHeap& H, size_t pos) noexcept
{
if (pos > 0u && ComparePtrND(H[(pos - 1u) >> 1u], H[pos]))
heap_sift_up(H, pos);
else
heap_sift_down(H, pos);
}

static __declspec(noalias) __forceinline
void heap_erase_at(IntervalHeap& H, size_t pos) noexcept
{
const size_t last = H.size() - 1u;
if (pos != last) {
H[pos] = H[last];
H.pop_back();
heap_fix_at(H, pos);
}
else {
H.pop_back();
}
}

static __declspec(noalias) __forceinline
void AgpFillIntervalWire(IntervalWire& w, const IntervalND* I) noexcept
{
w.x1 = I->x1;
w.x2 = I->x2;
w.y1 = I->y1;
w.y2 = I->y2;
w.N_factor = I->N_factor;
w.quadratic_term = I->quadratic_term;
w.M = I->M;
w.diam = I->diam;
w.i1 = I->i1;
w.i2 = I->i2;
w.span_pack = I->span_pack;
}

static __declspec(noalias) __forceinline
IntervalND* AgpCreateIntervalFromWire(const IntervalWire& w) noexcept
{
IntervalND* I = new IntervalND(w.x1, w.x2, w.y1, w.y2, 0u, 0u);
I->i1 = w.i1;
I->i2 = w.i2;
I->diam = w.diam;
I->N_factor = w.N_factor;
I->quadratic_term = w.quadratic_term;
I->M = w.M;
I->span_pack = w.span_pack;
return I;
}

static __declspec(noalias) __forceinline
void heap_make(IntervalHeap& H) noexcept
{
std::make_heap(H.begin(), H.end(), ComparePtrND);
}

static __declspec(noalias) __forceinline
void heap_push(IntervalHeap& H, IntervalND* p) noexcept
{
H.emplace_back(p);
heap_sift_up(H, H.size() - 1u);
}

static __declspec(noalias) __forceinline
IntervalND* heap_pop_front(IntervalHeap& H) noexcept
{
IntervalND* top = H[0];
const size_t last = H.size() - 1u;
if (last != 0u) {
H[0] = H[last];
H.pop_back();
heap_sift_down(H, 0u);
}
else {
H.pop_back();
}
return top;
}

static __declspec(noalias) __forceinline
void RecomputeR_ConstM_Mixed_ND(IntervalND** __restrict data, size_t sz, float m) noexcept
{
size_t i = 0u;
const size_t limit = sz & ~3ull;
while (i < limit) {
data[i + 0u]->ChangeCharacteristic(m);
data[i + 1u]->ChangeCharacteristic(m);
data[i + 2u]->ChangeCharacteristic(m);
data[i + 3u]->ChangeCharacteristic(m);
i += 4u;
}
while (i < sz) {
data[i]->ChangeCharacteristic(m);
++i;
}
}

static __declspec(noalias) __forceinline
void RecomputeR_AffineM_Mixed_ND(IntervalND** __restrict data, size_t sz, float GF, float alpha) noexcept
{
size_t i = 0u;
const size_t limit = sz & ~3ull;

text
while (i < limit) { IntervalND* I0 = data[i + 0u]; IntervalND* I1 = data[i + 1u]; IntervalND* I2 = data[i + 2u]; IntervalND* I3 = data[i + 3u]; I0->ChangeCharacteristic(fmaf(GF, I0->N_factor, fmaf(alpha, I0->M, 0.0f))); I1->ChangeCharacteristic(fmaf(GF, I1->N_factor, fmaf(alpha, I1->M, 0.0f))); I2->ChangeCharacteristic(fmaf(GF, I2->N_factor, fmaf(alpha, I2->M, 0.0f))); I3->ChangeCharacteristic(fmaf(GF, I3->N_factor, fmaf(alpha, I3->M, 0.0f))); i += 4u; } while (i < sz) { IntervalND* I = data[i]; I->ChangeCharacteristic(fmaf(GF, I->N_factor, fmaf(alpha, I->M, 0.0f))); ++i; }

}

static __declspec(noalias) __forceinline
void RecomputeR_ConstM_AVX2_ND(IntervalND* __restrict const* arr, size_t n, float m) noexcept
{
__declspec(align(32)) const __m256 vm = _mm256_set1_ps(m), v2 = _mm256_set1_ps(2.0f);
__m256 vinvm = _mm256_rcp_ps(vm);
vinvm = _mm256_mul_ps(vinvm, _mm256_fnmadd_ps(vm, vinvm, v2));

text
size_t i = 0u, limit = n & ~7ull; alignas(32) float q[8], nf[8], od[8], out[8]; while (i < limit) { int k = 0; while (k < 8) { const IntervalND* p = arr[i + static_cast<size_t>(k)]; q[k] = p->quadratic_term; nf[k] = p->N_factor; od[k] = p->ordinate_factor; ++k; } __m256 vq = _mm256_load_ps(q); __m256 vnf = _mm256_load_ps(nf); __m256 vod = _mm256_load_ps(od); __m256 t = _mm256_fmadd_ps(vm, vnf, vod); __m256 res = _mm256_fmadd_ps(vq, vinvm, t); _mm256_store_ps(out, res); k = 0; while (k < 8) { arr[i + static_cast<size_t>(k)]->R = out[k]; ++k; } i += 8u; } while (i < n) { arr[i]->ChangeCharacteristic(m); ++i; }

}

static __declspec(noalias) __forceinline
void RecomputeR_AffineM_AVX2_ND(IntervalND* __restrict const* arr, size_t n, float GF, float alpha) noexcept
{
__declspec(align(32)) const __m256 vGF = _mm256_set1_ps(GF), va = _mm256_set1_ps(alpha), v2 = _mm256_set1_ps(2.0f);
size_t i = 0u, limit = n & ~7ull;
alignas(32) float ln[8], Mv[8], q[8], nf[8], od[8], out[8];

text
while (i < limit) { int k = 0; while (k < 8) { const IntervalND* p = arr[i + static_cast<size_t>(k)]; ln[k] = fmaf(-p->x1, 1.0f, p->x2); Mv[k] = p->M; q[k] = p->quadratic_term; nf[k] = p->N_factor; od[k] = p->ordinate_factor; ++k; } __m256 vln = _mm256_load_ps(ln); __m256 vM = _mm256_load_ps(Mv); __m256 vq = _mm256_load_ps(q); __m256 vnf = _mm256_load_ps(nf); __m256 vod = _mm256_load_ps(od); __m256 vm = _mm256_fmadd_ps(vGF, vln, _mm256_mul_ps(va, vM)); __m256 vinvm = _mm256_rcp_ps(vm); vinvm = _mm256_mul_ps(vinvm, _mm256_fnmadd_ps(vm, vinvm, v2)); __m256 t = _mm256_fmadd_ps(vm, vnf, vod); __m256 res = _mm256_fmadd_ps(vq, vinvm, t); _mm256_store_ps(out, res); k = 0; while (k < 8) { arr[i + static_cast<size_t>(k)]->R = out[k]; ++k; } i += 8u; } while (i < n) { const IntervalND* p = arr[i]; const float mi = fmaf(GF, fmaf(-p->x1, 1.0f, p->x2), fmaf(p->M, alpha, 0.0f)); arr[i]->R = fmaf(1.0f / mi, p->quadratic_term, fmaf(mi, p->N_factor, p->ordinate_factor)); ++i; }

}

__declspec(noalias) __forceinline
void IntervalND::compute_span_level(const MortonND& map) noexcept
{
unsigned s = 0u;
int d = 0;
while (d < map.dim) {
const unsigned long long varying = (i1 ^ i2) & map.pextMask[d];
s += static_cast<unsigned>(_mm_popcnt_u64(varying));
++d;
}

text
s += static_cast<unsigned>((map.levels - map.chunk_bits[0]) * map.dim); if (s > 11u) s = 11u; span_level = static_cast<unsigned short>(s);

}

__declspec(align(32)) struct TransitionGeomSample final {
alignas(32) float q[AGP_MAX_FULL_DIM]{};
float x = 0.0f, y = 0.0f, clearance = FLT_MAX;
};" - это то что связано со структурой интервал сейчас - теперь давай думать о том что мы можем с этим сделать и как решить проблему - давай сделаем так что искоренить сам корень чтобы работа слаб аллокатора и кастомный выделений и освобождений памяти под него была корректной - какие пути решения ты видишь

Pensiero

Сейчас вижу две самые вероятные корневые причины: либо у вас ломается не сам free, а жизненный цикл объектов в slab (reset/reuse при ещё живых IntervalND*), либо delete-путь у класса определён не полностью и в части сценариев вызывается не тот деаллокатор. Ниже разложу оба сценария и дам вариант исправления, который убирает сам класс ошибок.

Thought for 20s

Risposta

Да, давай разберём это как инженерно, а не “подлатать симптомы”.

По коду видно, что у тебя IntervalND сейчас живёт в bump/slab allocator с такой моделью:

  • operator new:
    • берёт память из thread-local slab, если влезает;
    • иначе уходит в _aligned_malloc.
  • operator delete:
    • если указатель вне диапазона текущего TLS-slab, вызывает _aligned_free;
    • если внутри slab, ничего не делает.

Это допустимая схема, но только если строго соблюдены 3 инварианта:

  1. объект, выданный из slab, никогда не освобождается поштучно;
  2. slab не reset()-ится, пока жив хотя бы один объект из него;
  3. delete всегда проверяет принадлежность указателя тому slab, из которого он реально был выделен, а не “какому-то текущему локальному”.

И вот именно здесь у тебя архитектурная слабость.


Что в текущем коде принципиально опасно

1. operator delete смотрит только в tls.local()

Вот это место:

cpp
static __declspec(noalias) __forceinline void operator delete(void* p) noexcept { if (!p) return; Slab* s = tls.local(); const char* pc = static_cast<const char*>(p); if (pc < s->base || pc >= s->end) { _aligned_free(p); } }

Логика тут такая: “если указатель не из моего текущего slab, значит он heap/fallback, освобожу _aligned_free”.

Это неэквивалентно “если указатель не из slab вообще”.

Почему это опасно

Если объект был выделен:

  • в другом TLS slab;
  • в другом worker/thread context;
  • в предыдущем жизненном цикле slab;
  • или просто tls.local() в момент delete вернул другой slab instance,

то указатель:

  • не принадлежит текущему s,
  • но при этом может принадлежать какому-то другому slab.

И тогда код ошибочно делает:

cpp
_aligned_free(p);

на память, которая вообще не была выделена через _aligned_malloc для этого объекта.

Это уже прямой корень heap/slab corruption.

Даже если в логах один TID=8040, это ещё не гарантирует, что tls.local() всегда указывает на тот же slab-экземпляр, из которого объект был получен. Для enumerable_thread_specific это обычно так в пределах потока, но архитектурно код всё равно хрупкий: delete определяет происхождение объекта по текущему состоянию потока, а не по факту выделения.


2. AgpResetIntervalSlabs() может убивать ещё живые объекты

Вот это ещё более критично:

cpp
static __forceinline void AgpResetIntervalSlabs() noexcept { for (auto it = tls.begin(); it != tls.end(); ++it) { Slab* s = *it; if (s != nullptr) s->reset(); } }

reset() просто откатывает bump pointer:

cpp
current = base;

То есть вся память slab считается “свободной”, без проверки живых объектов.

Если где-то ещё остались:

  • указатели в IntervalHeap,
  • указатели в IntervalPtrVec,
  • сериализованные/десериализованные IntervalWire -> IntervalND*,
  • временные очереди,
  • копии контейнеров,
  • отложенные задачи,

то после reset() ты получаешь reuse той же памяти поверх живых объектов. А затем:

  • старый указатель удаляют;
  • новый объект уже занял тот же адрес;
  • логгер видит “ALLOC → FREE_IGNORED → странный DUMP”;
  • служебные заголовки/канарейки уже не соответствуют ожиданиям.

Это очень хорошо совпадает с тем, что ты описал про STORE_Q и перенос состояний.


3. У alloc/free нет явного тега происхождения

Сейчас delete пытается угадать, откуда пришёл p, по диапазону адресов. Это плохой контракт.

У корректного кастомного аллокатора должно быть одно из двух:

  • либо у каждого блока есть явный tag/header, где записано происхождение;
  • либо память, полученная из slab, вообще никогда не проходит через обычный delete.

Сейчас у тебя ни то ни другое не доведено до железобетона.


Самый вероятный корень

Если говорить прямо: корень проблемы не в том, что slab allocator “плохо освобождает”, а в том, что модель владения памятью у IntervalND нестрогая.

Симптомы указывают на смесь из двух проблем:

  • IntervalND*, выданные из slab, продолжают жить за пределами ожидаемой “эпохи” slab;
  • operator delete принимает решение об освобождении на основании tls.local(), а это ненадёжно как источник истины.

Какие пути решения я вижу

Ниже от самого надёжного к наименее инвазивному.


Путь 1. Полностью убрать пообъектный delete для slab-объектов

Это лучший архитектурный вариант.

Идея

Сделать IntervalND arena-allocated object:

  • все объекты из slab живут до конца фазы/итерации/branch-run;
  • потом одним вызовом AgpResetIntervalSlabs() память возвращается целиком;
  • никаких delete p для slab-объектов вообще.

Fallback-объекты тоже лучше убрать, либо выделять их из отдельного arena block list.

Что это даёт

  • нет вопроса “как понять, slab это или heap”;
  • нет риска mismatch alloc/free;
  • нет ложных _aligned_free на slab-указателях;
  • delete-path для IntervalND вообще исчезает как класс проблем.

Как реализовать

Вариант А:

  • запретить delete для IntervalND;
  • создавать только через фабрику AgpMakeInterval(...);
  • уничтожение делать только через AgpResetIntervalSlabs() в конце эпохи.

Например:

cpp
struct IntervalND final { // ... private: static void operator delete(void*) = delete; static void operator delete(void*, std::size_t) = delete; };

И наружу:

cpp
template<class... Args> static __forceinline IntervalND* AgpMakeInterval(Args&&... args) noexcept { void* mem = IntervalArenaAlloc(sizeof(IntervalND), alignof(IntervalND)); return new (mem) IntervalND(std::forward<Args>(args)...); }

Когда этот путь подходит

Если IntervalND тривиально разрушаем или деструктор не нужен. По твоему коду это именно так.


Путь 2. Оставить delete, но сделать явную маркировку происхождения блока

Это лучший компромисс, если delete нужен.

Идея

Каждому выделению предшествует небольшой header, где пишется:

  • magic;
  • origin (slab или heap);
  • optional: owner slab pointer.

Тогда operator delete не угадывает, а точно знает, что делать.

Схема

cpp
enum class IntervalAllocKind : uint32_t { Slab = 0x534C4142, // 'SLAB' Heap = 0x48454150 // 'HEAP' }; struct alignas(16) IntervalAllocHeader { uint32_t magic; uint32_t kind; Slab* owner; uint32_t requested_size; uint32_t reserved; };

Выделение

cpp
static void* operator new(size_t sz) noexcept { constexpr size_t A = 64; const size_t total = (sizeof(IntervalAllocHeader) + sz + (A - 1)) & ~(A - 1); Slab* s = tls.local(); char* raw = nullptr; if (s->current + total <= s->end) { raw = s->current; s->current += total; auto* h = reinterpret_cast<IntervalAllocHeader*>(raw); h->magic = 0xDEADC0DEu; h->kind = static_cast<uint32_t>(IntervalAllocKind::Slab); h->owner = s; h->requested_size = static_cast<uint32_t>(sz); return raw + sizeof(IntervalAllocHeader); } raw = static_cast<char*>(_aligned_malloc(total, A)); if (!raw) return nullptr; auto* h = reinterpret_cast<IntervalAllocHeader*>(raw); h->magic = 0xDEADC0DEu; h->kind = static_cast<uint32_t>(IntervalAllocKind::Heap); h->owner = nullptr; h->requested_size = static_cast<uint32_t>(sz); return raw + sizeof(IntervalAllocHeader); }

Освобождение

cpp
static void operator delete(void* p) noexcept { if (!p) return; char* raw = static_cast<char*>(p) - sizeof(IntervalAllocHeader); auto* h = reinterpret_cast<IntervalAllocHeader*>(raw); if (h->magic != 0xDEADC0DEu) { __debugbreak(); // или лог fatal return; } if (h->kind == static_cast<uint32_t>(IntervalAllocKind::Heap)) { _aligned_free(raw); return; } // slab block: intentionally no-op }

Плюсы

  • delete перестаёт зависеть от tls.local();
  • cross-thread free становится безопасным в том смысле, что slab-блок не будет случайно отдан в _aligned_free;
  • логирование станет точным.

Минусы

  • sizeof(IntervalND)==64 сломается, если ты хочешь header “рядом” с объектом и прежнюю адресную модель;
  • но сам размер объекта не поменяется, поменяется только layout аллокации.

Это, на мой взгляд, наиболее практичное исправление, если ты хочешь минимально трогать остальной код.


Путь 3. Держать metadata вне объекта: page map / ownership map

Это более “allocator-grade” путь.

Идея

Не класть header перед объектом, а держать внешний реестр:

  • slab pages/ranges;
  • fallback allocations set/map;
  • optional owner slab.

Тогда delete проверяет реестр, а не tls.local().

Например:

  • std::pmr::unordered_set<void*> fallback_ptrs;
  • массив slab ranges;
  • или битовая карта по страницам.

Почему это хуже для твоей ситуации

Работать будет, но сложнее, и нужно синхронизировать доступ. Для локального класса IntervalND header проще и надёжнее.


Путь 4. Сделать slab объектом эпохи и передавать allocator явно

Это самый чистый дизайн, если готов чуть сильнее переписать.

Идея

Вообще убрать глобальный tls из IntervalND, и сделать так:

cpp
struct IntervalArena { Slab* slab; IntervalND* create(...); void reset(); };

И все функции, создающие интервалы, получают IntervalArena&.

Плюсы

  • исчезает скрытая глобальная зависимость;
  • не надо гадать, какой slab “текущий”;
  • можно строго контролировать lifetime.

Минусы

  • инвазивный рефакторинг.

Что я бы рекомендовал как реальное решение

Рекомендация: сочетание 1 + 2

Самая надёжная стратегия:

Вариант A — если delete не нужен по архитектуре

Сделать IntervalND strictly arena-only:

  • запретить обычный delete;
  • выделять только из arena/slab;
  • fallback через linked list of big blocks внутри arena, а не _aligned_malloc на каждый объект;
  • reset только в конце эпохи, когда точно нет живых IntervalND*.

Это почти идеал.

Вариант B — если delete всё-таки нужен

Сделать явный allocation header и добавить полный набор delete-overloads:

cpp
static void operator delete(void*) noexcept; static void operator delete(void*, std::size_t) noexcept; static void operator delete(void*, std::align_val_t) noexcept; static void operator delete(void*, std::size_t, std::align_val_t) noexcept;

Все они должны идти в один и тот же внутренний deallocate(p).

Это убирает риск, что компилятор/CRT в части сценариев уйдёт не туда.


Что обязательно нужно исправить даже при минимальном патче

1. Нельзя принимать решение по tls.local() в delete

Вот это надо выкинуть как сам принцип:

cpp
Slab* s = tls.local(); if (pc < s->base || pc >= s->end) { _aligned_free(p); }

Потому что delete должен определять происхождение блока по самому блоку, а не по текущему состоянию потока.


2. Нужен debug guard на reset

Перед AgpResetIntervalSlabs() надо уметь ответить: живы ли ещё объекты?

Хотя бы в debug:

  • счётчик allocations;
  • счётчик live objects;
  • поколение slab;
  • poison/reset stamping.

Например:

cpp
struct Slab final { char* const base; char* current; char* const end; uint32_t generation = 0; uint32_t live_count = 0; void reset() noexcept { ++generation; current = base; live_count = 0; } };

И при slab allocation:

cpp
++s->live_count;

А при delete slab-object:

cpp
--owner->live_count; // если вообще допускается поштучный delete

И перед reset в debug:

cpp
AGP_ASSERT(s->live_count == 0);

Если у тебя модель “пообъектный delete не нужен”, тогда live_count можно использовать только как детектор утечек эпохи.


3. Добавить проверку “указатель из slab, но не на границе чанка”

Очень полезно для ловли испорченных указателей:

cpp
uintptr_t off = static_cast<uintptr_t>(pc - s->base); AGP_ASSERT((off % 64u) == 0);

Хотя лучше проверять это не через текущий s, а через owner/header.


4. Добавить sized delete overload

Даже если кажется, что не нужен — добавь.

cpp
static void operator delete(void* p, std::size_t) noexcept { operator delete(p); }

И аналоги для aligned delete, если хочешь закрыть тему полностью.


Какой конкретно дефект я подозреваю сильнее всего по твоему коду

Если выбирать один главный:

Главное подозрение: reset() вызывается, пока старые IntervalND* ещё где-то живут

Потому что твоя модель slab — это “allocate-many, free-all-at-once”. А остальной код по контейнерам с сырыми указателями выглядит так, что у объектов может быть более длинная жизнь, чем одна фаза.

Особенно настораживают:

  • IntervalHeap и IntervalPtrVec хранят сырые IntervalND*;
  • есть AgpCreateIntervalFromWire, который заново материализует объект из wire;
  • есть STORE_Q, про который ты уже упомянул как про копирование состояний;
  • удаление отдельных объектов, судя по логам, всё же происходит.

Это очень похоже на смешение двух моделей:

  • arena lifetime
  • individual ownership

Их одновременно держать на сырых указателях очень опасно.


Практический план исправления

Я бы делал так по шагам.

Шаг 1. Немедленный безопасный патч

Убрать зависимость delete от tls.local() и ввести header.

Это быстро даст стабилизацию и позволит точно отличать:

  • slab block;
  • heap fallback;
  • битый указатель.

Шаг 2. В debug-сборке включить жёсткие инварианты

Добавить:

  • magic в header;
  • owner slab pointer;
  • generation;
  • poison on reset;
  • assert на delete after reset.

Например, если slab allocation хранит owner_generation, то при обращении/delete можно ловить use-after-reset.


Шаг 3. Привести lifetime к одной модели

Нужно выбрать одно:

либо

IntervalND — short-lived arena objects
и тогда:

  • delete не используется,
  • reset делается только на барьере эпохи.

либо

IntervalND — individually managed objects
и тогда:

  • slab allocator должен поддерживать корректный free,
  • free-list, headers, ownership, reclaim.

Но второй вариант для такого объекта обычно не стоит сложности. Для IntervalND лучше arena model.


Минимальный рабочий redesign, который я считаю лучшим

Ниже концепция, которая, скорее всего, искоренит проблему.

1. Убрать member operator new/delete из IntervalND

Вообще.

2. Ввести IntervalArena

cpp
struct IntervalArena { Slab* slab; void* allocate(size_t sz, size_t align = 64) noexcept; void reset() noexcept; };

3. Создание только через фабрику

cpp
template<class... Args> IntervalND* MakeInterval(IntervalArena& arena, Args&&... args) noexcept { void* mem = arena.allocate(sizeof(IntervalND), 64); return new (mem) IntervalND(std::forward<Args>(args)...); }

4. Никаких delete IntervalND*

Совсем.

5. Все контейнеры с IntervalND* очищаются до arena.reset()

И это становится контрактом системы.

Это убирает саму возможность mismatch alloc/free.


Если хочешь оставить текущий стиль с new/delete, вот конкретный safe patch

Вот что я бы сделал минимально.

cpp
enum : uint32_t { AGP_INTERVAL_MAGIC = 0xA61F4E2Du, AGP_KIND_SLAB = 0x534C4142u, AGP_KIND_HEAP = 0x48454150u }; struct alignas(16) IntervalHdr { uint32_t magic; uint32_t kind; Slab* owner; uint32_t size; }; static constexpr size_t AGP_INTERVAL_ALIGN = 64u; struct IntervalND final { // поля... static __declspec(noalias) __forceinline void* operator new(size_t sz) noexcept { const size_t total = (sizeof(IntervalHdr) + sz + (AGP_INTERVAL_ALIGN - 1u)) & ~static_cast<size_t>(AGP_INTERVAL_ALIGN - 1u); Slab* s = tls.local(); char* raw = nullptr; if (s->current + total <= s->end) { raw = s->current; s->current += total; auto* h = reinterpret_cast<IntervalHdr*>(raw); h->magic = AGP_INTERVAL_MAGIC; h->kind = AGP_KIND_SLAB; h->owner = s; h->size = static_cast<uint32_t>(sz); return raw + sizeof(IntervalHdr); } raw = static_cast<char*>(_aligned_malloc(total, AGP_INTERVAL_ALIGN)); if (!raw) return nullptr; auto* h = reinterpret_cast<IntervalHdr*>(raw); h->magic = AGP_INTERVAL_MAGIC; h->kind = AGP_KIND_HEAP; h->owner = nullptr; h->size = static_cast<uint32_t>(sz); return raw + sizeof(IntervalHdr); } static __declspec(noalias) __forceinline void operator delete(void* p) noexcept { if (!p) return; char* raw = static_cast<char*>(p) - sizeof(IntervalHdr); auto* h = reinterpret_cast<IntervalHdr*>(raw); if (h->magic != AGP_INTERVAL_MAGIC) { __debugbreak(); return; } if (h->kind == AGP_KIND_HEAP) { _aligned_free(raw); return; } // slab allocation: no-op } static __declspec(noalias) __forceinline void operator delete(void* p, size_t) noexcept { operator delete(p); } };

Это уже на порядок безопаснее текущего решения.


Резюме в одну фразу

Корень проблемы не в slab как таковом, а в том, что IntervalND сейчас живёт сразу в двух несовместимых моделях управления памятью — arena lifetime и индивидуальный delete — при этом delete пытается определить происхождение блока по tls.local(), что концептуально ненадёжно.


Мой совет:

  • лучшее решение — перевести IntervalND в чистую arena-модель без delete;
  • минимально-инвазивное надёжное решение — добавить allocation header с явным tag/owner и убрать логику на tls.local() из delete;
  • параллельно проверить, что AgpResetIntervalSlabs() не вызывается до полной очистки всех контейнеров с IntervalND*.

Я могу следующим сообщением дать тебе уже готовый, аккуратно оформленный патч в стиле твоего файла: либо arena-only redesign, либо минимальный safe-patch с header + all delete overloads.

Condividi questa Q&A