Хабрахабр

Еще немного о неправильном тестировании

Однажды мне случайно попался на глаза код, которым пользователь пытался мониторить производительность 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/s\n"; 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/s\n"; for (int i=0;i<CNT;i++) { delete buffers[i]; } delete buffers; return 0;}

То есть, мы просто инициализируем выделяемые буферы значением по умолчанию (char 0).

Проверяем:

$ ./memtest
28.5714 GB/s

Другое дело.

Мораль — если вам нужны большие буферы чтобы быстро-быстро работать, не забывайте их инициализировать.

Показать больше

Похожие публикации

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

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

Кнопка «Наверх»