Ще трохи про неправильне тестування

Одного разу мені випадково потрапив на очі код, яким користувач намагався моніторити продуктивність 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 Гб / с

Приблизно 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 Гб / с

Інша справа.

Мораль - якщо вам потрібні великі буфери, щоб швидко-швидко працювати, не забувайте їх ініціалізувати.

Джерело: habr.com

Додати коментар або відгук