Jeszcze trochę o niewłaściwym testowaniu

Pewnego razu natknąłem się na kod, którym użytkownik próbował monitorować wydajność RAM w swojej maszynie wirtualnej. Nie będę przytaczał tego kodu (bo to „długi tekst”) i pozostawię tylko najważniejsze. A więc, kot w studiu!

#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;
}

Wszystko jest proste — przydzielamy pamięć i zapisujemy w niej jeden gigabajt. I co pokazuje ten test?

$ ./memtest
4.06504 GB/s

Około 4GB/s.

Co?!?!

Jak?!?!?

To Core i7 (choć nie najnowszy), DDR4, procesor prawie nieobciążony — DLACZEGO?!?!

Odpowiedź, jak zawsze, niezwykle zwyczajna.

Operator new (jak również funkcja malloc, swoją drogą) tak naprawdę nie przydziela pamięci. Przy tym wywołaniu alokator sprawdza listę wolnych bloków w puli pamięci, a jeśli ich nie ma, wywołuje sbrk(), aby zwiększyć segment danych, a potem zwraca programowi odwołanie do adresu z nowo przydzielonego bloku.

Problem w tym, że przydzielony blok jest w całości wirtualny. Rzeczywiste strony pamięci nie zostały przydzielone.

I kiedy następuje pierwsze odwołanie do każdej strony z tego przydzielonego segmentu, MMU „strzela” page fault, po czym wirtualnej stronie, do której następuje dostęp, przypisywana jest rzeczywista.

Dlatego tak naprawdę testujemy nie wydajność szyny i modułów RAM, a wydajność MMU i VMM systemu operacyjnego. A żeby testować rzeczywistą wydajność pamięci operacyjnej, musimy jednorazowo zainicjować przydzielone bloki. Na przykład tak:

#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;
}

To znaczy, po prostu inicjalizujemy przydzielane bufory wartością domyślną (char 0).

Sprawdzamy:

$ ./memtest
28.5714 GB/s

Inna sprawa.

Morał — jeśli potrzebujesz dużych buforów, aby pracować bardzo szybko, nie zapominaj ich inicjalizować.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster