NSF Back-end Dev Engineer

python代码性能测试

2023-03-21
nsf

本次测试测试的是运行速度,内存可自行分析,结果因硬件差异可能和本文结果有出入。

1.字符串拼接

测试代码

import timeit
no = 1000000
def test_add():
    a, k = '', 'k'
    for i in range(no):
        a = a + k
def test_append():
    a, k = [], 'k'
    for i in range(no):
        a.append(k)
    ''.join(a)
def test_f():
    a, k = '', 'k'
    for i in range(no):
        a = f'{a}{k}'
start = timeit.default_timer()
test_add()
elapsed = (timeit.default_timer() - start)
print(elapsed)

以下测试为简单列表展示,实际测试中比如’kkkk’这种字段也有测试,但是因为加入对实验结果没影响所以没有加入。

测试test_add()

替换k 替换a = a + k 输出
    0.15099272374363473
‘kk’   0.4066574444154053
  a + k + k 453.186699538277(和循环次数呈指数上升)

测试test_append()

替换k 替换a.append(k) 输出
    0.14970760136569225
‘kk’   0.15229560256203242
  a + k + k 0.28787823299002363

测试test_f()

替换k 替换a = f’{a}{k}’ 输出
    30.091696478065526
‘kk’   没必要了
  a + k + k 没必要了

由上可以看出,拼接次数很多建议使用List().append(),下面测试少量循环循环三个方法的性能

函数 替换n 替换k 替换表达式 输出
test_add() 100     9.433108991805958e-06
test_append() 100     1.3872219105596997e-05
test_f() 100     1.5536885398268637e-05
test_add() 10000     0.0015134591044206323
test_append() 10000     0.0011394640773337373
test_f() 10000     0.006813201691522909
test_add() 100   a + k + k + k 2.996399326808951e-05
test_append() 100   a.append(k) a.append(k) a.append(k) 3.329332585343279e-05
test_f() 100   a = f’{a}{k}{k}{k}’ 2.6079771918522353e-05

总结如下

1.循环次数(>1w)或者被拼接字符串长度较长时,建议使用List().append()

2.’+’作为官方应该做了简化,但是只适合于拼接次数少或者被拼接字符串长度短的情况

3.f’‘用法是格式化的方式,从测试来看不太好比较和’+’的效率,个人建议在循环次数不太多,但是被拼接字符较长时使用

ps:还有python不同版本的一些字符串格式化方式,和f’‘(3.6版本)差不多,如’%s %s’ % (‘hello’, ‘world’),和’{}{}’.format(‘hello’, ‘world’)

事实上,在拼接短的字面值时,由于CPython中的 常数折叠 (constant folding)功能,这些字面值会被转换成更短的形式,例如’a’+’b’+’c’ 被转换成’abc’,’hello’+’world’也会被转换成’hello world’。这种转换是在编译期完成的,而到了运行期时就不会再发生任何拼接操作,因此会加快整体计算的速度。

常数折叠优化有一个限度,它要求拼接结果的长度不超过20。所以,当拼接的最终字符串长度不超过20时,+号操作符的方式,会比后面提到的join等方式快得多,这与+号的使用次数无关。

2.min()和max()

测试代码

import timeit
n = 1000000
def test_if():
    k = 0
    for i in range(n):
        if k < i:
            k = i
def test_builtin():
    k = 0
    for i in range(n):
        k = max(i, k)
start = timeit.default_timer()
test_if()
elapsed = (timeit.default_timer() - start)
print(elapsed)

对比

n test_builtin()耗时(s) test_if()耗时(s)
10w 0.022117256026423737 0.00608655965663569
100w 0.21177082596373534 0.06097268983044029
1000w 2.12361059668427 0.611783033486066

结果一目了然,max()和min()不如if判断效率高。

注意:此测试为相同循环次数下测试结果,若取list中最值,内建函数效率更高。

3.collections Counter()

测试代码

n = 10000
ch_d = {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'f': 0, 'g': 0, 'h': 0, 'i': 0, 'j': 0, 'k': 0, 'l': 0, 'm': 0, 'n': 0, 'o': 0, 'p': 0, 'q': 0, 'r': 0, 's': 0, 't': 0, 'u': 0, 'v': 0, 'w': 0, 'x': 0, 'y': 0, 'z': 0}
s = 'oiszydfhsaugdfyyhqafgdfyyaolasdfasuiydgrdfuy'
def test_for():
    for i in range(n):
        for j in s:
            ch_d[j] += 1
def test_builtin():
    for i in range(n):
        Counter(s)
start = timeit.default_timer()
test_for()
elapsed = (timeit.default_timer() - start)
print(elapsed)

直接上结论,查看源码得知,Counter()做了类型判断,参数判断等许多工作,核心调用_count_elements(from _collections import _count_elements)

由上可知: iterable(代码中的s)如果较长,则test_builtin()效率较高,否则自己实现test_for()效率较高,原因猜测是Counter()做的额外工作太多,调用_count_elements效率最高,原因是该函数是内建函数。

综上,如果极限追求效率,使用内建_count_elements函数,但是要注意自己做异常判断。

4.split和find

测试代码

import functools
import time

def cost_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        t_s = time.time()
        f = func(*args, **kw)
        print(time.time() - t_s)
        return f
    return wrapper
    
@cost_time
def test_split_one(s):
    s = s.split('1', 1)

@cost_time
def test_find_one(s):
    index = s.find('1')
    s0, s1 = s[: index], s[index + 1:]

n = '0584kldsr' * 50000000 + '1' + '0584kldsr' * 50000000
test_find_one(n)
test_split_one(n)

测试结果有浮动,基本是保持在0.39(test_find_one)和0.57(test_split_one)左右水平,能明显看出还是有效率上的差异。

应用情景:查找字符串中某个字符第一次或者最后一次出现的位置或者根据位置切分。

注意:如果查找的是字符串时,随着字符串长度增加效率会相差不大,比如查找的字符串为“56789”时,耗时为0.52(test_find_one)和0.52(test_split_one)。

5.list转set

测试代码

import time

def test_list2set(n):
    l = [i for i in range(n)]
    t1 = time.time()
    s = set(l)
    print(f'test no: {n!s}, cost time: ', time.time() - t1)

test_list2set(1000)
test_list2set(10000)
test_list2set(100000)
test_list2set(1000000)
test_list2set(10000000)
输出如下:
test no: 1000, cost time:  2.574920654296875e-05
test no: 10000, cost time:  0.00033473968505859375
test no: 100000, cost time:  0.003763437271118164
test no: 1000000, cost time:  0.037119150161743164
test no: 10000000, cost time:  0.2964916229248047

易知转换时间和列表长度成正比。

6.count()

测试代码

import random

def f():
    n = 10000000
    l = []
    for i in range(n):
        l.append(random.randint(0, 20))
    t1 = time.time()
    no = 0
    for i in l:
        if i == 9:
            no += 1
    t2 = time.time()
    print(no, t2 - t1)
    no = l.count(9)
    t3 = time.time()
    print(no, t3 - t2)


if __name__ == '__main__':
    f()
    
输出:
475421 0.19590401649475098
475421 0.10589742660522461

总结:在可迭代对象中获取某元素出现次数时,建议使用count()。

7.遍历

测试代码

import time
import random

def f():
    n = 10000000
    l = []
    for i in range(n):
        l.append(random.randint(0, 20))
    t1 = time.time()
    no = 0
    for i in range(len(l)):
        pass
    t2 = time.time()
    print(no, t2 - t1)
    no = 0
    for i in l:
        pass
    # no = l.count(9)
    t3 = time.time()
    print(no, t3 - t2)


if __name__ == '__main__':
    f()
输出如下:
0 0.14916729927062988
0 0.05500936508178711

总结:

在遍历可迭代对象时,如果不需要使用下标,建议直接遍历元素本身。

8.list添加元素

测试代码

import time


def f1(c):
    l = []
    for i in range(c):
        l.append(str(i))
        l.append(str(i))


def f2(c):
    l = []
    for i in range(c):
        l.extend([str(i), str(i), ])


def f4(c):
    l = []
    for i in range(c):
        l += [str(i), str(i), ]


def f3(c):
    l = []
    for i in range(c):
        for _ in range(2):
            l.append(str(i))


c = 10000000
t1 = time.time()
n1 = f1(c)
t2 = time.time()
print(t2 - t1, n1)
n2 = f2(c)
t3 = time.time()
print(t3 - t2, n2)
n3 = f3(c)
t4 = time.time()
print(t4 - t3, n3)
n4 = f4(c)
t5 = time.time()
print(t5 - t4, n4)

输出如下:
4.281217098236084 None
4.407070875167847 None
5.6496241092681885 None
4.208817481994629 None

总结:

list需要添加较多元素时,使用集合合并效率最高,能不使用循环尽量不使用循环,易知集合合并效率和一次性添加元素数量成正比。

其中集合合并操作’+’比extend效率高

9.math.pow和pow和**

from timeit import timeit
from math import pow as m_pow

print(10 ** 32 * 1024)
print(int(pow(10, 32) * 1024))
print(int(m_pow(10, 32) * 1024))

print(timeit('10**50', number=10000))
print(timeit('pow(10, 50)', number=10000))
print(timeit('m_pow(10, 50)', 'from math import pow as m_pow', number=10000))
输出:
102400000000000000000000000000000000
102400000000000000000000000000000000
102400000000000005494950097298915328
0.0042372000170871615
0.0041748000076040626
0.002088800014462322

总结:速度:math.pow>pow=**,但是要注意pow会将底数和指数变为float,在结果比较大的时候会出现精度丢失


Similar Posts

下一篇 python多任务

Comments