Необычная разница в скорости между Python и С++

Недавно я написал короткий алгоритм для вычисления счастливых чисел в python. Программа позволяет вам выбрать верхнюю границу, и она будет определять все счастливые числа ниже нее. Для сравнения скорости я решил сделать самый прямой перевод алгоритма, о котором я знал, от python до С++.

Удивительно, что версия С++ работает значительно медленнее, чем версия python. Точные тесты скорости между временем выполнения для обнаружения первых 10 000 счастливых чисел показывают, что программа python запускается в среднем за 0,59 секунды, а версия С++ работает в среднем за 8.5 секунд.

Я бы назвал эту разницу скоростей тем фактом, что мне пришлось писать вспомогательные функции для частей вычислений (например, определяя, находится ли элемент в списке /array/vector ) в версии С++, которые уже были встроены в язык python.

Во-первых, это истинная причина такой абсурдной разницы в скорости, а во-вторых, как я могу изменить версию С++ для выполнения быстрее, чем версия python (как это должно быть на мой взгляд).

Две части кода, с проверкой скорости, находятся здесь: версия Python, Версия С++. Спасибо за помощь.

#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"

Ответ 1

Для 100000 элементов код Python занял 6,9 секунды, а С++ первоначально занял более 37 секунд.

Я сделал некоторые базовые оптимизации для вашего кода и смог получить код на С++ выше 100 раз быстрее, чем реализация Python. Теперь он делает 100000 элементов за 0,06 секунды. Это в 617 раз быстрее, чем исходный код на С++.

Самое главное - скомпилировать в режиме Release со всеми оптимизациями. Этот код буквально на порядок медленнее в режиме отладки.

Далее я объясню сделанные оптимизации.

  • Перемещено все векторные объявления за пределами цикла; заменили их на операцию clear(), которая намного быстрее, чем вызов конструктора.
  • Заменил вызов на pow (значение, 2) значением умножения: значение *.
  • Вместо того, чтобы иметь вектор квадратов и вызывать сумму на нем, я суммирую значения на месте, используя только целое число.
  • Избегайте всех строковых операций, которые очень медленны по сравнению с целыми операциями. Например, можно вычислить квадраты каждой цифры путем многократного деления на 10 и получения модуля 10 результирующего значения вместо преобразования значения в строку, а затем каждый символ обратно в int.
  • Избегайте всех векторных копий, сначала заменяя передачу по значению с помощью передачи по ссылке, и, наконец, полностью исключая вспомогательные функции.
  • Устранено несколько временных переменных.
  • И, вероятно, многие мелкие детали я забыл. Сравните ваш код и мой бок о бок, чтобы увидеть, что именно я сделал.

Возможно, будет еще лучше оптимизировать код, используя предварительно выделенные массивы вместо векторов, но это будет немного больше работы, и я оставлю его как упражнение для читателя.: P

Здесь оптимизированный код:

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

Ответ 2

Там новая, радикально более быстрая версия отдельный ответ, поэтому этот ответ устарел.


Я переписал ваш алгоритм, сделав его кешем всякий раз, когда он считает, что число будет счастливым или несчастным. Я также попытался сделать это как питонов, как мог, например, создав отдельные функции digits() и happy(). Извините за использование Python 3, но я также могу показать пару полезных вещей от него.

Эта версия намного быстрее. Он работает 1,7 с, который 10 раз быстрее, чем ваша оригинальная программа, которая занимает 18 с (ну, мой MacBook довольно старый и медленный:) )

#!/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 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))

Ответ 3

Похоже, вы передаете векторы по значению другим функциям. Это будет значительным замедлением, потому что программа фактически сделает полную копию вашего вектора перед тем, как передать его вашей функции. Чтобы обойти это, передайте постоянную ссылку на вектор вместо копии. Поэтому вместо:

int sum(vector<int> given)

Использование:

int sum(const vector<int>& given)

Когда вы это сделаете, вы больше не сможете использовать вектор:: iterator, потому что он не является постоянным. Вам нужно будет заменить его на вектор:: const_iterator.

Вы также можете передавать не константные ссылки, но в этом случае вам вообще не нужно изменять параметр.

Ответ 4

Я вижу, что у вас довольно много распределений кучи, которые не нужны

Например:

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

Это не выглядит очень оптимизированным. Итак, вы, вероятно, захотите, чтобы массив был предварительно выделен и использовал его внутри вашего цикла. Это базовый метод оптимизации, который легко заметить и сделать. Это тоже может стать беспорядком, поэтому будьте осторожны с этим.

Вы также используете функцию atoi(), которую я действительно не знаю, действительно ли она оптимизирована. Может быть, сделать модуль 10 и получить цифру может быть лучше (вы должны измерить, я не проверял это).

Тот факт, что у вас есть линейный поиск (inVector), может быть плохим. Замена структуры векторных данных на std:: set может ускорить работу. Также hash_set может сделать трюк.

Но я думаю, что худшей проблемой является строка и это распределение вещей в куче внутри этого цикла. Это не выглядит хорошо. Сначала я попробую в этих местах.

Ответ 5

Это мой второй ответ; который кэширует такие вещи, как сумма квадратов для значений <= 10**6:

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

То есть

  • число разделено на 3 цифры + 3 цифры
  • предварительно вычисленная таблица используется для получения суммы квадратов для обеих частей
  • эти два результата добавлены
  • Проконсультируемая таблица используется для получения счастья числа:

Я не думаю, что версия Python может быть сделана намного быстрее, чем это (хорошо, если вы выбросите резервную копию старой версии, то есть try: накладные расходы, это на 10% быстрее).

Я думаю, что это отличный вопрос, который показывает, что действительно

  • вещи, которые должны быть быстрыми, должны быть написаны на C
  • однако, как правило, вам не нужно, чтобы что-то было быстрым (даже если вам нужна была программа для работы в течение дня, это было бы меньше, чем в комбинированное время, когда программисты ее оптимизировали).
  • проще и быстрее писать программы в Python
  • но для некоторых проблем, особенно для вычислительных, решение на С++, как и выше, на самом деле более читаемо и красивее, чем попытка оптимизировать программу Python.

Хорошо, вот он идет (вторая версия сейчас...):

#!/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 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() 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!')

Ответ 6

Ну, я также дал его один раз. Я не тестировал и даже не компилировал.

Общие правила для числовых программ:

  • Никогда не обрабатывайте числа как текст. То, что делает менее Языки, чем Python медленно, поэтому, если вы делаете это в C, программа будет медленнее, чем Python.

  • Не используйте структуры данных, если их можно избежать. Вы строили массив, чтобы добавить номера вверх. Лучше сохранить общее количество.

  • Сохраните копию ссылки STL, чтобы вы могли использовать ее, а не писать свои собственные функции.


void calcMain(int upperBound)
{
    vector<int> known;
    for(int i = 0; i <= upperBound; i++)
    {
        int current = i;
        vector<int> history;
        do
        {
            squaresum = 0
            for ( ; current; current /= 10 )
            {
                int digit = current % 10;
                squaresum += digit * digit;
            }
            current = squaresum;
            history.push_back(current);
        } while ( ! count(history.begin(), history.end() - 1, current) );

        if(current == 1)
        {
            known.push_back(i);
            //cout << i << "\t";
        }

    }
    //cout << "\n\n";
}

Ответ 7

Чтобы получить немного больше информации об этой проблеме, зная, как быстро я могу найти эти числа, я написал многопоточную реализацию алгоритма Dr_Asik на С++. Есть две вещи, которые важны для понимания того факта, что эта реализация многопоточна.

  • Больше потоков не обязательно приводит к лучшему времени выполнения, для каждой ситуации есть счастливая среда, зависящая от объема чисел, которые вы хотите рассчитать.

  • Если вы сравниваете время между этой версией, запущенной с одним потоком и исходной версией, единственными факторами, которые могут привести к разнице во времени, являются накладные расходы от запуска потоков и проблем с производительностью системы. В противном случае алгоритм один и тот же.

Код для этой реализации (все кредиты для алгоритма идут на Dr_Asik) здесь. Кроме того, я написал несколько тестов скорости с двойной проверкой для каждого теста, чтобы помочь создать резервную копию этих трех точек.

Расчет первых 100 000 000 счастливых чисел:

Оригинал - 39.061/39.000 (оригинальная реализация Dr_Asik)
1 Тема - 39.000/39.079
2 Темы - 19.750/19.890
10 Темы - 11.872/11.888
30 Темы - 10.764/10.827
50 тем - 10.624/10.561 < -
100 тем - 11.060/11.216
500 тем - 13.385/12.527

Из этих результатов видно, что наша счастливая среда - это около 50 потоков, плюс-минус десять или около того.

Ответ 8

Другие оптимизации: используя массивы и прямой доступ, используя индекс цикла, а не поиск в векторе, и путем кэширования предыдущих сумм, следующий код (вдохновленный доктором Асиком, но, вероятно, не оптимизированный на все) работает 2445 раз быстрее, чем исходный код на С++, о 400 раза быстрее, чем код Python.

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

void calcMain(int upperBound, std::vector<int>& known)
{
    int tempDigitCounter = upperBound;
    int numDigits = 0;
    while (tempDigitCounter > 0)
    {
        numDigits++;
        tempDigitCounter /= 10;
    }
    int maxSlots = numDigits * 9 * 9;
    int* history = new int[maxSlots + 1];

    int* cache = new int[upperBound+1];
    for (int jj = 0; jj <= upperBound; jj++)
    {
        cache[jj] = 0;
    }

    int current, sum, temp;
    for(int i = 0; i <= upperBound; i++)
    {
        current = i;
        while(true)
        {
            sum = 0;
            temp = current;

            bool inRange = temp <= upperBound;
            if (inRange)
            {
                int cached = cache[temp];
                if (cached)
                {
                    sum = cached;
                }
            }

            if (sum == 0)
            {
                while (temp > 0)
                {
                    int tempMod = temp % 10;
                    sum += tempMod * tempMod;
                    temp /= 10;
                }
                if (inRange)
                {
                    cache[current] = sum;
                }
            }
            current = sum;
            if(history[current] == i)
            {
                if(current == 1)
                {
                    known.push_back(i);
                }
                break;
            }
            history[current] = i;
        }
    }
}

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

Ответ 9

Я не эксперт в области оптимизации на С++, но я считаю, что разница в скорости может быть связана с тем, что в списках Python было предварительно выделено больше места в начале, в то время как ваши векторы С++ должны перераспределяться и, возможно, копировать каждый раз, когда они растут.

Что касается комментария GMan о поиске, я считаю, что оператор "in" Python также является линейным поиском и имеет одинаковую скорость.

Edit

Также я заметил, что вы перевернули свою собственную функцию pow. Нет необходимости делать это, и stdlib скорее всего быстрее.

Ответ 10

Вот еще один способ, который опирается на запоминание всех уже изученных чисел. Я получаю коэффициент x4-5, который необычно устойчив к коду DrAsik для 1000 и 1000000, я ожидал, что кеш будет более эффективным, чем больше номеров мы изучали. В противном случае применялись такие же классические оптимизации. BTW, если компилятор принимает NRVO (/RNVO? Я никогда не помню точный термин) или ссылки rvalue, нам не нужно было бы передавать вектор как выходной параметр.

NB: микро-оптимизация по-прежнему возможна ИМХО, и, кроме того, кеширование наивно, поскольку оно выделяет гораздо больше памяти, чем нужно.

enum Status {
    never_seen,
    being_explored,
    happy,
    unhappy
};

char const* toString[] = { "never_seen", "being_explored", "happy", "unhappy" };


inline size_t sum_squares(size_t i) {
    size_t s = 0;
    while (i) {
        const size_t digit = i%10;
        s += digit * digit;
        i /= 10;
    }
    return s ;
}

struct Cache {
    Cache(size_t dim) : m_cache(dim, never_seen) {}
    void set(size_t n, Status status) {
        if (m_cache.size() <= n) {
            m_cache.resize(n+1, never_seen);
        }
        m_cache[n] = status;
        // std::cout << "(c[" << n << "]<-"<<toString[status] << ")";
    }
    Status operator[](size_t n) const {
        if (m_cache.size() <= n) {
            return never_seen;
        } else {
            return m_cache[n];
        }
    }

private:
    std::vector<Status> m_cache;
};

void search_happy_lh(size_t upper_bound, std::vector<size_t> & happy_numbers)
{
    happy_numbers.clear();
    happy_numbers.reserve(upper_bound); // it doesn't improve much the performances

    Cache cache(upper_bound+1);
    std::vector<size_t> current_stack;

    cache.set(1,happy);
    happy_numbers.push_back(1);
    for (size_t i = 2; i<=upper_bound ; ++i) {
        // std::cout << "\r" << i << std::flush;
        current_stack.clear();
        size_t s= i;
        while ( s != 1 && cache[s]==never_seen)
        {
            current_stack.push_back(s);
            cache.set(s, being_explored);
            s = sum_squares(s);
            // std::cout << " - " << s << std::flush;
        }
        const Status update_with = (cache[s]==being_explored ||cache[s]==unhappy) ? unhappy : happy;
        // std::cout << " => " << s << ":" << toString[update_with] << std::endl;
        for (size_t j=0; j!=current_stack.size(); ++j) {
            cache.set(current_stack[j], update_with);
        }
        if (cache[i] == happy) {
            happy_numbers.push_back(i);
        }
    }
}

Ответ 11

Наткнулся на эту страницу, пока скучал, и думал, что буду играть в гольф в js. Алгоритм мой собственный, и я не проверял его полностью на что-либо, кроме моих собственных вычислений (так что это может быть неправильно). Он вычисляет первые 1е7 счастливые числа и сохраняет их в ч. Если вы хотите изменить его, измените оба параметра.

m=1e7,C=7*81,h=[1],t=true,U=[,,,,t],n=w=2;
while(n<m){
z=w,s=0;while(z)y=z%10,s+=y*y,z=0|z/10;w=s;
if(U[w]){if(n<C)U[n]=t;w=++n;}else if(w<n)h.push(n),w=++n;}

Это будет печатать первые 1000 элементов для вас в консоли или в браузере:

o=h.slice(0,m>1e3?1e3:m);
(!this.document?print(o):document.load=document.write(o.join('\n')));

155 символов для функциональной части, и она кажется такой же быстрой *, как и предложение Dr. Asik на firefox или v8 (в 350-400 раз быстрее, чем исходная программа python в моей системе во время работы d8 happygolf.js или js -a -j -p happygolf.js в spidermonkey).
Я буду в восторге от аналитических навыков любого, кто может понять, почему этот алгоритм делает это хорошо, не ссылаясь на более длинную, прокомментированную версию fortran.

Я был заинтригован тем, как быстро это было, поэтому я научился fortran получить сравнение того же алгоритма, будь добр, если есть какие-то вопиющие ошибки новичка, это моя первая программа fortran. http://pastebin.com/q9WFaP5C Это статическая память, поэтому, чтобы быть честной с другими, она в самокомпилирующей оболочке script, если у вас нет gcc/bash/etc, вырезать препроцессор и bash материал вверху, установите макросы вручную и скомпилируйте его как fortran95.

Даже если вы включаете время компиляции, оно превосходит большинство других. Если вы этого не сделаете, это примерно в 3000-3500 раз быстрее, чем исходная версия python (и по расширению > в 40 000 раз быстрее, чем С++ *, хотя я не запускал ни одну из программ на С++).

Удивительно, но многие из оптимизаций, которые я пробовал в версии fortran (включая некоторые, подобные разворачиванию цикла, которые я забыл из вставной версии из-за небольшого эффекта и удобочитаемости), наносят ущерб версии js. Это упражнение показывает, что современные составители трассировки очень хороши (в пределах 7-10 из тщательно оптимизированной статической памяти fortran), если вы выходите из своего пути и не пробуйте какие-либо сложные вещи. уйти с их пути и попытаться сделать хитроумный материал Наконец, здесь намного лучше, рекурсивная версия js.

// to s, then integer divides x by 10.
// Repeats until x is 0.
function sumsq(x) {
  var y,s=0;
  while(x) {
    y = x % 10; 
    s += y * y;
    x = 0| x / 10; 
  }
  return s;
}
// A boolean cache for happy().
// The terminating happy number and an unhappy number in
// the terminating sequence.
var H=[];
H[1] = true;
H[4] = false;
// Test if a number is happy.
// First check the cache, if that empty
// Perform one round of sumsq, then check the cache
// For that. If that empty, recurse.
function happy(x) {
  // If it already exists.
  if(H[x] !== undefined) {
    // Return whatever is already in cache.
    return H[x];
  } else {
    // Else calc sumsq, set and  return cached val, or if undefined, recurse.
    var w = sumsq(x);
    return (H[x] = H[w] !== undefined? H[w]: happy(w));
  }
}
//Main program loop.
var i, hN = []; 
for(i = 1; i < 1e7; i++) {
  if(happy(i)) { hN.push(i); }
}

Удивительно, но хотя и довольно высокий уровень, он выполнял почти точно так же, как и императивный алгоритм spidermonkey (с оптимизацией), и закрывал (в 1,2 раза) в v8.

Мораль этой истории, я думаю, потратьте немного времени, думая о своем алгоритме, если это важно. Также языки высокого уровня уже имеют много накладных расходов (и иногда имеют свои собственные уловки, чтобы уменьшить его), поэтому иногда делать что-то более простое или использовать свои функции высокого уровня так же быстро. Также не всегда помогает микро-оптимизация.

* Если моя установка python необычно медленная, прямые времена несколько бессмысленны, так как это eee первого поколения. Время:
12s для версии fortran, без вывода, 1e8 счастливых чисел.
40s для версии fortran, выход трубы через gzip на диск.
8-12 для обеих версий js. 1e7 счастливых чисел, без вывода с полной оптимизацией 10-100s для обеих js версий 1e7 с меньшей/без оптимизации (в зависимости от определения оптимизации нет, 100s было с eval()) no output

Мне было бы интересно увидеть время для этих программ на реальном компьютере.

Ответ 12

Здесь есть пища для размышлений: если вам предлагается выбрать алгоритм для поиска простых чисел в компьютере 2009 года или алгоритм 2009 на компьютере 1979 года, который вы бы выбрали?

Новый алгоритм на древнем оборудовании был бы лучшим выбором с огромным запасом. Посмотрите на свои "вспомогательные" функции.

Ответ 13

Существует несколько оптимизаций:

(1) Использовать ссылки const

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

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

(2) Использовать счетные циклы

int pow(int given, int power)
{
    int current = 1;
    while(power--)
        current *= given;
    return current;
}

Или, как говорили другие, используйте стандартный библиотечный код.

(3) Не выделяйте буферы, где не требуется

        vector<int> squares;
        for (int temp = current; temp != 0; temp /= 10)
        {
            squares.push_back(pow(temp % 10, 2));
        }

Ответ 14

С аналогичной оптимизацией, такой как PotatoSwatter, я получил время на 10000 номеров с 1,063 секунды до 0,062 секунды (кроме того, что я заменил itoa стандартным sprintf в оригинале).

При всех оптимизации памяти (не пропускайте контейнеры по значению - на С++ вы должны явно решить, хотите ли вы копию или ссылку; переместить операции, выделяющие память из внутренних циклов, если у вас уже есть номер в char, то, что точка копирования его на std::string и т.д.), я получил его до 0,532.

Остальное время исходило из использования% 10 для доступа к цифрам, а не для преобразования чисел в строку.

Я полагаю, что может быть еще одна оптимизация алгоритмического уровня (числа, с которыми вы столкнулись при поиске счастливого числа, сами по себе также являются счастливыми цифрами?), но я не знаю, сколько это выигрывает (в этом не так много счастливых чисел в первое место), и эта оптимизация также не входит в версию Python.

Кстати, не используя преобразование строк и список в квадратные цифры, я получил версию Python с 0.825 секунд до 0.33.

Ответ 15

Здесь версия С#:

using System;
using System.Collections.Generic;
using System.Text;

namespace CSharp
{
  class Program
  {
    static void Main (string [] args)
    {
      while (true)
      {
        Console.Write ("Pick an upper bound: ");

        String
          input = Console.ReadLine ();

        uint
          upper_bound;

        if (uint.TryParse (input, out upper_bound))
        {
          DateTime
            start = DateTime.Now;

          CalcHappyNumbers (upper_bound);

          DateTime
            end = DateTime.Now;

          TimeSpan
            span = end - start;

          Console.WriteLine ("Time taken = " + span.TotalSeconds + " seconds.");
        }
        else
        {
          Console.WriteLine ("Error in input, unable to parse '" + input + "'.");
        }
      }
    }

    enum State
    {
      Happy,
      Sad,
      Unknown
    }

    static void CalcHappyNumbers (uint upper_bound)
    {
      SortedDictionary<uint, State>
        happy = new SortedDictionary<uint, State> ();

      SortedDictionary<uint, bool>
        happy_numbers = new SortedDictionary<uint, bool> ();

      happy [1] = State.Happy;
      happy_numbers [1] = true;

      for (uint current = 2 ; current < upper_bound ; ++current)
      {
        FindState (ref happy, ref happy_numbers, current);
      }

      //foreach (KeyValuePair<uint, bool> pair in happy_numbers)
      //{
      //  Console.Write (pair.Key.ToString () + ", ");
      //}

      //Console.WriteLine ("");
    }

    static State FindState (ref SortedDictionary<uint, State> happy, ref SortedDictionary<uint,bool> happy_numbers, uint value)
    {
      State
        current_state;

      if (happy.TryGetValue (value, out current_state))
      {
        if (current_state == State.Unknown)
        {
          happy [value] = State.Sad;
        }
      }
      else
      {
        happy [value] = current_state = State.Unknown;

        uint
          new_value = 0;

        for (uint i = value ; i != 0 ; i /= 10)
        {
          uint
            lsd = i % 10;

          new_value += lsd * lsd;
        }

        if (new_value == 1)
        {
          current_state = State.Happy;
        }
        else
        {
          current_state = FindState (ref happy, ref happy_numbers, new_value);
        }

        if (current_state == State.Happy)
        {
          happy_numbers [value] = true;
        }

        happy [value] = current_state;
      }

      return current_state;
    }
  }
}

Я сравнил его с кодом Dr_Asik С++. Для верхней границы 100000 версия С++ запускалась примерно через 2,9 секунды, а версия С# - через 0,35 секунды. Оба были скомпилированы с использованием Dev Studio 2005 с использованием стандартных вариантов сборки по умолчанию, и оба они были выполнены из командной строки.

Ответ 16


#!/usr/bin/env python

import timeit

upperBound = 0

def calcMain():
    known = set()
    for i in xrange(0,upperBound+1):
        next = False
        current = i
        history = set()
        while not next:
            squaresum=0
            while current > 0:
                current, digit = divmod(current, 10)
                squaresum += digit * digit
            current = squaresum
            if current in history:
                next = True
                if current == 1:
                    known.add(i)
            history.add(current)

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

Я сделал несколько незначительных изменений в вашем исходном примере кода python, которые улучшают производительность кода более чем на 16 раз. Изменения, которые я сделал, заняли 100 000 экземпляров примерно с 9,64 секунды до 3,38 секунды.

Основное изменение заключалось в том, чтобы изменения мод 10 и аккумулятора менялись в цикле while. Я сделал несколько других изменений, которые улучшили время выполнения всего за доли сотых долей секунды. Первым незначительным изменением было изменение основного цикла цикла из понимания списка диапазонов на итератор xrange. Второе незначительное изменение заключалось в замене класса set для класса списка как для известных, так и для переменных истории. Я также экспериментировал с пониманием итераторов и предварительным вычислением квадратов, но оба они отрицательно влияли на эффективность. Кажется, я использую более медленную версию python или более медленного процессора, чем некоторые другие участники. Я был бы заинтересован в результатах другого сравнения времени моего кода python с одной из оптимизированных версий С++ того же алгоритма. Я также попытался использовать оптимизацию python -O и -OO, но они имели обратную сторону предполагаемого эффекта.

Ответ 17

Почему каждый использует вектор в версии С++? Время поиска - O (N).

Даже если это не так эффективно, как набор python, используйте std:: set. Время поиска - O (log (N)).