Nummer in String möglichst schnell schreiben
-
Nathan schrieb:
Schnellere Methoden brauchen vermutlich Lookup-Table oder Assemblertricks.
Man könnte vielleicht noch was rausholen, wenn man statt der Dezimalschreibweise die hexadezimale wählt. Ist denn dezimal wirklich wichtig?
P.S.: Außerdem ldiv, falls der Compiler diese einfache Optimierung nicht selber finden sollte. Und testen, ob es wirklich was bringt (kann nämlich nach hinten losgehen).
P.P.S.: Zwar kein ausführlicher Test, aber beim GCC scheint es nach hinten los zu gehen, da der Funktionsaufruf nicht inlined wird. Also vergiss diesen ganzen Absatz
-
@SeppJ: Leider muss es dezimal sein. Die meisten Programme sortieren nicht nach Hex, sondern nach Dezimal, und beim Durchgehen der geschriebenen Dateien muss die Reihenfolge unbedingt beibehalten werden.
@Nathan: Die Lösung wäre wirklich klasse - braucht nur die Hälfte der Cycles meiner Routine - aber leider schreibt sie rückwärts. D.h. dass wenn der Buffer nicht vollständig gefüllt wird, steht am Anfang nur Unsinn oder gar nichts (weil Arrays zumindest mit dem gcc) mit 0 initialisiert werden. Wenn ich das fixen will, muss ich da wieder memmove machen und den Rest schiften, oder vorher die richtige Länge ermitteln (Augenblick mal ...), und das ist doof. Sonst wäre der Ansatz echt klasse.
-
Wie sieht denn das genaue Format im string buffer aus?
Kannst du den evtl. ganz rückwärts füllen?
Und so ein memmove sollte aber nicht allzu lahm sein.
-
Das Format ist wie im Beispielcode beschrieben, sowas wie:
"123456\0"
"777\0"
"42\0"
Das mit
memmove
kann ich vergessen. Selbst wenn diealloca
stattmalloc
undfree
verwenden, ist der Zugriff auf den Speicher zu langsam, das geht hoch in die 3000 Cycles.Wenn ich den ganz rückwärts füllen möchte, muss ich wissen, wo ich anfangen soll zu schreiben. Beim Vorwärtsfüllen ist das klar, in der Speicherzelle, auf die
string
zeigt. Beim Rückwärtsprüfen muss ich den Wert erst durchpotenzieren, damit ich die Länge habe (und dann habe ich das erste Problem, dass bei0xFFFF FFFF
Endlosschleife is), oder ich mache am Ende memmove, was aber langsam ist ... ich verstehe so langsam, wo die Performance beimsnprintf
hingeht.Selbst wenn es ein
memcpy
wäre (was ich nicht verwenden kann, weil nicht sichergestellt ist, in welche Richtung das kopiert), wäre es vermutlich zu teuer, nach dem Rückwärtsfüllen die Bytes an den Anfang des Bereiches zu setzen. Das lohnt sich nur, wenn die Zahl exakt in den Speicher passt, dann gibt es keine Lücke und damit auch keinen Grund zum Rumshiften.Ich muss da mal eine Nacht drüber schlafen. Eigentlich will ich die Länge ja angeben, damit es auf keinen Fall zu einem Buffer-Overflow kommt. Kaputte Namen sind immer noch besser als Stack-Corruption ...
-
Was willst du den mit dem String machen?
Wenn du den an eine Funktion übergibst, der Rückgabewert meiner Funktio. zeigt auf den Anfang der Zahl, das kannst du dann übergeben. Sollte eigentlich egal sein, ob der Cfap vorne oder hinten steht. Musst nur ggf. vorher das letzte Zeichen im Buffer nullen.
-
Dachstuhl schrieb:
Das mit
memmove
kann ich vergessen. Selbst wenn diealloca
stattmalloc
undfree
verwenden, ist der Zugriff auf den Speicher zu langsam, das geht hoch in die 3000 Cycles.Ich glaube, du stellst dir memmove falsch vor. memmove allokiert nichts .Hast du es mal ausprobiert?
Falls da bei dir irgendetwas sehr komisch optimiert wird oder du irgendein ganz komisches memmove hast, dass intern dynamisch Speicher allokiert, dann schreib dir ein eigenes naives memmove und fertig. Das ist möglicherweise sogar noch ein paar Takte flotter als das normale memmove, weil ein normales memmove noch Tests macht, welche Optimierung es benutzen soll. Für eine kleine Zeichenkette sollte aber ein naives 1:1-Verschieben schon recht nahe am Optimum sein (oder eventuell 4:4).
und dann habe ich das erste Problem, dass bei 0xFFFF FFFF Endlosschleife is
Wieso ist das eigentlich ein Problem?
size_t get_decimal_string_length(size_t value) { if (value < UL10) return 2; if (value < UL100) return 3; if (value < UL1000) return 4; if (value < UL10000) return 5; // usw.... 2^64 ist ganz schön groß if (value < UL1000000000000000000) return 20; return 21; }
(Obiger Code optimiert für kleine Zahlen. Wenn die Zahlen eher die vollen 64-Bit nutzen, dann macht man es umgekehrt.)
Man kann auch noch Bithacks nutzen, um den log2 ein bisschen schneller zu berechnen und dann auf den log10 umrechnen, aber ich schätze, wesentlich schneller wird es dadurch nicht werden. Vielleicht nochmal Faktor 2.
Siehe: http://graphics.stanford.edu/~seander/bithacks.htmlDas alles ist aber sicher wesentlich aufwändiger als ein memmove.
-
@SeppJ: Ich würde einfach mal sagen, mir sagt die Manpage, dass
memmove
Speicher reserviert (nicht mitmemcpy
verwechseln).The memory areas may overlap: copying takes place as though the bytes in src are first copied into a temporary array that does not overlap src or dest, and the bytes are then copied from the temporary array to dest.
Duh! Und natürlich habe ich es ausprobiert - oder denkst du, ich habe mir die 3000 aus dem Nichts herbeigezaubert?
Alles andere würde auch keinen Sinn ergeben. Nehmen wir mal an, ich sage
memmove
, dass es jetzt 64 MB verschieben soll. Die müssen gecacht werden, vor dem Schreiben. Das geht nicht mit Speicher auf dem Stack, der platzt sonst, selbst wenn es so intelligent sein sollte und erkennt, dass die Speicherbereiche sich eigentlich nicht in die Querre kommen und nur ein gewisser Teil wirklich gecacht werden muss. Und dann mussmalloc
undfree
verwendet werden. Und das ist auch der Grund, warum die damals nichtmemcpy
mitmemmove
überschreiben wollten.@Nathan: Ich habe mir gerade noch mal den alten Code angeschaut. In einigen Situationen könnte ich tatsächlich die Rückwärtsroutine verwenden, aber nicht für alle. Seufz. Naja, besser als nichts.
Jedenfalls Danke für die Hilfe an euch beide! Eventuell kann ich auch noch was aus dem Artikel extrahieren. Ich werde mich melden und die Routine pasten, wenn ich weitergekommen bin.
-
as though
Zwei wichtige Worte. Wenn Englisch nicht so deine Stärke ist, gibt es doch noch den Quelltext in C!
https://sourceware.org/git/?p=glibc.git;a=blob;f=string/memmove.c;hb=HEAD
http://git.musl-libc.org/cgit/musl/tree/src/string/memmove.c
http://anoncvs.estpak.ee/cgi-bin/cgit/openbsd-src/tree/lib/libc/string/bcopy.c
https://github.com/freebsd/freebsd/blob/master/lib/libc/string/bcopy.c
https://github.com/brho/plan9/blob/master/sys/src/libc/port/memmove.c
%VCINSTALLDIR%\crt\src\memmove.c (Visual Studio)
-
@Dachstuhlgang: Das ist der Originaltext aus der Manpage, Copy+Paste. Abgetippt hab ich den nicht.
Und was den Code angeht: das ist in der Tat höchst interessant. Die Routine erkennt selbstständig, ob es zu einem Überschneiden kommen könnte, und verwendet dann entweder Rückwärts- oder Vorwärtskopie. In diesem Fall muss dann nicht kopiert werden - ich fresse meinen Hut, da habe ich Quatsch erzählt. Danke!
Leider ändert das nichts am Problem selbst, dass memmove langsam ist. Hier ist der Code, den ich jetzt eingecheckt habe:
size_t write_number_reverse(char*string,size_t string_max,size_t value) { register size_t end=string_max; register size_t rvalue=value; if(!string || !end) return 0; do { string[--end]='0'+rvalue%10; rvalue/=10; } while(rvalue && end); if(end) memmove(&string[0],&string[end],string_max-end); return (string_max-end); }
Aufgerufen mit:
char xxx[11]; write_number_reverse(xxx,9,123456789LU);
Solange
end
0 ist, wenn er mit dem Schreiben fertig ist, ist alles kein Problem, hier haben ich sowas wie 200 Cycles. Sollte der Buffer aber größer sein, sind wir plötzlich bei 3500 Cycles. Zwar besser alssnprintf
, aber trotzdem ...Das hier ist nur ein bisschen langsamer, aber konstant (300 Cycles):
size_t get_decimal_string_length(size_t value) { register size_t rvalue=value; if (rvalue < 10UL) return 2; if (rvalue < 100UL) return 3; if (rvalue < 1000UL) return 4; if (rvalue < 10000UL) return 5; /*...*/ } size_t write_number(char*string,size_t string_max,size_t value) { register size_t end=get_decimal_string_length(value)-1; register size_t rvalue=value; if(string_max<end) end=string_max; if(!string || !end) return 0; string_max=end; do { string[end-1]='0'+rvalue%10; rvalue/=10; } while(rvalue && --end); return (string_max); }
Ich werde also zwei Routinen einfügen, eine, wenn man sich sicher ist, dass der Wert exakt in den Speicher reingeht oder bei dem es schlichtweg egal sein kann, und eine, bei der es egal ist, ob der Wert nicht exakt reingeht, die aber ein bisschen langsamer ist. Muss man wohl mit leben ...
P.S: Die Bithacks haben nichts an der Performance geändert, daher verwende ich lieber die statische Routine. Die kann ich wenigstens debuggen.
Danke für die Suggestionen. Ich hoffe, ich konnte auch damit Leuten, die künftig hier vorbeikommen, damit helfen.
-
Was spricht eigentlich dagegen, führende Leerzeichen zu benutzen?
-
DirkB schrieb:
Was spricht eigentlich dagegen, führende Leerzeichen zu benutzen?
Wäre nicht gut, weil da Namen für ein Dateisystem geschrieben werden müssen. Leerzeichen müssen für HTTP kodiert werden. Das ganze soll hochskalierbar sein, sonst kann ich es gleich sein lassen.
-
Hast du auch ganz sicher Optimierungen eingeschaltet? Sonst wird da am Ende noch irgendeine Debugversion von memmove genommen. Falls ja, dann kannst du auch noch versuchen, selber die Zeichen eines nach dem anderen zu verschieben. Diese paar Bytekopien können unmöglich tausende Takte fressen.
-
Definiere Optimierungen.
Der Source:
#include <stdio.h> #include <lib_zum_tickmessen.h> size_t write_number_reverse(char*string,size_t string_max,size_t value) { register size_t end=string_max; register size_t rvalue=value; if(!string || !end) return 0; do { string[--end]='0'+rvalue%10; rvalue/=10; } while(rvalue && end); if(end) memmove(&string[0],&string[end],string_max-end); return (string_max-end); } int main(void) { char xxx[12]; /*Tickmesserinitialisierung, kennste nicht.*/ /*<...>*/ /*Schreibung des Wertes.*/ write_number_reverse(xxx,11,4294967295LU); xxx[11]=0; /*Erstellen eines Snapshots. Der kostet 50-100 Ticks, die habe ich aber schon eingepreist.*/ /*<...>*/ /*Ausgabe des Strings*/ printf("%s\n",xxx); /*Und hier gibt der Tickmesser aus und wird wieder freigegeben.*/ /*<...>*/ return 0; }
Ich verwende zwar eine eigene Bibliothek für die Tickmessung, aber die kostet wie beschrieben nicht viel. Außerdem verwendet sie
memcpy
, nichtmemmove
, skaliert O(1) und wurde mit den gleichen Flags wie unten beschrieben kompiliert.Der Kommandozeilenaufruf (Linux, hier grad auf einem Ubuntu):
gcc write_shite.c -o write_shite.o <Viele tolle Sachen zur Tickmessung> -O2 -march=native -mtune=native -fno-stackprotector
Die Manpage meint, dass der Stackprotektor standardmässig an wäre, deswegen habe ich die Option negiert.
Ausgeben tut mir das Programm:
[0x65f312f10b4c|112094669245260|0x65f312f10b4c|112094669245260|"(Intern start)"] [0x65f312f11950|112094669248848|0xe04|3588|"Fertig!"]
Der Snapshot wird DIREKT nach dem Funktionsaufruf gemacht, nicht nach dem
printf
.Scheiße, erst hinterher wird mir bewusst, dass du mich hinter einer Windows-Maschine und/oder einer IDE vermutest ...
-
Dachstuhl schrieb:
Scheiße, erst hinterher wird mir bewusst, dass du mich hinter einer Windows-Maschine und/oder einer IDE vermutest ...
Nein, der Gedanke ist mir erst gekommen, als dein memmove so merkwürdig langsam war.
Zur Performancemessung: Ich hätte da einfach gprof genommen, da kann man auch ziemlich sicher sein, dass es kein Fehler in der Zeitmessung an sich ist. Oder noch einfacher ein time bei der Ausführung der Executable, wobei gprof den Vorteil hat, dass es einem sagt, was wie viel Zeit braucht. So kann man feststellen, ob es wirklich am memmove liegt.
Hast du mal probiert, das memmove durch ein:
if (anfang_des_buffers != anfang_der_ziffernfolge) do *anfang_des_buffers++ = *anfang_der_ziffernfolge; while(*anfang_der_ziffernfolge++);
zu ersetzen? Das können unmöglich 3000 Takte sein, eine handvoll Zeichen so zu kopieren. (Der Code oben ist eben schnell hier im Foreneditor getippt, ohne Test. Eventuelle Syntax- und Logikfehler dienen der Übung des Lesers
)
Mit Optimierung meinte ich schon so etwas wie die O-Stufen des GCC. Manche Leute haben die verrückte Idee, die Performance auf O0 zu testen. Wollte nur sicher sein, dass du nicht dazu zählst.
-
Dein Vertrauen in meine Fähigkeiten berührt mich.
size_t write_number_reverse(char*string,size_t string_max,size_t value) { register size_t end=string_max; register size_t rvalue=value; register size_t res; /*Prüfung auf ungültige Werte*/ if(!string || !end) return 0; /*Vom Ende her die Zahl bauen ...*/ do { string[--end]='0'+rvalue%10; rvalue/=10; } /*... solange wir noch Wert und Platz haben.*/ while(rvalue && end); /*Wieviel haben wir NICHT gebraucht? Das steht dann in res, und wenn es einen Unterschied gibt, **müssen wir ihn korrigieren.*/ res=string_max-end; if(end) { rvalue=0; /*What would memmove do? Vorwärtskopie ...*/ do string[rvalue]=string[rvalue+end]; /*... solange wir noch was zu schiften haben.*/ while(++rvalue<res); } /*Und hier die Rückgabe, damit man so Sachen wie **xxx[write_number_reverse(xxx,8,12345678LU)]=0; machen kann*/ return res; }
Braucht zwischen 180 und 250 Cycles, auch wenn zu viel Buffer verwendet wurde. Auch noch mal Danke für den Anstoß - anscheinend sollte ich mich nicht blind darauf verlassen, dass die glibc-Funktionen immer schneller sind als das, was ich aus den Fingern ergieße ... was merkwürdig ist, denn meine
memmem
-Routine war immer einige Cycle langsamer. Aber das ist eine andere Geschichte ...Erstaunlich, wie man zwei Seiten mit einem so trivialen Thema füllen kann.
Damit wäre meine Variante jetzt gut 100 schneller als
snprintf
- das kann sich sehen lassen.Vielen, vielen Dank an alle!
-
Nachschlag:
gprof zeigt mir an, dass no time accumulated wurde. Seitdem verwende ich mein eigenes API zur Cyclemessung - da weiß man, was man hat.
-
Wie wäre es den die Division durch eine Multiplikation zu ersetzen? Würde man uint32_t statt size_t verwenden, könnet man es mit fixed-point Arithmetik machen:
int uint32_to_sz(uint32_t value,char (*string)[11]) { char* psz = (char*)string; char *p = psz+9; int len=0; register uint32_t v = value; do { union { uint64_t full; struct { uint32_t low; uint32_t high; } part; } quad; /** * quotient = Floor( ( value * Round(2**32/10 * 2**3) * 2**(-3) ) * 2**(-32)) * modulo = value - quotient*10 */ quad.full = (uint64_t)v * 0xCCCCCCCDULL; register uint32_t r2, r = quad.part.high; r2 = r = r >> 3; r = v - r*10 + '0'; *p-- = (char)r; v = r2; len++; } while( v ); p++; for(int i=len; i; i--) { *psz++ = *p++; } *psz=0; return len; }
Hab ich seinerzeit für alle Werte von value getestet.
-
Dass du dich traust, so ein Gewusel zu veröffentlichen ...
Ne, bei mir erreicht der Code nur 450 Cycles. Das ist zwar immer noch schneller als die erste Routine, aber kommt an mein letztes Ergebnis (200 Cycles) nicht ran.
-
Dachstuhl schrieb:
Dass du dich traust, so ein Gewusel zu veröffentlichen ...
Dachstuhl schrieb:
Ne, bei mir erreicht der Code nur 450 Cycles.
Welche Testwerte nimmst du?
-
**xxx[write_number_reverse(xxx,8,12345678LU)]=0;
Da solltest du aber aufpassen, dass du '\0' nicht in undefinierten Speicher schreibst, weil dein xxx evtl. nicht ausreicht.
Ich würde auch beiwhile(rvalue && end);
auf end verzichten, und lieber zuvor sinnvolle Größen mittels assert absichern, denn size_t sollte nicht mehr als 20 Dezimalstellen benötigen.
Auch kannst du dir den ganzen Shift-Kram sparen, indem du den Beginnstring zurückgibst und im aufrufenden Kontext einfach wiederverwendest.const char *write_number_reverse(char*string,size_t string_max,size_t value) { static const char dezimal[]="0123456789"; char *p=string+string_max; assert(string_max>20); *--p=0; /* Stringende absichern */ do *--p=dezimal[value%10]; while(value/=10); return p; }
Aufrufen dann einfach mit:
char s[30]; puts( write_number_reverse(s,30,1234567890LU) );