Python 3 es lento (I)


Cansado de leer que Python 3 es lento, he decidido ver cómo de lento es. El resultado me ha dejado de una piedra.

He tratado de utilizar ejemplos lo más pequeños posible, con la idea de evitar otras posibles mejores implementaciones. Además, he ejecutado el código varias veces, para evitar posibles efectos de carga del sistema, mostrando aquí alguna de las medidas más representativas.

Admito que la sección más importante sea, probablemente, las conclusiones. Si yo fuera usted, probablemente saltaría allí directamente :D

El problema

Mi idea ha sido comenzar por medir lo que se tarda en filtrar un vector. Como en Python hay varias maneras de hacerlo, he decidido utilizar todas ellas.

Si se os ocurre alguna otra manera, estaré encantado de incluirla.

Para que los números se vean, vamos a filtrar un vector de un millón de números diferentes, previamente desordenados, de manera que se devuelvan la mitad.

Solución

Evidentemente, resulta más sencillo con un pequeñín framework de pruebas, que es el siguiente:

.. code:: python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/python
# -*- coding:utf-8; tab-width:4; mode:python -*-

import time
import random
import inspect
import re

ITEMS = 1000000
LIMIT = 500000

# python 3 do not support range assigment
v = [x for x in range(ITEMS)]
random.shuffle(v)


def filterfunc(x):
    return x < LIMIT

def print_result(msg, t):
    print ("{0:02.10f} - {1}".format(t, msg))

def exec_filter(f):
    t = time.time()
    r = f(v)
    end = time.time() - t

    # python3 returns a filter object
    c = [x for x in r]
    if len(c) != LIMIT:
        print("ERROR")
    print_result (re.sub('\n( )*', '\\\\n', inspect.getsource(f).strip()), end)

El problema, pues, queda reducido a lo siguiente:

.. code:: python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/python
# -*- coding:utf-8; tab-width:4; mode:python -*-

import helper


def filter_for(v):
    r = []
    for x in v:
        if x < helper.LIMIT:
            r.append(x)
    return r

def filter_for_function(v):
    r = []

    for x in v:
        if helper.filterfunc(x):
            r.append(x)
    return r

def filter_filter(v):
    return filter(lambda x:x<helper.LIMIT, v)

def filter_filter_function(v):
    return filter(helper.filterfunc, v)

def filter_filter_inline(v):
    inline = lambda x:x < helper.LIMIT
    return filter(inline, v)

helper.exec_filter(filter_for)
helper.exec_filter(filter_for_function)
helper.exec_filter(filter_filter)
helper.exec_filter(filter_filter_function)
helper.exec_filter(filter_filter_inline)
print('--')

#again, to avoid skews, AST generation, ...
helper.exec_filter(filter_for)
helper.exec_filter(filter_for_function)
helper.exec_filter(filter_filter)
helper.exec_filter(filter_filter_function)
helper.exec_filter(filter_filter_inline)

Resultados

A continuación, los resultados.

En Python 2.6, versión 2.6.8-0.2, los resultados son los siguientes:

.. code:: python

$ python2.6 ./filter.py
0.2856931686 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.5035881996 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.3672192097 - def filter_filter(v):\nreturn filter(lambda x:x<helper.LIMIT, v)
0.3303260803 - def filter_filter_function(v):\nreturn filter(helper.filterfunc, v)
0.3741078377 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn filter(inline, v)
--
0.2876720428 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.5014550686 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.3667922020 - def filter_filter(v):\nreturn filter(lambda x:x<helper.LIMIT, v)
0.3289639950 - def filter_filter_function(v):\nreturn filter(helper.filterfunc, v)
0.3746089935 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn filter(inline, v)

Como vemos, lo más eficiente es utilizar un @for@ y no llamar a ninguna función de manera interna. Sin embargo, lo menos eficiente es lo mismo, pero llamando a una función que realice el trabajo.

Sinceramente, esperaba que el "@filter@" ganara por goleada; una decepción por mi parte.

Veamos qué ocurre en python2.7, versión 2.7.3-5:

.. code:: python

$ python2.7 ./filter.py
0.2703018188 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.4657788277 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.3513321877 - def filter_filter(v):\nreturn filter(lambda x:x<helper.LIMIT, v)
0.3194220066 - def filter_filter_function(v):\nreturn filter(helper.filterfunc, v)
0.3497390747 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn filter(inline, v)
--
0.2715458870 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.4667217731 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.3523530960 - def filter_filter(v):\nreturn filter(lambda x:x<helper.LIMIT, v)
0.3194179535 - def filter_filter_function(v):\nreturn filter(helper.filterfunc, v)
0.3495810032 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn filter(inline, v)

Como vemos, los tiempos son ligeramente menores, pero más o menos se siguen observando las mismas diferencias entre implementaciones.

Veamos qué ocurre en Python 3.2, versión 3.2.3-5:

.. code:: python

$ python3.2 ./filter.py
0.2830071449 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.4660561085 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.0000162125 - def filter_filter(v):\nreturn filter(lambda x:x<helper.LIMIT, v)
0.0000150204 - def filter_filter_function(v):\nreturn filter(helper.filterfunc, v)
0.0000159740 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn filter(inline, v)
--
0.2845621109 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.4680821896 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.0000150204 - def filter_filter(v):\nreturn filter(lambda x:x<helper.LIMIT, v)
0.0000150204 - def filter_filter_function(v):\nreturn filter(helper.filterfunc, v)
0.0000140667 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn filter(inline, v)

¿¿Cómo?? ¡¡Hay 3 implementaciones que tardan 30.000 veces menos!! ¿Cómo es posible?

Bueno, vayamos desde el principio. Podemos observar que la diferencia entre python 2.6 y python 3.2 no es tan grande en las primeras funciones.

Pero... ¿Por qué tanta diferencia en las últimas implementaciones? Pues porque python 3 no devuelve el vector, sino un iterador. Dicho de otra manera: aún no ha realizado el filtro.

Así que no es una comparación justa. Modifico el código para que sean listas:

.. code:: python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/python
# -*- coding:utf-8; tab-width:4; mode:python -*-

import helper


def filter_for(v):
    r = []
    for x in v:
        if x < helper.LIMIT:
            r.append(x)
    return r

def filter_for_function(v):
    r = []

    for x in v:
        if helper.filterfunc(x):
            r.append(x)
    return r

def filter_filter(v):
    return list(filter(lambda x:x<helper.LIMIT, v))

def filter_filter_function(v):
    return list(filter(helper.filterfunc, v))

def filter_filter_inline(v):
    inline = lambda x:x < helper.LIMIT
    return list(filter(inline, v))

helper.exec_filter(filter_for)
helper.exec_filter(filter_for_function)
helper.exec_filter(filter_filter)
helper.exec_filter(filter_filter_function)
helper.exec_filter(filter_filter_inline)
print('--')

#again, to avoid skews
helper.exec_filter(filter_for)
helper.exec_filter(filter_for_function)
helper.exec_filter(filter_filter)
helper.exec_filter(filter_filter_function)
helper.exec_filter(filter_filter_inline)

Y los nuevos tiempos:

.. code:: python

$ python3.2 ./filter.py
0.2852241993 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.4744341373 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.4047920704 - def filter_filter(v):\nreturn list(filter(lambda x:x<helper.LIMIT, v))
0.3826260567 - def filter_filter_function(v):\nreturn list(filter(helper.filterfunc, v))
0.3997330666 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn list(filter(inline, v))
--
0.2863130569 - def filter_for(v):\nr = []\nfor x in v:\nif x < helper.LIMIT:\nr.append(x)\nreturn r
0.4698469639 - def filter_for_function(v):\nr = []\n\nfor x in v:\nif helper.filterfunc(x):\nr.append(x)\nreturn r
0.4008741379 - def filter_filter(v):\nreturn list(filter(lambda x:x<helper.LIMIT, v))
0.3857419491 - def filter_filter_function(v):\nreturn list(filter(helper.filterfunc, v))
0.3994090557 - def filter_filter_inline(v):\ninline = lambda x:x < helper.LIMIT\nreturn list(filter(inline, v))

Los tiempos del resto de versiones se ven ligeramente afectadas, pero no demasiado.

Conclusiones

Así que podemos decir que, para un filtrado, python3 es ligeramente más lento que los anteriores... aunque depende del uso que le demos. Pensad que si no llegáramos a necesitar recorrer todos los elementos podríamos llegar a ser hasta 30.000 veces más rápidos.

En definitiva, no se aprecia mayor diferencia al utilizar Python 3.2.

También resulta curioso comprobar que, si la regla de filtrado se encuentra en una función, nos interesa utilizar filter, mucho más que utilizar el "for" habitual.


Comentarios

Comments powered by Disqus