【问题标题】:"for" iteration vs. direct indexing performance“for”迭代与直接索引性能
【发布时间】:2012-09-10 16:11:21
【问题描述】:

刚刚得到一个奇怪的结果,我试图理解。我有一个大约 325k 行(列表)的数据集,每行大约 90 个项目(字符串、浮点数等 - 这并不重要)。说,如果我想对所有项目进行一些处理,那么我可以使用 2 "for"s 对它们进行迭代:

for eachRow in rows:
    for eachItem in eachRow:
        # do something

在我的系统中,这段代码执行了 41 秒。但是,如果我用一系列索引访问( eachRow[0]、eachRowm[1] 直到 eachRow[89] )替换嵌套循环,执行时间就会下降到 25 秒。

for eachRow in rows:
    eachRow[0]  # do something with this item
    eachRow[1]  # do something with this item
    ..
    eachRow[89] # do something with this item

当然,写这样的代码不是一个好主意——我只是在寻找一种提高数据处理性能的方法,偶然发现了这种奇怪的方法。有cmets吗?

【问题讨论】:

  • python 中的索引访问速度非常快......这就是为插入/删除设置的列表计算量很大/查找速度很快
  • 但它的开销仍然大于查找...虽然不多但分布在 300k 行上可能是一个很大的数量
  • 您能否展示一个我们可以尝试分析的可重现示例?
  • 为了在不“手动展开循环”的情况下获得更好的性能,您应该尝试使用函数式范式 - 即为“map”函数提供您想要执行的转换和您想要执行的完整数组正在努力。
  • 我的猜测是,除了索引之外,还有其他地方发生了变化。正如 David Robinson 建议的那样,发布两个完全正常工作的函数,它们使用两种方法做同样的事情,我们也可以尝试运行比较。

标签: python performance for-loop iteration


【解决方案1】:

展开似乎确实有轻微的性能优势,但可以忽略不计,因此除非您的 do_something 函数几乎什么都不做,否则您不会看到差异。我很难相信不同方法的等效行为可能会达到 60%,尽管我总是愿意对一些我从未想过的实现细节感到惊讶。

tl;博士总结,使用 32500 而不是 325000 因为我不耐烦:

do_nothing easy 3.44702410698
do_nothing indexed 3.99766016006
do_nothing mapped 4.36127090454
do_nothing unrolled 3.33416581154
do_something easy 5.4152610302
do_something indexed 5.95649385452
do_something mapped 6.20316290855
do_something unrolled 5.2877831459
do_more easy 16.6573209763
do_more indexed 16.8381450176
do_more mapped 17.6184959412
do_more unrolled 16.0713188648

CPython 2.7.3,代码:

from timeit import Timer

nrows = 32500
ncols = 90
a = [[1.0*i for i in range(ncols)] for j in range(nrows)]

def do_nothing(x):
    pass

def do_something(x):
    z = x+3
    return z

def do_more(x):
    z = x**3+x**0.5+4
    return z

def easy(rows, action):
    for eachRow in rows:
        for eachItem in eachRow:
            action(eachItem)

def mapped(rows, action):
    for eachRow in rows:
        map(action, eachRow)

def indexed(rows, action):
    for eachRow in rows:
        for i in xrange(len(eachRow)):
            action(eachRow[i])

def unrolled(rows, action):
    for eachRow in rows:
        action(eachRow[0])
        action(eachRow[1])
        action(eachRow[2])
        action(eachRow[3])
        action(eachRow[4])
        action(eachRow[5])
        action(eachRow[6])
        action(eachRow[7])
        action(eachRow[8])
        action(eachRow[9])
        action(eachRow[10])
        action(eachRow[11])
        action(eachRow[12])
        action(eachRow[13])
        action(eachRow[14])
        action(eachRow[15])
        action(eachRow[16])
        action(eachRow[17])
        action(eachRow[18])
        action(eachRow[19])
        action(eachRow[20])
        action(eachRow[21])
        action(eachRow[22])
        action(eachRow[23])
        action(eachRow[24])
        action(eachRow[25])
        action(eachRow[26])
        action(eachRow[27])
        action(eachRow[28])
        action(eachRow[29])
        action(eachRow[30])
        action(eachRow[31])
        action(eachRow[32])
        action(eachRow[33])
        action(eachRow[34])
        action(eachRow[35])
        action(eachRow[36])
        action(eachRow[37])
        action(eachRow[38])
        action(eachRow[39])
        action(eachRow[40])
        action(eachRow[41])
        action(eachRow[42])
        action(eachRow[43])
        action(eachRow[44])
        action(eachRow[45])
        action(eachRow[46])
        action(eachRow[47])
        action(eachRow[48])
        action(eachRow[49])
        action(eachRow[50])
        action(eachRow[51])
        action(eachRow[52])
        action(eachRow[53])
        action(eachRow[54])
        action(eachRow[55])
        action(eachRow[56])
        action(eachRow[57])
        action(eachRow[58])
        action(eachRow[59])
        action(eachRow[60])
        action(eachRow[61])
        action(eachRow[62])
        action(eachRow[63])
        action(eachRow[64])
        action(eachRow[65])
        action(eachRow[66])
        action(eachRow[67])
        action(eachRow[68])
        action(eachRow[69])
        action(eachRow[70])
        action(eachRow[71])
        action(eachRow[72])
        action(eachRow[73])
        action(eachRow[74])
        action(eachRow[75])
        action(eachRow[76])
        action(eachRow[77])
        action(eachRow[78])
        action(eachRow[79])
        action(eachRow[80])
        action(eachRow[81])
        action(eachRow[82])
        action(eachRow[83])
        action(eachRow[84])
        action(eachRow[85])
        action(eachRow[86])
        action(eachRow[87])
        action(eachRow[88])
        action(eachRow[89])


def timestuff():
    for action in 'do_nothing do_something do_more'.split():
        for name in 'easy indexed mapped unrolled'.split():
            t = Timer(setup="""
from __main__ import {} as fn
from __main__ import {} as action
from __main__ import a
""".format(name, action),
                      stmt="fn(a, action)").timeit(10)
            print action, name, t

if __name__ == '__main__':
    timestuff()

(请注意,我并没有费心使比较完全公平,因为我只是试图衡量变化的可能规模,即顺序统一的变化与否。)

【讨论】:

    【解决方案2】:

    对不起,这是我的错。我的系统出了点问题(这不是一个独立的 Python 解释器,而是一个内置的大系统)。重新启动整个系统后,我得到了正确的结果——两种变体大约 2.8 秒。我觉得我好笨。由于不相关,正在寻找删除我的问题的方法。

    【讨论】:

    • 没必要觉得自己很蠢! - 我们都去过那里 - 你的直觉认为这是一个奇怪的结果是正确的。不过,我绝对会建议将陌生感隔离到 short example 中,人们可以在询问 SO 之前复制和粘贴它。大多数时候你会意识到自己发生了什么,如果你不能完全理解,那么我相信 Python 社区中有一群大约 20 到 25 人可以在里面得到正确的答案五分钟,一开始是十分钟,如果他们可以将代码粘贴到他们的解释器中并立即开始播放。
    【解决方案3】:

    与其他计时的响应者不同,我发现计时有很大差异。首先,我的代码:

    import random
    import string
    import timeit
    
    r = 1000
    outer1 = [[[''.join([random.choice(string.ascii_letters) for j in range(10)])] for k in range(90)] for l in range(r)]
    outer2 = [[[''.join([random.choice(string.ascii_letters) for j in range(10)])] for k in range(90)] for l in range(r)]
    outer3 = [[[''.join([random.choice(string.ascii_letters) for j in range(10)])] for k in range(90)] for l in range(r)]
    
    def x1(L):
        for outer in L:
            for inner in L:
                inner = inner[:-1]
    
    def x2(L):
        for outer in L:
            for y in range(len(outer)):
                outer[y] = outer[y][:-1]
    
    def x3(L):
        for x in range(len(L)):
            for y in range(len(L[x])):
                L[x][y] = L[x][y][:-1]
    
    print "x1 =",timeit.Timer('x1(outer1)', "from __main__ import x1,outer1").timeit(10)
    print "x2 =",timeit.Timer('x2(outer2)', "from __main__ import x2,outer2").timeit(10)
    print "x3 =",timeit.Timer('x3(outer3)', "from __main__ import x3,outer3").timeit(10)
    

    请注意,我正在运行这 10 次。每个列表包含 3000 个项目,每个项目包含 90 个项目,每个项目都是由十个字母组成的随机字符串。

    代表性结果:

    x1 = 8.0179214353
    x2 = 0.118051644801
    x3 = 0.150409681521
    

    不使用索引的函数 (x1) 比仅对内部循环使用索引的函数 (x2) 的执行时间长 66 倍。奇怪的是,只对内循环使用索引的函数 (x2) 比对外循环和内循环都使用索引的函数 (x3) 执行得更好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-17
      • 2014-04-11
      • 2012-01-11
      • 2020-08-07
      • 1970-01-01
      相关资源
      最近更新 更多