Я относительно хорошо знаю, как работает виртуальная память. Вся память процесса разделяется на страницы и каждая страница виртуальной памяти сопоставляется с страницей в реальной памяти или страницей в файле подкачки, или может быть новой страницей, что означает, что физическая страница все еще не выделена. ОС сопоставляет новые страницы с реальной памятью по требованию, а не когда приложение запрашивает память с помощью malloc
, но только тогда, когда приложение действительно обращается к каждой странице из выделенной памяти. Но у меня все еще есть вопросы.
Я заметил это, когда профилировал мое приложение с помощью инструмента linux perf
.
Около 20% времени выполняет функции ядра: clear_page_orig
, __do_page_fault
и get_page_from_free_list
. Это намного больше, чем я ожидал для этой задачи, и я провел некоторое исследование.
Начнем с небольшого примера:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SIZE 1 * 1024 * 1024
int main(int argc, char *argv[]) {
int i;
int sum = 0;
int *p = (int *) malloc(SIZE);
for (i = 0; i < 10000; i ++) {
memset(p, 0, SIZE);
sum += p[512];
}
free(p);
printf("sum %d\n", sum);
return 0;
}
Предположим, что memset
- это просто обработка с привязкой к памяти. В этом случае мы выделяем небольшой кусок памяти один раз и повторно используем его снова и снова. Я запустил эту программу следующим образом:
$ gcc -O1 ./mem.c && time ./a.out
-O1
, потому что clang
с -O2
полностью исключает цикл и вычисляет значение моментально.
Результаты: user: 0.520s, sys: 0.008s. Согласно perf
, 99% этого времени находится в memset
от libc
. Таким образом, для этого случая производительность записи составляет около 20 гигабайт/с, что больше, чем теоретическая производительность 12,5 Гбит/с для моей памяти. Похоже, это связано с кэшем процессора L3.
Позвольте изменить тест и начать выделять память в цикле (я не буду повторять те же части кода):
#define SIZE 1 * 1024 * 1024
for (i = 0; i < 10000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
Результат точно такой же. Я считаю, что free
фактически не освобождает память для ОС, а просто помещает ее в некоторый свободный список в процессе. И malloc
на следующей итерации просто получит точно такой же блок памяти. Вот почему нет заметной разницы.
Пусть начнется увеличение SIZE с 1 мегабайта. Время исполнения будет постепенно увеличиваться и будет насыщаться около 10 мегабайт (для меня нет разницы между 10 и 20 мегабайтами).
#define SIZE 10 * 1024 * 1024
for (i = 0; i < 1000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
Время показывает: пользователь: 1.184s, sys: 0.004s. perf
по-прежнему сообщает, что 99% времени находится в memset
, но пропускная способность составляет около 8,3 Гбит/с. В этот момент я понимаю, что происходит, более или менее.
Если мы продолжим увеличивать размер блока памяти, в какой-то момент (для меня на 35 Мб) время выполнения резко возрастет: user: 0.724s, sys: 3.300s.
#define SIZE 40 * 1024 * 1024
for (i = 0; i < 250; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
Согласно perf
, memset
будет потреблять только 18% времени.
Очевидно, что память выделяется из ОС и освобождается на каждом шаге. Как я уже упоминал ранее, ОС должна очистить каждую выделенную страницу перед ее использованием. Таким образом, 27,3% от clear_page_orig
не выглядят экстраординарно: это просто 4s * 0,273 ≈ 1,1 с для ясного mem - то же самое мы получаем в третьем примере. memset
заняло 17,9%, что приводит к ≈ 700 мс, что является нормальным из-за памяти уже в кэше L3 после clear_page_orig
(первый и второй пример).
Что я не могу понять - почему последний случай в 2 раза медленнее, чем просто memset
для памяти + memset
для кеша L3? Могу ли я что-то сделать с ним?
Результаты воспроизводятся (с небольшими различиями) на родной Mac OS, Ubuntu под Vmware и Amazon c4.large.
Кроме того, я думаю, что есть место для оптимизации на двух уровнях:
- на уровне ОС. Если ОС знает, что возвращает страницу в то же приложение, к которому она принадлежала ранее, она не может ее очистить.
- на уровне CPU. Если ЦП знает, что страница была бесплатной, она может не очистить страницу в памяти. Он может просто очистить его в кеше и перенести его в память только после некоторой обработки в кеше.