【问题标题】:Trade off between code duplication and performance代码重复和性能之间的权衡
【发布时间】:2015-10-19 16:49:41
【问题描述】:

Python 作为一种动态语言,提供了多种方式来实现相同的功能。这些选项在可读性、可维护性和性能方面可能会有所不同。尽管我用 Python 编写的常用脚本是一次性的,但我现在有一个我正在从事的(学术)项目,该项目必须可读、可维护并且性能相当好。由于我之前没有在 Python 中进行过任何认真的编码,包括任何类型的分析,所以我需要帮助来决定我上面提到的三个因素之间的平衡。

这是我正在研究的科学包中的一个模块的代码 sn-p。它是一个具有非常基本骨架结构的 n 元树类。这是在考虑继承和子类的情况下编写的。

注意:在下面的代码中,树与节点相同。每棵树都是同一类树的一个实例。

class Tree(object):

    def __init__(self, parent=None, value=None):
        self.parent = parent
        self.value = value
        self.children = set()

以下两个函数属于这个类(以及许多其他函数)

    def isexternal(self):
        """Return True if this is an external tree."""
        return not bool(self.children)

    def isleaf(self):
        """Return True if this is a leaf tree."""
        return not bool(self.children)

这两个函数的作用完全相同——它们只是两个不同的名称。那么,为什么不将其更改为:

    def isleaf(self):
        """Return True of this is a leaf tree."""
        return self.isexternal()

我的疑惑是:

我读过 Python 中的函数调用相当昂贵(为每次调用创建新堆栈),但我不知道如果一个函数依赖于另一个函数是好事还是坏事。它将如何影响可维护性。这在我的代码中发生了很多次,我从另一种方法调用一个方法以避免代码重复。这样做是不好的做法吗?

这是同一类中这种代码重复场景的另一个示例:

def isancestor(self, tree):
    """Return True if this tree is an ancestor of the specified tree."""
    return tree.parent is self or (not tree.isroot() 
        and self.isancestor(tree.parent))

def isdescendant(self, tree):
    """Return True if this tree is a descendant of the specified tree."""
    return self.parent is tree or (not self.isroot() 
        and self.parent.isdescendant(tree))

我可以选择:

def isdescendant(self, tree):
    """Return True if this tree is a descendant of the specified tree."""
    return tree.isancestor(self)

【问题讨论】:

    标签: python performance optimization code-duplication


    【解决方案1】:

    非常广义地说,有两种优化:宏观优化微观优化。宏优化包括您选择的算法、决定不同的数据结构等。 会对性能产生重大影响的事情,如果您改变主意,通常会对您的代码库产生巨大的连锁反应。从具有线性 O(n) 的数据结构切换到具有恒定 O(1) 插入的数据结构可能是一个巨大的胜利,并且值得为此付出代价。添加缓存可能会将狗慢速算法变成闪电般的快速算法。

    微优化是诸如省略或内联函数调用、消除或添加变量、缓存非常短的窗口的计算结果、展开循环等。通常,您应该忘记这些类型的优化并关注关于代码的可读性和可维护性。微优化的效果太小了,不值得。

    您应该只考虑这些类型的更改在分析您的代码之后。如果您可以确定一个可以从这种优化中受益的关键循环,并且您的分析确认它会,并且您进行更改并验证改进在另一轮分析中是否有效-然后 你应该微优化。

    但在那之前,不要为小事操心。

    def isdescendant(self, tree):
        """Return True if this tree is a descendant of the specified tree."""
        return tree.isancestor(self)
    

    我绝对推荐这种类型的代码重用。它清楚地表明isdescendantisancestor 的倒数。它可确保两个函数以相同的方式工作,因此您不会无意中在其中一个中引入错误,而不会在另一个中引入错误。

    def isleaf(self):
        """Return True of this is a leaf tree."""
        return self.isexternal()
    

    在这里我会问自己isleafisexternal 在概念上是否相同。忽略它们的实现相同,它们在逻辑上是否相同?如果是这样,我会打电话给另一个。如果它们具有相同的实现只是偶然,我可能会复制代码。你能想象一个你想改变一个功能而不改变另一个功能的场景吗?这将指向重复。

    【讨论】:

    • 在 DonaldKnuth 的论文“StructuredProgrammingWithGoToStatements”中,他写道:“程序员浪费了大量时间来思考或担心他们程序中非关键部分的速度,而这些效率上的尝试实际上具有很强的考虑到调试和维护时的负面影响。我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源。但我们不应该放弃关键的 3% 的机会。”跨度>
    • 我开始使用 C 和汇编进行编程。毫不夸张地说,我花了好几年的时间才不再担心高级语言中所有的低效率小问题。没有理由将这 10 个字符的字符串循环 3 次,我可以手动循环并一次完成!我的天,这个循环很短,但它会将整个 2KB 配置文件读入内存。那不会缩放!我会用最大 4KB 的缓冲区重写它并分块处理文件,这样我就可以处理 100MB 的配置文件......理论上的 CS / 嵌入式系统背景确实会扭曲一个人的优先级。
    • 不要不同意大局的东西,但是很多微优化并不会特别降低代码的可读性或需要更长的时间,如果这是你通常编写代码的方式,那么当你去优化它时,你的代码看起来不会有太大的不同。
    • @PatrickMaupin 如果它更快且可读性不差,那么当然要这样做!
    • 我要补充一条建议:当已经存在好的现有实现时,不要自行开发。假设他们获得了一些中等水平的使用并具有可接受的性能,这将是可读性、可维护性和通常性能的巨大胜利。此处介绍的 OP 的特定用例可能不是这种情况,但可能适用于程序的其他部分。尤其是如果 OP 主要关注简短的一次性脚本,他们可能不知道那里有大量可用的 Python 库。
    【解决方案2】:

    这种方法在不引入额外堆栈帧的情况下效果很好。

    def isexternal(self):
        """Return True of this is an external tree."""
        return not bool(self.children)
    
    isleaf = isexternal
    

    在第二种情况下,两种方法的算法根本不同。我认为您提出的解决方案很好。

    【讨论】:

    • 这是一个非常好的解决方案。您能否建议如何处理第二个示例代码?与此相比,这是一个不同的案例。
    • 我认为你在第二种情况下已经做得很好了。在我的回答中添加了一条说明。
    【解决方案3】:

    只是一个小测试:

    >>> timeit('a()', setup="def a(): pass")
    0.08267402648925781
    >>> timeit('1+1')
    0.03854799270629883
    

    因此,与简单的算术表达式相比,简单的函数调用的运行时间不到 2.5 倍。我不认为,它算作“相当昂贵”。

    【讨论】:

    • 我的代码中的这些函数将根据Tree 的深度被多次调用。因此,我认为时间会增加。
    • timeit 运行代码 1,000,000 次以测量运行时间。无论如何,与算术运算相比,因子不会改变。
    • 我认为timeit 将总时间除以 1,000,000 - 我的错误。如果是这样的话,那么时差就不是问题了。
    【解决方案4】:

    大卫·赫斯的回答很好……

    不过,说not bool(x) 既不是最佳的也不是规范的 Python。

    not x 给出完全相同的结果,而且是一次全局查找和一次函数调用更便宜。

    另外,如果您在同一个调用中使用了两次self.parent,您可能会考虑是否要将其放在本地——parent = self.parent——因为本地查找比实例查找便宜很多。当然,您应该运行 timeit 以确保您得到好处。

    【讨论】:

      猜你喜欢
      • 2016-02-17
      • 1970-01-01
      • 2011-07-23
      • 2021-09-05
      • 2010-12-02
      • 2019-01-09
      • 1970-01-01
      • 1970-01-01
      • 2018-10-03
      相关资源
      最近更新 更多