Начинающий
Начинающий
- Статус
- Оффлайн
- Регистрация
- 25 Фев 2026
- Сообщения
- 6
- Реакции
- 2
Всех приветствую!

Недавно проводил рефакторинг низкоуровневого ядра обработки данных реального времени. В проекте активно используются контейнеры с полиморфными аллокаторами (PMR). Упрощенная схема: есть несколько пулов памяти, настроенных на разные размеры блоков, которые питаются из одного mmap-региона с флагом MAP_HUGETLB (2M страницы)..
Столкнулся с аномалией производительности, которую не могу объяснить с точки зрения микроархитектуры.
Суть проблемы:
При использовании пользовательского аллокатора (на основе std::pmr::memory_resource), который выделяет память с выравниванием на границу 64 байт (для избежания ложного шаринга кэш-линий), пропускная способность контейнера упала на примерно 15% по сравнению с версией, где использовался new/delete (или даже сырой malloc с выравниванием по умолчанию).
Код для воспроизведения сути (мойпсевдокод):
```cpp
// упрощенный аллокатор, всегда возвращающий адреса, кратные 64
class aligned_64_resource : public std::pmr::memory_resource {
void* do_allocate(size_t bytes, size_t alignment) override {
// alignment игнорируем, форсируем 64
return aligned_alloc(64, bytes);
}
// ... остальное
};
// Hot path (очень упрощенно)
void process_data(std::pmr::vector<uint64_t>& vec) {
for (size_t i = 0; i < vec.size(); ++i) {
vec = transform(vec); // простая целаячисленная операция
}
}
Что я замерил (perf):
1. С std::pmr::vector + default_resource: 3.2 ns/element.
2. С std::pmr::vector + aligned_64_resource: 3.7 ns/element.
3. Разбор perf stat: Основная разница ушла не в кэшпромахи L1/L2 (данных), как я ожидал, а в dTLB-load-misses и itlb-load-misses (хотя код исполняется с тех же страниц)
При анализе карты памяти оказалось, что при форсированном выравнивании в 64 байта аллокатор начинает размазывать объекты по физическим страницам таким образом, что виртуально-непрерывный буфер вектора отображается на гораздо большее количество разных huge pages (2M), чем при стандартном выравнивании. То есть плотность упаковки данных на одну физическую huge page падает.
Мои вопросы к вам:
1. Кто то сталкивался с тем, что TLB (особенно L2 TLB) может становиться узким местом даже при линейном проходе по памяти, если виртуальный адрес слишком часто скакает между разными huge pages? Я всегда считал, что TLB покрывает всю страницу целиком, и смена страницы на каждой 64-байтной границе не должна влиять, так как мы всё еще внутри той же виртуальной страницы 2M. Но perf показывает обратное.
2. Может ли быть так, что при выравнивании в 64 байта компилятор (в моем случае Clang 17 с -O3 -march=native) генерирует иной код для доступа к элементам, используя инструкции с поддержкой AVX-512 (маски и т.д.), которые имеют
другой паттерн доступа к памяти, провоцирующий большее количество page walks? Если да, то как принудительно заставить его генерировать scalar код для чистоты эксперимента?
3. Посмотрите на приведенный ниже листинг. Это результат компиляции цикла с aligned_64_resource.
```assembly
.LBB0_5: ; векторизованная версия
; aligned_64_resource
vmovdqu64 zmm0, zmmword ptr [rdx + 4*rax] ; загрузка 64 байт
; ... какаято логика ...
vmovdqu64 zmmword ptr [rdx + 4*rax], zmm0
add rax, 16
cmp rax, rcx
jb .LBB0_5
Почему здесь используется vmovdqu64 невыровненный du доступ, если я гарантирую выравнивание в 64 байта компилятору через __builtin_assume_aligned внутри аллокатора? Это баг моей аннотации или особенность генерации кода для PMR контейнеров, где информация о выравнивании теряется на уровне типов итераторов?
Буду благодарен за ссылки на статьи о взаимодействии пользовательских аллокаторов, huge pages и auto-векторизации. Особенно интересует поведение TLB при фрагментации данных внутри 2M страницы.
Всем спасибо и хорошего вечера!