为什么我的任务在Python3上比Python2慢得多?

6

我很惊讶地得知,Python 3.5.2Python 2.7.12慢得多。我写了一个简单的命令行指令,用于计算一个巨大CSV文件中的行数。

$ cat huge.csv | python -c "import sys; print(sum(1 for _ in sys.stdin))"
101253515
# it took 15 seconds

$ cat huge.csv | python3 -c "import sys; print(sum(1 for _ in sys.stdin))"
101253515
# it took 66 seconds

Python 2.7.12用了15秒,Python 3.5.2用了66秒。我预料到差别会存在,但为什么差距如此之大呢?在Python 3中有什么新内容使其对这种类型的任务变得更慢?有没有更快的方法在Python 3中计算行数?
我的CPU是Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz。
huge.csv的大小为18.1 Gb,包含101253515行。
问这个问题时,我不需要不惜一切代价找到一个大文件的确切行数。我只是写了一个特殊情况,Python 3中速度要慢得多。实际上,我正在开发一个Python 3脚本,处理大型CSV文件,其中一些操作不需要使用csv库。我知道,我可以用Python 2编写脚本,并且速度也可以接受。但我想知道如何以“诚实”的Python方式编写类似的脚本,以及Python 3中是什么使得它在我的示例中变慢,以及如何通过“诚实”的Python方法进行改进。

1
@CoryKramer 这并没有解释Python版本之间的区别。 - Fomalhaut
这并不回答你的问题,但为什么不使用wc而不是Python呢? cat huge.csv | wc -lwc -l huge.csv - mhawke
@mhawke 因为我想把它作为我的Python脚本的一部分。 - Fomalhaut
@mhawke,我的文件很大。我已经更新了我的问题。请看一下。 - Fomalhaut
@Fomalhaut: 或许 pandas 会更快?或者将文件读入数据库中一次。添加相关索引。使用聚合函数进行查询。 - mhawke
显示剩余9条评论
1个回答

6

sys.stdin对象在Python3中比Python2中更加复杂。例如,默认情况下从sys.stdin读取输入会将其转换为Unicode,因此对于非Unicode字节会失败:

$ echo -e "\xf8" | python3 -c "import sys; print(sum(1 for _ in sys.stdin))"

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 1, in <genexpr>
  File "/usr/lib/python3.5/codecs.py", line 321, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf8 in position 0: invalid start byte

请注意,Python2对该输入没有任何问题。因此,您可以看到Python3的sys.stdin在幕后执行更多操作。我不确定这是否完全负责性能损失,但您可以通过在Python3下尝试sys.stdin.buffer来进一步调查。
import sys
print(sum(1 for _ in sys.stdin.buffer))

请注意,在Python2中不存在.buffer。我已经进行了一些测试,发现Python2的sys.stdin和Python3的sys.stdin.buffer在性能上没有实质区别,但可能因系统而异。 编辑 这里是我机器上的一些随机结果:Ubuntu 16.04,i7 CPU,8GiB RAM。首先是一些C代码(作为比较基准):
#include <unistd.h>

int main() {
    char buffer[4096];
    size_t total = 0;
    while (true) {
        int result = ::read(STDIN_FILENO, buffer, sizeof(buffer));
        total += result;
        if (result == 0) {
            break;
        }
    }
    return 0;
};

现在的文件大小为:

$ ls -s --block-size=M | grep huge2.txt 
10898M huge2.txt

和测试:

// a.out is a simple C equivalent code (except for the final print)
$ time cat huge2.txt | ./a.out

real    0m20.607s
user    0m0.236s
sys     0m10.600s


$ time cat huge2.txt | python -c "import sys; print(sum(1 for _ in sys.stdin))"
898773889

real    1m24.268s
user    1m20.216s
sys     0m8.724s


$ time cat huge2.txt | python3 -c "import sys; print(sum(1 for _ in sys.stdin.buffer))"
898773889

real    1m19.734s
user    1m14.432s
sys     0m11.940s


$ time cat huge2.txt | python3 -c "import sys; print(sum(1 for _ in sys.stdin))"
898773889

real    2m0.326s
user    1m56.148s
sys     0m9.876s

所以我使用的文件要小一些, 时间会更长(看起来你的机器更好, 而我没有耐心处理更大的文件 :D)。无论如何,根据我的测试,Python2和Python3的sys.stdin.buffer非常相似。Python3的sys.stdin速度要慢得多。但是它们都远远落后于C代码(几乎没有用户时间)。


谢谢。我检查了一下,使用.buffer后只需要23秒,比没有使用要快得多。但是它比Python 2慢1.5倍。 - Fomalhaut
1
@Fomalhaut 你做了多次测试并取平均值吗?你从磁盘中读取数据并将其传输到进程中。当我在我的机器上运行此代码时,结果会有所不同:一次Python2更快,第二次Python3更快。这是由操作系统(和其他进程)产生的噪声造成的。但平均而言它们非常接近(Python2稍微快一点,大约2-3%,不多)。如果在您的情况下它们不是这样,那么我想这就是事实:在您的设置中,Python3只是较慢。 - freakish
我仔细测量了多次消耗的时间,并取平均数。你可能是对的,我的Python 2设置比较慢。我认为15秒和23秒是可以相互比较的。 - Fomalhaut
@freakish 这并不是计算行数,而只是计算大小。你需要在缓冲区上进行for循环并计算'\n'的数量。我想编辑你的答案,但它有一个基准测试,所以我不能编辑(因为它会影响输出)。最好的基准测试方法可能是包括一个最终的printf,这样它实际上与Python的输入/输出相同,并且您会注意到逻辑错误。 - Nicholas Pipitone
@NicholasPipitone,我为什么要计算行数?OP的代码并没有这样做。 - freakish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接