我在编写一段测试数据的生成代码,但我发现一个奇怪的现象
import random
import json
import tqdm
import sys
import humanize
num = 100000
test_data_num = 0
test_strings = []
print('生成随机字符串...')
for i in tqdm.tqdm(range(num * 10)):
test_strings.append(''.join(
[random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
for _ in range(random.randint(3, 10))]))
test_strings = tuple(test_strings) # 这一行
print('随机字符串生成完毕,大小为:',
humanize.naturalsize(sys.getsizeof(test_strings)))
data: list = []
print('开始生成测试数据...')
for i in tqdm.tqdm(range(num)):
test_data_str = ''.join(
[random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
for _ in range(random.randint(3, 8))])
data.append((test_data_str, {j for j in test_strings if j.startswith(test_data_str)}))
print('测试数据生成完毕,大小为:',
humanize.naturalsize(sys.getsizeof(data)))
json.dump({'num': num, 'test_strings': test_strings, 'data': data}, open(f'test_data_{test_data_num}.json', 'w'))
如果把test_strings = tuple(test_strings)
替换成test_strings = tuple(sorted(test_strings))
,那生成测试数据的部分耗时会增加为原来的2倍多(从2.5h变成了5.5h)。这是什么情况?(是生成测试代码的部分,不是排序,而且排序本身并没有消耗太多时间)
理论上来将生成部分的核心代码是{j for j in test_strings if j.startswith(test_data_str)}
,无论是排序还是不排序,时间复杂度应该都是O(n)才对啊。
对于这段代码,我有其他方式优化了,但我还是想知道这是啥情况。
替换前的测试:
生成随机字符串...
100%|██████████| 1000000/1000000 [00:02<00:00, 347291.67it/s]
随机字符串生成完毕,大小为: 8.0 MB
开始生成测试数据...
0%| | 23/100000 [00:01<2:22:16, 11.71it/s]
替换后的测试:
生成随机字符串...
100%|██████████| 1000000/1000000 [00:02<00:00, 351700.63it/s]
随机字符串生成完毕,大小为: 8.0 MB
开始生成测试数据...
0%| | 22/100000 [00:04<5:49:42, 4.76it/s]
我测试过把那一行的tuple换成list,效果一样
这是个值得研究的问题,我测试下来,这个现象和排序无关,可能和这个大型字符串数组的寻址效率有关。
只要打乱原有顺序,不论是排序的,还是随机的,都会使得遍历变慢,比如:
test_strings = sorted(test_strings)
或者random.shuffle(test_strings)
或者test_strings = random.sample(test_strings, len(test_strings))
和迭代内的操作无关
把这段代码从
改为一个空迭代:
也能明显比较出test_strings打乱原有顺序前后的效率差别
以下是我关于此例,内存寻址方面问题的一点推测,还待牛人指点:
test_strings中的字符串们,创建时即被append,所以内存地址大致是顺序的(堆上分配的对象)
CPU缓存命中
缓存从内存中抓取一般都是整个数据块,所以它的物理内存是连续的。如果打乱原有顺序遍历的话,将会使整个缓存块无法被利用,而不得不从内存中读取数据,而从内存读取速度是远远小于从缓存中读取数据的。
显然test_strings里所有的字符串跨了多个内存页了,但是如前提所言,内存地址还是大致连续的
for j in test_strings
遍历时,并没有频繁地页面调度。但sorted排序后,这种顺序没了,会有频繁的页面调度,这个操作是有时间消耗的,调度次数越多,遍历时间越长。PS: 你试一试
test_strings = list(reversed(test_strings))
参考阅读:https://zhuanlan.zhihu.com/p/549919985