Forum: PC-Programmierung sprintf() zu langsam!?


von Franz W. (wizzy450)


Lesenswert?

Hallo beisammen,

Ich benutze Microsoft's Active-X ADO per C um Daten in eine Datenbank zu 
klopfen. Die Daten liegen in meinem Programm als zwei Double-Arrays vor.

Im Zuge der Konvertierung in einen INSERT - String laufe ich über die 
Arrays in einer for-Schleife und füge an einen C-String (Char-Array) die 
jeweiligen nach double konvertierten Strings an.

Anschließend wird der String per .execute an die ADO übergeben.

Der Aufruf des .execute Befehls geht dabei erstaunlich flott, selbst bei 
sehr (!) langen Strings.

Das Zusammenbasteln des SQL-Strings selbst per sprintf() geht dabei 
allerdings gähnend langsam von statten.

Pseudo-Code:
1
for (i; i<alle_datenpunkte, i++)
2
{
3
  sprintf (bufferstring, "(%ld, %f, %f), ", array1[i], array2[i], array3[i]);
4
  strcat(sqlstring, bufferstring);
5
}

String: INSERT INTO ... (x,y,z) VALUES (xx,yy,zz), (xxx, yyy, zzz), ...


Bei 7 Tausend Punkten dauert das Zusammenkopieren etwa 3 Sekunden, das 
Übergeben an die ADO dauert etwa 200 ms.

Die Funktion Zeit/Datenpunkte ist leider auch nicht linear, habe den 
Eindruck das geht annähernd quadratisch.

Hat jemand nen besseren Vorschlag?

Schönen Abend beisammen!
Franz

von mr.chip (Gast)


Lesenswert?

Wenn ich es mir richtig überlege, wird dabei bei jedem 
Schleifendurchlauf der String sqlstring umkopiert. Im normalfall wird es 
ja kaum möglich sein, den anzuhängenden String direkt an das Ende des 
bestehenden Strings zu setzen. (Im Speicher.) So muss für den ganzen 
String ein Platz im Speicher gefunden werden, wo er in seiner vollen 
Länge Platz hat. Das kann schon ein bisschen dauern.

Besser wäre es, wenn du bereits am Anfang genügend Speicher für den 
sqlstring reservierst und bufferstring danach manuell dort reinkopierst.

von Karl H. (kbuchegg)


Lesenswert?

Dein Problem ist nicht der sprintf.
Dein Problem ist der strcat.
Dies deshalb weil er bei jedem Aufruf erst mal aufwändig
das Ende des bereits vorhandenen Strings suchen muss.
(Und das führt dann tatsächlich zu einer quadratischen
Komplexität)

Wenn du daher selbst mitzählst, bis wohin sqlstring bereits
gefüllt ist, und diese Startadresse dem strcat übergibst, dann
kannst du masenhaft Suchzeit einsparen.
1
  start = sqlstring;
2
  *strat = '\0';
3
4
  for (i; i<alle_datenpunkte, i++)
5
  {
6
    sprintf (bufferstring, "(%ld, %f, %f), ", array1[i], array2[i], array3[i]);
7
    strcat( start, bufferstring );
8
    start += strlen( bufferstring );
9
  }

von Karl H. (kbuchegg)


Lesenswert?

mr.chip wrote:
> String ein Platz im Speicher gefunden werden, wo er in seiner vollen
> Länge Platz hat. Das kann schon ein bisschen dauern.

Du hast aber schon gesehen, dass hier in C und nicht in C++
programmiert wird?

>
> Besser wäre es, wenn du bereits am Anfang genügend Speicher für den
> sqlstring reservierst und bufferstring danach manuell dort reinkopierst.

Genau das macht er.

von Franz W. (wizzy450)


Lesenswert?

Danke für den Tipp mit den Pointer-Zugriffen (Pointer auf den String). 
Das stimmt aber, daran hatte ich nicht gedacht, dass strcat erst mal 
durchnudelt und kuckt wo "\0" kommt. Das werde ich gleich am Montag 
probieren!

Frage: Tritt hier wieder ein typisches +-1-Problem auf? Also sprintf 
schließt den bufferstring mit "\0" ab. Zählt strlen() dann die letzte 
"\0" mit? Nicht dass strcat dann keine "\0" findet, da meckert es sonst.

----------------------------------

Mal abgesehen von der Lösung von oben (es ärgert mich dass ich da nicht 
selbst drauf gekommen bin) - hat jemand ne Idee wie man (auf andere Art 
und Weise) schnell und effektiv SQL-Strings zusammenbasteln kann?!
Die ADO-Funktionen möchte ich aufgrund ihrer Komplexität (.AddNew, 
.Update) nicht verwenden, da das Runterbrechen der Objektorientierung 
nach ANSI-C ein Riesenaufwand ist.

Danke - Franz

von Uhu U. (uhu)


Lesenswert?

So rein gefühlsmäßig würde ich sagen, das liegt nicht nur an strcat:

- Ordentlich optimierter Code führt den Scan auf einem x86 im 
wesentlichen in einem Maschinenbefehl aus; die String-Kopie genauso.
- Der Code für *printf und ftoa & Co. ist wesentlich komplexer, als 
strcat.

Miß mal die Zeiten, die von beiden Routinen insgesamt gebraucht werden. 
Bin auf das Ergebnis gespannt.

von Arc N. (arc)


Lesenswert?

Uhu Uhuhu wrote:
> So rein gefühlsmäßig würde ich sagen, das liegt nicht nur an strcat:
>
> - Ordentlich optimierter Code führt den Scan auf einem x86 im
> wesentlichen in einem Maschinenbefehl aus; die String-Kopie genauso.

Das ändert nichts daran, das die Laufzeit quadratisch wächst.
Originalvariante z.B. jeweils 20 Zeichen hinzufügen:
strcat (suchen + kopieren): 0 + 20, 20 + 20, 40 + 20, 60 + 20, 80 + 20, 
100 + 20, 120 + 20, ...
mit strlen 20 + 20, 20 + 20, 20 + 20, ...
Macht im letzteren Fall 7000 * (20 + 20) = 280000 Elementaroperationen
Die strcat Variante 7000 * 20 + 20 + 40 + 60 + 80 + ... + 6980 * 20 
~490000000

von Sven P. (Gast)


Lesenswert?

Zu deinem Off-by-One-Problem:

Bsp 1: String: x = "" (leer), strlen(x) = 0; x + strlen(x) zeigt auf \0.

Bsp 2: String: x = "abc", strlen(x) = 3; x + strlen(x) zeigt immer noch 
auf \0.

von Karl H. (kbuchegg)


Lesenswert?

Uhu Uhuhu wrote:
> So rein gefühlsmäßig würde ich sagen, das liegt nicht nur an strcat:
>

quadratische Komplexität ist quadratische Komplexität :-)
1
#include <stdio.h>
2
#include <string.h>
3
#include <time.h>
4
5
char total[300000];
6
7
void check1( int a )
8
{
9
  char buffer[100];
10
  int i;
11
12
  total[0] = '\0';
13
14
  for( i = 0; i < a; i++ )
15
  {
16
    sprintf( buffer, "%d,%d,", i, i );
17
    strcat( total, buffer );
18
  }
19
}
20
21
void check2( int a )
22
{
23
  char buffer[100];
24
  char* start = total;
25
  int i;
26
27
  *start = '\0';
28
29
  for( i = 0; i < a; i++ )
30
  {
31
    sprintf( buffer, "%d,%d,", i, i );
32
    strcat( start, buffer );
33
    start += strlen( buffer );
34
  }
35
}
36
37
int main()
38
{
39
  int i;
40
  clock_t startt;
41
  
42
  startt = clock();
43
  for( i = 0; i < 10; ++i )
44
    check1( 7000 );
45
  printf( "%ld\n", clock() - startt );
46
47
  startt = clock();
48
  for( i = 0; i < 10; ++i )
49
    check2( 7000 );
50
  printf( "%ld\n", clock() - startt );
51
52
  char c = getchar();
53
  return 0;
54
}

Compiler: VC++ 6.0, release compiliert

Laufzeiten:
  erster Abschnitt:  4046 clock ticks
  zweiter Abschnitt:   79 clock ticks

von Karl H. (kbuchegg)


Lesenswert?

Man könnte es auch noch eleganter machen.
Wozu den sprintf zuerst in einen Buffer schreiben lassen,
nur um diesen dann zu kopieren.
1
void check2( int a )
2
{
3
  char buffer[100];
4
  char* start = total;
5
  int i;
6
7
  *start = '\0';
8
9
  for( i = 0; i < a; i++ )
10
  {
11
    sprintf( start, "%d,%d,", i, i );
12
    start += strlen( buffer );
13
  }
14
}

Allerdings: Mein Compiler dürfte das geschnallt haben.
Am Timing ändert sich nichts zu der Umkopiervariante.

Was aber noch was gebracht hat:
Den Returnwert von sprintf zu benutzen, anstelle des strlen
1
void check2( int a )
2
{
3
  char* start = total;
4
  int i;
5
6
  *start = '\0';
7
8
  for( i = 0; i < a; i++ )
9
  {
10
    start += sprintf( start, "%d,%d,", i, i );
11
  }
12
}

von Uhu U. (uhu)


Lesenswert?

Da hast du dich aber fein säuberlich um den Aufwand für 
float-Konvertierung gedrückt ;-)

Das das strcat elegant ist und nichts kostet, habe ich nicht behauptet. 
Ich vermute aber nach wie vor, daß der Löwenanteil der Zeit in  Franz 
Wudys Code in der Konvertierung verbraten wird.

von Karl H. (kbuchegg)


Lesenswert?

Uhu Uhuhu wrote:
> Da hast du dich aber fein säuberlich um den Aufwand für
> float-Konvertierung gedrückt ;-)

7000 float sprintf sind im Vergleich zu den Millionen
Umkopieraktionen und String-Ende-Such-Schleifen nicht
das Zeitbestimmende.

Ausserdem schlagen die sprintf in beiden Code-Teilen mit
dem gleichen Zeitbedarf zu Buche.

>
> Das das strcat elegant ist und nichts kostet, habe ich nicht behauptet.
> Ich vermute aber nach wie vor, daß der Löwenanteil der Zeit in  Franz
> Wudys Code in der Konvertierung verbraten wird.

Hast du ein Programm, mit dem du es beweisen kannst?

Niemand sagt, dass float-String Wandlung gratis kommt.
Aber dann erklär mir doch bitte mal wie Franz zu seiner
'gefühlten' quadratischen Komplexität kommt. Auf die Erklärung
bin ich schon neugierig.

von Εrnst B. (ernst)


Angehängte Dateien:

Lesenswert?

Uhu Uhuhu wrote:

> Ich vermute aber nach wie vor, daß der Löwenanteil der Zeit in  Franz
> Wudys Code in der Konvertierung verbraten wird.

Halte ich für unwahrscheinlich.
Wenn der ADO- bzw Datenbank-Interpreter 21000 Floats in unter 200ms 
Parsen kann, wird das sprintf für das ausgeben nicht soo viel länger 
brauchen.

Bei mir brauchen übrigens 7000 sprintf-Aufrufe mit je drei Floats 
zusammen 82000000 CPU-Instruktionen...
(glibc-sprintf, gcc-4.2, -O2)

Bei der Schleife wie im Ursprungspost gehen bei mir 92.69% der Zeit für 
strcat drauf, nur 7.29% für das sprintf.
(Im Anhang die wenig aussagekräftige Graphik dazu...)

/Ernst

von Franz W. (wizzy450)


Lesenswert?

Hallo beisammen,
danke für die vielen Antworten, wenngleich ich nicht eine derartig 
hitzige Diskussion anfechten wollte.

Mir erscheint die Version
1
start += sprintf( start, "%d,%d,", i, i );
sehr sinnvoll; und ich werde sie gleich morgen ausprobieren.

Dass sprintf nicht die schnellste Angelegenheit ist, ist klar. 
Wenngleich es mir schon vorschwebte, dtostrf aus AVR zu transponieren, 
die begründete Hoffnung dass der Löwenanteil auf strcat entfällt, 
erspart mir (höchstwahrscheinlich) viel Zeit, da ich dieses Vorhaben nun 
sein lassen kann.

Eine Frage noch, interessehalber: Wie macht man diesen schönen 
Timing-Grafen?

Danke -
Franz

von Karl H. (kbuchegg)


Lesenswert?

Franz Wudy wrote:

> Eine Frage noch, interessehalber: Wie macht man diesen schönen
> Timing-Grafen?

Das wird irgendein Profiler sein.

Profiler: Programm welches ausmisst, wieviel Zeit in welchen
Programmteilen verbraucht wird.

http://de.wikipedia.org/wiki/Profiler_%28Programmierung%29

Bevor man sich an die Optimierung eines Programmes macht,
sollte man eigentlich immer einen Profiler zu Rate ziehen.
Menschen sind notorisch schlecht darin, durch Raten die
Bottlenecks in einem Programm aufzuspüren. Mit einem Profiler
hat man die Gewissheit sich um die richtigen 10% des Codes
zu kümmern.
Getreu dem Motto: 10% des Codes sind für 90% der Laufzeit
verantwortlich.

von Sven P. (Gast)


Lesenswert?

Japp. Es gibt/gab sogar Hardware-Profiler die direkt an der CPU 
angeklemmt werden/wurden. Eine äüßerst geistreiche Firma wollte so mal 
die Hardwareseite eines neu entwickelten Prozessor optimieren. Mittels 
Profiler fand man heraus, dass dieser Prozessor die meiste Zeit immer 
dieselbe Abfolge von vier  Befehlen ausgeführt hat. Man dachte sich 
also: "Mensch, da machen wir eine eigene Instruktion draus, dann läuft 
die Kiste viermal so schnell."

Gesagt, getan. Nun hatte man also erfolgreich die Leerlauf-Schleife des 
Prozessors optimiert.

JFYE :-)

von Bartli (Gast)


Lesenswert?

> - Ordentlich optimierter Code führt den Scan auf einem x86 im
> wesentlichen in einem Maschinenbefehl aus; die String-Kopie genauso.

Du meinst scas/movs/lods mit rep Präfix. Natürlich ist das nur ein 
Befehl, dahinter verbirgt sich aber eine Schleife in Microcode. Wär ja 
zu schön wenn die Komplexität von strlen() auf nem x86 O(1) wäre...

Nullterminierte Strings wie in C sind eigentlich so ziemlich die 
dämlichste Art, um Zeichenketten zu repräsentieren. Vor einer Weile 
hatten wir mal Probleme mit einer älteren Saftware, welche ziemlich 
langsam war. Was haben wir (unter anderem) gefunden? Massenhaft solche 
Schleifen:
1
for (size_t i=0; i<strlen(blah); ++i) {
2
}

Zuegegebenermassen ist obenstehender Code weitaus dämlicher als die Idee 
von nullterminierten Strings. Mit einem STL-String kann man sowas 
natürlich schon machen (Methode length()).

von Franz W. (wizzy450)


Lesenswert?

So, hier die Messergebnisse mit dem neuen Code:

Zusammenkopieren:
1
...
2
for(i=0; i < qcmSQLsaveSpec.datapoint-1; i++)
3
{
4
sqlstringpos += sprintf(sqlstringpos, "(%lu, %.8f, %.8f), ", ....);
5
}
6
...

Übertragen:
1
...
2
adoreturn = ADODB__ConnectionExecute(connectionHandle, NULL, sqlstring, NULL , -1, NULL);
3
...

Datenpunkte | Zusammenkopieren | Übertragen
  175000    |     855          |  8251
   80187    |     440          |  3497
    7215    |      41          |   296

Zeitangaben in clock()-Einheiten.
Jetzt skaliert das Ganze recht linear! Endlich.

Danke für die Hilfe!

Franz

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.