为什么代理模式这么慢?

时间:2011-05-08 13:06:20

标签: java proxy-pattern

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

为什么 50次?!为什么代理不会加倍?

<小时/> 编辑:=(

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

哦,here's我的50倍来源。正如文章详细说明的那样,代理在包装需要很长时间后没有明显的损失,如果你用一个小方法包装它们确实会产生很大的痛苦。

3 个答案:

答案 0 :(得分:7)

我不知道“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 

real    0m0.894s
user    0m0.900s
sys     0m0.000s

$ time java WithProxy

real    0m0.934s
user    0m0.940s
sys     0m0.000s

$ time java WithoutProxy 

real    0m0.883s
user    0m0.850s
sys     0m0.040s

$ time java WithProxy

real    0m0.937s
user    0m0.920s
sys     0m0.030s

$ time java WithoutProxy 

real    0m0.898s
user    0m0.880s
sys     0m0.030s

$ time java WithProxy

real    0m0.936s
user    0m0.950s
sys     0m0.000s

稍慢?是。慢50倍?否。

现在,JVM的计时非常困难,如上所述的简单实验必然是可疑的。但我认为可能会出现50倍的差异。

编辑:我应该已经提到,上面有非常非常少量的循环会发布如下数字:

real    0m0.058s
user    0m0.040s
sys     0m0.020s

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

答案 1 :(得分:4)

当代码编译成本机代码时,字节数组访问将类似于3个循环指令(只要源和目标数据在高速缓存中很热,未对齐的字节访问不会受到惩罚.YMMV取决于平台)。

添加一个方法调用来存储这四个字节(取决于平台,但是类似的东西)将推送寄存器添加到堆栈,调用指令,数组访问指令,返回指令以及从堆栈中弹出寄存器。将为每个层或代理添加推/调/返回/弹出序列,并且这些指令中的大多数都不会在1个周期内执行。如果编译器无法内联这些方法(这可能很容易发生),那么你将遭受相当大的惩罚。

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

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

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

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

答案 2 :(得分:2)

我见过的一个地方有所不同的是代码没有做任何事情。 JVM可以检测不执行任何操作的代码可以消除它。但是,使用方法调用可能会混淆此检查,并且不会消除代码。如果你比较这些例子中的时间和方法,你可以得到你想要的任何比例,但是如果你看一下无方法测试的进展情况,你会发现代码已经被淘汰并且不合理地快速进行。例如比每个循环一个时钟周期快得多。


简单的方法是内联的,比如getter和setter。它们根本不会对性能产生任何影响。我非常怀疑50倍的真实节目声称。如果测试得当,我会期待更接近无差异。