Trasování binárních programů

Každý vývojář jednou přejde z bodu, kdy ‚loví‘ chyby ve svých algoritmech do bodu, kdy se snaží algoritmy optimalizovat. Optimalizovat buď na rychlost, případně na paměťovou náročnost. Nejtěžší a samozřejmě nejdůležitější je nalezení střední varianty mezi těmito dvěma extrémy. Dokud je algoritmus pouze jeden, případně celý program není příliš rozsáhlý, je možné tuto optimalizaci dělat takovým lidským přístupem. Například přidáváním výpisů do algoritmu, vypisováním aktuálního času a podobnými mechanizmy. Jakmile ale složitost programu dojde do určitého bodu, kdy si začne víceméně žít vlastním životem, základní přístup optimalizace přestane být vhodným. Žít vlastním životem může znamenat například to, že spolu začnou interagovat části programu, které spolu ani nikdy interagovat neměly, případně ani nemají. Toto má často za následek citelné zdržení algoritmu. Pro účely testování by měl být dostačující následující program. Jediné, co tento program vykoná, je vysílání UDP paketů na specifikovanou adresu a port a to tak rychle, jak je to jenom možné.
#include <iostream>

#include <unistd.h>

#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int sockfd;

// Open UDP socket toward some socket, write to it as fast as possible
bool openUdpSocket()
{
	std::cout << "Open UDP socket" << std::endl;

	if((sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0)) == -1)
	{
		std::cerr << "Cannot open socket" << std::endl;
		return false;
	}

	return true;
}

void sendData()
{
	char buffer[1024];
	struct sockaddr_in dest;
	socklen_t destLen;
	struct in_addr addr;

	if(inet_aton("192.168.169.10", &addr) == 0)
	{
		std::cerr << "Cannot convert address" << std::endl;
		return;
	}

	dest.sin_family = AF_INET;
	dest.sin_port = htons(45869);
	dest.sin_addr.s_addr = htonl(addr.s_addr);


	for(int i = 0; i < 800000; i++)
	{
		// send socket with sendto
		ssize_t s = sendto(sockfd, buffer, 1024, 0, (struct sockaddr *)&dest, sizeof(dest));
	
		if(s < 0)
		{
			usleep(100);
		}
	}
}

int main(int argc, char *argv[])
{
	std::cout << "Execute" << std::endl;

	if(! openUdpSocket())
	{
		return 1;
	}

	sendData();
	close(sockfd);

	return 0;
}
Na řádku č.68 je možné vidět, že jakmile dojde k naplnění vysílacího bufferu, program vyčká 100us, než začne buffer opět plnit. Zde by mohla přijít zcela správná otázka a to, zda bude program déle vysílat data (plnit buffer), nebo čekat na místo v bufferu. V tuto chvíli se může hodit znát pár programů, které mohou pomoci. Souhrnně se tyto programy nazývají profilery, případně tracery, čili programy, které umí zaznamenávat běh jiného programu a to jak volané funkce, tak i alokovanou, případně uvolněnou paměť. Nejznámějším programem je sada programů valgrind. Pokud je valgrind spuštěn bez parametrů, je jeho výchozí konfigurací sledovat paměťovou náročnost programu. V tomto článku se ale máme věnovat trasování programu, čili zjistit, kolik času (CPU tiků) stráví procesor ve které funkci. Proto je nutné valgrind spustit s parametrem –tool=callgrind. Takto jednoduché spuštění provede vlastní program a následně vytvoří soubor s názvem callgrind.out.<xxxxx> . Výstup je textový soubor, který obsahuje záznam toho, jak byly jednotlivé funkce volány a jejich časy. Tento samotný soubor není úplně vhodný k přímému čtení, je mnohem lepší využít vizualizační program, který tyto data převede do grafů a statistik. Asi nejvhodnějším se jeví KCachegrind, který poskytuje velmi obsáhlé rozhraní a současně je velmi stabilní a přehledný. Na obrázku níže je vidět základní rozvržení programu s načtenými trasovacími daty. V levém sloupci je zobrazen čas, který program strávil v dané funkci. Funkce main dosahuje hodnot přes 90%, vždy ale záleží na tom, jak dlouho program běžel. Funkce sendData, která je zavolána právě jedenkrát dosahuje velmi podobných hodnot. Toto je poměrně jasné, neboť je aktivní více méně stejně dlouho. Mnohem zajímavější je však rozložení funkcí usleep a sendto. Zatímco první z nich nic nedělá, pouze čeká, druhá vykonává vlastní program – odesílá data. Rovněž je zcela zřejmě vidět, že funkce sendto je volána přesně 800 000x, což je požadavek programu. Funkce usleep je volána 595 488x, nicméně program uvnitř této funkce stráví delší dobu (více procesorového času). V článku byl představen způsob, jakým je možné zjistit slabší stránky programu a tím dojít k jeho výraznému zrychlení. V ukázkovém případě by například bylo vhodné implementovat jiný mechanizmus vysílání paketů, než blokující smyčku (sendto + sleep). Místo funkce sleep by bylo možné vykonávat nějaké další akce, toto nicméně přesahuje možnosti tohoto článku.

Posted

in

, ,

by