it-swarm.com.de

C++ effizienteste Methode zum Konvertieren von Zeichenfolgen in int (schneller als atoi)

Wie im Titel erwähnt, suche ich nach etwas, das mir mehr Leistung bringt als Atoi. Derzeit ist der schnellste Weg, den ich kenne 

atoi(mystring.c_str())

Schließlich würde ich eine Lösung vorziehen, die nicht auf Boost angewiesen ist. Hat jemand gute Performance-Tricks, um dies zu tun?

Weitere Informationen: int wird 2 Milliarden nicht überschreiten, es ist immer positiv, die Zeichenfolge enthält keine Dezimalstellen.

21
user788171

Ich experimentierte mit Lösungen mit Hilfe von Nachschlagetabellen, fand sie jedoch mit Problemen behaftet und eigentlich nicht sehr schnell. Die schnellste Lösung erwies sich als am wenigsten einfallsreich:

int fast_atoi( const char * str )
{
    int val = 0;
    while( *str ) {
        val = val*10 + (*str++ - '0');
    }
    return val;
}

Einen Benchmark mit einer Million zufällig generierten Strings ausführen:

fast_atoi : 0.0097 seconds
atoi      : 0.0414 seconds

Um fair zu sein, habe ich diese Funktion auch getestet, indem der Compiler gezwungen wurde, sie nicht inline zu integrieren. Die Ergebnisse waren immer noch gut:

fast_atoi : 0.0104 seconds
atoi      : 0.0426 seconds

Wenn Ihre Daten den Anforderungen der fast_atoi-Funktion entsprechen, ist dies eine ziemlich angemessene Leistung. Die Anforderungen sind:

  1. Die Eingabezeichenfolge enthält nur numerische Zeichen oder ist leer
  2. Eingabezeichenfolge repräsentiert eine Zahl von 0 bis INT_MAX
21
paddy

atoi kann unter bestimmten Annahmen erheblich verbessert werden. Dies wurde in einer Präsentation von Andrei Alexandrescu auf der Konferenz C++ und Beyond 2012 eindrucksvoll demonstriert. Hi's Ersatz verwendete Loop-Abrollung und ALU-Parallelität, um Größenordnungen bei der Perf-Verbesserung zu erreichen. Ich habe kein Material, aber dieser Link verwendet eine ähnliche Technik: http://tombarta.wordpress.com/2008/04/23/specializing-atoi/

15
Scott Jones

Diese Seite vergleicht die Konvertierungsgeschwindigkeit zwischen verschiedenen String-> Int-Funktionen unter Verwendung verschiedener Compiler. Die naive Funktion, die keine Fehlerprüfung bietet, bietet laut den dargestellten Ergebnissen Geschwindigkeiten, die etwa doppelt so schnell sind wie atoi ().

// Taken from http://tinodidriksen.com/uploads/code/cpp/speed-string-to-int.cpp
int naive(const char *p) {
    int x = 0;
    bool neg = false;
    if (*p == '-') {
        neg = true;
        ++p;
    }
    while (*p >= '0' && *p <= '9') {
        x = (x*10) + (*p - '0');
        ++p;
    }
    if (neg) {
        x = -x;
    }
    return x;
}

es ist immer positiv

Entfernen Sie die negativen Prüfungen im obigen Code für eine Mikrooptimierung.

Wenn Sie garantieren können, dass die Zeichenfolge nur aus numerischen Zeichen besteht, können Sie durch Ändern der Schleife die Optimierung weiter optimieren

while (*p >= '0' && *p <= '9') {

zu

while (*p != '\0' ) {

Was dich verlassen lässt

unsigned int naive(const char *p) {
    unsigned int x = 0;
    while (*p != '\0') {
        x = (x*10) + (*p - '0');
        ++p;
    }
    return x;
}
9
x-x

Einige der Codebeispiele hier sind ziemlich komplex und machen unnötige Arbeit, was bedeutet, dass der Code schlanker und schneller sein könnte. 

Konvertierungsschleifen werden oft so geschrieben, dass sie mit jedem Zeichen drei verschiedene Dinge tun:

  • bail-Out, wenn es sich um das Zeichen für das Zeichenende handelt
  • retten, wenn es keine Ziffer ist
  • wandelt sie von ihrem Codepunkt in den tatsächlichen Ziffernwert um

Erste Beobachtung: Es ist nicht erforderlich, das Zeichen für das Zeichenende separat zu prüfen, da es sich nicht um eine Ziffer handelt. Die Prüfung auf 'Digitness' deckt somit die EOS-Bedingung implizit ab.

Zweite Beobachtung: doppelte Bedingungen für den Reichweitentest wie in (c >= '0' && c <= '9') können in einen einzigen Testzustand umgewandelt werden, indem ein vorzeichenloser Typ verwendet und der Bereich bei Null verankert wird. Auf diese Weise können sich keine unerwünschten Werte unterhalb des Bereichsanfangs befinden. Alle unerwünschten Werte werden dem Bereich oberhalb der oberen Grenze zugeordnet: (uint8_t(c - '0') <= 9)

Es kommt einfach so vor, dass c - '0' hier ohnehin berechnet werden muss ...

Daher kann die innere Umwandlungsschleife aufgeschliffen werden

uint64_t n = digit_value(*p);
unsigned d;

while ((d = digit_value(*++p)) <= 9)
{
   n = n * 10 + d;
}

Der Code wird hier mit der Voraussetzung aufgerufen, dass p auf eine Ziffer zeigt, weshalb die erste Ziffer ohne weiteres extrahiert wird (was auch eine überflüssige MUL vermeidet). 

Diese Voraussetzung ist weniger abwegig als es auf den ersten Blick erscheinen könnte, da p auf eine Ziffer zeigt, weshalb dieser Code überhaupt vom Parser aufgerufen wird. In meinem Code sieht das ganze Shebang so aus (Behauptungen und andere Geräusche in Produktionsqualität):

unsigned digit_value (char c)
{
   return unsigned(c - '0');
}

bool is_digit (char c)
{
   return digit_value(c) <= 9;
}

uint64_t extract_uint64 (char const **read_ptr)
{
   char const *p = *read_ptr;
   uint64_t n = digit_value(*p);
   unsigned d;

   while ((d = digit_value(*++p)) <= 9)
   {
      n = n * 10 + d;
   }

   *read_ptr = p;

   return n;
}

Der erste Aufruf von digit_value() wird häufig vom Compiler abgewählt, wenn der Code eingebettet wird und der aufrufende Code diesen Wert bereits durch Aufruf von is_digit() berechnet hat. 

n * 10 ist schneller als manuelles Verschieben (z. B. n = (n << 3) + (n << 1) + d), zumindest auf meiner Maschine mit gcc 4.8.1 und VC++ 2013. Ich schätze, dass beide Compiler LEA mit Index-Skalierung verwenden, um bis zu drei Werte in einem Durchgang zu addieren und einen zu skalieren von ihnen um 2, 4 oder 8. 

In jedem Fall sollte dies genau so sein: Wir schreiben Nice Clean-Code in separaten Funktionen und drücken die gewünschte Logik aus (n * 10, x% CHAR_BIT, was auch immer), und der Compiler konvertiert ihn in Inlines des Shiftings, Masking, LEAing usw. alles in den großen schlechten Parserschleife und sorgt für die nötige Unordnung unter der Haube, um alles schnell zu machen. Wir müssen nicht einmal inline vor allem stehen. Wenn überhaupt, dann müssen wir das Gegenteil tun, indem wir __declspec(noinline) vernünftig verwenden, wenn die Compiler übermäßig sind.

Ich verwende den obigen Code in einem Programm, das Milliarden von Zahlen aus Textdateien und Pipes liest. es konvertiert 115 Millionen Uint pro Sekunde, wenn die Länge 9,10 Ziffern beträgt, und 60 Millionen/s für die Länge 19,20 Ziffern (gcc 4.8.1). Das ist mehr als zehnmal so schnell wie strtoull() (und gerade genug für meine Zwecke, aber ich schweife ab ...). Dies ist der Zeitpunkt für die Konvertierung von Textblobs mit jeweils 10 Millionen Zahlen (100, 200 MB). Dies bedeutet, dass die Speicherzeiten diese Zahlen etwas schlechter erscheinen lassen als in einem synthetischen Benchmark, der vom Cache aus ausgeführt wird.

6
DarthGizka

Warum nicht einen Stringstream benutzen? Ich bin mir nicht sicher, was für einen bestimmten Aufwand es gibt, aber Sie könnten Folgendes definieren:

int myInt; 
string myString = "1561";
stringstream ss;
ss(myString);
ss >> myInt;

Natürlich müsstest du das 

#include <stringstream> 
3
Rome_Leader

Paddys Implementierung vonfast_atoiist schneller alsatoi- ohne den Schatten des Zweifels - funktioniert jedoch nur für vorzeichenlose Ganzzahlen .

Nachfolgend habe ich die getestete Version von fast_atoi von Paddy angegeben, die auch nur vorzeichenlose Ganzzahlen zulässt, die Konvertierung jedoch noch weiter beschleunigt, indem kostspielige Operationen * durch + ersetzt werden.

unsigned int fast_atou(const char *str)
{
    unsigned int val = 0;
    while(*str) {
        val = (val << 1) + (val << 3) + *(str++) - 48;
    }
    return val;
}

Hier habe ich complete version of fast_atoi () gesetzt, das ich manchmal verwende, was auch einzelne Zahlen konvertiert:

int fast_atoi(const char *buff)
{
    int c = 0, sign = 0, x = 0;
    const char *p = buff;

    for(c = *(p++); (c < 48 || c > 57); c = *(p++)) {if (c == 45) {sign = 1; c = *(p++); break;}}; // eat whitespaces and check sign
    for(; c > 47 && c < 58; c = *(p++)) x = (x << 1) + (x << 3) + c - 48;

    return sign ? -x : x;
} 
2
soerium

Hier ist die Gesamtheit der Atoi-Funktion in gcc:

long atoi(const char *str)
{
    long num = 0;
    int neg = 0;
    while (isspace(*str)) str++;
    if (*str == '-')
    {
        neg=1;
        str++;
    }
    while (isdigit(*str))
    {
        num = 10*num + (*str - '0');
        str++;
    }
    if (neg)
        num = -num;
    return num;
 }

Whitespace und Negativcheck sind in Ihrem Fall überflüssig, verwenden aber nur Nanosekunden.

isdigit ist fast sicher inliniert, das kostet Sie also keine Zeit. 

Ich sehe hier wirklich keinen Raum für Verbesserungen.

1
Joel

Die einzige endgültige Antwort ist, dass Sie mit Ihrem Compiler Ihre tatsächlichen Daten überprüfen.

Etwas, was ich versuchen würde (auch wenn es Speicherzugriffe verwendet, kann dies je nach Caching langsam sein)

int value = t1[s[n-1]];
if (n > 1) value += t10[s[n-2]]; else return value;
if (n > 2) value += t100[s[n-3]]; else return value;
if (n > 3) value += t1000[s[n-4]]; else return value;
... continuing for how many digits you need to handle ...

wenn t1, t10 usw. statisch zugewiesen und konstant sind, sollte der Compiler kein Aliasing befürchten und der generierte Maschinencode sollte recht anständig sein.

0
6502

eine schnellere Konvertierungsfunktion

die Multiplikation ist immer langsamer als die Summe und die Verschiebung, daher mit der Verschiebung multiplizieren

int fast_atoi( const char * str )
{
    int val = 0;
    while( *str ) {
        val = (val << 4) - (val << 2) - (val << 1) + (*str++ - '0');
    }
return val;
}
0
hamSh

Hier ist mein. Atoi ist das schnellste was ich mir vorstellen kann. Ich habe mit msvc 2010 kompiliert, sodass es möglich ist, beide Vorlagen zu kombinieren. Wenn ich in msvc 2010 Vorlagen kombinierte, wurde der Fall, in dem Sie ein Argument cb angeben, langsamer.

Atoi behandelt fast alle speziellen Atoi-Fälle und ist genauso schnell oder schneller als diese:

int val = 0;
while( *str ) 
    val = val*10 + (*str++ - '0');

Hier ist der Code:

#define EQ1(a,a1) (BYTE(a) == BYTE(a1))
#define EQ1(a,a1,a2) (BYTE(a) == BYTE(a1) && EQ1(a,a2))
#define EQ1(a,a1,a2,a3) (BYTE(a) == BYTE(a1) && EQ1(a,a2,a3))

// Atoi is 4x faster than atoi.  There is also an overload that takes a cb argument.
template <typename T> 
T Atoi(LPCSTR sz) {
    T n = 0;
    bool fNeg = false;  // for unsigned T, this is removed by optimizer
    const BYTE* p = (const BYTE*)sz;
    BYTE ch;
    // test for most exceptions in the leading chars.  Most of the time
    // this test is skipped.  Note we skip over leading zeros to avoid the 
    // useless math in the second loop.  We expect leading 0 to be the most 
    // likely case, so we test it first, however the cpu might reorder that.
    for ( ; (ch=*p-'1') >= 9 ; ++p) { // unsigned trick for range compare
      // ignore leading 0's, spaces, and '+'
      if (EQ1(ch, '0'-'1', ' '-'1', '+'-'1'))
        continue;
      // for unsigned T this is removed by optimizer
      if (!((T)-1 > 0) && ch==BYTE('-'-'1')) {
        fNeg = !fNeg;
        continue;
      }
      // atoi ignores these.  Remove this code for a small perf increase.
      if (BYTE(*p-9) > 4)  // \t, \n, 11, 12, \r. unsigned trick for range compare
        break;
    }
    // deal with rest of digits, stop loop on non digit.
    for ( ; (ch=*p-'0') <= 9 ; ++p) // unsigned trick for range compare
      n = n*10 + ch; 
    // for unsigned T, (fNeg) test is removed by optimizer
    return (fNeg) ? -n : n;
}

// you could go with a single template that took a cb argument, but I could not
// get the optimizer to create good code when both the cb and !cb case were combined.
// above code contains the comments.
template <typename T>
T Atoi(LPCSTR sz, BYTE cb) {
    T n = 0;
    bool fNeg = false; 
    const BYTE* p = (const BYTE*)sz;
    const BYTE* p1 = p + cb;
    BYTE ch;
    for ( ; p<p1 && (ch=*p-'1') >= 9 ; ++p) {
      if (EQ1(ch,BYTE('0'-'1'),BYTE(' '-'1'),BYTE('+'-'1')))
        continue;
      if (!((T)-1 > 0) && ch == BYTE('-'-'1')) {
        fNeg = !fNeg;
        continue;
      }
      if (BYTE(*p-9) > 4)  // \t, \n, 11, 12, \r
        break;
    }
    for ( ; p<p1 && (ch=*p-'0') <= 9 ; ++p)
      n = n*10 + ch; 
    return (fNeg) ? -n : n;
}
0
johnnycrash