【问题标题】:Encapsulation severely hurts performance?封装严重损害性能?
【发布时间】:2010-11-24 16:21:33
【问题描述】:

我知道这个问题有点愚蠢,也许它只是编写代码的一部分,但似乎定义简单的函数真的会严重损害性能......我试过这个简单的测试:

def make_legal_foo_string(x):
    return "This is a foo string: " + str(x)

def sum_up_to(x):
    return x*(x+1)/2

def foo(x):
    return [make_legal_foo_string(x),sum_up_to(x),x+1]

def bar(x):
    return ''.join([str(foo(x))," -- bar !! "])

它的风格非常好,并且使代码清晰,但它的速度可能是直接编写它的三倍。对于可能有副作用的函数来说这是不可避免的,但实际上定义一些函数几乎是微不足道的,这些函数每次出现时都应该用代码行替换,将源代码翻译成那个,然后才编译。同样,我认为对于幻数,从内存中读取并不需要很多时间,但是如果它们不应该被更改,那么为什么不在代码编译之前用文字替换每个“幻数”实例呢?

【问题讨论】:

  • 对于Encapsulation(信息隐藏),这不是通常的含义。你在问什么我打电话给Decomposition
  • -1:按照这个衡量标准,所有“结构化”都会损害性能——我们应该只使用宏生成器来创建长字符串,而不需要函数调用,并且所有循环都展开为语句序列。

标签: python performance


【解决方案1】:

您的建议不可能实现是有充分的技术原因的。在 Python 中,函数、常量和其他所有内容在运行时都可以访问,并且可以在必要时随时更改;它们也可以由外部模块更改。这是 Python 的明确承诺,需要一些非常重要的理由来打破它。

例如,这是常见的日志记录习惯用法:

# beginning of the file xxx.py
log = lambda *x: None 

def something():
    ...
    log(...)
    ...

(这里log 什么都不做),然后在其他模块或交互式提示符处:

import xxx
xxx.log = print
xxx.something()

如您所见,这里的 log 被完全不同的模块 --- 或用户 --- 修改,因此现在可以进行日志记录。如果 log 被优化掉,那将是不可能的。

同样,如果在make_legal_foo_string 中发生异常(这是可能的,例如,如果x.__str__() 被破坏并返回None),您会被来自错误的行,甚至可能来自您场景中的错误文件。

有一些工具实际上对 Python 代码进行了一些优化,但我不认为你建议的那种。

【讨论】:

    【解决方案2】:

    使用函数会降低性能,因为跳转到新地址、将寄存器压入堆栈以及最后返回会产生开销。然而,这种开销非常小,即使在性能关键系统中担心这种开销也很可能是过早的优化。

    许多语言通过使用内联在频繁调用的小函数中避免了这些问题,这基本上就是您在上面所做的。

    Python 不做内联。您可以做的最接近的事情是使用宏来替换函数调用。

    这种性能问题最好由另一种语言解决,如果您需要通过内联获得的那种速度(大部分是边际的,有时是有害的),那么您需要考虑不使用 python 来处理您正在处理的任何事情。

    【讨论】:

      【解决方案3】:

      IMO,这与Function Call Costs 有关。通常可以忽略不计,但不是零。将代码拆分为许多非常小的函数可能会造成伤害。尤其是在无法进行全面优化的解释型语言中。

      内联函数可能会提高性能,但也可能会恶化。例如,请参阅C++ FQA Lite 以获取解释(“内联可以通过消除函数调用开销使代码更快,或者通过生成太多代码来降低速度,从而导致指令缓存未命中”)。这不是 C++ 特定的。最好保留编译器/解释器的优化,除非它们真的有必要

      顺便说一句,我看不出两个版本之间有很大的不同:

      $ python bench.py 
      fine-grained function decomposition: 5.46632194519
      one-liner: 4.46827578545
      $ python --version
      Python 2.5.2
      

      我认为这个结果是可以接受的。请参阅pastebin 中的 bench.py​​。

      【讨论】:

        【解决方案4】:

        我不知道 python 编译器有多好,但是对于许多语言来说,这个问题的答案是编译器会通过内联优化对小过程/函数/方法的调用。事实上,在某些语言实现中,您通常不会尝试自己“微优化”代码,从而获得更好的性能。

        【讨论】:

        • Python 不这样做;它的编译器相当幼稚。
        • 听起来应该有人修复它。
        • 希望我能投票两次。体面的编译器将根据需要(或不需要)内联,因此无需担心。如果您使用无法做到这一点的编译器/解释器,那么您显然也不关心性能:-)。
        • 我不认为是因为编译器很幼稚,而是因为函数是可以在运行时更改的对象
        • @ooboo:一个聪明的编译器可以应付这种事情。例如,Java JIT 编译器可以处理稍后在动态加载新类时失效的优化假设。
        【解决方案5】:

        封装只关乎一件事,而且只有一件事:可读性。如果您真的很担心性能以至于愿意开始剥离封装的逻辑,那么您不妨开始在汇编中编码。

        封装还有助于调试和添加功能。考虑以下情况:假设您有一个简单的游戏,并且需要添加在某些情况下会消耗玩家健康的代码。简单,是吗?

        def DamagePlayer(dmg):
            player.health -= dmg;
        

        这是非常简单的代码,因此很容易将“player.health -=”简单地散布在各处。但是,如果稍后您想添加一个可以在激活时对玩家造成的伤害减半的强化道具怎么办?如果还是封装了逻辑,那就简单了:

        def DamagePlayer(dmg):
            if player.hasCoolPowerUp:
                player.health -= dmg / 2
            else
                player.health -= dmg
        

        现在,考虑一下您是否因为简单而忽略了封装那部分逻辑。现在你正在考虑将相同的逻辑编码到 50 个不同的地方,其中至少有一个你几乎肯定会忘记,这会导致奇怪的错误,例如:“当玩家通电时,所有伤害都会减半,除非被 AlienSheep 敌人击中。 ..”

        你想对 Alien Sheep 有问题吗?我不这么认为。 :)

        说真的,我想说的是,在适当的情况下,封装是一件非常好的事情。当然,过度封装也很容易,这同样会带来问题。此外,在某些情况下,速度确实很重要(尽管很少见),额外的几个时钟周期是值得的。找到正确平衡的唯一方法是练习。不过,不要回避封装,因为它比较慢。收益通常远大于成本。

        【讨论】:

          【解决方案6】:

          您所说的是内联函数对提高效率的影响。

          在您的 Python 示例中确实如此,封装会损害性能。但也有一些反例:

          1. 在 Java 中,定义 getter&setter 而不是定义公共成员变量不会导致性能下降,因为 JIT 内联 getter&setter。

          2. 有时重复调用函数可能比执行内联更好,因为执行的代码可能适合缓存。内联可能会导致代码爆炸...

          【讨论】:

            【解决方案7】:

            函数调用开销不大;你通常不会注意到它们。您只在这种情况下看到它们,因为您的实际代码 (x*x) 本身是如此微不足道。在任何执行实际工作的真实程序中,花费在函数调用开销上的时间量将小到可以忽略不计。

            (无论如何,我并不是真的建议在示例中使用 foo、identity 和 square;它们是如此微不足道,将它们内联起来同样可读,而且它们并没有真正封装或抽象任何东西。)

            如果它们不应该被更改,那么为什么不在代码编译之前用文字替换每个 'magic' 实例?

            因为程序的编写目的是为了易于阅读和维护。你可以用它们的字面值替换常量,但这会使程序更难使用,因为它的好处是如此之小,你甚至可能永远无法测量它: premature optimisation.

            【讨论】:

            • 我并不是要用文字替换它们。我的意思是这样的: let M = 42 def f(x): return x + M 这就是观众看到的。然后在编译之后,它应该只是将源代码中的每个 M 实例替换为 42,然后编译新代码。
            • Python 没有不可变的常量;脚本可能会出现并稍后更改“M”。 Python 是一种后期绑定语言,它会违背该语言改变它的基本方式。话虽如此,我喜欢看到像 Python 这样的脚本语言具有早期绑定(通常与函数式语言相关联)。不是出于性能原因,因为性能提升很小,而是因为它解决了闭包的一些困难。
            【解决方案8】:

            弄清楚什么是函数,什么是内联是一门艺术。许多因素(性能、可读性、可维护性)都包含在等式中。

            我实际上在很多方面都觉得你的例子有点傻——一个只返回它的参数的函数?除非它是改变规则的超载,否则它是愚蠢的。平方的功能?再说了,何必呢。您的函数 'foo' 可能应该返回一个字符串,以便可以直接使用它:

            ''.join(foo(x)," -- bar !! "])
            

            这可能是本例中更正确的封装级别。

            正如我所说,这实际上取决于具体情况。不幸的是,这种事情不适合举例。

            【讨论】:

            • 这些例子非常愚蠢,我同意。不过,为什么每次都调用 foo ?我可以将 foo 定义为不应该编译的特殊函数,每次出现时都在我的代码中“翻译”!那我就不用牺牲什么了
            • 为什么要打电话?为什么要创建函数?您可以使用剪切和粘贴来编写所有内容,并拥有一大堆代码。但这不是很可读。你在某处有一条画线,我可能会在 foo 处画它。如果性能对于给定的应用程序是一个大问题,那么我可能会按照您的建议去做。基本上,没有好的规则。像这样的玩具例子并没有给我们任何真实的东西让我们沉迷其中。这不是一个真正的功能,那么为什么性能很重要?玩具中没有足够的信息让我们做出正确的决定,它们都是任意的。
            猜你喜欢
            • 1970-01-01
            • 2011-10-13
            • 2014-06-17
            • 2010-12-17
            • 1970-01-01
            • 1970-01-01
            • 2013-08-15
            • 2011-01-10
            • 2019-12-18
            相关资源
            最近更新 更多