web-dev-qa-db-fra.com

Très mauvais boost :: performances lexical_cast

Windows XP SP3. Core 2 Duo 2.0 GHz. Je trouve que les performances boost :: lexical_cast sont extrêmement lentes. Je voulais trouver des moyens d'accélérer le code. Utiliser les optimisations/O2 sur visual c ++ 2008 et en comparant avec Java 1.6 et python 2.6.2 je vois les résultats suivants.

Casting entier:

c++: 
std::string s ;
for(int i = 0; i < 10000000; ++i)
{
    s = boost::lexical_cast<string>(i);
}

Java:
String s = new String();
for(int i = 0; i < 10000000; ++i)
{
    s = new Integer(i).toString();
}

python:
for i in xrange(1,10000000):
    s = str(i)

Les fois que je vois sont

c ++: 6700 millisecondes

Java: 1178 millisecondes

python: 6702 millisecondes

c ++ est aussi lent que python et 6 fois plus lent que Java.

Double casting:

c++:
std::string s ;
for(int i = 0; i < 10000000; ++i)
{
    s = boost::lexical_cast<string>(d);
}

Java:
String s = new String();
for(int i = 0; i < 10000000; ++i)
{
    double d = i*1.0;
    s = new Double(d).toString();
}

python:
for i in xrange(1,10000000):
    d = i*1.0
    s = str(d)

Les fois que je vois sont

c ++: 56129 millisecondes

Java: 2852 millisecondes

python: 30780 millisecondes

Donc, pour les doubles, c ++ est en fait la moitié de la vitesse de python et 20 fois plus lent que la solution Java !!). Toutes les idées sur l'amélioration des performances boost :: lexical_cast Cela est-il dû à la mauvaise mise en œuvre du stringstream ou pouvons-nous nous attendre à une diminution générale de 10 fois les performances de l'utilisation des bibliothèques de boost.

45
Naveen

Modifier 2012-04-11

rve a très justement commenté les performances de lexical_cast, fournissant un lien:

http://www.boost.org/doc/libs/1_49_0/doc/html/boost_lexical_cast/performance.html

Je n'ai pas accès pour le moment à la version 1.49, mais je me souviens avoir accéléré mon code sur une ancienne version. Donc je suppose:

  1. la réponse suivante est toujours valable (ne serait-ce qu'à des fins d'apprentissage)
  2. il y avait probablement une optimisation introduite quelque part entre les deux versions (je vais chercher ça)
  3. ce qui signifie que le boost est de mieux en mieux

Réponse originale

Juste pour ajouter des informations sur les excellentes réponses de Barry et Motti:

Quelques antécédents

N'oubliez pas que Boost est écrit par les meilleurs développeurs C++ de cette planète et revu par les mêmes meilleurs développeurs. Si lexical_cast Était si faux, quelqu'un aurait piraté la bibliothèque soit avec des critiques soit avec du code.

Je suppose que vous avez manqué le point de la valeur réelle de lexical_cast ...

Comparaison des pommes et des oranges.

En Java, vous transformez un entier en une chaîne Java. Vous remarquerez que je ne parle pas d'un tableau de caractères ou d'une chaîne définie par l'utilisateur. Vous noterez également: Je ne parle pas de votre entier défini par l'utilisateur. Je parle de strict Java Entier et strict Java String.

En Python, vous faites plus ou moins la même chose.

Comme indiqué dans d'autres articles, vous utilisez essentiellement les Java et Python équivalents de sprintf (ou le moins standard itoa).

En C++, vous utilisez un cast très puissant. Pas puissant dans le sens des performances de vitesse brutes (si vous voulez de la vitesse, peut-être que sprintf serait mieux adapté), mais puissant dans le sens de l'extensibilité.

Comparer des pommes.

Si vous souhaitez comparer une méthode Java Integer.toString, Vous devez la comparer avec les fonctionnalités C sprintf ou C++ ostream.

La solution de flux C++ serait 6 fois plus rapide (sur mon g ++) que lexical_cast, Et bien moins extensible:

inline void toString(const int value, std::string & output)
{
   // The largest 32-bit integer is 4294967295, that is 10 chars
   // On the safe side, add 1 for sign, and 1 for trailing zero
   char buffer[12] ;
   sprintf(buffer, "%i", value) ;
   output = buffer ;
}

La solution C sprintf serait 8 fois plus rapide (sur mon g ++) que lexical_cast Mais beaucoup moins sûre:

inline void toString(const int value, char * output)
{
   sprintf(output, "%i", value) ;
}

Les deux solutions sont aussi rapides ou plus rapides que votre Java (selon vos données).

Comparaison des oranges.

Si vous voulez comparer un C++ lexical_cast, Alors vous devez le comparer avec ce Java pseudo code:

Source s ;
Target t = Target.fromString(Source(s).toString()) ;

La source et la cible sont de n'importe quel type, y compris les types intégrés comme boolean ou int, ce qui est possible en C++ à cause des modèles.

Extensibilité? Est-ce un gros mot?

Non, mais cela a un coût bien connu: lorsqu'elles sont écrites par le même codeur, les solutions générales à des problèmes spécifiques sont généralement plus lentes que les solutions spécifiques écrites pour leurs problèmes spécifiques.

Dans le cas actuel, dans un point de vue naïf, lexical_cast Utilisera les fonctionnalités de flux pour convertir d'un type A en un flux de chaîne, puis de ce flux de chaîne en un type B.

Cela signifie que tant que votre objet peut être sorti dans un flux et entré à partir d'un flux, vous pourrez utiliser lexical_cast Dessus, sans toucher à une seule ligne de code.

Alors, quelles sont les utilisations de lexical_cast?

Les principales utilisations de la coulée lexicale sont:

  1. Facilité d'utilisation (hé, une distribution C++ qui fonctionne pour tout étant une valeur!)
  2. Le combiner avec du code lourd de modèle, où vos types sont paramétrés, et en tant que tel, vous ne voulez pas traiter de spécificités, et vous ne voulez pas connaître les types.
  3. Toujours potentiellement relativement efficace, si vous avez des connaissances de base sur les modèles, comme je le démontrerai ci-dessous

Le point 2 est très très important ici, car cela signifie que nous avons une et une seule interface/fonction pour convertir une valeur d'un type en une valeur égale ou similaire d'un autre type.

C'est le vrai point que vous avez manqué, et c'est le point qui coûte en termes de performances.

Mais c'est tellement slooooooowwww!

Si vous voulez des performances de vitesse brutes, souvenez-vous que vous traitez avec C++ et que vous disposez de nombreuses fonctionnalités pour gérer efficacement la conversion, tout en conservant la fonctionnalité de facilité d'utilisation lexical_cast.

Il m'a fallu quelques minutes pour regarder la source lexical_cast et trouver une solution viable. Ajoutez à votre code C++ le code suivant:

#ifdef SPECIALIZE_BOOST_LEXICAL_CAST_FOR_STRING_AND_INT

namespace boost
{
   template<>
   std::string lexical_cast<std::string, int>(const int &arg)
   {
      // The largest 32-bit integer is 4294967295, that is 10 chars
      // On the safe side, add 1 for sign, and 1 for trailing zero
      char buffer[12] ;
      sprintf(buffer, "%i", arg) ;
      return buffer ;
   }
}

#endif

En activant cette spécialisation de lexical_cast pour les chaînes et les entiers (en définissant la macro SPECIALIZE_BOOST_LEXICAL_CAST_FOR_STRING_AND_INT), Mon code est allé 5 fois plus vite sur mon compilateur g ++, ce qui signifie, selon vos données, que ses performances devraient être similaires à celles de Java.

Et cela m'a pris 10 minutes pour regarder le code boost et écrire une version 32 bits à distance efficace et correcte. Et avec un peu de travail, cela pourrait probablement aller plus vite et plus en sécurité (si nous avions un accès direct en écriture au tampon interne std::string, Nous pourrions éviter un tampon externe temporaire, par exemple).

77
paercebal

Vous pourriez vous spécialiser lexical_cast pour les types int et double. Utilisez strtod et strtol dans vos spécialisations.

namespace boost {
template<>
inline int lexical_cast(const std::string& arg)
{
    char* stop;
    int res = strtol( arg.c_str(), &stop, 10 );
    if ( *stop != 0 ) throw_exception(bad_lexical_cast(typeid(int), typeid(std::string)));
    return res;
}
template<>
inline std::string lexical_cast(const int& arg)
{
    char buffer[65]; // large enough for arg < 2^200
    ltoa( arg, buffer, 10 );
    return std::string( buffer ); // RVO will take place here
}
}//namespace boost

int main(int argc, char* argv[])
{
    std::string str = "22"; // SOME STRING
    int int_str = boost::lexical_cast<int>( str );
    std::string str2 = boost::lexical_cast<std::string>( str_int );

    return 0;
}

Cette variante sera plus rapide que l'utilisation de l'implémentation par défaut, car dans l'implémentation par défaut, il y a construction d'objets de flux lourds. Et il devrait être un peu plus rapide que printf, car printf devrait analyser la chaîne de format.

20

lexical_cast Est plus général que le code spécifique que vous utilisez dans Java et Python. Il n'est pas surprenant qu'une approche générale qui fonctionne dans de nombreux scénarios (la distribution lexicale est un peu plus que streaming puis retour vers et depuis un flux temporaire) finit par être plus lent que des routines spécifiques.

(BTW, vous pouvez obtenir de meilleures performances avec Java en utilisant la version statique, Integer.toString(int). [1])

Enfin, l'analyse et l'analyse des chaînes ne sont généralement pas très sensibles aux performances, sauf si l'on écrit un compilateur, auquel cas lexical_cast Est probablement trop polyvalent, et les entiers, etc. seront calculés à chaque analyse.

[1] Le commentateur "stepancheg" doutait de mon indice que la version statique pourrait donner de meilleures performances. Voici la source que j'ai utilisée:

public class Test
{
    static int instanceCall(int i)
    {
        String s = new Integer(i).toString();
        return s == null ? 0 : 1;
    }

    static int staticCall(int i)
    {
        String s = Integer.toString(i);
        return s == null ? 0 : 1;
    }

    public static void main(String[] args)
    {
        // count used to avoid dead code elimination
        int count = 0;

        // *** instance

        // Warmup calls
        for (int i = 0; i < 100; ++i)
            count += instanceCall(i);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; ++i)
            count += instanceCall(i);
        long finish = System.currentTimeMillis();
        System.out.printf("10MM Time taken: %d ms\n", finish - start);


        // *** static

        // Warmup calls
        for (int i = 0; i < 100; ++i)
            count += staticCall(i);

        start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; ++i)
            count += staticCall(i);
        finish = System.currentTimeMillis();
        System.out.printf("10MM Time taken: %d ms\n", finish - start);
        if (count == 42)
            System.out.println("bad result"); // prevent elimination of count
    }
}

Les runtimes, utilisant JDK 1.6.0-14, VM ​​serveur:

10MM Time taken: 688 ms
10MM Time taken: 547 ms

Et dans la machine virtuelle cliente:

10MM Time taken: 687 ms
10MM Time taken: 610 ms

Même si théoriquement, l'analyse d'échappement peut permettre l'allocation sur la pile, et l'inline peut introduire tout le code (y compris la copie) dans la méthode locale, permettant l'élimination de la copie redondante, une telle analyse peut prendre beaucoup de temps et entraîner un peu de l'espace de code, qui a d'autres coûts dans le cache de code qui ne se justifient pas dans le vrai code, par opposition aux microbenchmarks comme on le voit ici.

14
Barry Kelly

Ce que fait le cast lexical dans votre code peut être simplifié comme suit:

string Cast( int i ) {
    ostringstream os;
    os << i;
    return os.str();
}

Il se passe malheureusement beaucoup de choses à chaque fois que vous appelez Cast ():

  • un flux de chaînes est créé en allouant éventuellement de la mémoire
  • l'opérateur << pour entier i est appelé
  • le résultat est stocké dans le flux, allouant éventuellement de la mémoire
  • une copie de chaîne est extraite du flux
  • une copie de la chaîne est (éventuellement) créée pour être renvoyée.
  • la mémoire est désallouée

Thn dans votre propre code:

 s = Cast( i );

l'affectation implique d'autres allocations et des désallocations sont effectuées. Vous pourrez peut-être réduire légèrement cela en utilisant:

string s = Cast( i );

au lieu.

Cependant, si les performances vous importent vraiment, vous devriez envisager d'utiliser un mécanisme différent. Vous pouvez écrire votre propre version de Cast () qui (par exemple) crée un flux de chaînes statique. Une telle version ne serait pas sécurisée pour les threads, mais cela pourrait ne pas avoir d'importance pour vos besoins spécifiques.

Pour résumer, lexical_cast est une fonctionnalité pratique et utile, mais cette commodité s'accompagne (comme elle doit toujours) de compromis dans d'autres domaines.

9
anon

Malheureusement, je n'ai pas encore assez de représentants pour commenter ...

lexical_cast N'est pas principalement lent car il est générique (les recherches de modèles se produisent au moment de la compilation, donc les appels de fonction virtuelle ou d'autres recherches/déréférences ne sont pas nécessaires). lexical_cast Est, à mon avis, lent, car il s'appuie sur les iostreams C++, qui sont principalement destinés aux opérations de streaming et non aux conversions uniques, et parce que lexical_cast Doit rechercher et convertir les signaux d'erreur iostream. Donc:

  • un objet de flux doit être créé et détruit
  • dans le cas de sortie de chaîne ci-dessus, notez que les compilateurs C++ ont du mal à éviter les copies de tampon (une alternative est de formater directement le tampon de sortie, comme le fait sprintf, bien que sprintf ne le fasse pas en toute sécurité gérer les dépassements de tampon)
  • lexical_cast Doit vérifier les erreurs stringstream (ss.fail()) afin de lever des exceptions sur les échecs de conversion

lexical_cast Est sympa parce que les exceptions (IMO) permettent de piéger toutes les erreurs sans effort supplémentaire et parce qu'il a un prototype uniforme. Personnellement, je ne vois pas pourquoi l'une de ces propriétés nécessite un fonctionnement lent (lorsqu'aucune erreur de conversion ne se produit), bien que je ne connaisse pas de telles fonctions C++ qui sont rapides (peut-être Spirit ou boost :: xpressive?).

Edit: Je viens de trouver un message mentionnant l'utilisation de BOOST_LEXICAL_CAST_ASSUME_C_LOCALE Pour activer une optimisation "itoa": http://old.nabble.com/lexical_cast-optimization-td20817583.html . Il y a aussi un lien article avec un peu plus de détails.

8
dhardy

lexical_cast peut ou peut ne pas être aussi lent par rapport à Java et Python comme vos bencharks l'indiquent parce que vos mesures de référence peuvent avoir un problème subtil. Toutes les allocations/désallocations d'espace de travail effectuées par lexical cast ou les méthodes iostream qu'il utilise sont mesurées par vos benchmarks car C++ ne diffère pas ces opérations. Cependant, dans le cas de Java et Python , les désallocations associées peuvent en fait avoir simplement été reportées à un futur cycle de collecte des ordures et manquées par les mesures de référence (sauf si un cycle GC se produit par hasard pendant que la référence est en cours et dans ce cas, vous mesureriez trop) . Il est donc difficile de savoir avec certitude sans examiner les spécificités des implémentations Java et Python combien de "coût" doit être attribué à la charge différée du GC qui peut (ou peut ne pas) être finalement imposé.

Ce type de problème peut évidemment s'appliquer à de nombreux autres benchmarks de langage C++ vs garbage collection.

8
jeff slesinger

Comme l'a dit Barry, lexical_cast est très général, vous devriez utiliser une alternative plus spécifique, par exemple consultez itoa (int->string) et atoi (string -> int).

2
Motti

si la vitesse est un problème, ou si vous êtes simplement intéressé par la rapidité avec laquelle ces transtypages peuvent être avec C++, il y a un thread intéressé à ce sujet.

Boost.Spirit 2.1 (qui doit être publié avec Boost 1.40) semble être très rapide, voire plus rapide que les équivalents C (strtol (), atoi () etc.).

1
t.g.

J'utilise cette solution très rapide pour les types de POD ...

namespace DATATYPES {

    typedef std::string   TString;
    typedef char*         TCString;
    typedef double        TDouble;
    typedef long          THuge;
    typedef unsigned long TUHuge;
};

namespace boost {

template<typename TYPE>
inline const DATATYPES::TString lexical_castNumericToString(

                                const TYPE& arg, 
                                const DATATYPES::TCString fmt) {

    enum { MAX_SIZE = ( std::numeric_limits<TYPE>::digits10 + 1 )  // sign
                                                            + 1 }; // null
    char buffer[MAX_SIZE] = { 0 };

    if (sprintf(buffer, fmt, arg) < 0) {
        throw_exception(bad_lexical_cast(typeid(TYPE),
                                         typeid(DATATYPES::TString)));
    }
    return ( DATATYPES::TString(buffer) );
}

template<typename TYPE>
inline const TYPE lexical_castStringToNumeric(const DATATYPES::TString& arg) {

    DATATYPES::TCString end = 0;
    DATATYPES::TDouble result = std::strtod(arg.c_str(), &end);

    if (not end or *end not_eq 0) {
        throw_exception(bad_lexical_cast(typeid(DATATYPES::TString),
                                         typeid(TYPE)));
    }
    return TYPE(result);
}

template<>
inline DATATYPES::THuge lexical_cast(const DATATYPES::TString& arg) {
    return (lexical_castStringToNumeric<DATATYPES::THuge>(arg));
}

template<>
inline DATATYPES::TString lexical_cast(const DATATYPES::THuge& arg) {
    return (lexical_castNumericToString<DATATYPES::THuge>(arg,"%li"));
}

template<>
inline DATATYPES::TUHuge lexical_cast(const DATATYPES::TString& arg) {
    return (lexical_castStringToNumeric<DATATYPES::TUHuge>(arg));
}

template<>
inline DATATYPES::TString lexical_cast(const DATATYPES::TUHuge& arg) {
    return (lexical_castNumericToString<DATATYPES::TUHuge>(arg,"%lu"));
}

template<>
inline DATATYPES::TDouble lexical_cast(const DATATYPES::TString& arg) {
    return (lexical_castStringToNumeric<DATATYPES::TDouble>(arg));
}

template<>
inline DATATYPES::TString lexical_cast(const DATATYPES::TDouble& arg) {
    return (lexical_castNumericToString<DATATYPES::TDouble>(arg,"%f"));
}

} // end namespace boost
1
David Larner