Я создаю класс дерева AVL, который будет иметь фиксированное максимальное количество элементов. Поэтому я решил вместо того, чтобы выделять каждый элемент сам по себе, я бы сразу выделил весь фрагмент и использовал растровое изображение для назначения новой памяти при необходимости.Производительность пользовательского распределителя
Моего код распределения/открепление:
avltree::avltree(UINT64 numitems)
{
root = NULL;
if (!numitems)
buffer = NULL;
else {
UINT64 memsize = sizeof(avlnode) * numitems + bitlist::storagesize(numitems);
buffer = (avlnode *) malloc(memsize);
memmap.init(numitems, buffer + numitems);
memmap.clear_all();
freeaddr = 0;
}
}
avlnode *avltree::newnode(keytype key)
{
if (!buffer)
return new avlnode(key);
else
{
UINT64 pos;
if (freeaddr < memmap.size_bits)
pos = freeaddr++;
else
pos = memmap.get_first_unset();
memmap.set_bit(pos);
return new (&buffer[pos]) avlnode(key);
}
}
void avltree::deletenode(avlnode *node)
{
if (!buffer)
delete node;
else
memmap.clear_bit(node - buffer);
}
Для того, чтобы использовать стандартные новый/удалить, я должен построить дерево с количество_элементами == 0. Для того, чтобы использовать свой собственный аллокатор, я просто передать номер предметов. Все функции встроены для максимальной производительности.
Все это прекрасно и денди, но мой собственный распределитель на 20% медленнее, чем новый/удалить. Теперь, я знаю, насколько сложны распределители памяти, нет никакого способа, чтобы этот код мог работать быстрее, чем поиск массива + один бит, но это точно так. Что еще хуже: мой деллакоратор медленнее, даже если я удалю из него весь код?!?
Когда я проверяю сборку, мой код распределителя управляется инструкциями QWORD PTR, связанными с растровым, avltree или avlnode. Это не похоже на новый путь/delete.
Например, выход сборки avltree :: newnode:
;avltree::newnode, COMDAT
mov QWORD PTR [rsp+8], rbx
push rdi
sub rsp, 32
;if (!buffer)
cmp QWORD PTR [rcx+8], 0
mov edi, edx
mov rbx, rcx
jne SHORT [email protected]
; return new avlnode(key);
mov ecx, 24
call [email protected][email protected] ; operator new
jmp SHORT [email protected]
;[email protected]:
;else {
; UINT64 pos;
; if (freeaddr < memmap.size_bits)
mov r9, QWORD PTR [rcx+40]
cmp r9, QWORD PTR [rcx+32]
jae SHORT [email protected]
; pos = freeaddr++;
lea rax, QWORD PTR [r9+1]
mov QWORD PTR [rcx+40], rax
; else
jmp SHORT [email protected]
[email protected]:
; pos = memmap.get_first_unset();
add rcx, 16
call [email protected]@@QEAA_KXZ ; bitlist::get_first_unset
mov r9, rax
[email protected]:
; memmap.set_bit(pos);
mov rcx, QWORD PTR [rbx+16] ;data[bindex(pos)] |= bmask(pos);
mov rdx, r9 ;return pos/(sizeof(BITINT) * 8);
shr rdx, 6
lea r8, QWORD PTR [rcx+rdx*8] ;data[bindex(pos)] |= bmask(pos);
movzx ecx, r9b ;return 1ull << (pos % (sizeof(BITINT) * 8));
mov edx, 1
and cl, 63
shl rdx, cl
; return new (&buffer[pos]) avlnode(key);
lea rcx, QWORD PTR [r9+r9*2]
; File c:\projects\vvd\vvd\util\bitlist.h
or QWORD PTR [r8], rdx ;data[bindex(pos)] |= bmask(pos)
; 195 : return new (&buffer[pos]) avlnode(key);
mov rax, QWORD PTR [rbx+8]
lea rax, QWORD PTR [rax+rcx*8]
; [email protected]:
test rax, rax
je SHORT [email protected]
; avlnode constructor;
mov BYTE PTR [rax+4], 1
mov QWORD PTR [rax+8], 0
mov QWORD PTR [rax+16], 0
mov DWORD PTR [rax], edi
; 196 : }
; 197 : }
; [email protected]:
mov rbx, QWORD PTR [rsp+48]
add rsp, 32 ; 00000020H
pop rdi
ret 0
[email protected]@@[email protected]@[email protected] ENDP ; avltree::newnode
_TEXT ENDS
Я проверил многократно превышающий выход компиляции, когда я создаю мой avltree с/по умолчанию пользовательского распределителя и остается тем же самым в данном регионе кода. Я попытался удалить/заменить все соответствующие части без значительного эффекта.
Честно говоря, я ожидал, что компилятор включит все это, так как переменных очень мало. Я надеялся на все, кроме самих объектов avlnode, которые будут помещены в регистры, но это, похоже, не так.
Тем не менее, разница в скорости заметно измерима. Я не звоню на 3 секунды на 10 миллионов узлов, вставленных медленно, но я ожидал, что мой код будет быстрее, а не медленнее, чем общий распределитель (2,5 секунды). Это особенно важно для более медленного deallocator, который работает медленнее, даже когда весь код удаляется из него.
Почему это медленнее?
Редактировать: Благодарим всех вас за отличные мысли об этом. Но я хотел бы еще раз подчеркнуть, что проблема заключается не столько в моем методе распределения, сколько в субоптимальном способе использования переменных: весь класс avltree содержит только 4 переменные UINT64, только у списка битлингов есть 3.
Однако, несмотря на это, компилятор не оптимизирует это в регистры. Он настаивает на инструкциях QWORD PTR, которые на несколько порядков ниже. Это потому, что я использую классы? Должен ли я перейти к переменным C/plain?
Поцарапать это. Глупый я. У меня есть весь код avltree, и все не может быть в реестрах.
Кроме того, у меня полная потеря, почему мой деллакоратор все равно будет медленнее, даже если я удалю из него ВСЕ код. Тем не менее QueryPerformanceCounter говорит мне именно об этом. Это безумие даже думать, что: тот же deallocator также получает вызов для нового/удалить код, и он должен удалить узел ... Он не должен ничего делать для моего пользовательского распределителя (когда я стираю код).
Редактировать 2: Теперь я полностью удалил список бит и осуществил отслеживание свободного места через односвязный список. Функция avltree :: newnode теперь намного более компактна (21 инструкция для моего пользовательского пути распределителя, из которых 7 - это операторы QWORD PTR, имеющие отношение к avltree и 4 - для конструктора avlnode). Конечный результат (время) уменьшился с ~ 3 секунд до ~ 2.95 секунд для 10 миллионов распределений.
Редактирование 3: Я также переписал весь код таким образом, что теперь все обрабатывается односвязным списком. Теперь класс avltree имеет только два соответствующих члена: root и first_free. Разница в скорости остается.
Edit4: Перегруппировка код и, глядя на цифры производительности, эти вещи, что помогло больше всего:
- Как было отмечено всеми участниками, имея битовую карту там было просто плохо. Удалено в пользу отдельного списка свободных слотов.
- Локальность кода: добавление зависимых функций (управление деревьями avl) в локально-локальный класс вместо того, чтобы объявить их глобально, помогло приблизительно 15% с кодовой скоростью (3 сек. -> 2,5 сек).
- Размер структуры avlnode : просто добавив
#pragma pack(1)
перед тем структура декларации уменьшить время выполнения еще на 20% (2,5 сек -> 2 сЕК)
Edit 5:
Поскольку querstion, кажется, был довольно популярным, Я опубликовал окончательный полный код в качестве ответа ниже. Я вполне доволен своей работой.
Вы, кажется, выполняете линейный поиск с 'get_first_unset', как только он был заполнен, это повредит производительности. Традиционный выбор структуры данных для этого случая был бы единственным связанным свободным списком со следующим указателем, накладывающим распределенные данные, тем самым избегая поиска. У стандартного распределителя, вероятно, есть пара таких специализированных распределителей пула для объектов фиксированной длины, хотя для обработки общего случая (для определения размера объекта, блокировки и т. Д.) Он должен по-прежнему страдать от некоторых дополнительных накладных расходов. – doynax
Да, но для эталона линейный поиск не вызван из-за ярлыка freeaddr. – velis