Python (AoC第5天)使用二进制搜索解码字符串与二进制转换一样快?

Python (AoC第5天)使用二进制搜索解码字符串与二进制转换一样快?,python,performance,optimization,binary,binary-search,Python,Performance,Optimization,Binary,Binary Search,这个问题是关于解决问题的(上半年) 为了得到相同的结果,我编写了两个不同的函数,即将登机牌字符串解码为行、列坐标。在第一种情况下,我根据字符串中的每个字符进行了二进制搜索: def decode(bp): row = bp[:7] col = bp[-3:] row_lower, row_upper = 0, 127 col_lower, col_upper = 0, 7 for char in row: if char=='F': row_u

这个问题是关于解决问题的(上半年)

为了得到相同的结果,我编写了两个不同的函数,即将登机牌字符串解码为行、列坐标。在第一种情况下,我根据字符串中的每个字符进行了二进制搜索:

def decode(bp):
  row = bp[:7]
  col = bp[-3:]
  row_lower, row_upper = 0, 127
  col_lower, col_upper = 0, 7
  for char in row:
      if char=='F':
          row_upper = ((row_upper+row_lower))//2
      else:
          row_lower = ((row_upper+row_lower)+1)//2
  for char in col:
      if char=='L':
          col_upper = ((col_upper+col_lower))//2
      else:
          col_lower = ((col_upper+col_lower)+1)//2
  sid = (row_lower*8)+col_lower
  return (row_lower, col_lower, sid)

我意识到,如果把字符串看成二进制数,每个行、列坐标都有1∶1的映射,所以我也写了这个问题的另一种解决方案:

def alternative_decode(bp):
  bp = bp.replace('F', '0').replace('L', '0').replace('B', '1').replace('R', '1')
  return(int(bp[:7], 2), int(bp[-3:], 2), (int(bp[:7], 2)*8)+int(bp[-3:], 2))
我写第二个解决方案是因为我希望它比第一个快得多,因为它是一个简单的二进制转换,而不是二进制搜索。然而,当对这两种方法计时时,我注意到它们都有相同的运行时间,即大约在0.0000020和0.0000025秒之间


这是什么原因?是否有一些Python魔术在幕后发生,使这两种解决方案都同样有效,或者我编写它们的方式使它们同样无效?

在我的计算机上,如果您重用行/列结果,而不是重复对
sid
返回的int()调用,则您的替代方法会显著加快:

def alternative_decode_reuse(bp):
  bp = bp.replace('F', '0').replace('L', '0').replace('B', '1').replace('R', '1')
  row = int(bp[:7], 2)
  col = int(bp[-3:], 2)
  sid = row*8 + col
  return (row, col, sid)
将所有三个函数放入一个文件
decoding.py
,这将为我们提供:

tmp$ python -m timeit -s 'import decoding' -- 'decoding.decode("FFBBFBFLLR")'
1000000 loops, best of 3: 1.75 usec per loop
tmp$ python -m timeit -s 'import decoding' -- 'decoding.alternative_decode("FFBBFBFLLR")'
1000000 loops, best of 3: 1.62 usec per loop
tmp$ python -m timeit -s 'import decoding' -- 'decoding.alternative_decode_reuse("FFBBFBFLLR")'
1000000 loops, best of 3: 1.32 usec per loop
此时,大部分时间用于执行
replace()
行。如果我们没有呢

def third_decode(bp):
    row = 0
    for c in bp[:7]:
        row <<= 1
        if c == 'B':
            row += 1
    col = 0
    for c in bp[7:]:
        col <<= 1
        if c == 'R':
            col += 1
    sid = row * 8 + col
    return (row, col, sid)
稍微差一点,或者至少没有明显好转。如果我们还使用所需的
sid
相当于行/列编号的(二进制)串联这一事实,会怎么样

def fourth_decode(bp):
    sid = 0
    for c in bp:
        sid <<= 1
        if c in 'BR':
            sid += 1
    row = sid >> 3
    col = sid & 7
    return (row, col, sid)
现在,我已经厌倦了编辑cmdline参数来重新运行所有内容,所以让我们将其添加到
decoding.py
的底部:

if __name__ == '__main__':
    loops = 1000000
    funcs = (
            decode, alternative_decode, alternative_decode_reuse, replace_only,
            third_decode, fourth_decode,
    )
    from timeit import Timer
    for fun in funcs:
        cmd = 'decoding.%s("FFBBFBFLLR")' % fun.__name__
        timer = Timer(cmd, setup='import decoding')
        totaltime = min(timer.repeat(5, loops))
        fmt = '%25s returned %14r -- %8d loops, best of 5: %6d ns per loop'
        arg = (fun.__name__, fun('FFBBFBFLLR'), loops, totaltime*1000000000/loops)
        print(fmt % arg)
这让我们可以轻松地运行所有功能:

tmp$ python decoding.py
                   decode returned   (26, 1, 209) --  1000000 loops, best of 5:   2090 ns per loop
       alternative_decode returned   (26, 1, 209) --  1000000 loops, best of 5:   1829 ns per loop
 alternative_decode_reuse returned   (26, 1, 209) --  1000000 loops, best of 5:   1414 ns per loop
             replace_only returned           None --  1000000 loops, best of 5:    700 ns per loop
             third_decode returned   (26, 1, 209) --  1000000 loops, best of 5:   1368 ns per loop
            fourth_decode returned   (26, 1, 209) --  1000000 loops, best of 5:   1123 ns per loop
在那之后,我就不知道如何让它跑得更快了。但去年,一位代码解决熟人告诉我,他正在使用pypypyPython实现来提高速度。也许能帮上忙

tmp$ pypy decoding.py
                   decode returned   (26, 1, 209) --  1000000 loops, best of 5:    151 ns per loop
       alternative_decode returned   (26, 1, 209) --  1000000 loops, best of 5:      4 ns per loop
 alternative_decode_reuse returned   (26, 1, 209) --  1000000 loops, best of 5:      3 ns per loop
             replace_only returned           None --  1000000 loops, best of 5:      3 ns per loop
             third_decode returned   (26, 1, 209) --  1000000 loops, best of 5:    141 ns per loop
            fourth_decode returned   (26, 1, 209) --  1000000 loops, best of 5:    138 ns per loop
好吧,我所有的努力都白费了!:)


看起来pypy的
replace()
int()
函数要快得多。此外,虽然它的JIT确实使我们的各种循环函数更快,但在可能的情况下还是最好使用内置函数。

在我的计算机上,如果您重用行/列结果,而不是重复对
sid
返回的int()调用,那么您的选择速度会显著加快:

def alternative_decode_reuse(bp):
  bp = bp.replace('F', '0').replace('L', '0').replace('B', '1').replace('R', '1')
  row = int(bp[:7], 2)
  col = int(bp[-3:], 2)
  sid = row*8 + col
  return (row, col, sid)
将所有三个函数放入一个文件
decoding.py
,这将为我们提供:

tmp$ python -m timeit -s 'import decoding' -- 'decoding.decode("FFBBFBFLLR")'
1000000 loops, best of 3: 1.75 usec per loop
tmp$ python -m timeit -s 'import decoding' -- 'decoding.alternative_decode("FFBBFBFLLR")'
1000000 loops, best of 3: 1.62 usec per loop
tmp$ python -m timeit -s 'import decoding' -- 'decoding.alternative_decode_reuse("FFBBFBFLLR")'
1000000 loops, best of 3: 1.32 usec per loop
此时,大部分时间用于执行
replace()
行。如果我们没有呢

def third_decode(bp):
    row = 0
    for c in bp[:7]:
        row <<= 1
        if c == 'B':
            row += 1
    col = 0
    for c in bp[7:]:
        col <<= 1
        if c == 'R':
            col += 1
    sid = row * 8 + col
    return (row, col, sid)
稍微差一点,或者至少没有明显好转。如果我们还使用所需的
sid
相当于行/列编号的(二进制)串联这一事实,会怎么样

def fourth_decode(bp):
    sid = 0
    for c in bp:
        sid <<= 1
        if c in 'BR':
            sid += 1
    row = sid >> 3
    col = sid & 7
    return (row, col, sid)
现在,我已经厌倦了编辑cmdline参数来重新运行所有内容,所以让我们将其添加到
decoding.py
的底部:

if __name__ == '__main__':
    loops = 1000000
    funcs = (
            decode, alternative_decode, alternative_decode_reuse, replace_only,
            third_decode, fourth_decode,
    )
    from timeit import Timer
    for fun in funcs:
        cmd = 'decoding.%s("FFBBFBFLLR")' % fun.__name__
        timer = Timer(cmd, setup='import decoding')
        totaltime = min(timer.repeat(5, loops))
        fmt = '%25s returned %14r -- %8d loops, best of 5: %6d ns per loop'
        arg = (fun.__name__, fun('FFBBFBFLLR'), loops, totaltime*1000000000/loops)
        print(fmt % arg)
这让我们可以轻松地运行所有功能:

tmp$ python decoding.py
                   decode returned   (26, 1, 209) --  1000000 loops, best of 5:   2090 ns per loop
       alternative_decode returned   (26, 1, 209) --  1000000 loops, best of 5:   1829 ns per loop
 alternative_decode_reuse returned   (26, 1, 209) --  1000000 loops, best of 5:   1414 ns per loop
             replace_only returned           None --  1000000 loops, best of 5:    700 ns per loop
             third_decode returned   (26, 1, 209) --  1000000 loops, best of 5:   1368 ns per loop
            fourth_decode returned   (26, 1, 209) --  1000000 loops, best of 5:   1123 ns per loop
在那之后,我就不知道如何让它跑得更快了。但去年,一位代码解决熟人告诉我,他正在使用pypypyPython实现来提高速度。也许能帮上忙

tmp$ pypy decoding.py
                   decode returned   (26, 1, 209) --  1000000 loops, best of 5:    151 ns per loop
       alternative_decode returned   (26, 1, 209) --  1000000 loops, best of 5:      4 ns per loop
 alternative_decode_reuse returned   (26, 1, 209) --  1000000 loops, best of 5:      3 ns per loop
             replace_only returned           None --  1000000 loops, best of 5:      3 ns per loop
             third_decode returned   (26, 1, 209) --  1000000 loops, best of 5:    141 ns per loop
            fourth_decode returned   (26, 1, 209) --  1000000 loops, best of 5:    138 ns per loop
好吧,我所有的努力都白费了!:)


看起来pypy的
replace()
int()
函数要快得多。此外,虽然它的JIT确实使我们的各种循环函数更快,但如果可能,最好还是使用内置函数。

感谢您提供了令人惊讶的深入答案!我不知道pypy在这里的优势,我将有一些跟进工作:)感谢您提供了令人惊讶的深入回答!我不知道pypy在这里的优势,我将有一些事情要做:)