web-dev-qa-db-fra.com

C++ le moyen le plus efficace de convertir string en int (plus rapide que atoi)

Comme mentionné dans le titre, je cherche quelque chose qui puisse me donner plus de performance que atoi. Actuellement, le moyen le plus rapide que je connaisse est 

atoi(mystring.c_str())

Enfin, je préférerais une solution qui ne repose pas sur Boost. Est-ce que quelqu'un a de bons trucs de performance pour faire ça?

Informations supplémentaires: int ne dépassera pas 2 milliards, il est toujours positif, la chaîne ne comporte pas de décimale.

21
user788171

J'ai expérimenté des solutions en utilisant des tables de consultation, mais je les ai trouvées pleines de problèmes, et en réalité pas très vite. La solution la plus rapide s’est avérée la moins imaginative:

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

Exécution d'un point de repère avec un million de chaînes générées aléatoirement:

fast_atoi : 0.0097 seconds
atoi      : 0.0414 seconds

Pour être juste, j’ai aussi testé cette fonction en forçant le compilateur à ne pas l’aligner. Les résultats étaient toujours bons:

fast_atoi : 0.0104 seconds
atoi      : 0.0426 seconds

Si vos données sont conformes aux exigences de la fonction fast_atoi, les performances sont raisonnables. Les exigences sont:

  1. La chaîne d'entrée contient uniquement des caractères numériques ou est vide
  2. La chaîne d'entrée représente un nombre compris entre 0 et INT_MAX
21
paddy

atoi peut être amélioré de manière significative, compte tenu de certaines hypothèses. Cela a été démontré avec force par Andrei Alexandrescu lors de la conférence C++ et Beyond 2012. Son remplacement a utilisé le déroulement de la boucle et le parallélisme des UAL pour atteindre des ordres de grandeur en amélioration des performances. Je n'ai pas son matériel, mais ce lien utilise une technique similaire: http://tombarta.wordpress.com/2008/04/23/specializing-atoi/

15
Scott Jones

Cette page compare la vitesse de conversion entre différentes fonctions string-> int utilisant différents compilateurs. La fonction naïve, qui n'offre aucune vérification d'erreur, offre des vitesses environ deux fois plus rapides que atoi (), selon les résultats présentés.

// 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;
}

c'est toujours positif

Supprimez les contrôles négatifs dans le code ci-dessus pour une micro optimisation.

Si vous pouvez garantir que la chaîne ne comportera que des caractères numériques, vous pouvez optimiser davantage la micro en modifiant la boucle.

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

à

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

Ce qui vous laisse avec

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

Quelques exemples de code ici sont assez complexes et font un travail inutile, ce qui signifie que le code pourrait être plus mince et plus rapide. 

Les boucles de conversion sont souvent écrites pour faire trois choses différentes avec chaque caractère:

  • renflouer si c'est le caractère de fin de chaîne
  • renflouer si ce n'est pas un chiffre
  • convertissez-le de son point de code en valeur numérique

Première observation: il n'est pas nécessaire de vérifier séparément le caractère de fin de chaîne, car il ne s'agit pas d'un chiffre. Par conséquent, la vérification de la "numérisation" couvre implicitement la condition EOS.

Deuxième observation: les conditions doubles pour le test de plage, comme dans (c >= '0' && c <= '9'), peuvent être converties en une condition de test unique en utilisant un type non signé et en ancrant la plage à zéro; De cette façon, il ne peut y avoir aucune valeur indésirable en dessous du début de la plage. Toutes les valeurs indésirables sont mappées à la plage au-dessus de la limite supérieure: (uint8_t(c - '0') <= 9)

Il se trouve que c - '0' doit être calculé ici de toute façon ...

Par conséquent, la boucle de conversion interne peut être réduite à

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

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

Le code ici est appelé avec la condition préalable que p soit pointé sur un chiffre, raison pour laquelle le premier chiffre est extrait sans plus tarder (ce qui évite également une MUL superflue). 

Cette condition préalable est moins étrange que ce qui pourrait paraître au début, puisque p qui pointe sur un chiffre est la raison pour laquelle ce code est appelé par l’analyseur en premier lieu. Dans mon code, tout le Shebang ressemble à ceci (affirmations et autres bruits de qualité de production élidés):

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;
}

Le compilateur élude souvent le premier appel à digit_value() si le code est en ligne et que le code appelant a déjà calculé cette valeur en appelant is_digit()

n * 10 est plus rapide que le décalage manuel (par exemple, n = (n << 3) + (n << 1) + d), du moins sur ma machine avec gcc 4.8.1 et VC++ 2013. Mon hypothèse est que les deux compilateurs utilisent LEA avec la mise à l'échelle d'index pour ajouter jusqu'à trois valeurs à la fois et en mettre une à l'échelle d'entre eux par 2, 4 ou 8. 

Dans tous les cas, c'est exactement ce que cela devrait être: nous écrivons du code propre et propre dans des fonctions séparées et exprimons la logique souhaitée (n * 10, x% CHAR_BIT, peu importe) et le compilateur le convertit en décalage, masquage, LEAing, etc. Tout est dans la grande boucle de l'analyseur et prend en charge tout le désordre requis sous le capot pour accélérer les choses. Nous n'avons même plus besoin de coller inline devant tout. Si nous devons faire quelque chose, nous devons faire le contraire en utilisant judicieusement __declspec(noinline) lorsque les compilateurs deviennent trop pressés.

J'utilise le code ci-dessus dans un programme qui lit des milliards de nombres à partir de fichiers texte et de pipes; il convertit 115 millions d'uints par seconde si la longueur est de 9,10 chiffres et 60 millions/s pour la longueur de 19..20 chiffres (gcc 4.8.1). C'est plus de dix fois plus vite que strtoull() (et à peine suffisant pour mes besoins, mais je m'éloigne du sujet ...). C’est le moment idéal pour convertir des blobs de texte contenant 10 millions de chiffres chacun (100, 200 Mo), ce qui signifie que les timings de la mémoire font apparaître ces chiffres un peu moins bien qu’ils ne le seraient dans un repère synthétique fonctionnant en cache.

6
DarthGizka

Pourquoi ne pas utiliser un stringstream? Je ne suis pas sûr de ses frais particuliers, mais vous pouvez définir:

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

Bien sûr, vous auriez besoin de 

#include <stringstream> 
3
Rome_Leader

L'implémentation de Paddy defast_atoiest plus rapide queatoi- sans l'ombre du doute - mais cela ne fonctionne que pour entiers non signés .

Ci-dessous, je mets une version évaluée de fast_atoi de Paddy qui autorise également uniquement les entiers non signés, mais accélère encore plus la conversion en remplaçant une opération coûteuse * par +

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

Ici, je mets la version complète de fast_atoi () que j'utilise parfois pour convertir les entiers singes également:

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

Voici l'intégralité de la fonction atoi dans 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;
 }

Les espaces et les chèques négatifs sont superflus dans votre cas, mais n'utilisez que des nanosecondes.

isdigit est presque certainement en ligne, cela ne vous coûte donc pas beaucoup de temps. 

Je ne vois vraiment pas de place à l'amélioration ici.

1
Joel

La seule réponse définitive est de vérifier avec votre compilateur, vos données réelles.

Quelque chose que j’essayerais (même s’il utilise des accès mémoire, donc il peut être lent en fonction de la mise en cache)

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 ...

si t1, t10 et ainsi de suite sont alloués de manière statique et constante, le compilateur ne devrait craindre aucun aliasing et le code machine généré devrait être assez correct.

0
6502

une fonction de conversion plus rapide

la multiplication est toujours plus lente que la somme et le décalage, donc le changement se multiplie avec

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

Voici le mien. Atoi est le plus rapide que je puisse trouver. J'ai compilé avec msvc 2010 afin qu'il soit possible de combiner les deux modèles. Dans msvc 2010, lorsque j'ai combiné les modèles, le cas où vous fournissez un argument cb plus lentement a été créé.

Atoi traite presque tous les cas spéciaux atoi, et est aussi rapide ou rapide que cela:

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

Voici le 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