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

от автора

Однажды мне случайно попался на глаза код, которым пользователь пытался мониторить производительность 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

Другое дело.

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

ссылка на оригинал статьи https://habr.com/ru/post/487248/


Комментарии

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

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