[Перевод] Использование больших страниц в памяти в PHP 7
Разбивка на страницы — это способ управления памятью, выделяемой для пользовательских процессов. Все доступы процессов к памяти являются виртуальными, а преобразование их адресов в адреса физической памяти выполняют ОС и аппаратный MMU.
При разбивке на страницы память делится на блоки фиксированного размера. В Linux на x86/64-платформах размер страниц обычно составляет 4 Кб. Каждый процесс содержит в себе таблицу, в которой хранится информация о соответствии адресов страницы и физической памяти — элемент таблицы страниц (page table entry). Чтобы ОС не лезла в эту таблицу при каждом обращении к памяти (иначе для обработки каждого запроса на обращение к памяти потребуется обращаться к ней дважды), применяется небольшой кэш — буфер ассоциативной трансляции (Translationlookaside Buffer, TLB). Этот аппаратный компонент находится в MMU и работает чрезвычайно быстро и эффективно. Система сканирует TLB с целью поиска записи о соответствии адресов страницы и физической памяти. Если нужной записи там не оказывается, тогда ядру ОС приходится обращаться к памяти, искать нужное соответствие и обновлять информацию в TLB, чтобы получить из памяти нужные нам данные.
Если вы хотите больше узнать об управлении виртуальной памятью, то можете изучить эту публикацию. А пока давайте разберем, как в PHP 7 устроена работа с большими страницами (Huge Page).
Все просто: чем больше размер страницы, тем больше данных в нее можно поместить. Значит, ядро ОС за одно обращение к памяти получает доступ к большему объему данных. Также снижается вероятность промаха в TLB, ведь каждая запись теперь «покрывает» больше данных. Начиная с версии 2.6.20 ядро Linux получило возможность работать с большими страницами (подробнее об этом: раз, два, три). Большая страница обычно в 512 раз больше стандартной: 2 Мб вместо 4 Кб. Чаще всего ядро выполняет так называемое прозрачное выделение больших страниц (transparent huge page mapping): виртуальная память делится на стандартные страницы по 4 Кб, но иногда группа из следующих друг за другом страниц объединяется в одну большую. Обычно это используется при работе с занимающим огромное адресное пространство массивом. Но будьте внимательны: эта память может возвращаться операционной системе маленькими порциями, что приведет к потере огромного объема страницы, а ядру придется откатывать процедуру объединения, снова выделяя 512 страниц по 4 Кб.
Инициировать процедуру объединения может сам пользовательский процесс. Если вы уверены, что сможете заполнить данными всю большую страницу, то лучше запросите у ядра о ее выделении. Наличие больших страниц облегчает управление памятью, ведь ядру приходится просматривать меньше элементов таблицы страниц. К тому же уменьшается количество записей в TLB, да и система в целом будет работать эффективнее и быстрее.
Трудясь над PHP 7, мы потратили много сил на более эффективную работу с памятью. Критически важные внутренние структуры в PHP 7 были переписаны с целью более эффективного использования кэша ЦПУ. В частности, улучшена пространственная локальность, поэтому в кэш помещается больше односвязных данных, а движок реже обращается к памяти. Расширение OPCache теперь имеет больше возможностей по работе с большими страницами.
В мире Unix существует два API для работы с распределением виртуальной памяти. Предпочтительнее использовать функцию mmap (), поскольку она действительно позволяет выделять большие страницы. Также есть функция madvise (), которая лишь дает подсказки (рекомендации) ядру относительно преобразования части памяти в большую страницу, но гарантий никаких.
Прежде чем запрашивать выделение большой страницы, нужно удостовериться, что:
- ваша ОС способна с ними работать,
- есть в наличии свободные большие страницы.
С помощью sysctl нужно настроить vm.nr_hugepages, а затем проверить доступность больших страниц с помощью cat /proc/meminfo:
> cat /proc/meminfo
HugePages_Total: 20
HugePages_Free: 20
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
В этом примере доступно 20 больших страниц по 2 Мб. Linux на x86/64-платформе может работать со страницами до 1 Гб, хотя для PHP такой размер не рекомендован, в отличие от СУБД, где возможен выигрыш от больших размеров.
Далее можно использовать API. Чтобы выделить часть памяти под большую страницу, необходимо удостовериться в том, что границы адресного пространства совпадают с границами большой страницы. В любом случае, это нужно делать для повышения эффективности ЦПУ. После этого можно запросить у ядра выделение страницы. В следующем примере выравнивание адресов будет сделано с помощью языка С, а буфер для этой задачи взят из кучи. Ради кроссплатформенной совместимости мы не будем использовать существующие функции для выравнивания, вроде posix_memalign ().
#include
#include
#include
#include
#define ALIGN 1024*1024*2 /* We assume huge pages are 2Mb */
#define SIZE 1024*1024*32 /* Let's allocate 32Mb */
int main(int argc, char *argv[])
{
void *addr;
void *buf = NULL;
void *aligned_buf;
/* As we're gonna align on 2Mb, we need to allocate 34Mb if
we want to be sure we can use huge pages on 32Mb total */
buf = malloc(SIZE + ALIGN);
if (!buf) {
perror("Could not allocate memory");
exit(1);
}
printf("buf is at: %p\n", buf);
*/ Align on ALIGN boundary */
aligned_buf = (void *) ( ((unsigned long)buf + ALIGN - 1) & ~(ALIGN -1) );
printf("aligned buf: %p\n", aligned_buf);
/* Turn the address to huge page backed address, using MAP_HUGETLB */
addr = mmap(aligned_buf, SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_HUGETLB | MAP_FIXED, -1, 0);
if (addr == MAP_FAILED) {
printf("failed mapping, check address or huge page support\n");
exit(0);
}
printf("mmapped: %p with huge page usage\n", addr);
return 0;
}
Если вы знакомы с языком С, то пояснять особо нечего. Память не освобождается явным образом, поскольку выполнение приложения все равно завершится, да и пример этот нужен лишь для того, чтобы проиллюстрировать идею.
Когда процесс разметил память и практически завершился, можно наблюдать те самые большие страницы, зарезервированные ядром:
HugePages_Total: 20
HugePages_Free: 20
HugePages_Rsvd: 16
HugePages_Surp: 0
Hugepagesize: 2048 kB
Зарезервированы, потому что страница не будет занесена в виртуальную память, пока вы не запишете в нее данные. Здесь 16 страниц помечены как зарезервированные. 16×2 Мб = 32 Мб — такой объем памяти мы можем использовать для создания большой страницы с помощью mmap ().
Объем кодового сегмента PHP 7 весьма велик. На моей LP64×86/64-машине он составляет около 9 Мб (отладочная сборка):
> cat /proc/8435/maps
00400000-00db8000 r-xp 00000000 08:01 4196579 /home/julien.pauli/php70/nzts/bin/php /* text segment */
00fb8000-01056000 rw-p 009b8000 08:01 4196579 /home/julien.pauli/php70/nzts/bin/php
01056000-01073000 rw-p 00000000 00:00 0
02bd0000-02ce8000 rw-p 00000000 00:00 0 [heap]
... ... ...
В этом примере текстовый сегмент занимает кусок памяти с 00400000 по 00db8000. То есть общий объем бинарного машинного кода PHP составляет больше 9 Мб. Да, PHP развивается, обрастает функциями, и содержит все больше С-кода, преобразованного в машинный код.
Рассмотрим свойства нашего сегмента памяти. Он выделен с помощью традиционных страниц по 4 Кб:
> cat /proc/8435/smaps
00400000-00db8000 r-xp 00000000 08:01 4196579 /home/julien.pauli/php70/nzts/bin/php
Size: 9952 kB /* VM size */
Rss: 1276 kB /* PM busy load */
Pss: 1276 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 1276 kB
Private_Dirty: 0 kB
Referenced: 1276 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB /* page size is 4Kb */
MMUPageSize: 4 kB
Locked: 0 kB
Ядро не использовало прозрачное выделение большой страницы для данного сегмента. Возможно, оно прибегнет к этому позднее, по мере дальнейшего использования процесса с pid8435. Не станем углубляться в вопросы управления ядром большими страницами, однако с помощью OPCache можем перераспределить наш сегмент в большую страницу.
Использование больших страниц в данном случае целесообразно, поскольку кодовый сегмент не меняется в размере и не перемещается при завершении процесса. Наши 9 952 Кб можно уместить в четыре страницы по 2 Мб, а остаток рассредоточить по обычным страницам по 4 Кб.
Если:
- вы используете PHP 7;
- ваша система поддерживает большие страницы;
- вы присвоили opcache.huge_code_pages значение 1 (вместо 0);
- и PHP не является модулем вебсервера,
- тогда OPCache сразу после запуска попытается разместить ваш кодовый сегмент в больших страницах. Это делается с помощью функции accel_move_code_to_huge_pages ().
static void accel_move_code_to_huge_pages(void)
{
FILE *f;
long unsigned int huge_page_size = 2 * 1024 * 1024;
f = fopen("/proc/self/maps", "r");
if (f) {
long unsigned int start, end, offset, inode;
char perm[5], dev[6], name[MAXPATHLEN];
int ret;
ret = fscanf(f, "%lx-%lx %4s %lx %5s %ld %s\n", &start, &end, perm, &offset, dev, &inode, name);
if (ret == 7 && perm[0] == 'r' && perm[1] == '-' && perm[2] == 'x' && name[0] == '/') {
long unsigned int seg_start = ZEND_MM_ALIGNED_SIZE_EX(start, huge_page_size);
long unsigned int seg_end = (end & ~(huge_page_size-1L));
if (seg_end > seg_start) {
zend_accel_error(ACCEL_LOG_DEBUG, "remap to huge page %lx-%lx %s \n", seg_start, seg_end, name);
accel_remap_huge_pages((void*)seg_start, seg_end - seg_start, name, offset + seg_start - start);
}
}
fclose(f);
}
}
OPCache открывает /proc/self/maps и ищет кодовый сегмент памяти. По-другому сделать это не получится, поскольку доступ к подобной информации нельзя получить без явного использования зависимостей ядра. Сегодня procfs используется во всех Unix-системах.
Сканируем файл, находим кодовый сегмент, выравниваем границы в соответствии с адресным пространством большой страницы. Затем вызываем accel_remap_huge_pages () с указанием выровненных границ.
# if defined(MAP_HUGETLB) || defined(MADV_HUGEPAGE)
static int accel_remap_huge_pages(void *start, size_t size, const char *name, size_t offset)
{
void *ret = MAP_FAILED;
void *mem;
mem = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (mem == MAP_FAILED) {
return -1;
}
memcpy(mem, start, size);
# ifdef MAP_HUGETLB
ret = mmap(start, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_HUGETLB,
-1, 0);
# endif
# ifdef MADV_HUGEPAGE
if (ret == MAP_FAILED) {
ret = mmap(start, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
-1, 0);
if (-1 == madvise(start, size, MADV_HUGEPAGE)) {
munmap(mem, size);
return -1;
}
}
# endif
if (ret == start) {
memcpy(start, mem, size);
mprotect(start, size, PROT_READ | PROT_EXEC);
}
munmap(mem, size);
return (ret == start) ? 0 : -1;
}
#endif
Все достаточно просто. Мы создали новый временный буфер (mem), скопировали в него данные, затем с помощью mmap () попытались распределить выровненный буфер по большим страницам. Если попытка не увенчалась успехом, то можно подсказать ядру с помощью madvise (). После распределения сегмента по страницам копируем данные обратно и возвращаемся.
00400000-00c00000 r-xp 00000000 00:0b 1008956 /anon_hugepage
Size: 8192 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 2048 kB
MMUPageSize: 2048 kB
Locked: 0 kB
00c00000-00db8000 r-xp 00800000 08:01 4196579 /home/julien.pauli/php70/nzts/bin/php
Size: 1760 kB
Rss: 224 kB
Pss: 224 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 224 kB
Private_Dirty: 0 kB
Referenced: 224 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
8 Мб распределены по четырем большим страницам, а 1760 Кб — по стандартным. Мне это дало прирост производительности Zend в 3% при больших нагрузках.
При использовании больших страниц:
- в 512 раз снижается общее количество страниц виртуальной памяти;
- TLB задействуется гораздо активнее, что снижает частоту обращения к памяти;
- можно оптимизировать исполнение машинных инструкций, которые PHP загружает в ЦПУ.
Теперь понятно, каким образом расширение OPCache для PHP 7 помогает повысить производительность системы при использовании теперь уже распространенной техники управления памятью, известной как «большие страницы».
Кстати, ряд СУБД (например, Oracle, PostgreSQL) уже несколько лет используют преимущества больших страниц.