Solaris memory allocator

Аллокатор памяти определяет то, как распределяется память в системе, точнее как она выделяется и как освобождается приложениями. В зависимости от разных методов «аллокации», мы можем получить существенное увеличение производительности для конкретных приложений.

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.

Выводы.

Какой же аллокатор использовать? Приведу выдержку, без перевода, что бы не потерялся смысл:

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.

От себя могу добавить: если ваше приложение работает не совсем так, как надо — попробуйте сменить аллокатор памяти для него. Можно методом проб найти тот аллокатор, на котором ваше приложение даёт максимальную производительность.

Работа с аллокаторами.

Узнать какой именно используется, можно через 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 описано здесь

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *