重要的重新编码:如何加速我的程序? Cython、numba、多处理和 numpy?

Non-trivial recoding: How to speed up my program? Cython, numba, multiprocessing and numpy?

我有(或正在开发)一个程序(一些配对交易策略),它执行以下操作:

  1. 检索位于 postgres 数据库中的较大数据集(财务数据:日期时间指数和约 100 只股票的股票价格)的子集。
  2. 清理数据(删除 NaN >30% 的股票)并计算 returns 和索引(相对于每只股票的第一次观察)
  3. 求出所有的股票对组合,计算相关性(其实有一些类似的度量,但是这里太重要了)
  4. 将相关性最高的对排序为最低的,或者只选择相关性 > 定义阈值的对,即 0.9
  5. 双向检查这些对中的每一对是否协整!并根据他们的测试值对他们进行排名
  6. 选择要交易的前 n 个,即 10 对,并根据移动平均线和标准计算一些信号
  7. 检索 "out-of-sample" window 并交易股票
  8. 在日志中记录每天的return(即在 5 天内)
  9. 计算一些统计数据

完成这 9 个步骤后,重新开始,检索另一个训练 window 并执行分析...

我的方法是 - 如果您看到更好的地方,请更正:
1. 从程序中提取尽可能多的功能
2. 通过多次训练和交易循环步骤 1-9 windows

和我提出的问题(受到论坛中许多主题的启发,即 How to make your python code run faster

请道歉许多 - 相当概念性的 - 问题,但我认为,如果我能理解以上所有 "pain" 点,那将真正提高我的 "logical" 理解,这也会非常有益对于新 python 加入者。

Non-trivial 问题可以产生但简化的答案:

性能改进需要多多关注,~[man*decades] ...

简单地说,不要指望读几个例子就成为这方面的专家。

没有。 1: 糟糕的算法永远不会仅仅通过一些(半)自动转换来改进。智能 re-factoring 可能会在本机普通 python 代码(下面的示例)中实现 +100% 的性能提升,但是精心制作的代码,与 code-execution 设备的 close-to-silicon 属性相匹配将在其他方面展示这种努力,如所述的 code-improvement ~ +100% 性能提升结果一旦转换为 jit 编译单元,其性能几乎相同。这意味着 pre-mature 优化可能在进入精心设计的 high-performance 代码时变得毫无用处。至少你已经被警告过。

python 是一个很棒的工具,我喜欢它 almost-无限精确的数学。然而,同时追求极致精度和极致性能似乎更接近海森堡原则,而不是计算机科学家,而且粉丝们更愿意承认这一点。只是花很长的时间做这两件事才能够把它压缩成几句话的段落。


Q4:“numba.jit()”所有函数有意义吗?

numba是稳定code-base的好工具,让我们开始吧:

使用 numba.jit() 工具可以轻松 cherry-picked automated-transformations 的低垂果实。基准测试将帮助您 shave-off 几乎所有您的代码不需要的开销。

如果依赖code-elements,那还在进化的numba.jit()code-transformers无法转码,你就完蛋了。由于 numba 是非常初始的版本,因此与 numba 一起工作,{ list | dict | class | ... } 是让代码(自动)转换得更接近硅的任何进一步梦想的杀手.此外,所有引用的函数都必须能够获得 numba.jit(),所以几乎忘记了一些 high-level import-ed code-base 使用 numba 很容易翻译,如果他们的原始代码没有系统地设计 numba


问题 5:我是否应该将我的数据的所有格式更改为 float64
会发生什么不利情况?(在他们是 "standard" 数字 )

float32 除了将 [SPACE] 中的 memory-footprint 的静态大小减半之外-域,一些主要缺点。

一些模块(通常是那些继承自 FORTRAN 数值求解器和类似遗产的模块)auto-convert 任何外部传递到它们本地 float64 副本的数据(所以 [SPACE][TIME] 惩罚增加,超出你的控制范围)。

最好在 [TIME] 域中增加 code-execution 惩罚,因为 non-aligned cell-boundaries 很昂贵(这深入 assembly-level 的代码和 CPU-instruction 集和 cache-hierarchies 以掌握该级别的所有细节)。

在下面的基准测试中 float32 上的执行速度 几乎慢了 3 倍


问题 6:是否有任何清单可供我查看何时可以矢量化循环?

Auto-vectorising 变形金刚不亚于 Nobel-prize 目标。

聪明灵巧的设计师可以进行一些调整。不要指望在这个域中有任何 low-hanging 成果用于任何更复杂的操作,而不是简单的广播或一些易于设计的 numpy 跨步技巧。

专业 code-tranformation 软件包很昂贵(有人必须支付许多 [man*years] 中收集的专业知识)并且通常只有在大规模部署时才能调整其投资回报率。


Q1:如何识别我的代码的哪一部分可以运行并行?

您很高兴不必将代码设计为 运行 真-[PARALLEL],而是 "just"-[CONCURRENT]时尚。如果有人说并行,检查系统是否确实需要满足 true-[PARALLEL] process-scheduling 的所有条件,在大多数情况下,"just"-[CONCURRENT] 调度就是这样演讲者要求(详细信息超出了本 post 的范围)。 Python GIL-stepped 锁定可防止除 sub-process-based 之外的任何工作流拆分,但要付出一定的代价,因此收获您的处理独立性,因为这将奖励您的意图 without paying any penalty of additional overhead add-on costs, if going against rules of the overhead-strict Amdahl's Law.


基准,基准,基准:

实际的 cost/effect 比率必须在 python、numba、CPU/缓存架构的相应版本上进行验证,因此基准测试是确认任何改进的唯一方法(费用 ).

以下示例显示了一个简单的指数移动平均函数的保存,以或多或少的智能方式实现。

def plain_EMA_fromPrice(                   N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float32[:]( int32, float32[:] )" )
def numba_EMA_fromPrice_float32(           N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float32[:]( int32, float32[:] )", nopython = True, nogil = True )
def numba_EMA_fromPrice_float32_nopython(  N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float64[:]( int64, float64[:] )" )
def numba_EMA_fromPrice_float64(           N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float64[:]( int64, float64[:] )", nopython = True, nogil = True )
def numba_EMA_fromPrice_float64_nopython(  N_period, aPriceVECTOR ):
    ...    


def plain_EMA_fromPrice2(                  N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float32[:]( int32, float32[:] )" )
def numba_EMA_fromPrice2_float32(          N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float32[:]( int32, float32[:] )", nopython = True, nogil = True )
def numba_EMA_fromPrice2_float32_nopython( N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float64[:]( int64, float64[:] )" )
def numba_EMA_fromPrice2_float64(          N_period, aPriceVECTOR ):
    ...
@numba.jit(                                                         "float64[:]( int64, float64[:] )", nopython = True, nogil = True )
def numba_EMA_fromPrice2_float64_nopython( N_period, aPriceVECTOR ):
    ...

710 [us] -> 160 [us] 提高性能
,通过代码-re-factoring 和 memory-alignment,下一步
通过 numba.jit():

下降到 -> 12 ~ 17 [us]
>>> aPV_32 = np.arange( 100, dtype = np.float32 )
>>> aPV_64 = np.arange( 100, dtype = np.float32 )

>>> aClk.start();_ = plain_EMA_fromPrice(                  18, aPV_32 );aClk.stop()
715L
723L
712L
722L
975L

>>> aClk.start();_ = plain_EMA_fromPrice(                  18, aPV_64 );aClk.stop()
220L
219L
216L
193L
212L
217L
218L
215L
217L
217L

>>> aClk.start();_ = numba_EMA_fromPrice_float32(          18, aPV_32 );aClk.stop()
199L
15L
16L
16L
17L
13L
16L
12L

>>> aClk.start();_ = numba_EMA_fromPrice_float64(          18, aPV_64 );aClk.stop()
170L
16L
16L
16L
18L
14L
16L
14L
17L

>>> aClk.start();_ = numba_EMA_fromPrice_float64_nopython( 18, aPV_64 );aClk.stop()
16L
17L
17L
16L
12L
16L
14L
16L
15L

>>> aClk.start();_ = plain_EMA_fromPrice2(                 18, aPV_32 );aClk.stop()
648L
654L
662L
648L
647L

>>> aClk.start();_ = plain_EMA_fromPrice2(                 18, aPV_64 );aClk.stop()
165L
166L
162L
162L
162L
163L
162L
162L

>>> aClk.start();_ = numba_EMA_fromPrice2_float32(         18, aPV_32 );aClk.stop()
43L
45L
43L
41L
41L
42L

>>> aClk.start();_ = numba_EMA_fromPrice2_float64(         18, aPV_64 );aClk.stop()
17L
16L
15L
17L
17L
17L
12L

>>> aClk.start();_ = numba_EMA_fromPrice2_float64_nopython( 18, aPV_64 );aClk.stop()
16L
15L
15L
14L
17L
15L