我需要减少一些列表,其中取决于元素类型,二进制操作的速度和实现方式各不相同,即通过首先减少具有特定功能的一些对可以获得大的速度降低。
例如foo(a[0], bar(a[1], a[2]))
可能比bar(foo(a[0], a[1]), a[2])
慢很多,但在这种情况下给出相同的结果。
我的代码已经以元组(pair_index, binary_function)
列表的形式产生最佳排序。我正在努力实现一个有效的函数来执行简化,理想情况是返回一个新的部分函数,然后可以在相同类型排序但变化值的列表上重复使用。
这是我的天真解决方案,涉及for循环,删除元素和关闭(pair_index, binary_function)
列表以返回预先计算的'功能
def ordered_reduce(a, pair_indexes, binary_functions, precompute=False):
"""
a: list to reduce, length n
pair_indexes: order of pairs to reduce, length (n-1)
binary_functions: functions to use for each reduction, length (n-1)
"""
def ord_red_func(x):
y = list(x) # copy so as not to eat up
for p, f in zip(pair_indexes, binary_functions):
b = f(y[p], y[p+1])
# Replace pair
del y[p]
y[p] = b
return y[0]
return ord_red_func if precompute else ord_red_func(a)
>>> foos = (lambda a, b: a - b, lambda a, b: a + b, lambda a, b: a * b)
>>> ordered_reduce([1, 2, 3, 4], (2, 1, 0), foos)
1
>>> 1 * (2 + (3-4))
1
预先计算如何运作:
>>> foo = ordered_reduce(None, (0, 1, 0), foos)
>>> foo([1, 2, 3, 4])
-7
>>> (1 - 2) * (3 + 4)
-7
然而,它涉及复制整个列表,因此(因此?)慢。有没有更好/标准的方法来做到这一点?
from operators import add
from functools import reduce
from itertools import repeat
from random import random
r = 100000
xs = [random() for _ in range(r)]
# slightly trivial choices of pairs and functions, to replicate reduce
ps = [0]*(r-1)
fs = repeat(add)
foo = ordered_reduce(None, ps, fs, precompute=True)
>>> %timeit reduce(add, xs)
100 loops, best of 3: 3.59 ms per loop
>>> %timeit foo(xs)
1 loop, best of 3: 1.44 s per loop
这是一种最糟糕的情况,并且略有作弊,因为reduce不会使用可迭代的函数,但是(但没有顺序)的函数仍然非常快:
def multi_reduce(fs, xs):
xs = iter(xs)
x = next(xs)
for f, nx in zip(fs, xs):
x = f(x, nx)
return x
>>> %timeit multi_reduce(fs, xs)
100 loops, best of 3: 8.71 ms per loop
(EDIT2):为了好玩,一场大规模作弊的表演'版本,它可以了解发生的总开销。
from numba import jit
@jit(nopython=True)
def numba_sum(xs):
y = 0
for x in xs:
y += x
return y
>>> %timeit numba_sum(xs)
1000 loops, best of 3: 1.46 ms per loop
答案 0 :(得分:1)
当我读到这个问题时,我立刻想到了reverse Polish notation(RPN)。虽然它可能不是最好的方法,但在这种情况下它仍然可以提供大幅加速。
我的第二个想法是,如果您只是对序列xs
进行适当重新排序以取消del y[p]
,您可能会得到相同的结果。 (如果整个减少程序用C语写成,可以说是最好的表现。但这是一个不同的鱼。)
反向波兰表示法
如果您不熟悉RPN,请阅读维基百科文章中的简短说明。基本上,所有操作都可以在没有括号的情况下写下来,例如,(1-2)*(3+4)
在RPN中为1 2 - 3 4 + *
,而1-(2*(3+4))
变为1 2 3 4 + * -
。
这是RPN解析器的简单实现。我从RPN序列中分离了一个对象列表,因此相同的序列可以直接用于不同的列表。
def rpn(arr, seq):
'''
Reverse Polish Notation algorithm
(this version works only for binary operators)
arr: array of objects
seq: rpn sequence containing indices of objects from arr and functions
'''
stack = []
for x in seq:
if isinstance(x, int):
# it's an object: push it to stack
stack.append(arr[x])
else:
# it's a function: pop two objects, apply the function, push the result to stack
b = stack.pop()
#a = stack.pop()
#stack.append(x(a,b))
## shortcut:
stack[-1] = x(stack[-1], b)
return stack.pop()
使用示例:
# Say we have an array
arr = [100, 210, 42, 13]
# and want to calculate
(100 - 210) * (42 + 13)
# It translates to RPN:
100 210 - 42 13 + *
# or
arr[0] arr[1] - arr[2] arr[3] + *
# So we apply `
rpn(arr,[0, 1, subtract, 2, 3, add, multiply])
要将RPN应用于您的案例,您需要从头开始生成rpn序列或将(pair_indexes, binary_functions)
转换为它们。我没有想过转换器,但肯定可以做到。
<强>测试强>
您的原始测试首先出现:
r = 100000
xs = [random() for _ in range(r)]
ps = [0]*(r-1)
fs = repeat(add)
foo = ordered_reduce(None, ps, fs, precompute=True)
rpn_seq = [0] + [x for i, f in zip(range(1,r), repeat(add)) for x in (i,f)]
rpn_seq2 = list(range(r)) + list(repeat(add,r-1))
# Here rpn_seq denotes (_ + (_ + (_ +( ... )...))))
# and rpn_seq2 denotes ((...( ... _)+ _) + _).
# Obviously, they are not equivalent but with 'add' they yield the same result.
%timeit reduce(add, xs)
100 loops, best of 3: 7.37 ms per loop
%timeit foo(xs)
1 loops, best of 3: 1.71 s per loop
%timeit rpn(xs, rpn_seq)
10 loops, best of 3: 79.5 ms per loop
%timeit rpn(xs, rpn_seq2)
10 loops, best of 3: 73 ms per loop
# Pure numpy just out of curiosity:
%timeit np.sum(np.asarray(xs))
100 loops, best of 3: 3.84 ms per loop
xs_np = np.asarray(xs)
%timeit np.sum(xs_np)
The slowest run took 4.52 times longer than the fastest. This could mean that an intermediate result is being cached
10000 loops, best of 3: 48.5 µs per loop
因此,rpn
比reduce
慢10倍,但比ordered_reduce
快20倍。
现在,让我们尝试更复杂的事情:交替添加和乘以矩阵。我需要一个特殊的功能来测试reduce
。
add_or_dot_b = 1
def add_or_dot(x,y):
'''calls 'add' and 'np.dot' alternately'''
global add_or_dot_b
if add_or_dot_b:
out = x+y
else:
out = np.dot(x,y)
add_or_dot_b = 1 - add_or_dot_b
# normalizing out to avoid `inf` in results
return out/np.max(out)
r = 100001 # +1 for convenience
# (we apply an even number of functions)
xs = [np.random.rand(2,2) for _ in range(r)]
ps = [0]*(r-1)
fs = repeat(add_or_dot)
foo = ordered_reduce(None, ps, fs, precompute=True)
rpn_seq = [0] + [x for i, f in zip(range(1,r), repeat(add_or_dot)) for x in (i,f)]
%timeit reduce(add_or_dot, xs)
1 loops, best of 3: 894 ms per loop
%timeit foo(xs)
1 loops, best of 3: 2.72 s per loop
%timeit rpn(xs, rpn_seq)
1 loops, best of 3: 1.17 s per loop
此处,rpn
比reduce
慢约25%,比ordered_reduce
快2倍。