V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Buffer2Disk
V2EX  ›  程序员

Python 和 Go 在循环时候的性能对比

  •  1
     
  •   Buffer2Disk · 2019-07-16 23:07:22 +08:00 · 7383 次点击
    这是一个创建于 1960 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题

    两个 for 循环分别 6000 次,来嵌套; 中间休息 5 秒钟;

    python 代码如下(版本 2.7 )

    333.png

    golang 的代码如下(版本 1.12)

    111.png

    python 在循环的时候,cpu 消耗 70%

    golang 在循环的时候,cpu 消耗 0.6%、

    这 2 个仅仅是简单的循环,性能差别就有这么大么,还是我姿势哪里不对?

    第 1 条附言  ·  2019-07-16 23:46:38 +08:00
    看网上说,python 那边,for 循环内部加个 sleep(0.1) , cpu 就降下来了,但是这样的话就牺牲了性能了。。。
    第 2 条附言  ·  2019-07-16 23:51:11 +08:00
    其实原本是这样的,两个 list 互相嵌套循环,然后判断内部的那个 list 的每个元素 是否 在外部的 list 中存在,

    然后发现 python 情景下的性能特别低,最后就抽象成了上面两个 for 循环来对比
    第 3 条附言  ·  2019-07-17 00:56:40 +08:00
    本帖是以探讨问题为主,不是专门为了比较语言优劣势哈
    第 4 条附言  ·  2019-07-17 01:33:16 +08:00
    有人说到是 range 的问题,更新一下 python 的代码,提前组装好 list

    <img src="https://i.loli.net/2019/07/17/5d2e09582d18548030.png" alt="5555.png" title="5555.png" />
    第 5 条附言  ·  2019-07-17 11:17:47 +08:00
    有人说是 go 在编译时候,内部对这种 空循环 做过了优化
    我在 go 的 for 循环里面加了 println 打印后,cpu 确实有变化了,大概 20%的占用,还是没有 python 那么的夸张

    所以 python 的这种大数循环有没有办法优化呢
    第 6 条附言  ·  2019-07-17 12:27:14 +08:00
    目前找到了一个办法
    因为我的需求其实本来是算 2 个 list 的差集,然后抽象成了上面的 2 个嵌套的 for 循环

    所以像下面这么写就可以了

    c = set(a).difference(b) //差集 a-b

    看了下 top,两个大的 list(各 6000 个元素)算差集,cpu 占用不到 4%

    但是我也没太明白这么写为啥就性能提升了,可能是实现方式上和单纯的 for 循环有区别。。。。
    有没有大牛解释下。。。
    第 7 条附言  ·  2019-07-17 12:42:49 +08:00
    网上说用 set 来处理差集比 list 更快,我没太懂这句话的意思,

    是因为 set 去除掉了 list 中的重复元素导致变快还是什么其他原因
    第 8 条附言  ·  2019-07-17 12:47:20 +08:00
    我猜测是因为 set 不需要排序 + 外加去除了重复元素

    所以会比 list 算差集的时候更快
    第 9 条附言  ·  2019-07-17 12:47:50 +08:00
    我猜测是因为 set 不需要排序 + 外加去除了重复元素

    所以会比 list 算差集的时候更快
    第 10 条附言  ·  2019-07-17 12:48:45 +08:00
    晕,v2 最近这么卡,连发了 2 条。。。
    54 条回复    2019-07-17 23:29:00 +08:00
    ysc3839
        1
    ysc3839  
       2019-07-16 23:29:16 +08:00
    你是如何测量 “ cpu 消耗” 的呢?
    Buffer2Disk
        2
    Buffer2Disk  
    OP
       2019-07-16 23:39:47 +08:00
    @ysc3839 看 top 啊
    shakespaces
        3
    shakespaces  
       2019-07-16 23:43:53 +08:00 via Android
    其实 python 里面是两个 5999 次😂,虽然和性能没关系
    moodasmood
        4
    moodasmood  
       2019-07-16 23:45:30 +08:00 via Android
    我虽然说不出哪里不对,到直觉告诉我,这他妈明显有问题啊。至于哪里有问题,请楼下解答
    niubee1
        5
    niubee1  
       2019-07-17 00:00:56 +08:00   ❤️ 1
    for num in xrange(1, 100):
    [(lambda _:time.sleep(5))([j for j in xrange(1, 6000)]) for i in xrange(1, 6000)]

    试试嘛
    niubee1
        6
    niubee1  
       2019-07-17 00:04:41 +08:00
    事实上你用 xrange 替换 range 就能让 CPU 开销降到 20%, 用列表解析, 就是我这个就只有 0 附近了。开不开心, 意不意外?
    neoblackcap
        7
    neoblackcap  
       2019-07-17 00:10:08 +08:00
    range 返回列表,耗时在这
    Buffer2Disk
        8
    Buffer2Disk  
    OP
       2019-07-17 00:13:10 +08:00
    @niubee1 我要循环的其实是 2 个 list,只不过抽象成了上面的 range,

    list 有办法优化么。。。。
    Buffer2Disk
        9
    Buffer2Disk  
    OP
       2019-07-17 00:15:07 +08:00
    @neoblackcap 对,但是我要循环的,其实就是列表
    Buffer2Disk
        10
    Buffer2Disk  
    OP
       2019-07-17 00:19:16 +08:00
    @neoblackcap 你的意思是,range 返回列表的时候,是消耗 cpu 的主要部分嘛?

    我怎么感觉是循环的时候消耗的,我另外一份代码循环的时候,是直接 2 个 list 来嵌套循环的,没有 range

    然后加了 sleep(0.1) ,cpu 就降价来了,但是这样的话,循环的耗时就大大增加了
    niubee1
        11
    niubee1  
       2019-07-17 00:21:09 +08:00
    你是要判断一个列表里的内容是不是存在与另外一个列表嘛, 设有列表 l1, l2
    for i in [idx for idx, flag in enumerate([ i in l2 for i in l1]) if flag]:
    print "列表 l1 中 %s 位的元素在列表 l2 中存在......"
    Buffer2Disk
        12
    Buffer2Disk  
    OP
       2019-07-17 00:24:58 +08:00
    @niubee1 我看你这个是用 in 和 not in 来判断元素是否在 list 存在是嘛?

    这个我试过了,cpu 消耗依然很高,估计 in 和 not in 的内部实现也是通过这种循环来实现的
    fy
        13
    fy  
       2019-07-17 00:26:20 +08:00   ❤️ 1
    看到 2.7,吓得我赶紧看看日历,怀疑自己穿越了
    niubee1
        14
    niubee1  
       2019-07-17 00:27:25 +08:00   ❤️ 2
    你也可以使用 itertools.product

    要高效循环, 肯定要用到 itertools, 你基础库都不搞明白就出来浪, 这样子不好
    neoblackcap
        15
    neoblackcap  
       2019-07-17 00:31:32 +08:00   ❤️ 1
    @Buffer2Disk 你每次调用 range 都要申请内存,创建一个列表啊,能快才奇怪。你用 top 来计算 CPU 耗时的方法本身就不对。
    而且你只是判断一个元素是否在另外一个列表里面,你转成集合,用求交集的方法啊。那个才 O(n),你现在这个粗糙的实现方式可是 O(n^2)。
    你觉得是循环在耗时,那么请你在 Python 里面创建好两个列表,然后再开始你的循环计时,而且停 5s 有什么用啊?你写日志不好么?
    raysonx
        16
    raysonx  
       2019-07-17 00:32:07 +08:00 via iPad
    Go 是编译型语言。内部的两层循环没有动作默认会被编译器优化掉
    guog
        17
    guog  
       2019-07-17 00:36:17 +08:00 via Android
    这都不是一个层次的对比,如果仅仅对比循环,你完全可以用 while 啊,不创建任何 list,CPU 也是零。
    aheadlead
        18
    aheadlead  
       2019-07-17 00:39:27 +08:00 via iPhone   ❤️ 1
    上次看到这么比较语言性能的,还是在易语言的官网
    niubee1
        19
    niubee1  
       2019-07-17 00:40:14 +08:00
    之前的例子不是很恰当, 重新写了个
    两个长度 6000 的列表对比元素
    Buffer2Disk
        20
    Buffer2Disk  
    OP
       2019-07-17 00:55:00 +08:00
    @niubee1 好的,谢谢大佬,我研究研究
    Buffer2Disk
        21
    Buffer2Disk  
    OP
       2019-07-17 01:26:55 +08:00
    @niubee1 大佬,我的关注点其实是在 cpu 和 运行耗时 一起的

    我试了下你的代码

    排除掉了 range 的因素,提前创建好 list,cpu 的消耗依然不低啊(相对于 go 的 cpu 占用来说,使用的是 1 核的虚拟机)

    当然我是通过 top 粗略的看了下 python 进程的 cpu 占用,可能不是那么的精确,但是能侧面反映出来大概的占用情况
    Buffer2Disk
        22
    Buffer2Disk  
    OP
       2019-07-17 01:29:04 +08:00
    <img src="https://i.loli.net/2019/07/17/5d2e09582d18548030.png" alt="5555.png" title="5555.png" />
    iPhoneXI
        23
    iPhoneXI  
       2019-07-17 01:29:47 +08:00 via Android
    为什么用 2.7 这种老古董?
    niubee1
        24
    niubee1  
       2019-07-17 01:42:25 +08:00
    @Buffer2Disk for 没有优化的, 提速用列表解析或者用 itertools 里的函数代替
    niubee1
        25
    niubee1  
       2019-07-17 01:44:01 +08:00
    当然要说优化了就秒天秒地秒空气了那不可能,Python 确实不快, 只是优化好了并不会那么的慢, 而已。还有个办法是用 Cyhon 来优化这种纯跑圈的逻辑
    blless
        26
    blless  
       2019-07-17 01:46:58 +08:00 via Android
    解释型语言跟编译型语言 这种对比完全没意义,编译型语言很多预编译的时候会优化一些语句,比如内联优化之类的…你的 go 的空循环可能就被优化成一个连续执行的 cpu 指令了 基本上 0 切换。不过现在晚了 也没空测试,我只是大概猜一下可能性,有兴趣可以自己找找资料
    blless
        27
    blless  
       2019-07-17 01:50:51 +08:00 via Android
    要测试的话,我建议 for 循环里面加一些额外业务处理,然后内联优化应该可以关闭
    真的想用 python 可以试试 pypy,自带 jit,直接用 docker 跑一个就可以
    ysc3839
        28
    ysc3839  
       2019-07-17 02:33:26 +08:00
    @Buffer2Disk 看 top 的话你怎么知道你看的时刻程序是不是在 sleep ?你给出的结果完全有可能是 Python 在循环而 Golang 在 sleep。
    ysc3839
        29
    ysc3839  
       2019-07-17 02:34:54 +08:00
    @Buffer2Disk 另外看到附言说是要解决性能问题,那应该测量代码的执行时间。
    ysc3839
        30
    ysc3839  
       2019-07-17 02:38:58 +08:00
    @Buffer2Disk 另外你所说的 CPU 消耗是 CPU 使用率,意思是一段时间内 CPU 非空闲的时间占这段时间的百分比。大部分工具是隔几秒测一下 CPU 使用率,没办法估量全部代码的执行效率。
    LokiSharp
        31
    LokiSharp  
       2019-07-17 08:34:27 +08:00
    Go 没试过,我只知道 Python 整体比 Java 慢 10 倍
    www5070504
        32
    www5070504  
       2019-07-17 09:14:26 +08:00
    猜测是 golang 可能优化到操作高速缓存或者寄存器了 python 你这样写肯定有频繁读或者写内存
    xdeng
        33
    xdeng  
       2019-07-17 09:23:03 +08:00
    go for 里面没写东西可能会被优化掉的
    dongxiaozhuo
        34
    dongxiaozhuo  
       2019-07-17 10:31:05 +08:00 via iPhone
    如果以 Golang 的写法为基准,应该是 Python 里面使用 while 判断一个变量和一个数字的比较结果,例如 while i < 6000: 这样,不管使用 range 和 xrange 都是一堆数字的合集,只是类型与方式不同。
    ipwx
        35
    ipwx  
       2019-07-17 11:59:13 +08:00   ❤️ 5
    Python 的 for 循环性能是不可优化的,别想了,不可能的。

    去考虑如何优化 Python 的 for 循环是没有意义的,因为一个老道的 Python 程序员,不是在 sentence-level 优化 Python 程序性能的。

    比如我要算向量 x + y ( for 循环裸写是 O(N)),我们会直接使用 NumPy (它是用 C 语言写的 Python 库):

    import numpy as np

    x = np.asarray(...)
    y = np.asarray(...)
    out = x + y

    矩阵点积( for 循环裸写是 O(N^2)):

    M = np.asarray(...)
    N = np.asarray(...)
    out = np.dot(M, N)

    如果是可以转化成这种张量运算的,就尝试转化一下。如果不能,就上 Cython。比如 Scikit Learn 的 Tree Classifier 系列代码:

    https://github.com/scikit-learn/scikit-learn/blob/7813f7efb5b2012412888b69e73d76f2df2b50b6/sklearn/tree/_tree.pyx

    如果 Cython 再不行,就上 C/C++ 语言。比如 TensorFlow 一坨运算:

    https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/ops/

    有时候偷懒,会用 JIT 引擎把 Python 代码直接在内存中即时编译成机器码,比如:

    Numba: https://numba.pydata.org/
    Pytorch: https://pytorch.org/docs/stable/_modules/torch/jit.html
    TensorFlow: https://www.tensorflow.org/xla/jit
    ----

    以上所有总而言之,Python 的优化方法不是去一条一条语句优化,而是直接暴力把整个 block 替换成更高效的实现。所以你考虑优化 python for 循环没有意义。作为一个 Python 程序员,你应该先考虑实现业务逻辑,然后找出真正的性能瓶颈,再考虑用高级手段(以上种种)优化它们。
    ipwx
        36
    ipwx  
       2019-07-17 12:00:50 +08:00
    顺便 Cython 是一种特别发明出来的 C/Python 混合语言,即没有完全的 C 语言功能,也没有完全的 Python 功能,会经过编译器编译成本机代码。适用于 Python 写起来太慢,但是又不需要 C 语言全部功能的场景。
    ipwx
        37
    ipwx  
       2019-07-17 12:01:08 +08:00
    Python 写起来太慢 -> Python 写的代码跑起来太慢。
    ipwx
        38
    ipwx  
       2019-07-17 12:04:52 +08:00
    顺便 Python JIT 还有一个 PyPy,适用于网络编程。
    crella
        39
    crella  
       2019-07-17 12:32:44 +08:00 via Android
    你应该在最内层循环加个判断或者赋值,比如 if 1 = true ; a = 1,要不然哪家语言都会优化掉。
    crella
        40
    crella  
       2019-07-17 12:39:13 +08:00 via Android
    反正 g4560,我做文本按列切割整理保存的工作,单核,perl6 1500 行 /s,perl5 2300 行 /s,vb.net3.5 3300 行 /s,不会用 python
    crella
        41
    crella  
       2019-07-17 12:40:04 +08:00 via Android
    都是读取一行,按 tab 取列,修改文字,io 写入一行
    Buffer2Disk
        42
    Buffer2Disk  
    OP
       2019-07-17 13:07:24 +08:00
    @neoblackcap 确实,算差集的时间复杂度要小很多
    shuax
        43
    shuax  
       2019-07-17 13:28:37 +08:00
    用 sleep 测试效率,学到了。
    SeaRecluse
        44
    SeaRecluse  
       2019-07-17 14:12:50 +08:00
    python3.6 未复现
    占用大概 8%
    encro
        45
    encro  
       2019-07-17 14:23:20 +08:00
    python 每次 for 循环都重新 range 生成列表,而 golang 没有,这明显不是同样的对比啊。
    encro
        46
    encro  
       2019-07-17 14:24:42 +08:00
    这种比 cpu 没有意义,使用 time 命令比总执行时间还好点。
    mengzhuo
        47
    mengzhuo  
       2019-07-17 14:25:51 +08:00
    空的 For 循环会被 Go 的 SSA 干掉的
    Buffer2Disk
        48
    Buffer2Disk  
    OP
       2019-07-17 15:13:00 +08:00
    @neoblackcap 不过有个疑问就是,为什么 Python 计算集合的差集的时候时间复杂度是 O(n)呢? python 计算差集原理我不太了解

    因为 golang 的官方并没有提供计算差集的库,我去网上看了一些第三方 golang 库的源码,实际上也是通过嵌套循环来实

    现集合计算差集的,并且这种嵌套循环在数据量比较大的时候,cpu 占用也并不是太高
    neoblackcap
        49
    neoblackcap  
       2019-07-17 15:43:09 +08:00
    @Buffer2Disk
    简单的理解就是只是用其中迭代其中一个集合,然后判断一下这个元素是否在,每次查询是否在集合的操作是 O(1),n 个就是 O(n)
    Buffer2Disk
        50
    Buffer2Disk  
    OP
       2019-07-17 17:07:30 +08:00
    @neoblackcap 网上那些 golang 的第三方库, 判断一下这个元素是否在 的这个操作(contains),内部实现也是一个循环,所以实际上也就变成了 2 个循环在叠代

    python 我不清楚是怎么个实现法儿的
    Buffer2Disk
        51
    Buffer2Disk  
    OP
       2019-07-17 18:00:17 +08:00
    @neoblackcap
    我尝试了下,golang 里面用 map 这种哈希结构的集合来存储数据,它判断元素是否存在的时间复杂度也是 O(1)
    使用之后,cpu 的消耗 比 单纯的用 for 循环来迭代降下来不少

    所以伪代码就是这样
    for i in list {
    if map[i] != null {
    //存在
    }
    }

    两个 6000 个元素的集合情况下
    map 时间复杂度 O(n) , cpu 消耗 1%
    for 循环迭代 O(n^2) ,cpu 消耗 4%

    在元素比较多的情况,map 这种能够快速检索 key 的结构还是有优势的
    neoblackcap
        52
    neoblackcap  
       2019-07-17 18:27:04 +08:00 via iPhone
    @Buffer2Disk 本身集合一般就是用哈希表实现
    Buffer2Disk
        53
    Buffer2Disk  
    OP
       2019-07-17 18:31:02 +08:00
    @neoblackcap 没有啊,golang 里面的 list 就是双向链表实现的 = =
    way2create
        54
    way2create  
       2019-07-17 23:29:00 +08:00
    看见这个帖子...我想起曾经某语言某性能网站上的代码拿到本地一测,调换下测试顺序,结果完全不同了...所以严重怀疑那个网站测试的真实性
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1134 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 18:56 · PVG 02:56 · LAX 10:56 · JFK 13:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.