Начинающий
Начинающий
- Статус
- Оффлайн
- Регистрация
- 25 Фев 2026
- Сообщения
- 6
- Реакции
- 2
Всем, снова, привет! 
После предыдущего разбора с std::pmr::vector я решил глубже исследовать поведение ассоциативных контейнеров при работе с пользовательскими аллокаторами и huge pages. Экспериментирую с высоконагруженным в памяти хранилищем (ключ 64-битный хеш, небольшая структура). Задача: максимизировать плотность упаковки данных и снизить накладные расходы на page walk.
Логика подсказывала перевести всё на huge pages (2M) через собственный memory_resource, который делает mmap с MAP_HUGETLB, и получить скорость за счёт уменьшения количества записей в TLB. Однако практика оказалась сложнее
Схема эксперимента(мойпсевдокод):
```cpp
// ресурс, выдающий память из заранее зарезервированного пула на huge page
class huge_page_resource : public std::pmr::memory_resource {
void* do_allocate(size_t bytes, size_t alignment) override {
// вылеляю из huge-страницы, но без доп выравнивания
return pool_aligned_alloc(bytes, alignment, huge_page_pool);
}
другой код
};
// бенчмарк через вставку N элементов, затем поиск случайных ключей
void benchmark() {
huge_page_resource res;
std::pmr::unordered_map<uint64_t, Value> map{&res};
for (int i = 0; i < N; ++i) {
map.emplace(/* ключ */, /* значение */);
}
// поиск
for (int i = 0; i < M; ++i) {
auto it = map.find(rand_key());
// ...
}
}
Я ждал, что за счёт огромных страниц должно сократиться число page walks, особенно при большом объёме данных (> десятков миллионов записей) TLB промахи упадут и производительность вырастит.
Но при размере словаря около 50 млн записей (данные занимают несколько гигабайт) производительность поиска на huge-страницах оказалась на 20% ниже, чем при использовании обычного std::pmr::new_delete_resource() (т.е malloc/new).
perf stat показал:
С new_delete_resource высокие показатели L1/L2 cache misses, но мало промахов L3 и умеренное количество dTLB-load-misses.
С huge_page_resource dTLB-load-misses упали в 4 раза (как я и думал), но резко выросли L2/L3 cache misses и, что самое страшное, существенно увеличилось количество промахов в iTLB (хотя код исполняется с тех же страниц).
фрагмент поиска, сгенерированный Clang 17:
```assembly
; new_delete_resource
.Lfind_loop:
mov r10, qword ptr [rdx + 8] ; загрузка next-узла
cmp [rdx], rbx ; сравнение ключа
je .Lfound
mov rdx, r10
test rdx, rdx
jne .Lfind_loop
```
```assembly
; huge_page_resource
.Lfind_loop_huge:
mov r10, qword ptr [rdx + 16] ; обратите внимание: смещение 16 вместо 8
vmovq xmm0, qword ptr [rdx] ; SIMD-загрузка ключа
vpextrq rax, xmm0, 1 ; извлечение старшей части?
cmp eax, ebx ; сравнение только младшей части
je .Lfound_huge
mov rdx, r10
test rdx, rdx
jne .Lfind_loop_huge
```
Обратите внимание, что для huge-ресурса компилятор сгенерировал код, использующий SIMD-регистры (vmovq/vpextrq) даже для простого сравнения 64-битного ключа, и смещения в узлах изменились на16 вместо 8. Похоже, что из-за особенностей выравнивания, гарантированного аллокатором (huge page пул выдаёт адреса с большим выравниванием?), компилятор решил, что можно оптимизировать доступ к узлам, упаковывая ключ и хеш в 16 байт и используя векторные инструкции. Но это привело к увеличению размера кода и, возможно, к декодированию инструкций, которые хуже предсказываются и занимают больше места в uOP-кэше.
Вопросы к вам:
1. Может ли аллокатор, возвращающий адреса с повышенным выравниванием (например, из-за того, что huge page пул выровнен по 2M), влиять на внутреннюю компоновку узлов? Насколько я понимаю, стандартный аллокатор через rebind используется для выделения сырой памяти под узел, и структура узла определяется реализацией STL. Однако способен ли компилятор, видя выравнивание выше alignof(max_align_t), изменить раскладку полей узла или сгенерировать иной код доступа, как в моёмпсевдокоде? Это нормальноее поведение или баг компилятора?
2. Всем понятно, что адреса внутри одной huge page имеют одинаковые старшие биты. Эти биты могут использоваться для индексирования в кэше (если кэш имеет физическую индексацию, но в Intel обычно VIPT). Может ли такое совпадение вызывать конфликты ассоциативности и рост cache misses даже при случайном доступе? Я замерял: latency случайного доступа выросла, хотя TLB промахов стало меньше. Кто-то исследовал подобный эффект?
3. Почему iTLB-misses выросли, если код исполняется с тех же huge-страниц? Кодовая секция не менялась, она лежит в обычных 4K страницах. Но увеличение числа инструкций в цикле из-за SIMD-пролога могло привести к тому, что горячий цикл перестал помещаться в uOP-кэш или в L1 ICache? Однако iTLB промахи обычно связаны с обращением к новым страницам кода. Возможно, при использовании huge-страниц данных изменился паттерн переходов, и процессор начал чаще обращаться к коду обработчиков страничных ошибок? Или это просто артефакт измерения? Может, кто то сталкивался с подобным при включении MAP_HUGETLB?
Буду благодарен за ссылки на исследования производительности huge pages для хеш-таблиц, а также за опыт использования pmr с нестандартными выравниваниями. Если кто-то может объяснить, почему компилятор вдруг решил применить SIMD для скалярного сравнения, и как это отключить без потери других оптимизаций, тоже буду рад!!
Всем спасибо!

После предыдущего разбора с std::pmr::vector я решил глубже исследовать поведение ассоциативных контейнеров при работе с пользовательскими аллокаторами и huge pages. Экспериментирую с высоконагруженным в памяти хранилищем (ключ 64-битный хеш, небольшая структура). Задача: максимизировать плотность упаковки данных и снизить накладные расходы на page walk.
Логика подсказывала перевести всё на huge pages (2M) через собственный memory_resource, который делает mmap с MAP_HUGETLB, и получить скорость за счёт уменьшения количества записей в TLB. Однако практика оказалась сложнее
Схема эксперимента(мойпсевдокод):
```cpp
// ресурс, выдающий память из заранее зарезервированного пула на huge page
class huge_page_resource : public std::pmr::memory_resource {
void* do_allocate(size_t bytes, size_t alignment) override {
// вылеляю из huge-страницы, но без доп выравнивания
return pool_aligned_alloc(bytes, alignment, huge_page_pool);
}
другой код
};
// бенчмарк через вставку N элементов, затем поиск случайных ключей
void benchmark() {
huge_page_resource res;
std::pmr::unordered_map<uint64_t, Value> map{&res};
for (int i = 0; i < N; ++i) {
map.emplace(/* ключ */, /* значение */);
}
// поиск
for (int i = 0; i < M; ++i) {
auto it = map.find(rand_key());
// ...
}
}
Я ждал, что за счёт огромных страниц должно сократиться число page walks, особенно при большом объёме данных (> десятков миллионов записей) TLB промахи упадут и производительность вырастит.
Но при размере словаря около 50 млн записей (данные занимают несколько гигабайт) производительность поиска на huge-страницах оказалась на 20% ниже, чем при использовании обычного std::pmr::new_delete_resource() (т.е malloc/new).
perf stat показал:
С new_delete_resource высокие показатели L1/L2 cache misses, но мало промахов L3 и умеренное количество dTLB-load-misses.
С huge_page_resource dTLB-load-misses упали в 4 раза (как я и думал), но резко выросли L2/L3 cache misses и, что самое страшное, существенно увеличилось количество промахов в iTLB (хотя код исполняется с тех же страниц).
фрагмент поиска, сгенерированный Clang 17:
```assembly
; new_delete_resource
.Lfind_loop:
mov r10, qword ptr [rdx + 8] ; загрузка next-узла
cmp [rdx], rbx ; сравнение ключа
je .Lfound
mov rdx, r10
test rdx, rdx
jne .Lfind_loop
```
```assembly
; huge_page_resource
.Lfind_loop_huge:
mov r10, qword ptr [rdx + 16] ; обратите внимание: смещение 16 вместо 8
vmovq xmm0, qword ptr [rdx] ; SIMD-загрузка ключа
vpextrq rax, xmm0, 1 ; извлечение старшей части?
cmp eax, ebx ; сравнение только младшей части
je .Lfound_huge
mov rdx, r10
test rdx, rdx
jne .Lfind_loop_huge
```
Обратите внимание, что для huge-ресурса компилятор сгенерировал код, использующий SIMD-регистры (vmovq/vpextrq) даже для простого сравнения 64-битного ключа, и смещения в узлах изменились на16 вместо 8. Похоже, что из-за особенностей выравнивания, гарантированного аллокатором (huge page пул выдаёт адреса с большим выравниванием?), компилятор решил, что можно оптимизировать доступ к узлам, упаковывая ключ и хеш в 16 байт и используя векторные инструкции. Но это привело к увеличению размера кода и, возможно, к декодированию инструкций, которые хуже предсказываются и занимают больше места в uOP-кэше.
Вопросы к вам:
1. Может ли аллокатор, возвращающий адреса с повышенным выравниванием (например, из-за того, что huge page пул выровнен по 2M), влиять на внутреннюю компоновку узлов? Насколько я понимаю, стандартный аллокатор через rebind используется для выделения сырой памяти под узел, и структура узла определяется реализацией STL. Однако способен ли компилятор, видя выравнивание выше alignof(max_align_t), изменить раскладку полей узла или сгенерировать иной код доступа, как в моёмпсевдокоде? Это нормальноее поведение или баг компилятора?
2. Всем понятно, что адреса внутри одной huge page имеют одинаковые старшие биты. Эти биты могут использоваться для индексирования в кэше (если кэш имеет физическую индексацию, но в Intel обычно VIPT). Может ли такое совпадение вызывать конфликты ассоциативности и рост cache misses даже при случайном доступе? Я замерял: latency случайного доступа выросла, хотя TLB промахов стало меньше. Кто-то исследовал подобный эффект?
3. Почему iTLB-misses выросли, если код исполняется с тех же huge-страниц? Кодовая секция не менялась, она лежит в обычных 4K страницах. Но увеличение числа инструкций в цикле из-за SIMD-пролога могло привести к тому, что горячий цикл перестал помещаться в uOP-кэш или в L1 ICache? Однако iTLB промахи обычно связаны с обращением к новым страницам кода. Возможно, при использовании huge-страниц данных изменился паттерн переходов, и процессор начал чаще обращаться к коду обработчиков страничных ошибок? Или это просто артефакт измерения? Может, кто то сталкивался с подобным при включении MAP_HUGETLB?
Буду благодарен за ссылки на исследования производительности huge pages для хеш-таблиц, а также за опыт использования pmr с нестандартными выравниваниями. Если кто-то может объяснить, почему компилятор вдруг решил применить SIMD для скалярного сравнения, и как это отключить без потери других оптимизаций, тоже буду рад!!
Всем спасибо!
