piątek, 27 listopada 2009

Tysiąc insertów na sekundę

Dokładniej 50,000 na typowym komputerze - tak przynajmniej twierdzą twórcy sqlite3 w FAQ. Miałem okazję przekonać się o tym na własnej skórze. W programie o którym wcześniej pisałem chciałbym zapisywać dane o cząsteczkach w danej iteracji algorytmu. Oto kod przed optymalizacją:
void SaveResultsToDatabase(sqlite3* db, Particle* host_agents, const int N, const int generation) {
char stmt[90];
char* errorMsg = NULL;
int returnCode;

int i = 0;
for (i = 0; i < N; ++i) {
sprintf(stmt, "insert into pso values (%d, %d, %f, %f); ",
generation,
i,
host_agents[i].current.x,
host_agents[i].current.y);
returnCode = sqlite3_exec(db, stmt, NULL, NULL, &errorMsg);
if (returnCode != SQLITE_OK) {
if (errorMsg != NULL) {
printf("%s", errorMsg);
sqlite3_free(errorMsg);
} else {
printf("Return code: %d\n", returnCode);
}
}
}
}
W tym przypadku czas wykonania to około minuty dla 1000 wpisów. Każdy insert jest wykonywany, baza czeka aż dane będą bezpieczne i dopiero wykonuje następny wpis. Jednak tak jak to jest opisane w FAQ można tego uniknąć tworząc jedną wielką transakcję zamkniętą w klamry BEGIN TRANSACTION; i COMMIT:
 void SaveResultsToDatabase(sqlite3* db, Particle* host_agents, const int N, const int generation) {
char buffer[90];
char stmt [N * 90];
strcpy(stmt, "BEGIN TRANSACTION; ");
char* errorMsg = NULL;
int returnCode;

int i = 0;
for (i = 0; i < N; ++i) {
sprintf(buffer, "insert into pso values (%d, %d, %f, %f); ",
generation,
i,
host_agents[i].current.x,
host_agents[i].current.y);
strcat(stmt, buffer);
}
strcat(stmt, "COMMIT");
returnCode = sqlite3_exec(db, stmt, NULL, NULL, &errorMsg);
if (returnCode != SQLITE_OK) {
if (errorMsg != NULL) {
printf("%s", errorMsg);
sqlite3_free(errorMsg);
} else {
printf("Return code: %d\n", returnCode);
}
}
}
W tym przypadku czas wykonania jest poniżej sekundy. Niezła optymalizacja.
BREAKING NEWS. Właśnie spróbowałem zapisać 100000 wpisów i okazało się, że trwa to około 1,5 minuty. Nie wiedziałem dlaczego aż tyle. Z początku myślałem, że chodzi o manipulowanie char*, ciągłe przydzielanie pamięci itp, ale potem przypomniało mi się, że ktoś pisał o tym, żeby znaleźć "optimal insert size for your database". Czas zatem na kolejną zmianę kodu:
void SaveResultsToDatabase(sqlite3* db, Particle* host_agents, const int N, const int generation) {
printf("Saving to database\n");
const int partSize = 5000;
char buffer[90];
char stmt [partSize * 90];
int i = 0;
while (i < N) {
strcpy(stmt, "BEGIN TRANSACTION; ");
char* errorMsg = NULL;
int returnCode;

do {
sprintf(buffer, "insert into pso values (%d, %d, %f, %f); ",
generation,
i,
host_agents[i].current.x,
host_agents[i].current.y);
strcat(stmt, buffer);
++i;
} while ((i % partSize != 0) && i < N);
strcat(stmt, "COMMIT");
returnCode = sqlite3_exec(db, stmt, NULL, NULL, &errorMsg);
if (returnCode != SQLITE_OK) {
if (errorMsg != NULL) {
fprintf(stderr, "%s\n", errorMsg);
sqlite3_free(errorMsg);
} else {
fprintf(stderr, "sqlite3 return code: %d\n", returnCode);
}
}
}
}
W obecnej postaci wykonanie kodu trwa 23 sekundy dla 3 iteracji po 100000 cząsteczek, włączając w to przesyłanie danych, obliczenia na karcie graficznej i wyświetlanie komunikatów w terminalu (to chyba najbardziej spowalnia program). Przypuszczam, że jeszcze będę eksperymentować z wielkością wrzutów do bazy danych.

Przeszukiwanie listy argumentów linii poleceń w C

Po długich i wyczerpujących walkach z nvcc (kod skompilowany nvcc nie bardzo się komponuje z g++, nie wiem czemu, innym DZIAŁA - SOA#1) postanowiłem, że algorytm optymalizacji rojem będzie miał postać programu w C, który zapisze wynik działania algorytmu w bazie sqlite3, a ja sobie w innej aplikacji skompilowanej już normalnym kompilatorem będę te dane obrabiał. W międzyczasie okazało się, że fajnie byłoby ustalać liczbę cząsteczek roju i postanowiłem do tego wykorzystać argumenty linii poleceń. Wtedy pojawił się problem jak z argc i argv wyciągnąć to o co mi chodzi. Czynność ta jest wykorzystywana na porządku dziennym w programach, więc istnieje w tym celu odpowiednia procedura, a nazywa się ona getopt() i znajduje się w pliku nagłówkowym unistd.h. W obecnej chwili używam dwóch opcji: -n oraz -o. Obie wymagają argumentu. Oto kod:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char** argv) {
//initialize variables
int N = 10; // Ilosc agentow
int c = 0;
char* dbfilename = NULL;

while((c = getopt(argc, argv, "n:o:")) != -1)
{
switch(c)
{
case 'n':
N = atoi(optarg);
printf("Number of particles: %d\n", N);
if (N <= 0)
{
fprintf(stderr, "Invalid number of particles, setting to default = 10\n");
N = 10;
}
break;
case 'o':
dbfilename = optarg;
printf("Output to file: %s\n", dbfilename);
break;
case '?':
switch(optopt)
{
case 'n':
fprintf(stderr, "Option -n sets number of particles and requires integer argument.\n");
break;
case 'o':
fprintf(stderr, "Option -o sets name of output file and requires a string argument.\n");
break;
default:
fprintf(stderr, "Unknown option: %c\n", optopt);
};
break;
default:
printf("Abort: %d = %c\n", c, c);
abort();
}
}
Przede wszystkim po "przegryzieniu się" przez wszystkie argumenty getopt() zwraca -1, dlatego taki warunek jest w while. W przeciwnym przypadku dostaniemy wartość znaku, który reprezentuje opcję. Opcje jakie przewidujemy w programie podajemy jako trzeci argument. Aby zaznaczyć, że opcja ma wymagany argument należy dodać po niej ":". Aby zaznaczyć, że opcja ma opcjonalny argument dodajemy "::". W switch podejmujemy odpowiednią akcję w zależności od opcji.
Dla -n będzie to zamiana alphanumeric to integer, czyli zamiana łańcucha znaków na liczbę całkowitą, a następnie sprawdzenie czy wartość ta jest większa od 0. Argument dla opcji jest przechowywany w optarg jako łańcuch znaków.
Opcja -o odpowiada za nazwę pliku wynikowego. Jak widać można przypisać wskaźnik optarg do wskaźnika typu char*, czyli nie jest on typu const char* (tak jakby to było w przypadku np printf("Tekst typu const char*"). Dokładniej optarg wskazuje na jeden z elementów argv, więc można go modyfikować. W przypadku błędów podawana jest wartość '?' co może oznaczać brak argumentu dla opcji lub nieznaną opcję.
Opcja default z tego co zrozumiałem nie ma prawa się wydarzyć, ale dodałem tam trochę kodu, żeby dowiedzieć się co się dzieje w razie czego. W końcu dobry programista patrzy w obie strony przed przejściem przez jednokierunkową ulicę.

piątek, 6 listopada 2009

CUDA 2.3 na Ubuntu 9.10 cz. 2

Teraz należałoby sprawdzić czy wszystko jest poprawnie zainstalowane kompilując przykładowe projekty.
Skracając cały proces pokonywania błędów jakie wystąpiły w czasie tego polecenia:
1) CUDA nie działa z kompilatorami w wersji 4.4. Niestety odinstalowanie gcc-4.4 nie wchodziło w grę, gdyż musiałbym również odinstalować świeżo zainstalowane sterowniki. Zainstalowałem więc gcc i g++ w wersji 4.3.
sudo apt-get install gcc-4.3 g++-4.3
Potem próbowałem zmienić makefile, aby zamiast
CXX := g++
CC := gcc
LINK := g++ -fPIC
mieć
CXX := g++-4.3
CC := gcc-4.3
LINK := g++-4.3 -fPIC
Niestety to nic nie dało. Jeden z użytkowników z forum NVIDIA doszedł do wniosku, że zmienne te nadpisywane są w którymś z innych makefile'ów. Cofnąłem zmiany i zamiast tego zmieniłem dowiązanie /usr/bin/gcc z gcc-4.4 na gcc-4.3.
sudo rm /usr/bin/gcc
sudo ln -s -v gcc-4.3 /usr/bin/gcc
Analogiczną operację przeprowadziłem też dla g++. Sprawdziłem czy pod linkiem gcc jest kompilator o właściwej wersji:
gcc --version
gcc (Ubuntu 4.3.4-5ubuntu1) 4.3.4
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
2) Potrzebne też będą paczki libxmu-dev libxi-dev freeglut3 freeglut3-dev.

3) Teraz można już budować projekty.
cd ~/Projects/NVIDIA_GPU_Computing_SDK/C
make

Jeśli wszystko dobrze zapamiętałem to nie powinno być żadnych błędów kompilacji i będzie można uruchomić podstawowe programy testujące na obecność urządzenia cuda capable.
daniel@daniel-laptop:~/Projects/NVIDIA_GPU_Computing_SDK/C/bin/linux/release$ ./deviceQuery
CUDA Device Query (Runtime API) version (CUDART static linking)
There is 1 device supporting CUDA

Device 0: "GeForce 8400M GS"
CUDA Driver Version: 2.30
CUDA Runtime Version: 2.30
CUDA Capability Major revision number: 1
CUDA Capability Minor revision number: 1
Total amount of global memory: 267714560 bytes
Number of multiprocessors: 2
Number of cores: 16
Total amount of constant memory: 65536 bytes
Total amount of shared memory per block: 16384 bytes
Total number of registers available per block: 8192
Warp size: 32
Maximum number of threads per block: 512
Maximum sizes of each dimension of a block: 512 x 512 x 64
Maximum sizes of each dimension of a grid: 65535 x 65535 x 1
Maximum memory pitch: 262144 bytes
Texture alignment: 256 bytes
Clock rate: 0.80 GHz
Concurrent copy and execution: Yes
Run time limit on kernels: Yes
Integrated: No
Support host page-locked memory mapping: No
Compute mode: Default (multiple host threads can use this device simultaneously)

Test PASSED
oraz
Running on......
device 0:GeForce 8400M GS
Quick Mode
Host to Device Bandwidth for Pageable memory
.
Transfer Size (Bytes) Bandwidth(MB/s)
33554432 1473.1

Quick Mode
Device to Host Bandwidth for Pageable memory
.
Transfer Size (Bytes) Bandwidth(MB/s)
33554432 898.1

Quick Mode
Device to Device Bandwidth
.
Transfer Size (Bytes) Bandwidth(MB/s)
33554432 4197.4

&&&& Test PASSED

CUDA 2.3 na Ubuntu 9.10 cz. 1

Kolejne podejście do używania CUDA na Ubuntu. Po kłopotach jakie miałem ze sterownikami NVIDIA na Ubuntu 8.04 postanowiłem przenieść się na najnowsze wydanie 9.10 (flash nie reaguje na kliknięcia, komputer nie wykrywa, że podłączyłem słuchawki - nie ukrywam, że to niemiłe rozczarowanie). Przy okazji zrobiłem upgrade z XP na Windows7.
Najnowsze sterowniki (w wersji 190) zainstalowałem z pomocą tego poradnika z Ubuntu Geek. W skrócie:
sudo add-apt-repository ppa:nvidia-vdpau/ppa
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CEC06767
sudo apt-get update
sudo apt-get install nvidia-190-modaliases nvidia-glx-190 nvidia-settings-190

Należy je jeszcze aktywować w System -> Administracja -> Sterowniki.

Teraz potrzebne będą Cuda Toolkit 2.3 - nie ma wersji dla Ubuntu 9.10, więc wybrałem wersję dla 9.04. Instalacja według instrukcji przebiega bezproblemowo. Potem zainstalowałem CUDA SDK również w wersji 2.3 dla Ubuntu 9.10.

Kolejnym krokiem było dodanie ścieżek cuda toolkit do zmiennych środowiskowych. W pliku ~/.bashrc dodałem:
if [ -d "/usr/local/cuda" ] ; then
PATH="/usr/local/cuda:$PATH"
fi

if [ -d "/usr/local/cuda/lib64" ] ; then
LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
fi

if [ -d "/usr/local/cuda/bin" ] ; then
PATH="/usr/local/cuda/bin:$PATH"
fi
Pierwszy if sprawdza czy istnieje /usr/local/cuda, a jeśli tak to dodaje tę ścieżkę do zmiennej systemowej PATH. Kolejny if dodaje ścieżkę do bibliotek odpowiednich dla 64-bitowego systemu. Następnie dodawany jest katalog z plikami wykonawczymi aby móc korzystać z polecenia nvcc bez wpisywania całej ścieżki.
Nie jestem pewien czy w umieściłem ten skrypt w dobrym pliku - jeśli dobrze pamiętam to jest on wywoływany przy każdym włączeniu konsoli. Dobrze by było by dodawało się ono tylko raz przy starcie systemu i tak już zostawało. Po kilku uruchomieniach PATH wygląda tak:
daniel@daniel-laptop:~$ printenv PATH
/usr/local/cuda/bin:/usr/local/cuda:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
Do tej pory wszystko przebiegało gładko i prawdziwe problemy dopiero się zaczną.