elif和else if有什么不同?哪个更快?并查看Python汇编代码。

3
这两个代码片段是否完全相同(就像在C++中一样),还是它们会产生略微不同的运行时间?
第一个:
x = 'hello joe'

if x == 'hello':
  print('nope')
elif x == 'hello joe':
  print(x)

第二点:

x = 'hello joe'

if x == 'hello':
  print('nope')
else:
  if x == 'hello joe':
    print(x)

我想亲自了解,但不确定如何实时观看此代码以汇编形式运行。这就带来了我的第二个问题:当我编译Python程序时,如何查看生成的汇编指令?


3
你尝试过使用 dis 模块吗?https://dev59.com/t2Ik5IYBdhLWcg3wIq9U - Jean-François Fabre
取决于你的解释器/编译器。我猜你是在问关于CPython的? - Aran-Fey
太棒了,谢谢大家! - Rob
1
@EmettSpeer 你使用什么程序将Python直接编译成Intel汇编语言?Python字节码是由解释器执行的,而不是裸CPU。 - chepner
@chepner 我没有运行代码或使用工具来进行转换。我只是已经知道现代英特尔/AMD CPU上的比较需要 2 个周期,跳转需要 1 个周期。这意味着在 CPU 上执行 if 至少需要 2 个周期,并且如果不匹配,则需要 1 个跳转。如果你在 else 中再做一次比较,那么至少需要 2 个额外周期,可能还要再加一个跳转。虽然我没有看到第二个 if,但我的陈述是 100% 不正确的,在汇编语言中两者相等。 - SudoKid
显示剩余3条评论
2个回答

9
首先,让我们把你的代码放入一个函数中。
def func():               # line 1
    x = 'hello joe'       # line 2

    if x == 'hello':      # line 4
      print('nope')       # line 5
    else:                 # line 6
     if x == 'hello joe': # line 7
      print(x)            # line 8

现在使用CPython 3.4进行反汇编:

import dis
dis.dis(func)

我们得到:
  2           0 LOAD_CONST               1 ('hello joe')
              3 STORE_FAST               0 (x)

  4           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               2 ('hello')
             12 COMPARE_OP               2 (==)
             15 POP_JUMP_IF_FALSE       31

  5          18 LOAD_GLOBAL              0 (print)
             21 LOAD_CONST               3 ('nope')
             24 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             27 POP_TOP
             28 JUMP_FORWARD            25 (to 56)

  7     >>   31 LOAD_FAST                0 (x)
             34 LOAD_CONST               1 ('hello joe')
             37 COMPARE_OP               2 (==)
             40 POP_JUMP_IF_FALSE       56

  8          43 LOAD_GLOBAL              0 (print)
             46 LOAD_FAST                0 (x)
             49 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             52 POP_TOP
             53 JUMP_FORWARD             0 (to 56)
        >>   56 LOAD_CONST               0 (None)
             59 RETURN_VALUE

现在改为elif:
  2           0 LOAD_CONST               1 ('hello joe')
              3 STORE_FAST               0 (x)

  4           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               2 ('hello')
             12 COMPARE_OP               2 (==)
             15 POP_JUMP_IF_FALSE       31

  5          18 LOAD_GLOBAL              0 (print)
             21 LOAD_CONST               3 ('nope')
             24 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             27 POP_TOP
             28 JUMP_FORWARD            25 (to 56)

  6     >>   31 LOAD_FAST                0 (x)
             34 LOAD_CONST               1 ('hello joe')
             37 COMPARE_OP               2 (==)
             40 POP_JUMP_IF_FALSE       56

  7          43 LOAD_GLOBAL              0 (print)
             46 LOAD_FAST                0 (x)
             49 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             52 POP_TOP
             53 JUMP_FORWARD             0 (to 56)
        >>   56 LOAD_CONST               0 (None)
             59 RETURN_VALUE

唯一的区别就是行号不同。
    else:                 # line 6
     if x == 'hello joe': # line 7

变成(并将其余部分一起移动)
    elif x == 'hello joe': # line 6

两个版本中都有同样数量的指令。在这种情况下,elseif关键字似乎已经被转换成与elif完全相同的方式。并非所有实现都能保证。个人建议使用最短的代码(elif),因为它更加“有意义”,如果代码应该更快,那么很可能就是这个。


我不确定行号代表什么。它们是内存中的位置还是块?正如你所说,elif实际上只是else if{}的简写关键字。如果是这样的话,那么也许elif需要更长的时间,因为它必须花时间查找elif的含义?这可能解释了行号的差异吗? - Rob
1
我已经更新了我的帖子,添加了我展示的源代码的行号以及为什么这些行号会改变的说明。请注意,字节编译是在加载程序时完成的,而不是在执行时完成的。 - Jean-François Fabre
哦,这里有实际的行号...当然有了,我总是把事情搞得太复杂了。 - Rob
这个启发性的答案让我找到了一种简单的比较编译代码的方法(无需保存和比较文件),顺便消除了不相关的细节。(请参见我的回答的第二部分) - Wolf

1

正如Jean-François Fabre已经回答的那样,在这种情况下(即字节码相同),两个变量是等价的。在其他需要检查结果代码是否相等的情况下,我们可以通过比较其二进制表示来实现。字符串可以通过dis.Bytecode(str)进行编译,而二进制表示则存储在codeobj.co_code属性中。

first = """
x = 'hello joe'

if x == 'hello':
  print('nope')
elif x == 'hello joe':
  print(x)
"""

second = """
x = 'hello joe'

if x == 'hello':
  print('nope')
else:
  if x == 'hello joe':
    print(x)
"""

import dis

# binary representation of bytecode of compiled str 
def str_to_binary(f):
    return dis.Bytecode(f).codeobj.co_code

print(f"{str_to_binary(first) == str_to_binary(second) = }")

输出:

str_to_binary(first) == str_to_binary(second) = True


为了获取函数的字节码,甚至不需要导入dis模块。你可以通过__code__访问代码对象。下面是一个例子,其中有两个函数执行完全相同的操作:1
def f(a):
    return a
    
def g(b):
    return b

# binary representation of a function's bytecode
def fun_binary(function):
    return function.__code__.co_code
    
print(f"{fun_binary(f) == fun_binary(g) = }")

输出:

fun_binary(f) == fun_binary(g) = True

1 函数fg使用不同的本地变量名称,这反映在dis.dis()给出的括号信息中,但两个函数的生成的字节码是相同的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接