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

в 10:44, , рубрики: linux, memory management, performance testing, ram

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js