Archive for the ‘C++’ Category.

Pobieranie kodu HTML stron w C++ za pomocą WinSock2

Dzisiejsza notka będzie o tym jak pobrać kod HTML strony WWW w aplikacji C++ korzystając z WinSock2.

Pierwszą rzeczą, którą trzeba wiedzieć jest to w jaki sposób robi to przeglądarka internetowa. Zacznę od tego jak wygląda typowy adres strony WWW na przykładzie adresu do działu artykułów na stronie http://www.gamedev.pl/:

http://www.gamedev.pl/articles.php

Powyższy adres składa się z kilku części:

  • “http://” – protokół reprezentujący sposób transmisji danych, dzięki niemu przeglądarka wie w jaki sposób komunikować się z serwerem oraz na jaki port wysyłać żądania
  • “www.gamedev.pl” – domena na którą będzie wysłane zapytanie – podany adres jest tłumaczony na adres IP przez serwer DNS
  • “/articles.php” – adres żądanego plik lub żądanie dla serwera WWW, które aplikacja wykorzystuje do stworzenia nagłówka

Przeglądarka mając adres strony tworzy odpowiedni nagłówek, który zostaje wysłany do serwera WWW. Jak już wspomniałem adresem tego serwera jest domena zawarta w adresie WWW natomiast port jest określany na podstawie protokołu, czyli w przypadku “http://” jest to port 80. Nagłówek HTTP powinien zawierać następujące elementy:

  • rodzaj zapytania – w przypadku pobrania strony jest to “GET”
  • adres żądanego pliku lub żądanie – to co chcemy od serwera otrzymać, najczęściej jest to adres pliku na serwerze, czyli “/articles.php” w tym przykładzie
  • wersja protokołu – typowo HTTP/1.1
  • domena hosta

Dodatkowo, jeśli jest to konieczne, można wysłać informacje o tym, jakiej przeglądarki używamy (UserAgent), informacje o akceptowanych plikach, kodowaniu, ciasteczkach oraz czasie trwania połączenia, więcej można dowiedzieć się z tej strony wiki oraz samych nagłówków, które wysyła przeglądarka. Jako ciekawostkę mogę dodać, że istnieje fajna wtyczka dla Firefoxa, która pokazuje nagłówki wysyłane przez przeglądarkę – Live HTTP headers.

Oto nagłówek uzyskany z pomocą tej wtyczki podczas łączenia się do przykładowej strony:

1
2
3
4
5
6
7
8
9
GET /articles.php HTTP/1.1
Host: www.gamedev.pl
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

Jak widać jest on trochę długi (usunąłem informacje o ciasteczkach, nie są tutaj potrzebne). Aby pobrać kod strony wystarczą tak naprawdę dwie pierwsze linijki. Jednak ważną rzeczą jest, aby po każdej linijce występowała para znaków \r\n a koniec nagłówka reprezentowany był przez dwie pary tych znaków. W przypadku, gdy nagłówek nie będzie zawierał tak skonstruowanego nagłówka, serwer po prostu udrzuci zapytanie.

Teraz pora na kod C++ z WinSock2, oto on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Usuwa zbędne definicje z nagłówka
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
 
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
 
#define BUFFER_SIZE 2048
#pragma comment(lib, "ws2_32.lib")	// Niezbędna biblioteka
using namespace std;
 
int main()
{
	//************************************************
	// Inicjalizacja WinSock2
	//************************************************
	WSADATA wsaData;
	int error;
	string answer;
	ZeroMemory(&wsaData, sizeof(wsaData));
 
	if(FAILED(WSAStartup(MAKEWORD(2,2), &wsaData)))	// MAKEWORD(2,2) - Wersja WinSock
	{
		return 1;
	}
 
	char recvBuffer[BUFFER_SIZE];
	addrinfo hint;					// Struktura przechowująca dane o połączeniu
	addrinfo* wsResult;				// Wskaźnik na rezultat
	SOCKET pSocket;					// Właściwy pSocket
 
	ZeroMemory(recvBuffer, sizeof(recvBuffer));
	ZeroMemory(&hint, sizeof(hint));
	hint.ai_family = AF_UNSPEC;		// Rodzaj transmisji - nieokreślony
	hint.ai_socktype = SOCK_STREAM;	// Typ gniazda - strumień
	wsResult = NULL;
	pSocket = INVALID_SOCKET;
 
	//************************************************
	// Tworzenie zapytania	
	//************************************************
 
	// Wyciąganie informacji z adresu
	string httpAddress = "http://www.gamedev.pl/articles.php";
	string temp = httpAddress.substr(httpAddress.find("http://") + sizeof("http://") - 1);	// Tylko http:// więc można wyciąć
	string domain = temp.substr(0, temp.find_first_of('/'));								// Domena
	string addressTail = temp.substr(temp.find_first_of('/'));								// Żądanie pliku
 
	// Tworzenie nagłówka
	temp = string("GET ") + addressTail + " HTTP/1.1\r\n"
			   + "Host: " + domain + "\r\n"
			   //+ "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5\r\n"
			   //+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
			   //+ "Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"
			   //+ "Accept-Encoding: gzip,deflate\r\n"
			   //+ "Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7\r\n"
			   //+ "Keep-Alive: 300\r\n"
			   //+ "Connection: keep-alive\r\n"
			   + "\r\n\r\n";
 
	// Pobieranie IP serwera z serwera DNS
	if(FAILED(getaddrinfo(domain.c_str(), TEXT("80"), &hint, &wsResult)))
	{
		printf("Nie udalo sie pobrac IP\n");
		return 1;
	}
 
	// Wskaźnik na zwrócone adresy
	addrinfo* ptr = wsResult;
 
	// Tworzenie socketu
	if(FAILED(pSocket = socket(wsResult->ai_family, wsResult->ai_socktype, wsResult->ai_protocol))) 
	{
		freeaddrinfo(wsResult);
		printf("Nie udało się utworzyc socketu\n");
		return 1;
	}
 
	//************************************************
	// Wysyłanie zapytania	
	//************************************************
 
	// Łączenie się do serwera
	if(FAILED(connect(pSocket, ptr->ai_addr, (int)ptr->ai_addrlen))) 
	{
		printf("Nie udalo sie polaczyc do serwera\n");
		goto Error;
	}
 
	// Wysyłanie nagłówka
	if(FAILED(send(pSocket, temp.c_str(), temp.size(), 0)))
	{
		printf("Nie udalo sie wyslac naglowka\n");
		goto Error;
	}
 
	// Kończenie wysyłania
	if(FAILED(shutdown(pSocket, SD_SEND)))
	{
		printf("Nie udalo sie zamknac polaczenia\n");
		goto Error;
	}
 
	//************************************************
	// Odbieranie danych
	//************************************************
 
	// Odbieranie danych
	do
	{
		ZeroMemory(recvBuffer, sizeof(recvBuffer));
		error = recv(pSocket, recvBuffer, sizeof(recvBuffer), 0);
 
		if(error > 0)
		{
			answer += string(recvBuffer);
		}
	}
	while(error > 0);
 
	closesocket(pSocket);		// Zamknięcie socketu
	freeaddrinfo(wsResult);		// oraz zwolnienie struktury addrinfo
 
	printf("Kod HTML strony \"http://www.gamedev.pl/articles.php\":\n\n%s", answer.c_str());
	return 0;
 
Error:
	closesocket(pSocket);
	freeaddrinfo(wsResult);
	return 1;
}

Wynikiem tego kodu jest wyświetlony kod HTML przykładowej strony.

Defekt optymalizacji kompilatora x64

Podczas dalszego kodowania mojego silnika natrafiłem na bardzo dziwny błąd kompilatora Visual Studio. Zaczynając od początku – mam oto taki kod:

enum
{
	// Rozmiary pól
	MAX_BITS_INDEX = sizeof(Size_t) * 4,
	MAX_BITS_MAGIC = sizeof(Size_t) * 4,
	// Maksymalne wartości pól (Przesunięcie 1 w dwóch operacjach, ponieważ inaczej kompilator nie daje rady)
	MAX_INDEX = ((1 << MAX_BITS_INDEX / 2) << MAX_BITS_INDEX / 2) - 1,
	MAX_MAGIC = ((1 << MAX_BITS_MAGIC / 2) << MAX_BITS_MAGIC / 2) - 1
};
/* ... */
 
// handle = 0x0000000400000006;   - przykladowo
// Size_t ma rozmiar wskaźnika (jest zależny od platformy)
Size_t value = handle & MAX_MAGIC;

Otóż problem z tym kodem jest taki, że gdy zostanie skompilowany na platformę x64, zostaje wygenerowany następujący kod w assemblerze:

    79:                 {
    80:                         Size_t value = handle & MAX_MAGIC;
 
000000013FC96F94  mov         rax,qword ptr [handle]
000000013FC96F9C  mov         qword ptr [value],rax
 
    81:                         if(value == *it) return true; // Dalszy kod

Jak widać problem polega na braku kluczowej instrukcji and, której zadaniem jest odcięcie najstarszych 32-bitów. Dla porównania kod assemblera wygenerowanego dla platformy x86 jest następujący:

    79:                 {
    80:                         Size_t value = handle & MAX_MAGIC;
00DC9DDC  mov         eax,dword ptr [handle]
00DC9DDF  and          eax,0FFFFh
00DC9DE4  mov         dword ptr [value],eax
    81:                         if(value == *it) return true; // Dalszy kod

Tutaj instrukcja ta występuje.

Błąd ten jednak nie występuje gdy samemu sprecyzuję stałą, tj.:

Size_t value = handle & 0xffffffff;

W tym przypadku zostanie wygenerowany poprawny kod.

Wniosek jest taki, że kompilator widząc operację and zmiennej ze stałą typu enum (który przechowuje wartości 32-bitowe), traktuje tą zmienną jako typ 32-bitowy. W tym przypadku stwierdza, że instrukcja and nie ma sensu, bo wartość MAX_MAGIC zawiera maksymalną wartość (0xffffffff). Według mnie jest to ewidentny błąd i nie powinno takie coś wystąpić.

// Edit :)
Problemem oczywiście jest to, że enum jest tak naprawdę typem int, więc się dokładnie tak zachowuje.

AutomaticHandle ciąg dalszy

Jak się okazuje nic nie jest na początku idealne. Dzięki uwagom Revo udało mi się usprawnić szablon automatycznych uchwytów. Revo poradził mi dodanie dodatkowego wskaźnika na licznik odwołań do danego zasobu. Dzięki temu możliwe było usunięcie jednej funkcji wirtualnej, a druga została sprowadzona do roli sprzątaczki, która zostaje wywołana tylko w momencie, gdy licznik osiąga wartość 0. Dodatkowo dodałem sprawdzenie czy uchwyt nie jest zerowy przed odwołaniem się do wskaźników.

Poniżej znajduję się kod poprawionej klasy szablonowej AutomaticHandle:

typedef unsigned int Dword;
 
template< typename TAG>
class AutomaticHandle
{
public:
	// Podstawowy konstruktor
	AutomaticHandle(NLib::Dword handle, MHandleMgr* manager, NLib::Dword* resCounter) : m_handle(handle), m_manager(manager), m_resCounter(resCounter)
	{ if(m_handle) ++(*m_resCounter); }
	// Konstruktor kopiujący
	AutomaticHandle(const AutomaticHandle& src) : m_manager(src.m_manager), m_handle(src.m_handle), m_resCounter(src.m_resCounter)
	{ if(m_handle) ++(*m_resCounter); }
	// Destruktor
	~AutomaticHandle()
	{
		if(m_handle)
		{ if(!(--(*m_resCounter))) m_manager->ReleaseResource(m_handle); }
	}
	// Operator przypisania
	const AutomaticHandle& operator =(const AutomaticHandle& src)
	{
		if(m_handle)
		{ if(!(--(*m_resCounter))) m_manager->ReleaseResource(m_handle); }
		m_handle = src.m_handle;
		if(m_handle) ++(*(m_resCounter = src.m_resCounter));
		return src;
	}
	// Operator równości
	bool operator ==(const AutomaticHandle& src)
	{ return m_handle == src.m_handle; }
	// Operator różności
	bool operator !=(const AutomaticHandle& src)
	{ return m_handle != src.m_handle; }
	// Operator Dword
	operator NLib::Dword() const
	{ return m_handle; }
	// Operator bool
	operator bool() const
	{ return !!m_handle; }
 
private:
	// Uchwyt
	NLib::Dword m_handle;
	// Wskaźnik do managera
	MHandleMgr* m_manager;
	// Wskaźnik do licznika zasobu
	NLib::Dword* m_resCounter;
};

Kolejną klasą jest AutoHandleMgr, czyli klasa implementująca obsługę automatycznych uchwytów. Aktualnie tylko trzy funkcje są dostępne klasie pochodnej, w tym jedna dla uchwytów. Klasa ta posiada jedną funkcję abstrakcyjną, w której po zdefiniowaniu w klasie pochodnej należy usuwać odpowiednie zasoby zgodnie z przekazanym indeksem.

Oto jej definicja:

typedef unsigned int Dword;
 
class AutoHandleMgr
{
	// Unia do wyciągania informacji z uchwytu
	enum
	{
		// Rozmiary pól
		MAX_BITS_INDEX = sizeof(NLib::Dword) * 4,
		MAX_BITS_MAGIC = sizeof(NLib::Dword) * 4,
		// Maksymalne wartości pól
		MAX_INDEX = (1 << MAX_BITS_INDEX) - 1,
		MAX_MAGIC = (1 << MAX_BITS_MAGIC) - 1
	};
 
public:
	// Funkcja do zwracania uchwytu
	void ReleaseResource(NLib::Dword handle)
	{
		NAssert(GetIndex(handle) < m_magicValues.size(), "Wrong Handle");
		NAssert(CheckMagic(handle), "Wrong handle");
		DeleteHandleAt(GetIndex(handle));
		ReleaseResourceByIndex(GetIndex(handle));
	}
 
protected:
	// Funkcja tworzy nowy uchwyt i go zwraca
	// Index zawarty w uchwycie odpowiada indeksowi w tablicy z danymi
	// w odziedziczonej klasie
	NLib::Dword CreateNewHandle()
	{
		NLib::Dword handle;
		static NLib::Dword s_autoMagic = 0;
		if(++s_autoMagic > MAX_MAGIC) s_autoMagic = 1;	// 0 oznacza pusty uchwyt
		// Jeżeli nie ma wolnych miejsc to tworze nowy index
		if(m_freeSlots.empty())
		{
			handle = m_magicValues.size() << (MAX_BITS_INDEX - 1);
			m_magicValues.push_back(s_autoMagic);
		}
		else
		{
			handle = m_freeSlots.back() << (MAX_BITS_INDEX - 1);
			m_magicValues[m_freeSlots.back()] = s_autoMagic;
			m_freeSlots.pop_back();
		}
		handle |= s_autoMagic;
		return handle;
	}
	// Funkcja zwraca index
	NLib::Dword GetIndex(NLib::Dword handle)
	{ return (handle >> (MAX_BITS_INDEX - 1)) & MAX_INDEX; }
 
private:
	// Funkcja kasuje wskazany uchwyt
	void DeleteHandleAt(NLib::Dword index)
	{	m_freeSlots.push_back(index);
		m_magicValues[index] = 0; }
	// Funkcja zwraca część magiczną
	NLib::Dword GetMagic(NLib::Dword handle)
	{ return handle & MAX_MAGIC; }
	// Funkcja sprawdzająca wartośc magic
	bool CheckMagic(NLib::Dword handle)
	{	NAssert(GetIndex(handle) < m_magicValues.size(), "Wrong Handle");
		return GetMagic(handle) == m_magicValues[GetIndex(handle)]; }
 
private:
	// Funkcja dekrementująca licznik w zasobach
	virtual void ReleaseResourceByIndex(NLib::Dword index) = 0;
 
private:
	// Vector zawierający magiczne wartości
	std::vector m_magicValues;
	// Vector zawierający indeksy z wolnymi miejscami
	std::vector m_freeSlots;
};

W przypadku tego menadżera uchwyty muszą być usunięte przed nim, inaczej pojawią się błędy z dostępem do pamięci.

Refactor silnika + automatyczne uchwyty

Właśnie mija tydzień od zakończenia dwutygodniowego pobytu w domu w czasie końcówki sesji i przerwy egzaminacyjnej. W tym czasie rozpocząłem przepisywania mojego “silnika” od nowa, tym razem w oparciu o interfejsy. Są one bardzo poręczne, gdy chce się ukryć implementację klas wewnątrz statycznych, bądź dynamicznych bibliotek. Głównym założeniem przy projektowaniu drugiej wersji NIne było właśnie ukrycie implementacji wewnątrz statycznej biblioteki .lib. Dzięki takiemu postępowaniu biblioteka wygląda ładnie i pozwala ładnie rozszerzać własną funkcjonalność.

Drugim zagadnieniem poruszanym w tej notce są uchwyty, które same zarządzają licznikiem danego zasobu.
Z początku implementację uchwytów opierałem na artykule znajdującym się w książce “Game Programming Gems”, jednak problemem okazało się zmuszenie tychże uchwytów do sprzątania po sobie zasobów. Na szczęście rozwiązanie okazało się proste, ponieważ wystarczyło umieścić w klasie uchwytu wskaźnik do menadżera zasobów.

Poniżej znajduje się moja implementacja takiego uchwytu:

template< typename TAG>
class AutomaticHandle
{
public:
	// Podstawowy konstruktor
	AutomaticHandle(Dword handle, MHandleMgr* manager) : m_handle(handle), m_manager(manager)
	{}
	// Konstruktor kopiujący
	AutomaticHandle(const AutomaticHandle& src) : m_manager(src.m_manager), m_handle(src.m_handle)
	{ m_manager->Copy(src.m_handle); }
	// Destruktor
	~AutomaticHandle()
	{ m_manager->Release(m_handle); }
	// Operator przypisania
	const AutomaticHandle& operator =(const ManagedHandle& src)
	{ m_manager->Release(m_handle);
	  m_handle = src.m_handle;
	  m_manager->Copy(src.m_handle);
	  return src; }
	// Operator Dword
	operator Dword() const
	{ return m_handle; }
	/*
	...
	*/
 
private:
	// Uchwyt
	Dword m_handle;
	// Wskaźnik do managera
	MHandleMgr* m_manager;
};

Jak widać klasa zawiera dodatkową zmienną – wskaźnik na klasę menadżera ustawiany przy konstrukcji. Klasa menadżera musi zawierać używane metody, które wewnątrz zarządzają licznikiem odwołań na dany zasób, a w momencie jego spadku do 0, likwidować dany zasób.

Wyrównanie pamięci dla operatorów new i new[]

Gdy programuje się coś z użyciem funkcji wewnętrznych kompilatora (Intrinsics) dla SSE, najlepiej jest gdy to, co do nich przekazujemy jest wyrównanie do 16 bajtów, w przeciwnym wypadku powstaje problem z odczytaniem z pamięci i program się wysypuje. Oczywiście można korzystać z funkcji ładujących, które przyjmują dane niewyrównane, ale funkcje te są z oczywistych względów wolniejsze. Do ustawienia wyrównania danych pól stosuję się specjalną deklaracje __declspec(align(16)), którą należy umieścić przed definicją zmiennej (oczywiście dotyczy to Microsoft Visual Studio).

__declspec(align(16)) __m128 m_vector;

Niestety taka deklaracja dotyczy tylko obiektów na stosie. Te, które są tworzone na stercie, czyli przez operator new oraz operator new[], nie zawsze są wyrównane, dlatego trzeba zadbać o to samemu odpowiednio przeciążając te funkcje. Przeciążanie tych operatorów nie jest trudne. Oba przyjmują jako argument ilość potrzebnego miejsca wyrażoną w bajtach, a zwracają wskaźnik typu void, który zawiera adres odpowiedniego obszaru pamięci. Nagłówek jednego z tych operatorów wygląda następująco:

void* operator new(const size_t nBytes)

W ciele takiej funkcji należy zaalokować odpowiednią ilość miejsca, a następnie wyrównać wskaźnik tak, aby dzielił się przez 16. Dopiero ten wskaźnik można zwrócić. Należy oczywiście pamiętać o zapisaniu gdzieś poprzedniego wskaźnika oraz o alokacji pamięci tak, żeby po wyrównaniu nie wychodzić poza przydzielony zakres.
W przypadku mojej funkcji adres jest zapisany przed wyrównanym adresem (dzięki poradom kolegów z forum ), a ilość alokowanego miejsca jest większa o rozmiar alokowanej struktury + 3 bajty.

void* operator new(const size_t nBytes)
{
 char* ptr = new char[nBytes + sizeof(NVector) + 3];
 unsigned int temp = reinterpret_cast<unsigned int>(ptr);  // Konwersja na postać odpowiednią do obliczeń
 unsigned int t = temp % 16;   // Obliczam o ile jest przesunięty od wyrównania
 temp += (t &lt; 13) ? (16 - t) : (32 - t);  // Dodaje odpowiednie przesunięcie do wskaźnika
 void* aPtr = reinterpret_cast<void*>(temp); // Zapis nowego wskaźnika
 temp -= sizeof(void*);
 *reinterpret_cast<unsigned int*>(temp) = reinterpret_cast<unsigned int>(ptr); // Zapis poprzedniego wskaźnika
 return aPtr;
}

NVector jest klasą, w której umieściłem ten operator. Liczba 13 występująca przy porównywaniu w powyższym kodzie reprezentuje przesunięcie, dla którego nie jest możliwe zapisanie wskaźnika przed najbliższym wyrównaniem, dlatego trzeba dodać więcej. Operator new[]wygląda identycznie, dlatego można w nim wywołać powyższą funkcję. Odpowiednik delete dla tego operatora wygląda następująco:

void operator delete(void* p)
{
	unsigned int temp = reinterpret_cast<unsigned int>(p) - sizeof(void*);
	char* temp2 = reinterpret_cast<char*>(*reinterpret_cast<int*>(temp));
	delete[] temp2;
}

Powyższa funkcja oblicza poprzedni wskaźnik i korzystając z niego dealokuje obszar. Operator delete[] jest identyczny.

Niskopoziomowa zabawa

Zabawa w kodzie na niskim poziomie może być bardzo fajna, ja porównuję ją do rozwiązywania łamigłówek (myślę, że są osoby, które podzielają moje zdanie :)). Czasem ta zabawa sprowadza się zbadania, czy zmiana jednej funkcji assemblerowej powoduje przyspieszenie kodu o ten jeden cykl, chociaż jest to już skrajność. Przykładem tego może być funkcja obliczająca długość wektora 4D wykorzystująca funkcje wewnętrzne kompilatora – Intrinsics:

float Length()
{
   float f;
   __m128 temp = _mm_mul_ps(m_vector, m_vector);
   __m128 temp2 = _mm_add_ps(_mm_movehl_ps(temp, temp), temp);
   temp = _mm_shuffle_ps(temp2, temp2, _MM_SHUFFLE(0,0,0,1));
   _mm_store_ss(&f, _mm_sqrt_ss(_mm_add_ss(temp2, temp)));
   return f;
}

Powyższy kod jest najszybszą wersją jaką udało mi się uzyskać. Polega on na obliczeniu kwadratów wszystkich składowych, dodaniu ich do siebie oraz zpierwiastkowanie. Najbardziej pracochłonną częścią jest tutaj wbrew pozorom dodawanie, gdyż kwadrat składowych można załatwić jednym mnożeniem wektorowym. Dodawanie jednak rozbija się na dwie operacje dodawania oraz dwie operacje przemieszania, ponieważ SSE nie oferuje funkcji, która dodawałaby wszystkie elementy pojedynczego rejestru. Do wykonywania operacji przemieszania najczęściej stosuje się funkcje _mm_shuffle(), ponieważ pozwala ona dowolnie rozmieścić elementy w rejestrze. Rozszerzenie SSE umożliwia jednak wykonanie przemieszań za pomocą innych funkcji (_mm_unpackhi_ps(), _mm_unpacklo_ps(), _mm_movehl_ps(), _mm_movelh_ps()), które mieszają elementy tylko w określony sposób. W powyższym kodzie zastosowałem właśnie _mm_unpackhi(), ponieważ funkcja zapisuje elementy x i y na pozycji elementow z i w.

Inną sprawą jest, w jaki sposób kompilator radzi sobie z takim kodem. Otóż posiada on specjalnie zaprogramowane optymalizacje dla tego typu funkcji oraz typu __m128. Kompilator widząc te funkcje zamienia je na wywołania odpowiednich funkcji w kodzie assemblera, przy czym sam zarządza wykorzystaniem rejestrów xmm. Nie trzeba się zatem martwić o ilość zmiennych typu __m128, ponieważ nie wpływa to bezpośrednio na rejestry.

NLib

Postanowiłem udostępnić moją “bibliotekę”, którą posługuję się od jakiegoś czasu. Nie jest ona jakoś super wypasiona, ale zawiera wszystkie elementy, które są mi aktualnie potrzebne, czyli:

  • Moduł podstawowy – konwersje, typedefy, jakieś funkcje liczące w czasie kompilacji
  • Profile
  • Logger
  • FStream – strumień dla plików w oparciu o funkcje WinApi
  • StructReader – funkcja do obsługi bardzo prostych plików konfiguracyjnych
  • Timer – obsługa zegara, licznik FPS w oparciu o QPC lub GetTickCount()
  • Window – prosta klasa do obsługi okna

Update’y pojawiają się wraz z pomysłami i zapotrzebowaniem na nowe wynalazki, więc nie wiem kiedy nowa wersja.

Pobierz

Assert

Ostatnio dowiedziałem się o bardzo fajnej funkcji jaką udostępnia biblioteka standardowa, czyli o funkcji Assert. Funkcja ta ma tę zaletę, że warunek, który jej przekazujemy jest sprawdzany tylko, gdy jest umieścimy definicje NDEBUG. Wtedy, gdy warunek zwróci fałsz, zostaje wyświetlone okienko, gdzie i kiedy miało zdarzenie. W przeciwnym wypadku, gdy nie zdefiniujemy NDEBUG, sprawdzanie warunku po prostu znika, dzięki czemu osiągamy większą wydajność.

Oczywiście standardowa makrodefinicja nie jest idealna, ponieważ wyświetla różne rodzaje okienka w zależności od tego, czy używamy main czy WinMain. Ale napisanie swojego odpowiednika tej funkcji nie nastręcza problemów, wystarczy się przyjrzeć makrodefinicji zawartej w pliku assert.h. Polecam również artykuł w książce Perełki Programowania Gier część pierwsza, rozdział 1.12. Są tam świetne porady na ten temat.

StructReader

Wpadłem na pomysł prostego serializer’a, który by potrafił czytać dane z pliku podzielonego na sekcje i zapisywać je do prostej struktury – agregata. Miał być to szablon funkcji, który pobierałby jako jeden z argumentów wskaźnik do tej struktury, następnie wyliczał miejsce zmiennej w strukturze, na podstawie tablicy z typami (enum) i w końcu zapisywał przeczytane dane odpowiednio, sformatowane przez bibliotekę standardową. Niestety jak się okazało (czego do tej pory nie wiedziałem) miedzy składnikami klasy, struktury są przerwy, więc nie można skorzystać z wskaźnika, którego chciałem przesuwać o rozmiar przeczytanego typu, gdyż spowoduje to błędy zapisu.
Na szczęście rozwiązanie tego problemu okazało się bardzo proste i jest o wiele bardziej uniwersalne, otóż zamiast wskaźnika na obiekt struktury, przekazuję funkcji (ponieważ szablon jest już niepotrzebny) tablicę wskaźników do zmiennych i jej rozmiar. Dzięki niej wiem gdzie w pamięci znajduje się dana zmienna. Rozwiązanie to ma też inna zaletę, ponieważ do funkcji mogę przekazać nie tylko obiekty struktury, ale również dowolne obiekty nienależące do struktury.

Oto przykładowy plik cfg:

1
2
3
4
5
6
7
8
Sekcja1{
   // Komentarz
}
// Komentarz 2
Sekcja2
{
   zmienna1 = 3.14;     // Komentarz np. opisuje jaki to typ, tu float
}

Jak widać schemat jest prosty:

  1. Sekcje oznaczamy nazwą i klamerkami
  2. Komentarze tylko po //, są ważne do końca linii
  3. Nazwy zmiennych nie są brane pod uwagę, liczy się znak ‘=’ i ‘;’, pierwszy rozpoczyna czytanie wartości, drugi kończy.

PS. Schemat pliku zmyślony z Regedit’a :).

W kodzie wygląda to tak:

1
2
3
4
5
6
7
8
9
10
11
// Tworzymy tablicę z typami
int types[] = { TYPE_FLOAT };        // Przykładowo float
 
// Zmienna
float temp;
 
// Tablica ze wskaźnikami
void* pointers = { &amp;temp };
 
// Odczyt z pliku
FillUpStruct("nazwa_pliku_jako_string", types, sizeof(types),  "Sekcja2", pointers, sizeof(pointers));

Według mnie wygląda to ładnie, i jest proste w użyciu, szczególnie przydatne przy zapisie ustawień np. urządzenia DX.