python if语句报错_怎样写python的if语句比较高效?

跳转,尤其是条件跳转有可能会打断CPU指令的流水线执行,所以在性能要求比较高的代码中,我都是系统尽量不要出现if,那实在要出现怎么办了,比如有如下代码:1

2

3

4for component in components:

if component.enabled:

# do sth...

这种情况下我是应该写成上面这种形式,还是应该写成下面的形式呢,哪种更高效?

1

2

3

4

5for component in components:

if not component.enabled:

continue

# do sth...

别问我为什么性能要求比较高的时候还要用python,我也不想……那么假设我们知道条件为True和False的概率,哪一种比较高效?

Python解释器中if语句的实现细节

字节码

上述的第一种编码方式更容易生成箭头型的代码,直观的解释就是下面这种:1

2

3

4

5for a in balabala:

if a.b:

if a.c:

if a.d:

# do sth...

所以我们经常使用Guard Clauses的方式去规避这样的箭头型代码,看起来会比较漂亮一些,就是下面这种:

1

2

3

4

5

6

7

8

9

10

11for a in balabala:

if not a.b:

continue

if not a.c:

continue

if not a.d:

continue

# do sth...

回到之前的问题,哪种比较高效?首先我们给出示例代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23In [1]: from dis import dis

In [2]: class (object):

...: def __init__(self):

...: self.enabled = True

...: def update(self, delta_time_s):

...: pass

...:

In [3]: components = [Component() for idx in xrange(10000)]

In [4]: def update_in_arrow_form(delta_time_s):

...: for c in components:

...: if c.enabled:

...: c.update(delta_time_s)

...:

In [5]: def update_in_guard_clauses_form(delta_time_s):

...: for c in components:

...: if not c.enabled:

...: continue

...: c.update(delta_time_s)

...:

在上边的代码中,我们模拟了一个遍历Component容器并且调用每一个Component的update方法(这种Component的设计是很糟糕的,状态和行为还是需要分割开来比较好,参考ECS),遍历过程中需要判定Component是否需要处理,这就引入了分支逻辑。

在看看字节码之前,我先根据过往教科书上的知识做一个猜想:引发分支预测错误少、分支预测错误代价小的写法,性能更好。

然后我们来看看箭头型代码的字节码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21In [6]: dis(update_in_arrow_form)

2 0 SETUP_LOOP 39 (to 42)

3 LOAD_GLOBAL 0 (components)

6 GET_ITER

>> 7 FOR_ITER 31 (to 41)

10 STORE_FAST 1 (c)

3 13 LOAD_FAST 1 (c)

16 LOAD_ATTR 1 (enabled)

19 POP_JUMP_IF_FALSE 38

4 22 LOAD_FAST 1 (c)

25 LOOKUP_METHOD 2 (update)

28 LOAD_FAST 0 (delta_time_s)

31 CALL_METHOD 1

34 POP_TOP

35 JUMP_ABSOLUTE 7

>> 38 JUMP_ABSOLUTE 7

>> 41 POP_BLOCK

>> 42 LOAD_CONST 0 (None)

45 RETURN_VALUE

可以看出只有在判断c.enabled为False的情况下,才需要跳转,假如c.enabled大概率为True的时候,类似逻辑写成箭头型符合之前的猜想,但是如果c.enabled大概率为False,就不符合了。

那Guard Clauses这种写法呢?也看看字节码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23In [7]: dis(update_in_guard_clauses_form)

2 0 SETUP_LOOP 42 (to 45)

3 LOAD_GLOBAL 0 (components)

6 GET_ITER

>> 7 FOR_ITER 34 (to 44)

10 STORE_FAST 1 (c)

3 13 LOAD_FAST 1 (c)

16 LOAD_ATTR 1 (enabled)

19 POP_JUMP_IF_TRUE 28

4 22 JUMP_ABSOLUTE 7

25 JUMP_FORWARD 0 (to 28)

5 >> 28 LOAD_FAST 1 (c)

31 LOOKUP_METHOD 2 (update)

34 LOAD_FAST 0 (delta_time_s)

37 CALL_METHOD 1

40 POP_TOP

41 JUMP_ABSOLUTE 7

>> 44 POP_BLOCK

>> 45 LOAD_CONST 0 (None)

48 RETURN_VALUE

只要c.enabled为True,就会引发跳转,那么就和之前的箭头型代码反过来了:假如c.enabled大概率为True的时候,类似逻辑写成Guard Clauses型是不符合猜想的,但是如果c.enabled大概率为False,就符合猜想。

虽然上面的例子没有加上else分支,但是如果你自己加上else分支,也是类似的字节码。

CPython的底层实现

到目前为止,我们看到的都是字节码层面的内容,CPython底层POP_JUMP_IF_TRUE和POP_JUMP_IF_FALSE是如何处理跳转的呢?答案在ceval.c中,这里我只贴POP_JUMP_IF_FALSE相关的代码,POP_JUMP_IF_TRUE其实是类似的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23PREDICTED(POP_JUMP_IF_FALSE);

TARGET(POP_JUMP_IF_FALSE) {

PyObject *cond = POP();

int err;

if (cond == Py_True) {

Py_DECREF(cond);

FAST_DISPATCH();

}

if (cond == Py_False) {

Py_DECREF(cond);

JUMPTO(oparg);

FAST_DISPATCH();

}

err = PyObject_IsTrue(cond);

Py_DECREF(cond);

if (err > 0)

;

else if (err == 0)

JUMPTO(oparg);

else

goto error;

DISPATCH();

}

果然,字面意思就是如果判断为False我才跳转,JUMPTO只有在cond == Py_False的时候才会被调用到。

那这么说,是不是意味着:最可能运行到的代码应该放到if分支中,箭头型代码比Guard Clauses重构过的代码性能要好?

Benchmark

如果没有Benchmark,一切都只是猜想,因为一方面CPU的流水线分支预测非常复杂,还有乱序执行等等高级技术在里面;另一方面除了JUMPTO,上面解释执行Python代码的C代码中还有大量的if-else分支,也就是说只要出现这条字节码,对CPU就会多出非常多的分支。所以单从字节码层面的跳转分析可能是不准的,只好测一测了。

这里我构造两组components,第一组只有每十个中只有一个Component的enabled为True,另一组反过来,然后看看运行时间的对比:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50In [8]: def test_almost_true(f):

...: for idx, component in enumerate(components):

...: if idx % 10 == 0:

...: component.enabled = False

...: else:

...: component.enabled = True

...: return timeit.repeat(lambda: f(0), number=10)

...:

In [9]: def test_almost_false(f):

...: for idx, component in enumerate(components):

...: if idx % 10 == 0:

...: component.enabled = True

...: else:

...: component.enabled = False

...: return timeit.repeat(lambda: f(0), number=10)

...:

In [10]: a1 = test_almost_true(update_in_arrow_form)

In [11]: b1 = test_almost_true(update_in_guard_clauses_form)

In [12]: for m, n in zip(a1, b1):

...: print (m - n) / n * 100.0

...:

23.0675013609

-23.0445608311

-16.3107397091

30.4611497157

-2.43304228392

-4.31911240259

79.3438445612

In [13]: def no_branch(delta_time_s):

...: for c in components:

...: c.update(delta_time_s)

...:

In [14]: g = test_almost_true(no_branch)

In [15]: for m, n in zip(a1, g):

...: print (m - n) / n * 100

...:

204.991568297

151.134771645

209.973251815

313.04

303.284671533

174.186222559

357.392363932

&*%^)$#,看起来在字节码和c代码层面根据跳转与否做出的猜想,与测试结果不一致…

经过了非常多的测试,看起来在Intel i5平台上(移动版,性能很差,从上面的数据就可以看出来了),有这样的结论:

即使在有先验概率支持的情况下,箭头型代码并不比Guard Clauses重构过的代码快,两者的测试看不出显著差别,那么我们还是继续使用Guard Clauses形式的代码好了;

相对于没有分支的代码,有分支的代码要慢(相对百分之两三百,不过绝对值非常小),尽管没分支的测试代码多调用了11%的函数(这个和经验倒是吻合的)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值