为什么 try ... except 比 if 快?

Why `try ... except` is faster than `if`?

在我的代码中,我有一个列表 l,我正在从中创建一个列表字典。 (我正在对具有相同 key 的对象进行分组)。通过 try 语句和 if 条件实现它,我在 line_profiler 中注意到前者似乎更有效率:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================

   293  44378450     59805020      1.3     16.9       for element in l:

                                                        # stuff that compute 'key' from 'element'

   302   2234869      2235518      1.0      0.6              try:                                                         
   303   2234869     82486133     36.9     23.3                  d[key].append(element)                              
   304     57358        72499      1.3      0.0              except KeyError:                                             
   305     57358      1758248     30.7      0.5                  d[key] = [element]  

对比:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================

   293  44378450     60185880      1.4     14.0       for element in l:      

                                                        # stuff that compute 'key' from 'element'

   307   2234869     81683512     36.5     19.1              if key in d.keys():                                  
   308   2177511     76393595     35.1     17.8                  d.get(key).append(element)                       
   309                                                       else:                                                
   310     57358      1717679     29.9      0.4                  d[key] = [element]                               

我知道使用 try 时,您只会在引发异常时进入 except(因此除了少数例外,总的来说它比每次测试条件的成本都低是有道理的),但在这里,即使 Time per hit 对于异常 (1.3+30.7 µs) 也比测试条件 (36.5 µs) 慢。我认为引发异常比检查字典中是否有键(in 只是测试散列键,不?这不是行搜索)的成本更高。那是为什么?

额外的运行时间来自 .keys() 调用。如果你想阻止额外的调用并仍然使用 ifelse,请尝试这样的事情:

obj = d.get(key)
if obj:
      obj.append(element)
else:
      d[key] = [element]

或者您可以使用 defaultdict 在后台执行此操作。示例:

from collections import defaultdict
d = defaultdict(list)
d['123'].append('abc')

您应该认为在每次迭代中您都会浪费一些时间来测试是否有条件检查。如果您不引发异常,您的代码将使用 try except 更快,否则处理异常需要更多时间。

换句话说,如果你确定你的异常是异常的(它只发生在特殊情况下),那么使用 try-except 会更便宜。否则,如果您在约 50% 的情况下排除异常,则最好使用 if.

("it's easier to ask for forgiveness than permission")

try...except 仅当实际引发的异常数量与循环执行次数相当时才会变慢。在您的情况下,异常仅引发 2.5% 的循环迭代。

下面分析四种情况-

def func1():
    l = [1,2,3,4]
    d = {}
    for e in l:
        k = e - 1
        try:
            d[k].append(e)
        except KeyError:
            d[k] = [e]
    return d


def func2():
    l = [1,2,3,4]
    d = {}
    for e in l:
        k = e - 1
        if k in d.keys():
            d.get(k).append(e)
        else:
            d[k] = [e]
    return d

def func3():
    l = [1,2,3,4]
    d = {}
    for e in l:
        k = 1
        try:
            d[k].append(e)
        except KeyError:
            d[k] = [e]
    return d


def func4():
    l = [1,2,3,4]
    d = {}
    for e in l:
        k = 1
        if k in d.keys():
            d.get(k).append(e)
        else:
            d[k] = [e]
    return d

这个的计时结果 -

In [7]: %timeit func1()
The slowest run took 4.17 times longer than the fastest. This could mean that an intermediate result is being cached
100000 loops, best of 3: 2.55 µs per loop

In [8]: %timeit func2()
1000000 loops, best of 3: 1.77 µs per loop

In [10]: %timeit func3()
The slowest run took 4.34 times longer than the fastest. This could mean that an intermediate result is being cached
1000000 loops, best of 3: 2.01 µs per loop

In [11]: %timeit func4()
The slowest run took 6.83 times longer than the fastest. This could mean that an intermediate result is being cached
100000 loops, best of 3: 2.4 µs per loop
  1. func1()func2() 的情况下,每个元素都进入一个单独的列表,因此对于每个键, try..except 块引发并捕获一个例外。在那种情况下,func2() 更快。

  2. func3()func4() 的情况下,异常只会抛出一次,因此异常的开销只会发生一次,而仍然会为每个键检查条件(即使它存在),这就是为什么在这种情况下,try..except 更快。


我猜你的情况可能会发生类似的情况,同一个密钥被多次计算,因此 try..except 块的速度更快。您可以查看列表中有多少个实际元素以及字典中有多少个键,看看是否是这个原因。

假设 hits 列是特定行被执行的次数,您可以看到,行 -

d[key].append(element)

执行了 2234869 次,而异常仅引发 - 57358 次,仅占元素总数的 2.56%。

我在我的一个 classes 中了解到,处理器试图重新预测指令并将其加载到内存中。 如果你放一个 if 语句,处理器不知道加载哪个代码并且会浪费时间 loaing/unloading 指令等

可能,在发生异常的情况下,处理器假设异常很少见,并在不考虑异常的情况下加载主代码...

我不知道这里是不是这样,但这是我们在代码优化中学到的东西 class。

来源(class https://www.etsmtl.ca/etudes/cours/gti320/)