Однажды мне случайно попался на глаза код, которым пользователь пытался мониторить производительность RAM в своей виртуальной машине. Код этот я приводить не буду (там «портянка») и оставлю только самое существенное. Итак, кот в студии!
#include <sys/time.h>
#include <string.h>
#include <iostream>
#define CNT 1024
#define SIZE (1024*1024)
int main() {
struct timeval start;
struct timeval end;
long millis;
double gbs;
char ** buffers;
buffers = new char*[CNT];
for (int i=0;i<CNT;i++) {
buffers[i] = new char[SIZE];
}
gettimeofday(&start, NULL);
for (int i=0;i<CNT;i++) {
memset(buffers[i], 0, SIZE);
}
gettimeofday(&end, NULL);
millis = (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_usec - start.tv_usec) / 1000;
gbs = 1000.0 / millis;
std::cout << gbs << " GB/sn";
for (int i=0;i<CNT;i++) {
delete buffers[i];
}
delete buffers;
return 0;
}
Всё просто — выделяем память и пишем в неё один гигабайт. И что показывает этот тест?
$ ./memtest
4.06504 GB/s
Примерно 4GB/s.
Что?!?!
Как?!?!?
Это Core i7 (пусть и не самый новый), DDR4, процессор почти не загружен — ПОЧЕМУ?!?!
Ответ, как всегда, необыкновенно обыкновенный.
Оператор new (как и функция malloc, кстати) на самом деле не выделяет память. При этом вызове аллокатор смотрит список свободных участков в пуле памяти, и если их нет, вызывает sbrk() чтобы увеличить сегмент данных, и затем возвращает программе ссылку на адрес из нового только что выделенного участка.
Проблема в том, что выделенный участок целиком виртуальный. Реальные страницы памяти не выделены.
И когда происходит первое обращение к каждой странице из этого выделенного сегмента, MMU «выстреливает» page fault, после чего виртуальной странице, к которой производится доступа, назначается реальная.
Поэтому на самом деле мы тестируем не производительность шины и модулей RAM, а производительность MMU и VMM операционной системы. А для того, чтобы тестировать реальную производительность оперативной памяти, нам нужно просто однократно инициализировать выделенные участки. Например так:
#include <sys/time.h>
#include <string.h>
#include <iostream>
#define CNT 1024
#define SIZE (1024*1024)
int main() {
struct timeval start;
struct timeval end;
long millis;
double gbs;
char ** buffers;
buffers = new char*[CNT];
for (int i=0;i<CNT;i++) {
// FIXED HERE!!!
buffers[i] = new char[SIZE](); // Add brackets, &$# !!!
}
gettimeofday(&start, NULL);
for (int i=0;i<CNT;i++) {
memset(buffers[i], 0, SIZE);
}
gettimeofday(&end, NULL);
millis = (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_usec - start.tv_usec) / 1000;
gbs = 1000.0 / millis;
std::cout << gbs << " GB/sn";
for (int i=0;i<CNT;i++) {
delete buffers[i];
}
delete buffers;
return 0;
}
То есть, мы просто инициализируем выделяемые буферы значением по умолчанию (char 0).
Проверяем:
$ ./memtest
28.5714 GB/s
Другое дело.
Мораль — если вам нужны большие буферы чтобы быстро-быстро работать, не забывайте их инициализировать.
Автор: outlingo