Аллокатор памяти определяет то, как распределяется память в системе, точнее как она выделяется и как освобождается приложениями. В зависимости от разных методов “аллокации”, мы можем получить существенное увеличение производительности для конкретных приложений.
Malloc
По умолчанию в Solaris 11 используется именно он. Вызов malloc не только увеличить адресное пространство, доступное процессу, но также связан со случайным доступом к памяти (Random Access Memory). Malloc по прежнему увеличивает адресное пространство, но не выделяет памяти, пока соответствующая страница (в памяти) не будет создана.
По умолчанию, большинство операционных систем UNIX использовать версию malloc () или free (), которая находится в Libc. В Solaris доступ malloc и free контролируется по-процессной блокировкой. Для определения блокировок, используется prstat с параметрами -mL и частотой обновления 1 секунда.
Ниже приведёт пример для тестового 2-х поточного приложения (malloc_test), один поток использует malloc(), а другой – free():
PID USERNAME USR SYS TRP TFL DFL LCK SLP LAT VCX ICX SCL SIG PROCESS/LWPID 4050 root 100 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 51 0 0 malloc_test/2 4050 root 97 3.0 0.0 0.0 0.0 0.0 0.0 0.0 0 53 8K 0 malloc_test/3
Для 8-ми поточного приложения это будет выглядеть так:
PID USERNAME USR SYS TRP TFL DFL LCK SLP LAT VCX ICX SCL SIG PROCESS/LWPID 4054 root 100 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 52 25 0 malloc_test/8 4054 root 100 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 52 23 0 malloc_test/7 4054 root 100 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 54 26 0 malloc_test/6 4054 root 100 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 51 25 0 malloc_test/9 4054 root 94 0.0 0.0 0.0 0.0 5.5 0.0 0.0 23 51 23 0 malloc_test/3 4054 root 94 0.0 0.0 0.0 0.0 5.6 0.0 0.0 25 48 25 0 malloc_test/4 4054 root 94 0.0 0.0 0.0 0.0 6.3 0.0 0.0 26 49 26 0 malloc_test/2 4054 root 93 0.0 0.0 0.0 0.0 6.7 0.0 0.0 25 50 25 0 malloc_test/5
А для 16-ти так:
PID USERNAME USR SYS TRP TFL DFL LCK SLP LAT VCX ICX SCL SIG PROCESS/LWPID 4065 root 63 37 0.0 0.0 0.0 0.0 0.0 0.0 51 222 .4M 0 malloc_test/31 4065 root 72 26 0.0 0.0 0.0 1.8 0.0 0.0 42 219 .3M 0 malloc_test/21 4065 root 66 30 0.0 0.0 0.0 4.1 0.0 0.0 47 216 .4M 0 malloc_test/27 4065 root 74 22 0.0 0.0 0.0 4.2 0.0 0.0 28 228 .3M 0 malloc_test/23 4065 root 71 13 0.0 0.0 0.0 15 0.0 0.0 11 210 .1M 0 malloc_test/30 4065 root 65 9.0 0.0 0.0 0.0 26 0.0 0.0 10 186 .1M 0 malloc_test/33 4065 root 37 28 0.0 0.0 0.0 35 0.0 0.0 36 146 .3M 0 malloc_test/18 4065 root 38 27 0.0 0.0 0.0 35 0.0 0.0 35 139 .3M 0 malloc_test/22 4065 root 58 0.0 0.0 0.0 0.0 42 0.0 0.0 28 148 40 0 malloc_test/2 4065 root 57 0.0 0.0 0.0 0.0 43 0.0 0.0 5 148 14 0 malloc_test/3 4065 root 37 8.1 0.0 0.0 0.0 55 0.0 0.0 12 112 .1M 0 malloc_test/32 4065 root 41 0.0 0.0 0.0 0.0 59 0.0 0.0 40 108 44 0 malloc_test/13 4065 root 23 15 0.0 0.0 0.0 62 0.0 0.0 23 88 .1M 0 malloc_test/29 4065 root 33 2.9 0.0 0.0 0.0 64 0.0 0.0 7 91 38K 0 malloc_test/24 4065 root 33 0.0 0.0 0.0 0.0 67 0.0 0.0 42 84 51 0 malloc_test/12 4065 root 32 0.0 0.0 0.0 0.0 68 0.0 0.0 1 82 2 0 malloc_test/14 4065 root 29 0.0 0.0 0.0 0.0 71 0.0 0.0 5 78 10 0 malloc_test/8 4065 root 27 0.0 0.0 0.0 0.0 73 0.0 0.0 5 72 7 0 malloc_test/16 4065 root 18 0.0 0.0 0.0 0.0 82 0.0 0.0 3 50 6 0 malloc_test/4 4065 root 2.7 0.0 0.0 0.0 0.0 97 0.0 0.0 7 9 18 0 malloc_test/11 4065 root 2.2 0.0 0.0 0.0 0.0 98 0.0 0.0 3 7 5 0 malloc_test/17
Если для 2 и 8 поточных приложений, процессы тратили всё своё время на выполнение заданий, то в 16-ти поточном некоторые потоки тратят время на ожидание блокировки. Но какой именно блокировки? В этом нам поможет утилита plockstat (точнее скрипт на dtrace). Запустим её:
# plockstat -C -e 10 -p `pgrep malloc_test` 0 Mutex block Count nsec Lock Caller 72 306257200 libc.so.1`libc_malloc_lock malloc_test`malloc_thread+0x6e8 64 321494102 libc.so.1`libc_malloc_lock malloc_test`free_thread+0x70ckstat -c kmem_cache
Мы видим насколько часто (значение Count) и среднее время на ожидание блокировки (значение nsec). Итого, 136 раз мы ожидаем блокировок по 1/3 секунды, что очень расточительно.
Появление многопоточных приложений требует мультипоточного аллокатора памяти. Некоторые, наиболее известные, которые используются в Solaris: mtmalloc, libumem, hoard. О них будет рассказано ниже.
Hoard
Hoard стремится обеспечить скорость и масштабируемость, избежать ложных обменов, а также обеспечить низкую фрагментацию. Ложное разделение происходит, когда потоки на разных процессорах случайно делят строки кэша. Ложного разделение ухудшает эффективность использования кэш-памяти, что негативно влияет на производительность. Фрагментация происходит, когда фактическое потребление памяти процессом, превышает реальные потребности памяти приложения. Вы можете подумать о фрагментации как неиспользуемое пространство адресов или своего рода утечки памяти. Это может произойти, когда для каждого пула поток имеет адресное пространство для распределения, но другой поток не может его использовать.
Hoard поддерживает по-поточную кучу и одну глобальную кучу. Динамическое распределение адресного пространства между двумя типами кучи позволяет hoard’y уменьшить или предотвратить фрагментацию, и это позволяет потокам повторно использовать адресное пространство первоначально выделенное другим потоком.
Хэш-алгоритм, основанный на идентификаторе потока карты для куч. Отдельные кучи расположены в виде серии суперблоки, где каждая является кратным размеру страницы системы. Распределения больше половины суперблока производится с использованием mmap() и освобождается через munmap().
Все суперблоки одинакового размера. Пустые суперблоки повторно используют и может быть назначен новый класс. Эта функция уменьшает фрагментацию. Что бы узнать размер суперблока, откройте horde.h и найдите строку
# define SUPERBLOCK_SIZE 65536.
Отсюда, любое распределение больше, чем половина суперблока или в 32 КБ будет использовать MMAP().
MtMalloc
Как и hoard, mtmalloc поддерживает полу-частные кучи и глобальные кучи. С mtmalloc, сегменты (bucket) создаются в два раза больше, чем число процессоров. Идентификатор потока используется в качестве индекса в сегмента. Каждый сегмент содержит связанный список кэшей. Каждый кэш содержит распределение определенного размера. Каждое распределение округляется до степени 2 распределения. Например, 100-байтовый запрос будет дополнен до 128 байт и на выходе получим 128-байт из кэш-памяти.
Каждый кэш связанный списком чанков (кусков). Когда в кэше заканчивается свободное место, sbrk() выделяет новый блок. Размер блока является настраиваемым. Выделение больше, чем пороговое (64k) выделяются из глобального сегмента.
Libumem
LibUmem – user-land представление slab аллокатора, который был в SunOS 5.4. Slab аллокатор кеширует объекты общего типа для ускорения повторного использования. Он является смежной областью памяти, разделённый на равные фиксированные куски.
LibUmem использует per CPU структуры кеша, называемые магазинным слоем. Магазин – по существу является стеком. Мы помещаем распределение вверху стека и толкаем его вниз, по принципу “магазина” оружия. Когда стек оказывается на дне, магазин перегружается из vmem слоя, в так называемое “депо”. vmem аллокатор предоставляет универсальное хранилище для магазина (магазин может “тюнить” себя динамически, поэтому требуется всего несколько минут для достижения оптимальной производительности). С libumem, структуры данных дополняются тщательно, чтобы каждая находилась на своей собственной линии кэша, тем самым уменьшая потенциал для ложного шаринга.
Новый MtMalloc
В версии Oracle Solaris 10 8/11 mtmalloc был переписан и теперь он называется Новым MtMalloc. В версии Solaris 11 доступен именно новая версия mtmalloc’a. Защита блокировки каждого кэша была ликвидирована, а обновления на защищаемой информации выполняется с использованием атомарных операций. Указатель на местоположение, где последний распределение произошло сохраняется для облегчения поиска.
Вместо связанных списков кэшей, есть связанный список массивов, в которых каждый элемент массива указывает на кэш. Это помогает в локальности ссылок, что повышает производительность. Когда установлены определенные флаги, потоки, чьи идентификаторы менее чем в два раза превосходят количество виртуальных CPU получают эксклюзивные сегменты памяти, что исключает использование по-сегментные блокировки.
По умолчанию размер прирост кэша для 64-разрядных приложений составляет 64, а не 9, как это было первоначально. В ходе обсуждения, новый алгоритм mtmalloc с использованием уникальных сегментов называют новый эксклюзивный mtmalloc, а когда уникальные сегменты не используется, он называются как новые не-уникальные mtmalloc.
JeMalloc
Jemalloc — это реализация malloc(3) общего назначения, в которой особое внимание уделяется предотвращению фрагментации и поддержке масштабируемого параллелизма. jemalloc впервые начал использоваться в качестве распределителя libc во FreeBSD в 2005 году, и с тех пор он нашел применение во многих приложениях, использующих его предсказуемое поведение. В 2010 году усилия по разработке jemalloc расширились и теперь включают функции поддержки разработчиков, такие как профилирование кучи и расширенные возможности мониторинга/настройки. Современные выпуски jemalloc продолжают интегрироваться обратно во FreeBSD, поэтому универсальность остается критически важной. Постоянные усилия по разработке направлены на то, чтобы сделать jemalloc одним из лучших распределителей для широкого спектра требовательных приложений, а также устранить/смягчить недостатки, которые имеют практические последствия для реальных приложений.
Выводы.
Какой же аллокатор использовать? Приведу выдержку, без перевода, что бы не потерялся смысл:
If low latency (speed) is important as well as quick startup, use the new not-exclusive mtmalloc allocator. If, in addition, your application uses long-lived threads or thread pools, turn on the exclusive feature.
If you want reasonable scalability with a lower RAM footprint, libumem is appropriate. If you have short-lived, over-sized segments, Hoard uses mmap() automatically and, thus, the address space and RAM will be released when free() is called.
От себя могу добавить: если ваше приложение работает не совсем так, как надо – попробуйте сменить аллокатор памяти для него. Можно методом проб найти тот аллокатор, на котором ваше приложение даёт максимальную производительность или возникают ошибки выделения памяти. К примеру, я столкнулся с тем, что nodejs не могла выделить память, хотя она была. Смена libumem на jemalloc решила эту проблему, так как всему виной была большая фрагментация.
Работа с аллокаторами.
Узнать какой именно используется, можно через pmap к процессу:
# pmap -x 131317 | grep libmtmalloc FFFF80FFBBF20000 20 20 - - r-x---- libmtmalloc.so.1 FFFF80FFBBF35000 4 4 4 - rw----- libmtmalloc.so.1
Изменить используемый аллокатор памяти для приложения:
– для 32-битных:
LD_PRELOAD=/usr/lib/libumem.so; export LD_PRELOAD
или
LD_PRELOAD_32=/usr/lib/libumem.so; export LD_PRELOAD_32
– для 64-битых
LD_PRELOAD_64=/usr/lib/64/libumem.so; export LD_PRELOAD_64
Если приложение запускается как сервис, то для этого предусмотрен параметр enviroment, но меня его нужно именно так:
# svccfg -s service_name setenv LD_PRELOAD libumem.so
Замечу, что выполнение такой команды:
# svccfg -s service_name setprop start/environment = astring:LD_PRELOAD=libmtmalloc.so
приведёт к ошибке:
svccfg: Syntax error.
Если нужно добавить несколько enviroment, то делается это так:
# svccfg -s system/cron:default setenv UMEM_DEBUG default
# svccfg -s system/cron:default setenv LD_PRELOAD libumem.so
Статистику по использованию каждого кеша можно посмотреть так:
# kstat -c kmem_cache
Ещё очень хорошо и доступно про LD_PRELOAD описано здесь