37 votes

Différence de vitesse inhabituelle entre Python et C++

J'ai récemment écrit un petit algorithme pour calculer des chiffres heureux en python. Le programme vous permet de choisir une limite supérieure et il déterminera tous les nombres heureux en dessous de celle-ci. Pour une comparaison de vitesse, j'ai décidé de faire la traduction la plus directe de l'algorithme que je connaissais de python à c++.

De manière surprenante, la version c++ fonctionne beaucoup plus lentement que la version python. Des tests de vitesse précis entre les temps d'exécution pour découvrir les 10 000 premiers nombres heureux indiquent que le programme python s'exécute en moyenne en 0,59 seconde et la version c++ en 8,5 secondes.

J'attribue cette différence de vitesse au fait que j'ai dû écrire des fonctions d'aide pour certaines parties des calculs (par exemple déterminer si un élément est dans une liste/un tableau/un vecteur) dans la version c++ qui étaient déjà intégrées dans le langage python.

Premièrement, est-ce la vraie raison d'une différence de vitesse aussi absurde, et deuxièmement, comment puis-je changer la version c++ pour qu'elle s'exécute plus rapidement que la version python (comme cela devrait être le cas à mon avis).

Les deux morceaux de code, avec les tests de vitesse, sont ici : Version Python , Version C++ . Merci pour votre aide.

#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <windows.h>

using namespace std;

bool inVector(int inQuestion, vector<int> known);
int sum(vector<int> given);
int pow(int given, int power);
void calcMain(int upperBound);

int main()
{
    while(true)
    {
        int upperBound;
        cout << "Pick an upper bound: ";
        cin >> upperBound;
        long start, end;
        start = GetTickCount();
        calcMain(upperBound);
        end = GetTickCount();
        double seconds = (double)(end-start) / 1000.0;
        cout << seconds << " seconds." << endl << endl;
    }
    return 0;
}

void calcMain(int upperBound)
{
    vector<int> known;
    for(int i = 0; i <= upperBound; i++)
    {
        bool next = false;
        int current = i;
        vector<int> history;
        while(!next)
        {
            char* buffer = new char[10];
            itoa(current, buffer, 10);
            string digits = buffer;
            delete buffer;
            vector<int> squares;
            for(int j = 0; j < digits.size(); j++)
            {
                char charDigit = digits[j];
                int digit = atoi(&charDigit);
                int square = pow(digit, 2);
                squares.push_back(square);
            }
            int squaresum = sum(squares);
            current = squaresum;
            if(inVector(current, history))
            {
                next = true;
                if(current == 1)
                {
                    known.push_back(i);
                    //cout << i << "\t";
                }
            }
            history.push_back(current);
        }
    }
    //cout << "\n\n";
}

bool inVector(int inQuestion, vector<int> known)
{
    for(vector<int>::iterator it = known.begin(); it != known.end(); it++)
        if(*it == inQuestion)
            return true;
    return false;
}

int sum(vector<int> given)
{
    int sum = 0;
    for(vector<int>::iterator it = given.begin(); it != given.end(); it++)
        sum += *it;
    return sum;
}

int pow(int given, int power)
{
    int original = given;
    int current = given;
    for(int i = 0; i < power-1; i++)
        current *= original;
    return current;
}

#!/usr/bin/env python

import timeit

upperBound = 0

def calcMain():
    known = []
    for i in range(0,upperBound+1):
        next = False
        current = i
        history = []
        while not next:
            digits = str(current)
            squares = [pow(int(digit), 2) for digit in digits]
            squaresum = sum(squares)
            current = squaresum
            if current in history:
                next = True
                if current == 1:
                    known.append(i)
                    ##print i, "\t",
            history.append(current)
    ##print "\nend"

while True:    
    upperBound = input("Pick an upper bound: ")
    result = timeit.Timer(calcMain).timeit(1)
    print result, "seconds.\n"

154voto

Asik Points 6599

Pour 100000 éléments, le code Python a pris 6,9 secondes, tandis que le code C++ original a pris plus de 37 secondes.

J'ai effectué quelques optimisations de base sur votre code et j'ai réussi à rendre le code C++ plus de 100 fois plus rapide que l'implémentation Python. Il fait maintenant 100000 éléments en 0.06 secondes. C'est 617 fois plus rapide que le code C++ original.

La chose la plus importante est de compiler en mode Release, avec toutes les optimisations. Ce code est littéralement plus lent de plusieurs ordres de grandeur en mode Debug.

Ensuite, je vais expliquer les optimisations que j'ai faites.

  • Déplacement de toutes les déclarations de vecteurs en dehors de la boucle ; remplacement par une opération clear(), qui est beaucoup plus rapide que l'appel au constructeur.
  • Remplacement de l'appel à pow(valeur, 2) par une multiplication : valeur * valeur.
  • Au lieu d'avoir un vecteur de carrés et d'appeler sum dessus, j'additionne les valeurs sur place en utilisant simplement un entier.
  • Éviter toutes les opérations sur les chaînes de caractères, qui sont très lentes par rapport aux opérations sur les entiers. Par exemple, il est possible de calculer les carrés de chaque chiffre en divisant de manière répétée par 10 et en récupérant le module 10 de la valeur résultante, au lieu de convertir la valeur en chaîne de caractères, puis chaque caractère en int.
  • Éviter toutes les copies de vecteurs, d'abord en remplaçant le passage par valeur par le passage par référence, et finalement en éliminant complètement les fonctions d'aide.
  • J'ai éliminé quelques variables temporaires.
  • Et probablement beaucoup de petits détails que j'ai oubliés. Comparez votre code et le mien côte à côte pour voir exactement ce que j'ai fait.

Il est peut-être possible d'optimiser encore plus le code en utilisant des tableaux pré-alloués au lieu de vecteurs, mais cela demanderait un peu plus de travail et je le laisse comme exercice au lecteur :P

Voici le code optimisé :

#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <algorithm>
#include <windows.h>

using namespace std;

void calcMain(int upperBound, vector<int>& known);

int main()
{
    while(true)
    {
        vector<int> results;
        int upperBound;
        cout << "Pick an upper bound: ";
        cin >> upperBound;
        long start, end;
        start = GetTickCount();
        calcMain(upperBound, results);
        end = GetTickCount();
        for (size_t i = 0; i < results.size(); ++i) {
            cout << results[i] << ", ";
        }
        cout << endl;
        double seconds = (double)(end-start) / 1000.0;
        cout << seconds << " seconds." << endl << endl;
    }
    return 0;
}

void calcMain(int upperBound, vector<int>& known)
{
    vector<int> history;
    for(int i = 0; i <= upperBound; i++)
    {
        int current = i;
        history.clear();
        while(true)
        {
                int temp = current;
                int sum = 0;
                while (temp > 0) {
                    sum += (temp % 10) * (temp % 10);
                    temp /= 10;
                }
                current = sum;
                if(find(history.begin(), history.end(), current) != history.end())
                {
                        if(current == 1)
                        {
                                known.push_back(i);
                        }
                        break;
                }
                history.push_back(current);
        }
    }
}

20voto

ilya n. Points 6610

Il existe une nouvelle version, radicalement plus rapide, en tant que une réponse séparée Cette réponse est donc obsolète.


J'ai réécrit votre algorithme en le mettant en cache chaque fois qu'il trouve que le nombre est heureux ou malheureux. J'ai aussi essayé de le rendre aussi pythique que possible, par exemple en créant des fonctions séparées digits() et happy() . Désolé d'avoir utilisé Python 3, mais j'ai pu montrer quelques trucs utiles de ce langage.

Cette version est beaucoup plus rapide . Il fonctionne à 1.7s qui est 10 fois plus rapide que votre programme original qui prend 18s (enfin, mon MacBook est assez vieux et lent :) )

#!/usr/bin/env python3

from timeit import Timer
from itertools import count

print_numbers = False
upperBound = 10**5  # Default value, can be overidden by user.

def digits(x:'nonnegative number') -> "yields number's digits":
    if not (x >= 0): raise ValueError('Number should be nonnegative')
    while x:
        yield x % 10
        x //= 10

def happy(number, known = {1}, happies = {1}) -> 'True/None':
    '''This function tells if the number is happy or not, caching results.

    It uses two static variables, parameters known and happies; the
    first one contains known happy and unhappy numbers; the second 
    contains only happy ones.

    If you want, you can pass your own known and happies arguments. If
    you do, you should keep the assumption commented out on the 1 line.

    '''

#        assert 1 in known and happies <= known  # <= is expensive

    if number in known:
        return number in happies

    history = set()
    while True:
        history.add(number)
        number = sum(x**2 for x in digits(number))
        if number in known or number in history:
            break

    known.update(history)
    if number in happies:
        happies.update(history)
        return True

def calcMain():
    happies = {x for x in range(upperBound) if happy(x) }
    if print_numbers:
        print(happies)

if __name__ == '__main__':
    upperBound = eval(
            input("Pick an upper bound [default {0}]: "
                    .format(upperBound)).strip()
            or repr(upperBound))
    result = Timer(calcMain).timeit(1)
    print ('This computation took {0} seconds'.format(result))

16voto

John Points 216

On dirait que vous passez des vecteurs par valeur à d'autres fonctions. Cela entraînera un ralentissement significatif car le programme fera une copie complète de votre vecteur avant de le transmettre à votre fonction. Pour contourner ce problème, passez une référence constante au vecteur au lieu d'une copie. Ainsi, au lieu de :

int sum(vector<int> given)

Utilisez :

int sum(const vector<int>& given)

Lorsque vous faites cela, vous ne pourrez plus utiliser l'itérateur vector::iterator car il n'est pas constant. Vous devrez le remplacer par vector::const_iterator.

Vous pouvez également transmettre des références non constantes, mais dans ce cas, vous n'avez pas besoin de modifier le paramètre du tout.

7voto

Je peux voir que vous avez un certain nombre d'allocations de tas qui ne sont pas nécessaires.

Par exemple :

while(!next)
        {
            char* buffer = new char[10];

Cela ne semble pas très optimisé. Donc, vous voudrez probablement avoir le tableau pré-alloué et l'utiliser dans votre boucle. Il s'agit d'une technique d'optimisation de base qui est facile à repérer et à réaliser. Mais elle peut aussi devenir un véritable gâchis, alors faites attention.

Vous utilisez également la fonction atoi(), dont je ne sais pas vraiment si elle est vraiment optimisée. Peut-être que faire un modulus 10 et obtenir le chiffre serait mieux (il faut mesurer, je n'ai pas testé cela).

Le fait que vous ayez une recherche linéaire (inVector) pourrait être mauvais. Remplacer la structure de données vectorielle par un std::set pourrait accélérer les choses. Un hash_set pourrait aussi faire l'affaire.

Mais je pense que le pire problème est la chaîne de caractères et cette allocation de choses sur le tas à l'intérieur de cette boucle. Ça n'a pas l'air bon. J'essaierais d'abord à ces endroits.

6voto

ilya n. Points 6610

C'est ma deuxième réponse, qui met en cache des choses comme la somme des carrés pour les valeurs <= 10**6 :

        happy_list[sq_list[x%happy_base] + sq_list[x//happy_base]]

C'est-à-dire,

  • le numéro est divisé en 3 chiffres + 3 chiffres
  • le site tableau précalculé est utilisé pour obtenir la somme des carrés pour les deux parties.
  • ces deux résultats sont ajoutés
  • la table précalculée est consultée pour obtenir le bonheur du nombre :

Je ne pense pas que la version Python puisse être beaucoup plus rapide que cela (ok, si on laisse tomber le retour à l'ancienne version, c'est à dire try: les frais généraux, il est 10 % plus rapide).

Je pense que c'est une excellente question ce qui montre qu'en effet,

  • les choses qui doivent être rapides doivent être écrites en C
  • Cependant, en général, vous n'avez pas besoin que les choses soient rapides (même si vous aviez besoin que le programme fonctionne pendant une journée, ce serait moins que le temps combiné des programmeurs qui l'optimisent).
  • il est plus facile et plus rapide d'écrire des programmes en Python
  • mais pour certains problèmes, en particulier les problèmes de calcul, une solution C++, comme celles ci-dessus, est en fait plus lisible et plus belle qu'une tentative d'optimisation d'un programme Python.

Ok, c'est parti (2ème version maintenant...) :

#!/usr/bin/env python3
'''Provides slower and faster versions of a function to compute happy numbers.

slow_happy() implements the algorithm as in the definition of happy
numbers (but also caches the results).

happy() uses the precomputed lists of sums of squares and happy numbers
to return result in just 3 list lookups and 3 arithmetic operations for
numbers less than 10**6; it falls back to slow_happy() for big numbers.

Utilities: digits() generator, my_timeit() context manager.

'''

from time import time  # For my_timeit.
from random import randint # For example with random number.

upperBound = 10**5  # Default value, can be overridden by user.

class my_timeit:
    '''Very simple timing context manager.'''

    def __init__(self, message):
        self.message = message
        self.start = time()

    def __enter__(self):
        return self

    def __exit__(self, *data):
        print(self.message.format(time() - self.start))

def digits(x:'nonnegative number') -> "yields number's digits":
    if not (x >= 0): raise ValueError('Number should be nonnegative')
    while x:
        yield x % 10
        x //= 10

def slow_happy(number, known = {1}, happies = {1}) -> 'True/None':
    '''Tell if the number is happy or not, caching results.

    It uses two static variables, parameters known and happies; the
    first one contains known happy and unhappy numbers; the second 
    contains only happy ones.

    If you want, you can pass your own known and happies arguments. If
    you do, you should keep the assumption commented out on the 1 line.

    '''
    # This is commented out because <= is expensive.
    # assert {1} <= happies <= known 

    if number in known:
        return number in happies

    history = set()
    while True:
        history.add(number)
        number = sum(x**2 for x in digits(number))
        if number in known or number in history:
            break

    known.update(history)
    if number in happies:
        happies.update(history)
        return True

# This will define new happy() to be much faster ------------------------.

with my_timeit('Preparation time was {0} seconds.\n'):

    LogAbsoluteUpperBound = 6 # The maximum possible number is 10**this.
    happy_list = [slow_happy(x)
                  for x in range(81*LogAbsoluteUpperBound + 1)]
    happy_base = 10**((LogAbsoluteUpperBound + 1)//2)
    sq_list = [sum(d**2 for d in digits(x))
               for x in range(happy_base + 1)]

    def happy(x):
        '''Tell if the number is happy, optimized for smaller numbers.

        This function works fast for numbers <= 10**LogAbsoluteUpperBound.

        '''
        try:
            return happy_list[sq_list[x%happy_base] + sq_list[x//happy_base]]
        except IndexError:
            return slow_happy(x)

# End of happy()'s redefinition -----------------------------------------.

def calcMain(print_numbers, upper_bound):
    happies = [x for x in range(upper_bound + 1) if happy(x)]
    if print_numbers:
        print(happies)

if __name__ == '__main__':
    while True:

        upperBound = eval(input(
            "Pick an upper bound [{0} default, 0 ends, negative number prints]: "
            .format(upperBound)).strip() or repr(upperBound))
        if not upperBound:
            break

        with my_timeit('This computation took {0} seconds.'):
            calcMain(upperBound < 0, abs(upperBound))

        single = 0
        while not happy(single):
            single = randint(1, 10**12)
        print('FYI, {0} is {1}.\n'.format(single,
                    'happy' if happy(single) else 'unhappy')) 

    print('Nice to see you, goodbye!')

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X