MATLAB比Python快吗?(一个简单的实验)

5

我已经阅读了这篇文章(MATLAB比Python更快吗?),但我发现它有很多条件语句。

我在一台仍在运行Windows XP的旧计算机上尝试了这个小实验。

在MATLAB R2010b中,我已将以下代码复制并粘贴到命令窗口中:

tic
x = 0.23;
for i = 1:100000000
  x = 4 * x * (1 - x);
end
toc
x

结果是:

Elapsed time is 0.603583 seconds.

x =

    0.947347510922557

然后我保存了一个包含以下脚本的py文件:
import time
t = time.time()
x = 0.23
for i in range(100000000): x = 4 * x * (1 - x)
elapsed = time.time() - t
print(elapsed)
print(x)

我按下了 F5,结果是

49.78125
0.9473475109225565

在MATLAB中,它只需0.60秒; 而在Python中,需要49.78秒(一段漫长的时间!)。
因此问题是:是否有简单的方法使Python与MATLAB一样快?
具体来说:我如何更改我的py脚本以使其像MATLAB一样快?

更新

我已经在同一台机器上尝试了在PyPy中进行相同实验(复制并粘贴与上述相同的代码):它在1.0470001697540283秒内完成。

我重复了1e9个循环的实验。

MATLAB结果:

Elapsed time is 5.599789 seconds.
1.643573442831396e-004

PyPy结果:

8.609999895095825
0.00016435734428313955

我还尝试了普通的while循环,结果类似:

t = time.time()
x = 0.23
i = 0
while (i < 1000000000):
    x = 4 * x * (1 - x)
    i += 1

elapsed = time.time() - t
elapsed
x

结果:

8.218999862670898
0.00016435734428313955

我马上要尝试使用NumPy


2
(1) 使用NumPy数组代替循环。 (2) 使用PyPy代替CPython。 (3) 手动将计算提取出循环,因为它是静态的,然后你就可以消除循环。 :) - abarnert
2
Python2?如果是,我首先会将range更改为xrange()。 - Łukasz Rogalski
1
你读了你所链接的问题吗?因为它讨论了如何提高Python的性能... - poke
这个问题有点毫无意义,因为你还没有优化代码。除非你是两种语言的性能专家,否则你不适合进行比较。 - David Heffernan
2
至少有三个人现在提到了range。首先,这看起来像是Python 3的代码(他正在使用Python 3的print语法)。其次,分配那个列表只需要几毫秒的时间;优化它是错误的目标,除非他实际上遇到了空间问题。 - abarnert
3个回答

11

首先,使用time不是测试此类代码的好方法。但我们忽略这一点。


当您有大量循环并且每次循环都执行非常相似的工作时,PyPy的JIT效果非常好。当该代码每次执行完全相同的操作,并且针对可以提前移出循环的常量值执行时,它会做得更好。另一方面,CPython必须为每个循环迭代执行多个字节码,因此它会变慢。从我的机器上进行快速测试,CPython 3.4.1需要24.2秒,但PyPy 2.4.0/3.2.5只需要0.0059秒。

IronPython和Jython也都是JIT编译的(尽管使用了更通用的JVM和.NET JIT),因此对于这种工作它们通常比CPython更快。


您还可以通过使用NumPy数组和向量操作而不是Python列表和循环来加速CPython自身的此类工作。例如,以下代码只需要0.011秒:

i = np.arange(10000000)
i[:] = 4 * x * (1-x)

当然,在那种情况下,我们明确地只计算一次值,然后将其复制10000000次。但是,我们可以强制它反复计算,而且仍然只需要0.12秒:

i = np.zeros((10000000,))
i = 4 * (x+i) * (1-(x+i))
其他选项包括使用Cython编写部分代码(它会编译成Python的C扩展),以及使用Numba,它可以在CPython内对代码进行JIT编译。对于像这样的玩具程序,两者都可能不合适 - 如果您只想优化一次24秒的过程,则花费时间自动生成和编译C代码可能会超过运行C代码而不是Python代码所节省的时间。但在实际的数值编程中,它们都非常有用。(并且两者都与NumPy兼容。)

而且总是有新项目即将到来。

谢谢您的回答。我需要一些时间来阅读您的答案和评论,并检查其他选择。顺便说一下,我正在使用Python 3。 - rappr
@rappr:同时阅读你在一开始链接的问题的答案中的信息。虽然其中一些信息已经过时(例如,在Windows上使用NumPy时,您不应该获取自定义ATLAS构建,而应该获取MKL构建 - 而获取它的方法就是访问Christoph Gohlke的存储库),但基本思路仍然大多数仍然相关。 - abarnert
@abamert 我尝试了 i = np.zeros((10000000,)) 然后 i = 4 * (x+i) * (1-(x+i)) 但它创建了 10,000,000 次相同的数字 (4 * 0.23 * .77)。为什么会这样? - rappr
1
@rappr:因为那是你要求它做的事情。如果x是标量,并且i的每个元素具有相同的值,并且该表达式除了xi之外没有引用任何东西,那么结果中的每个元素当然都将相同。您正在计算相同的值10000000次。如果您想计算10000000个不同的值,则需要从10000000个不同的值开始(例如,如果i = arange(10000000),则在i = 4 *(x + i)*(1-(x + i))之后,您将有10000000个不同值)。 - abarnert

4
一个(有点学过的)猜测是 Python 在你的代码上没有执行循环展开,而 MATLAB 执行了。这意味着 MATLAB 代码执行了一个大计算,而不是许多(!)小计算。这是选择 PyPy 而不是 CPython 的主要原因,因为 PyPy 执行了循环展开
如果你使用的是 Python 2.X,你应该将 range 替换为 xrange,因为 range (在 Python 2.X 中)会创建一个列表进行迭代。

1
除非他的RAM非常小,否则与迭代列表并一遍又一遍地进行相同的计算相比,创建该列表的成本几乎可以忽略不计。 - abarnert
我知道,这就是为什么我把循环展开的参数放在第一位的原因。但使用比必要内存更少的内存从来都不是一个坏主意。 - EvenLisle
1
当然,但在任何实际的程序中,我通常会使用NumPy来浪费类似数量的内存,以便可以进行向量化而不是迭代; 花费80MB的内存来节省20秒以上的时间通常是不言而喻的选择... - abarnert
我没有异议,只是指出如果OP可以在CPython和PyPy之间自由选择,那么在这种情况下他可能更喜欢选择PyPy而不是CPython和numpy(根据您的答案,PyPy的性能优于numpy)。 - EvenLisle
当然。这就是为什么我首先解释了PyPy的原因。任何有助于人们克服PyPy是一些实验性的、不适合生产的想法的东西都是好的。它有一些不擅长的领域(比如将一堆C库粘在一起,以及遗憾的是,使用大部分SciPy堆栈),有时你不能使用它(因为你要在无法安装软件的机器上部署某个应用程序),而且它在3.x功能方面略逊于CPython——但当它适合时,一定要使用它。 - abarnert
你包含的MATLAB链接不相关 - 它是指向MATLAB Coder的链接,这是MATLAB的一个附加产品,用于将MATLAB代码转换为C代码。链接的内容描述如何在生成的C代码中控制循环展开,而不是在原始的MATLAB代码中。现代版本的MATLAB是JIT编译的,并且将对某些for循环进行矢量化,但绝不是全部。 - Sam Roberts

0

问:如何修改我的py脚本,使其像MATLAB一样运行得快?

由于abarnet已经给出了许多有见地的指导,让我再添砖加瓦(并提供一些定量结果)。

(同样地,我希望您可以原谅跳过for:并假设一个更复杂的计算任务)

  • 检查代码是否存在任何可能的算法改进、值重用和寄存器/缓存友好的排列方式(例如numpy.asfortranarray()等)

  • 在可能的情况下,在numpy中使用向量化代码执行/循环展开

  • 对于代码的稳定部分,使用类似LLVM编译器的numba

  • 仅在代码的最终阶段使用额外的(JIT)编译器技巧(nogil = True,nopython = True),以避免常见的过早优化错误

可以实现的成就确实是巨大的:

Where nanoseconds matter

从FX arena获取初始代码示例(毫秒,微秒和浪费的纳秒确实很重要-检查一下50%的市场事件,您的反应时间远低于900毫秒(端到端双向事务),更不用说高频交易了...)用于处理在大约5200多行的GBPUSD蜡烛/条数组中的过去200个的非平凡指数移动平均线EMA(200,CLOSE)

import numba
#@jit                                               # 2015-06 @autojit deprecated
@numba.jit('f8[:](i8,f8[:])')
def numba_EMA_fromPrice( N_period, aPriceVECTOR ):
    EMA = aPriceVECTOR.copy()
    alf = 2. / ( N_period + 1 )
    for aPTR in range( 1, EMA.shape[0] ):
        EMA[aPTR] = EMA[aPTR-1] + alf * ( aPriceVECTOR[aPTR] - EMA[aPTR-1] )
    return EMA

对于这个“经典”的代码,仅使用numba编译步骤就比普通的Python / numpy代码执行有所改进。 21倍的性能提升,执行时间缩短至约半毫秒
#   541L

从大约11499 [us](是的,从大约11500微秒到只有541 [us])

#       classical numpy
# aClk.start();X[:,7] = EMA_fromPrice( 200, price_H4_CLOSE );aClk.stop()
# 11499L

但是,如果你更加谨慎地考虑算法,并重新设计它以便更加智能和资源高效地工作,结果将会更加丰硕

@numba.jit
def numba_EMA_fromPrice_EFF_ALGO( N_period, aPriceVECTOR ):
    alfa    = 2. / ( N_period + 1 )
    coef    = ( 1 - alfa )
    EMA     = aPriceVECTOR * alfa
    EMA[1:]+= EMA[0:-1]    * coef
    return EMA

#   aClk.start();numba_EMA_fromPrice_EFF_ALGO( 200, price_H4_CLOSE );aClk.stop()
#   Out[112]: 160814L                               # JIT-compile-pass
#   Out[113]:    331L                               # re-use 0.3 [ms] v/s 11.5 [ms] CPython
#   Out[114]:    311L
#   Out[115]:    324L

最后的点睛之笔——多CPU核心处理


46倍速加快,降至约四分之一毫秒

# ___________vvvvv__________# !!!     !!! 
#@numba.jit( nogil = True ) # JIT w/o GIL-lock w/ multi-CORE ** WARNING: ThreadSafe / DataCoherency measures **
#   aClk.start();numba_EMA_fromPrice_EFF_ALGO( 200, price_H4_CLOSE );aClk.stop()
#   Out[126]: 149929L                               # JIT-compile-pass
#   Out[127]:    284L                               # re-use 0.3 [ms] v/s 11.5 [ms] CPython
#   Out[128]:    256L

最后的奖励。更快有时并不等于更好。

惊讶吗?

不,这并不奇怪。试着让MATLAB计算SQRT(2)精确到小数点后约500,000,000位。它就会出错。

纳秒确实很重要,尤其是在追求精度的情况下。


这难道不值得时间和努力吗?当然值得。


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