Оптимизация кода Python (в 20 раз медленнее, чем C) - PullRequest
4 голосов
/ 24 февраля 2010

Я написал этот очень плохо оптимизированный C-код, который выполняет простые математические вычисления:

#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))


unsigned long long int p(int);
float fullCheck(int);

int main(int argc, char **argv){
  int i, g, maxNumber;
  unsigned long long int diff = 1000;

  if(argc < 2){
    fprintf(stderr, "Usage: %s maxNumber\n", argv[0]);
    return 0;
  }
  maxNumber = atoi(argv[1]);

  for(i = 1; i < maxNumber; i++){
    for(g = 1; g < maxNumber; g++){
      if(i == g)
        continue;
      if(p(MAX(i,g)) - p(MIN(i,g)) < diff &&  fullCheck(p(MAX(i,g)) - p(MIN(i,g))) && fullCheck(p(i) + p(g))){
          diff = p(MAX(i,g)) - p(MIN(i,g));
          printf("We have a couple %llu %llu with diff %llu\n", p(i), p(g), diff);
      }
    }
  }

  return 0;
}

float fullCheck(int number){
  float check = (-1 + sqrt(1 + 24 * number))/-6;
  float check2 = (-1 - sqrt(1 + 24 * number))/-6;
  if(check/1.00 == (int)check)
    return check;
  if(check2/1.00 == (int)check2)
    return check2;
  return 0;
}

unsigned long long int p(int n){
  return n * (3 * n - 1 ) / 2;
}

А потом я попытался (просто для удовольствия) перенести его под Python, чтобы посмотреть, как он отреагирует. Моей первой версией было почти 1: 1 преобразование, которое выполнялось очень медленно (120 + сек в Python против <1 сек в C). Я провел небольшую оптимизацию, и вот что я получил: </p>

#!/usr/bin/env/python
from cmath import sqrt
import cProfile
from pstats import Stats

def quickCheck(n):
        partial_c = (sqrt(1 + 24 * (n)))/-6 
        c = 1/6 + partial_c
        if int(c.real) == c.real:
                return True
        c = c - 2*partial_c
        if int(c.real) == c.real:
                return True
        return False

def main():        
        maxNumber = 5000
        diff = 1000
        for i in range(1, maxNumber):
                p_i = i * (3 * i - 1 ) / 2
                for g in range(i, maxNumber):
                        if i == g:
                                continue
                        p_g = g * (3 * g - 1 ) / 2
                        if p_i > p_g:
                                ma = p_i
                                mi = p_g
                        else:
                                ma = p_g
                                mi = p_i

                        if ma - mi < diff and quickCheck(ma - mi):
                                if quickCheck(ma + mi):
                                        print ('New couple ', ma, mi)
                                        diff = ma - mi


cProfile.run('main()','script_perf')
perf = Stats('script_perf').sort_stats('time', 'calls').print_stats(10)

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

Ответы [ 6 ]

17 голосов
/ 24 февраля 2010

Поскольку quickCheck вызывается около 25 000 000 раз, вы можете использовать памятку для кэширования ответов.

Вы можете сделать памятку в C, а также Python. В C все будет намного быстрее.

Вы вычисляете 1/6 в каждой итерации quickCheck. Я не уверен, будет ли это оптимизировано Python, но если вы сможете избежать пересчета постоянных значений, вы обнаружите, что все происходит быстрее. Компиляторы Си делают это за вас.

Делать что-то вроде if condition: return True; else: return False глупо и отнимает много времени. Просто сделайте return condition.

В Python 3.x /2 должен создавать значения с плавающей точкой. Вам, кажется, нужны целые числа для этого. Вы должны использовать //2 деление. Это будет ближе к версии C с точки зрения того, что она делает, но я не думаю, что это значительно быстрее.

Наконец, Python обычно интерпретируется. Переводчик всегда будет значительно медленнее, чем C.

9 голосов
/ 24 февраля 2010

На моей машине я сделал это с ~ 7 до ~ 3 секунд:

  • Предварительно вычислено i * (3 * i - 1 ) / 2 для каждого значения, у вас оно было вычислено дважды довольно много
  • Кэшированные звонки на quickCheck
  • Удалено if i == g путем добавления +1 к диапазону
  • Удалено if p_i > p_g, поскольку p_i всегда меньше, чем p_g

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

def main():
        maxNumber = 5000
        diff = 1000

        p = {}
        quickCache = {}

        for i in range(maxNumber):
            p[i] = i * (3 * i - 1 ) / 2

        def quickCheck(n):
            if n in quickCache: return quickCache[n]
            partial_c = (sqrt(1 + 24 * (n)))/-6 
            c = 1/6 + partial_c
            if int(c.real) == c.real:
                    quickCache[n] = True
                    return True
            c = c - 2*partial_c
            if int(c.real) == c.real:
                    quickCache[n] = True
                    return True
            quickCache[n] = False
            return False

        for i in range(1, maxNumber):
                mi = p[i]
                for g in range(i+1, maxNumber):
                        ma = p[g]
                        if ma - mi < diff and quickCheck(ma - mi) and quickCheck(ma + mi):
                                print('New couple ', ma, mi)
                                diff = ma - mi
5 голосов
/ 25 февраля 2010

Поскольку функция p () монотонно возрастает, вы можете избежать сравнения значений: g> i подразумевает p (g)> p (i) . Кроме того, внутренний цикл может быть разорван рано, потому что p (g) - p (i)> = diff подразумевает p (g + 1) - p (i)> = diff .

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

На моей машине это сократило время выполнения до 7,8 мс с использованием Python 2.6. Использование PyPy с JIT уменьшило это значение до 0,77 мс.

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

EPS = 0.00000001
def quickCheck(n):
    partial_c = sqrt(1 + 24*n) / -6
    c = 1/6 + partial_c
    if abs(int(c) - c) < EPS:
        return True
    c = 1/6 - partial_c
    if abs(int(c) - c) < EPS:
        return True
    return False

def p(i):
    return i * (3 * i - 1 ) / 2

def main(maxNumber):
    diff = 1000

    for i in range(1, maxNumber):
        for g in range(i+1, maxNumber):
            if p(g) - p(i) >= diff:
                break 
            if quickCheck(p(g) - p(i)) and quickCheck(p(g) + p(i)):
                print('New couple ', p(g), p(i), p(g) - p(i))
                diff = p(g) - p(i)
4 голосов
/ 24 февраля 2010

Есть несколько компиляторов Python, которые могут действительно помочь вам. Взгляните на Psyco .

Еще один способ работы с математически интенсивными программами - переписать большую часть работы в математическое ядро, такое как NumPy , так что сильно оптимизированный код выполняет свою работу, а ваш код на Python только направляет расчет. Чтобы получить максимальную отдачу от этой стратегии, избегайте выполнения вычислений в циклах, и вместо этого позвольте математическому ядру делать все это.

2 голосов
/ 24 февраля 2010

Другие респонденты уже упомянули несколько оптимизаций, которые помогут. Однако, в конечном счете, вы не сможете сопоставить производительность C в Python. Python - хороший инструмент, но, поскольку он интерпретируется, он на самом деле не подходит для тяжелых вычислений или других приложений, где производительность является ключевым фактором.

Кроме того, даже в вашей C-версии ваш внутренний цикл может использовать немного помощи. Обновленная версия:

  for(i = 1; i < maxNumber; i++){
    for(g = 1; g < maxNumber; g++){
      if(i == g)
        continue;
      max=i;
      min=g;

      if (max<min) { 
          // xor swap - could use swap(p_max,p_min) instead. 
          max=max^min;
          min=max^min;
          max=max^min;
      }

      p_max=P(max);
      p_min=P(min);
      p_i=P(i);
      p_g=P(g);

      if(p_max - p_min < diff &&  fullCheck(p_max-p_min) && fullCheck(p_i + p_g)){
          diff = p_max - p_min;
          printf("We have a couple %llu %llu with diff %llu\n", p_i, p_g, diff);
      }
    }
  }


///////////////////////////
float fullCheck(int number){
  float den=sqrt(1+24*number)/6.0;
  float check = 1/6.0 - den;
  float check2 = 1/6.0 + den;


  if(check == (int)check)
    return check;
  if(check2 == (int)check2)
    return check2;

  return 0.0;
}

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

Вы можете рассмотреть объявление P () как inline или переписать как макрос препроцессора. В зависимости от того, насколько хорош ваш оптимизатор, вы можете захотеть выполнить некоторую арифметику самостоятельно и упростить ее реализацию.

Ваша реализация fullCheck () вернет результаты, которые кажутся недействительными, поскольку 1/6 == 0, где 1 / 6.0 вернет 0,166 ... как и следовало ожидать.

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

1 голос
/ 24 февраля 2010

20-кратная разница между Python и C для задачи вычисления чисел мне кажется довольно хорошей.

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

Но обратите внимание на то, какова 1 минута процессорного времени по сравнению с мозгом и временем печати, которое вы сэкономили, написав Python вместо C? : -)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...