【问题标题】:Why is the proxy pattern so slow?为什么代理模式这么慢?
【发布时间】:2011-05-08 13:06:20
【问题描述】:

至少在 java 中,代理模式有很多开销——我不记得确切的数字,但是当包装微小的方法时,代理需要的时间大约是包装方法的 50 倍。这就是为什么java.awt.image.BufferedImage.setRGB&getRGB真的慢的原因;大约有三个代理包装了实际的byte[]

为什么50次?!为什么代理不只是加倍时间?


编辑:=(

对于 SO 来说似乎很平常,我得到了一堆答案,告诉我我的问题是错误的。它不是。查看 BufferedImage 或其他一些真正的代理模式,而不是那些微基准。事实上,如果您必须对 BufferedImage 进行大量像素操作并且您知道它的结构,那么您可以通过手动撤消代理来实现上述巨大的加速;见this answer

哦,还有here's 我的 50 倍来源。正如文章所详述的那样,代理在包装需要很长时间时不会受到明显的惩罚,但是如果你包装一个小方法,它们确实会有很大的痛苦开销。

【问题讨论】:

  • “我不记得确切的数字,但是当包装微小的方法时,代理需要的时间大约是包装方法的 50 倍。” 对不起,你会需要为此引用参考。听起来你在重复一些 Java 仇恨者的 FUD。
  • 我在某处读到 Java 代理比本地代码快 50 倍。那里!问题解决了。
  • @Vuntic:尝试解决您提出的问题的人(显然是通过和大规模投票)是幼稚的,说得好听点。您采用了一个特定的代理并从中得出了完全错误的结论。如果您不喜欢被指出来,我很抱歉。
  • @vuntic:并且你歪曲了那篇文章所说的。注意:“在我们继续之前,让我们首先意识到这里添加的开销是固定的。如果 doIt() 方法本身需要 5 秒,则绝对不是的情况,代理调用需要 50 倍的时间。不,相反,调用需要 5 秒 + 约 500 纳秒。” (他的重点,不是我的)
  • T.J. 中的方法下面的克劳德的例子,很小,几乎可以用任何你可以提供的“微小”定义。他的时间安排几乎说明了整个故事。

标签: java proxy-pattern


【解决方案1】:

我不知道“50 次”这个数字是从哪里来的,但这很可疑。 一个特定的代理可能比它所代理的要慢得多,这取决于他们每个人在做什么,但概括地说“代理模式太慢”是要采取逻辑上非常戏剧性和高度可疑的飞跃。

试试这个:

Thingy.java:

public class Thingy
{
    public int foo(int param1, int param2)
    {
        return param2 - param1;
    }
}

ThingyProxy.java:

public class ThingyProxy
{
    Thingy thingy;

    public ThingyProxy()
    {
        this.thingy = new Thingy();
    }

    public int foo(int param1, int param2)
    {
        return this.thingy.foo(param1, param2);
    }
}

WithoutProxy.java:

public class WithoutProxy
{
    public static final void main(String[] args)
    {
        Thingy t;
        int sum;
        int counter;
        int loops;

        sum = 0;
        t = new Thingy();
        for (loops = 0; loops < 300000000; ++loops) {
            sum = 0;
            for (counter = 0; counter < 100000000; ++counter) {
                sum += t.foo(1, 2);
            }
            if (sum != 100000000) {
                System.out.println("ERROR");
                return;
            }
        }
        System.exit(0);
    }
}

WithProxy.java:

public class WithProxy
{
    public static final void main(String[] args)
    {
        ThingyProxy t;
        int sum;
        int counter;
        int loops;

        sum = 0;
        t = new ThingyProxy();
        for (loops = 0; loops < 300000000; ++loops) {
            sum = 0;
            for (counter = 0; counter < 100000000; ++counter) {
                sum += t.foo(1, 2);
            }
            if (sum != 100000000) {
                System.out.println("ERROR");
                return;
            }
        }
        System.exit(0);
    }
}

在我的机器上进行简单的试验:

$ time java withoutProxy

实际0m0.894s
用户 0m0.900s
系统 0m0.000s

$ 时间 java WithProxy

实际0m0.934s
用户 0m0.940s
系统 0m0.000s

$ time java withoutProxy

实际0m0.883s
用户 0m0.850s
系统 0m0.040s

$ 时间 java WithProxy

真正的 0m0.937s
用户 0m0.920s
系统 0m0.030s

$ time java withoutProxy

实际0m0.898s
用户 0m0.880s
系统 0m0.030s

$ 时间 java WithProxy

实际0m0.936s
用户 0m0.950s
系统 0m0.000s

有点慢?是的。慢 50 倍?没有。

现在,JVM 的计时非常困难,像上面这样的简单实验肯定是值得怀疑的。但我认为可能会出现 50 倍的差异。

编辑:我应该提到,上面的循环数量非常非常少,会发布这样的数字:

真正的 0m0.058s
用户 0m0.040s
系统 0m0.020s

...它让您了解环境中的 VM 启动时间。例如,上面的时间主要不是虚拟机启动,实际执行时间只有一微秒的差异,它们主要是执行时间。

【讨论】:

  • 我刚刚在运行 Ubuntu 的 6 核 AMD 和 2 核 Mac OS X 笔记本电脑上进行了基本相似的测试。我的结果与上述一致。
  • 我喜欢这个答案,声称有测试支持。
  • 不错的测试;您可以在不包括 VM 启动和 JITing 的情况下重新运行它吗?此外,有问题的代理不仅仅是一个愚蠢的代理,它增加了相当多的功能
  • @Henrik:我的观点是,从具体到普遍地说“代理模式很慢”是一种不合理的飞跃。由于您有能力指出的原因,某些代理会非常慢。一旦 JIT 完成,一些代理从性能角度来看将是完全透明的。而且我认为消除 JIT 没有任何意义,它的存在是有原因的。我会在问题中注明虚拟机启动时间,我应该指出:它被测试中的操作所淹没,但你不知道我为什么知道。
  • 哦,我不是说要消除JIT,只是编译的开销和编译前的解释代码的使用已经完成。但是再次阅读您的代码可能不会引起注意,因为您运行了如此多的迭代
【解决方案2】:

当代码被编译为本机代码时,字节数组访问将类似于 3 个 1 周期指令(只要源和目标数据在缓存中是热的并且未对齐的字节访问不会受到惩罚。YMMV 取决于平台)。

添加一个方法调用来存储四个字节将(取决于平台,但类似这样)将推送寄存器添加到堆栈、调用指令、数组访问指令、返回指令和从堆栈中弹出寄存器。将为每个层或代理添加 push/call/return/pop 序列,并且这些指令大多不会在 1 个周期内执行。如果编译器未能内联这些方法(这很容易发生),您将面临相当严重的惩罚。

代理添加了在颜色深度等之间进行转换的功能,从而增加了额外的开销。

此外,编译器还可以进一步优化顺序数组访问(例如,将存储操作转换为多字节访问操作 - 一次最多 8 位,同时仍然只需要 1 个周期),而代理调用则很难做到这一点。

50x 听起来有点高,但并非不合理,具体取决于实际代码。

BufferedImage 尤其会增加大量开销。虽然代理模式本身可能不会增加明显的开销,但使用 BufferedImage 可能会增加。请特别注意 setRGB() 是同步的,这在某些情况下可能会对性能产生严重影响。

【讨论】:

  • 或者 JIT 可以内联调用,从而消除代理的所有开销。
  • @downvoter:您愿意与 Henrik 分享一些建设性的反馈吗?
  • 它可以而且可能,只要它只是一个没有附加功能的代理调用。编译器永远不会内联超过一定大小的方法(可以调整,但我怀疑它在询问者的情况下)。而且它也可能因为其他原因没有内联它。
  • 不内联代码并删除所有边界检查的唯一方法是 JIT 炸毁内联缓存;最近 Cliff Click 解决了关于内联的问题。拥有单一目标代理几乎肯定意味着完全内联。 azulsystems.com/blog/cliff/…
【解决方案3】:

我看到他们有所作为的一个地方是代码没有做任何事情。 JVM 可以检测到不执行任何操作的代码,可以将其消除。但是,使用方法调用可能会混淆此检查,并且不会消除代码。如果您在此类示例中比较使用和不使用方法的时间,您可以获得任何您想要的比率,但是如果您查看无方法测试的进展情况,您会发现代码已被消除并且运行速度不合理.例如比每个循环一个时钟周期快得多。


简单的方法是内联的,例如 getter 和 setter。它们根本不会对性能产生影响。我非常怀疑真正程序的 50 倍要求。如果测试得当,我希望更接近无差异。

【讨论】:

  • @downvoter:您愿意与 Peter 分享一些建设性的反馈吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-04-29
  • 2018-07-22
  • 2011-04-24
  • 1970-01-01
  • 2017-01-10
  • 2011-08-31
  • 1970-01-01
相关资源
最近更新 更多