0

我有一些很慢的代码(最后一次计算为 30-60 分钟),我需要对其进行优化,它是用于结构工程模型的 Abaqus 数据提取脚本。脚本最糟糕的部分是循环,它首先逐帧(即模拟时间历史中的时间)遍历对象模型数据库,并嵌套在此循环下,它通过每个节点进行迭代。愚蠢的是,有大约 10 万个“节点”,但只有大约 2 万个有用节点。但幸运的是,节点总是以相同的顺序排列,这意味着我不需要查找节点的 uniqueLabel,我可以在单独的循环中执行一次,然后过滤最后得到的内容。这就是为什么我将所有内容都转储到一个列表中,然后删除所有重复的节点。但是从代码中可以看出:

    timeValues = []
    peeqValues = []
    for frame in frames: #760 loops
        setValues = frame.fieldOutputs['@@@fieldOutputType'].getSubset
                    region=abaqusSet, position=ELEMENT_NODAL).values
        timeValues.append(frame.frameValue)
        for value in setValues: # 100k loops
            peeqValues.append(value.data)

它仍然需要进行value.data不必要的调用,大约 8 万次。如果有人熟悉 Abaqus odb(对象数据库)对象,那么它们在 python 下非常慢。雪上加霜的是,它们只在一个线程中运行,在 Abaqus 下,它有自己的 python 版本(2.6.x)和包(例如,numpy 可用,pandas 不可用)。另一件可能令人讨厌的事情是,您可以按位置寻址对象,例如frames[-1]给您最后一帧,但您不能切片,所以例如您不能这样做for frame in frames[0:10]: # iterate first 10 elements

我对 itertools 没有任何经验,但我想为它提供一个 nodeID 列表(或 True/False 列表)以映射到 setValues。对于 760 帧中的每一帧,要跳过的 setValues 的长度和模式始终相同。也许是这样的:

    for frame in frames: #still 760 calls
        setValues = frame.fieldOutputs['@@@fieldOutputType'].getSubset(
                    region=abaqusSet, position=ELEMENT_NODAL).values
        timeValues.append(frame.frameValue)
        # nodeSet_IDs_TF = [True, True, False, False, False, ...] same length as       
        # setValues
        filteredSetValues = ifilter(nodeSet_IDs_TF, setValues)  
        for value in filteredSetValues: # only 20k calls
            peeqValues.append(value.data)

任何其他提示也很感激,在此之后我确实想通过.append()从循环中删除“避免点”,然后将整个事情放在一个函数中,看看它是否有帮助。整个脚本已经运行了不到 1.5 小时(从 6 小时缩短到 21 小时),但是一旦开始优化,就无法停止。

内存方面的考虑也值得赞赏,我在集群上运行这些,我相信我曾经用 80 GB 的 RAM 侥幸逃脱。脚本绝对可以在 160 GB 上运行,问题是分配给我的资源。

我已经四处寻找解决方案,但也许我使用了错误的关键字,我确信这在循环中不是一个不常见的问题。

编辑 1

这是我最终使用的:

    # there is no compress under 2.6.x ... so use the equivalent recipe:
    from itertools import izip
    def compress(data, selectors):
        # compress('ABCDEF', [1,0,1,0,1,1]) --> ACEF
        return (d for d, s in izip(data, selectors) if s)
    def iterateOdb(frames, selectors): # minor speed up
        peeqValues = []
        timeValues = []
        append = peeqValues.append # minor speed up
        for frame in frames:
            setValues = frame.fieldOutputs['@@@fieldOutputType'].getSubset(
            region=abaqusSet, position=ELEMENT_NODAL).values
            timeValues.append(frame.frameValue)
            for value in compress(setValues, selectors): # massive speed up
                append(value.data)
        return peeqValues, timeValues

    peeqValues, timeValues = iterateOdb(frames, selectors)

最大的改进来自使用该compress(values, selectors)方法(整个脚本,包括 odb 部分从约 1:30 小时缩短到 25 分钟。从append = peeqValues.append以及将所有内容包含在def iterateOdb(frames, selectors):.

我使用了以下提示:https ://wiki.python.org/moin/PythonSpeed/PerformanceTips

感谢大家的回答和帮助!

4

2 回答 2

1

如果您对 itertools 没有信心,请先尝试在 for 循环中使用 if 语句。

例如。

for index, item in enumerate(values):
    if not selectors[index]:
        continue
    ...
# where selectors is a truth array like nodeSet_IDs_TF

通过这种方式,您可以更加确定自己获得了正确的行为,并将获得使用 itertools 所获得的大部分性能提升。

等效的 itertools 是compress.

for item in compress(values, selectors):
    ...

我不熟悉 abaqus,但您可以实现的最佳优化是看看是否有办法给 abaqus 您的选择器,这样就不必浪费创建每个值,而只是将其丢弃。如果 abaqus 用于对数据进行基于数组的大型操作,那么情况就是这样。

于 2014-07-06T18:56:15.900 回答
1

除了Dunes的解决方案之外的另一个变体:

for value, selector in zip(setValues, selectors):
    if selector:
        peeqValue.append(value.data)

如果要保持输出列表长度与长度相同setValue,则添加一个else子句:

for value, selector in zip(setValues, selectors):
    if selector:
        peeqValue.append(value.data)
    else:
        peeqValue.append(None)

selector这里是一个带有True/的向量False,它的长度与 相同setValues

在这种情况下,你喜欢哪一个真的是一个品味问题。如果 7600 万个节点(760 x 100 000)的完整迭代需要 30 分钟,那么时间并没有花在 python 的循环中。

我试过这个:

def loopit(a):  
    for i in range(760):
        for j in range(100000):
             a = a + 1
    return a

IPython%timeit报告循环时间为 3.54 秒。因此,循环花费了总时间的 0.1%。

于 2014-07-06T20:20:12.520 回答