效率与可读性:使用嵌套布尔索引数组时的混淆

时间:2014-10-19 01:24:06

标签: python arrays numpy readability

我有一些非常丑陋的索引正在进行中。例如,像

这样的东西
valid[ data[ index[valid[:,0],0] ] == 0, 1] = False

其中validindex分别是{Nx2}数组或bool s和int,而data的长度为{N}。

如果我真的很努力,我可以说服自己,这就是我想做的......但它令人难以置信的混淆。 如何有效地对此类内容进行非混淆处理?

我可以将其分解,例如:

valid_index = index[valid[:,0],0]
invalid_index = (data[ valid_index ] == 0)
valid[ invalid_index, 1 ] = False

但是我的阵列将拥有数百万的条目,所以我不想复制内存;我需要尽可能提高速度。

1 个答案:

答案 0 :(得分:2)

这两个代码序列几乎相同,并且应具有非常相似的性能。这是我的“直觉” - 然后我做了静态分析并运行了部分基准确认。

更清晰的选项需要四个以上的字节码来实现,因此可能会稍慢一些。但额外的工作仅限于LOAD_FASTSTORE_FAST,它们只是从堆栈顶部(TOS)移动到/从变量移动。由于额外的工作是适度的,因此应该是性能影响。

您可以在目标设备上对这两种方法进行基准测试,以获得更高的定量精度,但在我3岁的笔记本电脑上,标准的1亿多LOAD_FAST / STORE_FAST对只需3秒钟CPython 2.7.5。所以我估计这种清晰度每100M条目大约需要6秒。虽然PyPy即时Python编译器不使用相同的字节码,但我将其清单版本的开销大约为每100M的一半或3秒。与您正在处理项目的其他工作相比,更清晰的版本可能不是重要的摊牌。

TL; DR Backstory

我的第一印象是代码序列虽然在可读性和清晰度上有所不同,但在技术上非常相似,并且不应具有类似的性能特征。但是让我们使用Python反汇编程序进一步分析。我将每个代码段放入一个函数中:

def one(valid, data):
    valid[ data[ index[valid[:,0],0] ] == 0, 1] = False

def two(valid, data):
    valid_index = index[valid[:,0],0]
    invalid_index = (data[ valid_index ] == 0)
    valid[ invalid_index, 1 ] = False

然后使用Python's bytecode dissassember

import dis
dis.dis(one)
print "---"
dis.dis(two)

给予:

15           0 LOAD_GLOBAL              0 (False)
             3 LOAD_FAST                0 (valid)
             6 LOAD_FAST                1 (data)
             9 LOAD_GLOBAL              1 (index)
            12 LOAD_FAST                0 (valid)
            15 LOAD_CONST               0 (None)
            18 LOAD_CONST               0 (None)
            21 BUILD_SLICE              2
            24 LOAD_CONST               1 (0)
            27 BUILD_TUPLE              2
            30 BINARY_SUBSCR       
            31 LOAD_CONST               1 (0)
            34 BUILD_TUPLE              2
            37 BINARY_SUBSCR       
            38 BINARY_SUBSCR       
            39 LOAD_CONST               1 (0)
            42 COMPARE_OP               2 (==)
            45 LOAD_CONST               2 (1)
            48 BUILD_TUPLE              2
            51 STORE_SUBSCR        
            52 LOAD_CONST               0 (None)
            55 RETURN_VALUE        

18           0 LOAD_GLOBAL              0 (index)
             3 LOAD_FAST                0 (valid)
             6 LOAD_CONST               0 (None)
             9 LOAD_CONST               0 (None)
            12 BUILD_SLICE              2
            15 LOAD_CONST               1 (0)
            18 BUILD_TUPLE              2
            21 BINARY_SUBSCR       
            22 LOAD_CONST               1 (0)
            25 BUILD_TUPLE              2
            28 BINARY_SUBSCR       
            29 STORE_FAST               2 (valid_index)

19          32 LOAD_FAST                1 (data)
            35 LOAD_FAST                2 (valid_index)
            38 BINARY_SUBSCR       
            39 LOAD_CONST               1 (0)
            42 COMPARE_OP               2 (==)
            45 STORE_FAST               3 (invalid_index)

20          48 LOAD_GLOBAL              1 (False)
            51 LOAD_FAST                0 (valid)
            54 LOAD_FAST                3 (invalid_index)
            57 LOAD_CONST               2 (1)
            60 BUILD_TUPLE              2
            63 STORE_SUBSCR        
            64 LOAD_CONST               0 (None)
            67 RETURN_VALUE        

相似但不完全相同,但顺序不同。快速diff of the two表示相同,加上更清晰的函数需要更多字节代码的可能性。

我从每个函数的反汇编程序列表中解析出字节码操作码,将它们放入collections.Counter,然后比较计数:

Bytecode       Count(s) 
========       ======== 
BINARY_SUBSCR  3        
BUILD_SLICE    1        
BUILD_TUPLE    3        
COMPARE_OP     1        
LOAD_CONST     7        
LOAD_FAST      3, 5     *** differs ***
LOAD_GLOBAL    2        
RETURN_VALUE   1        
STORE_FAST     0, 2     *** differs ***
STORE_SUBSCR   1   

这里显而易见的是,第二种更清晰的方法仅使用了四个字节码,以及简单,快速LOAD_FAST / STORE_FAST种类。因此,静态分析没有特别的理由担心额外的内存分配或其他影响性能的副作用。

然后,我构建了两个非常相似的函数,反汇编程序显示的不同之处仅在于第二个函数有一个额外的LOAD_FAST / STORE_FAST对。我跑了他们100,000,000次,并比较了他们的运行时间。它们在CPython 2.7.5中差异超过3秒,在PyPy 2.2.1下差异大约1.5秒(基于Python 2.7.3)。即使你把这些时间加倍(因为你有两对),很明显这些额外的加载/存储对不会让你慢下来。