Python性能处理二进制文件

4

我正在尝试将YCbCr-file从8 bpp转换为10 bpp。

到目前为止,我最好的方法仍然比最基本的naive C实现慢一个数量级。

C中的naive方法大约需要8秒。将代码分块处理后,时间降至不到1秒。

我很想知道使用标准Python处理二进制文件可以获得什么样的性能。示例文件在CIF-resolution中,与1080p内容相比较“小”。也可以提供numpy建议,但我主要关注标准Python。

测试文件可从以下位置下载

http://trace.eas.asu.edu/yuv/foreman/foreman_cif.7z

sha1sum 的正确 10 位输出是:

c511dabc793383f7fd0ed69b4bb9b9f89ef73b84

Python:

#!/usr/bin/env python

import array

f_in = 'foreman_cif.yuv'
f_out = 'py_10bpp.yuv'

def bytesfromfile(f):
    while True:
        raw = array.array('B')
        raw.fromstring(f.read(8192))
        if not raw:
            break
        yield raw

with open(f_in, 'rb') as fd_in, \
        open(f_out, 'wb') as fd_out:

    for byte in bytesfromfile(fd_in):
        data = []
        for i in byte:
            i <<= 2
            data.append(i & 0xff)
            data.append((i >> 8) & 0xff)

        fd_out.write(array.array('B', data).tostring())

天真的 C-dito:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    int c;
    int d[2];

    FILE* fd_in;
    FILE* fd_out;

    fd_in = fopen("foreman_cif.yuv", "rb");
    fd_out = fopen("c_10bpp.yuv", "wb");

    while((c = fgetc(fd_in)) != EOF) {
        c <<= 2;
        d[0] = c & 0xff;
        d[1] = (c >> 8) & 0xff;

        fwrite(&d[0], 1, 1, fd_out);
        fwrite(&d[1], 1, 1, fd_out);
    }

    fclose(fd_in);
    fclose(fd_out);

    return EXIT_SUCCESS;
}

1
值得一提的是,在CPython中运行此代码需要33秒,而在(未修改的)PyPy中只需7秒,而在C中则约为3秒。 - DSM
在这个领域,CPython 大约需要 20 秒,而使用 -O2 编译时 C 则需要大约 2.5 秒。首先我在没有连接电源线的情况下进行了测量,这导致我的核心以 800MHz 运行。"数量级"就是从这里来的。当连接电源线并且核心以 2.2GHz 运行时,我得到了上面的数字。 - Fredrik Pihl
1个回答

5

这个问题的代码在我的电脑上需要 25 秒,而使用 numpy 只需要 0.37 秒:

import numpy as np

a_in = np.memmap('foreman_cif.yuv', mode='readonly')
a_out = np.memmap('py_10bpp.yuv', mode='write', shape=2*len(a_in))
a_out[::2] = a_in << 2
a_out[1::2] = a_in >> 6

cython -- 0.20 seconds:

from functools import partial

import pyximport; pyximport.install() # pip install cython
from bpp8to10 import convert # bpp8to10.pyx

f_in = 'foreman_cif.yuv'
f_out = 'py_10bpp.yuv'

def main():
    with open(f_in, 'rb') as fd_in, open(f_out, 'wb') as fd_out:
        for chunk in iter(partial(fd_in.read, 8192), b''):
            fd_out.write(convert(chunk))
main()

bpp8to10.pyx在哪里:

from cpython.bytes cimport PyBytes_FromStringAndSize

def convert(bytes chunk not None):
    cdef:
        bytes data = PyBytes_FromStringAndSize(NULL, len(chunk)*2)
        char* buf = data # no copy
        Py_ssize_t j = 0
        unsigned char c
    for c in chunk:
        buf[j] = (c << 2) 
        buf[j + 1] = (c >> 6)
        j += 2
    return data

在纯CPython版本中,主要的加速来自将代码从模块级别移动到一个函数(main())—— 6.7 秒(2个CPU):

from functools import partial
from multiprocessing import Pool

f_in = 'foreman_cif.yuv'
f_out = 'py_10bpp.yuv'

def convert(chunk):
    data = bytearray() # [] -> bytearray(): 17 -> 15 seconds
    data_append = data.append # 15 -> 12  seconds
    for b in bytearray(chunk): # on Python 3: `for b in chunk:`
        data_append((b << 2) & 0xff)
        data_append((b >> 8) & 0xff)
    return data

def main(): # put in main(): # 25 -> 17 seconds
    pool = Pool(processes=2) # 12 -> 6.7 seconds
    with open(f_in, 'rb') as fd_in, open(f_out, 'wb') as fd_out:
        for data in pool.imap(convert, iter(partial(fd_in.read, 8192), b'')):
            fd_out.write(data)
main()

pypy -- 1.6 seconds:

f_in = 'foreman_cif.yuv'
f_out = 'py_10bpp.yuv'

def convert(chunk):
    data = bytearray() # 1.6 -> 1.5 seconds for preallocated data
    for b in bytearray(chunk): 
        data.append((b << 2) & 0xff)
        data.append((b >> 6) & 0xff)
    return data

with open(f_in, 'rb') as fd_in, open(f_out, 'wb') as fd_out:
    while True:
        chunk = fd_in.read(8192)
        if not chunk:
            break
        fd_out.write(convert(chunk))

这看起来非常有前途。我印象深刻!谢谢。 - Fredrik Pihl

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