Pull to refresh
VK
Building the Internet

Использование больших страниц в памяти в PHP 7

Reading time 8 min
Views 18K
Original author: Julien Pauli
Разбивка на страницы — это способ управления памятью, выделяемой для пользовательских процессов. Все доступы процессов к памяти являются виртуальными, а преобразование их адресов в адреса физической памяти выполняют ОС и аппаратный 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, да и система в целом будет работать эффективнее и быстрее.

OPCache вам в помощь


Трудясь над 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 <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>

#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 в большой странице


Объем кодового сегмента PHP 7 весьма велик. На моей LP64 x86/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) уже несколько лет используют преимущества больших страниц.
Tags:
Hubs:
+28
Comments 16
Comments Comments 16

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен