2010-02-24 4 views
4

Я написал это очень плохо оптимизированный код C, который делает простую математическую расчет:оптимизация кода Python (20x медленнее, чем 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). Я сделал немного оптимизации, и это то, что я получил:

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

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

+1

Пожалуйста, вы можете описать то, что ваша программа делает –

+4

профайлер может замедлить вещи совсем немного –

+1

я вижу, что, как представляется, много дополнительных управляющими операторами.Давным-давно давным-давно в галактике я заметил, что для такого рода алгоритмов, которые уменьшали количество управляющих утверждений, как можно больше, приводили меня к огромным повышениям производительности. – TheJacobTaylor

ответ

9

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

  • предвычисленных i * (3 * i - 1)/2 для каждого значения, в вашем он был вычислен дважды довольно много
  • Cached звонков 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 
+0

Я собирался опубликовать почти тот же список оптимизаций, примерно с тем же относительным ускорением. Кэширование вычисления «i * (3 * i - 1)/2» и удаление «if i ​​== g путем добавления +1 к диапазону» были двумя самыми большими улучшениями. Вызовы quickCheck не повторялись так сильно, как я думал, но по-прежнему рекомендуется кэшировать значения. –

+0

Приятный, я уже пытался включить quickCheck в основной корпус, но преимущество было медленным по сравнению с более сложным кодом. – 2010-02-25 08:38:44

+0

Дал ему вихрь с сундуком PyPy. Примерно в 3 раза быстрее, чем python 2.6 и примерно в 3 раза медленнее, чем версия C, скомпилированная с GCC 4.4-O3. –

17

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

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

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

Ведение дел, подобных if condition: return True; else: return False, является глупым и требует много времени. Просто сделайте return condition.

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

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

+1

* всегда значительно медленнее, чем C *, true, если вы не считаете время разработки и отладки, которое всегда значительно меньше C, по моему опыту: P – Seth

+3

Как это происходит ..? «Вы напишете более быстрый код на C, но быстрее код в Python» – MattH

2

Другие респонденты уже упомянули несколько оптимизаций, которые помогут. Однако, в конечном счете, вы не сможете сопоставить производительность 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; 
} 

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

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

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

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

+1

Python не обязательно интерпретируется. – Chuck

+0

@Chuck ack. хорошая точка зрения. –

1

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

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

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

4

Есть некоторые компиляторы python, которые могут на самом деле сделать для вас хороший бит. Посмотрите на Psyco.

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

+0

+1: Не транслитерировать C; переосмыслить его в numpy. –

5

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

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

На моей машине это сократило время выполнения до 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) 
+1

Как и все вещи .... сначала оптимизируйте проблему, а затем поработайте над кодом. «Если изменение алгоритма p (g) - p (i)> = diff: break» делает ОГРОМНОЕ различие. Это изменение взяло мой код (который имеет те же основные изменения, что и @truppo) с ~ 2,3 до 13 мс .... или на два порядка уменьшения во время выполнения. –

Смежные вопросы