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.