1

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

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


编辑:=(

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

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

4

3 回答 3

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

实际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 启动时间。例如,上面的时间主要不是虚拟机启动,实际执行时间只有一微秒的差异,它们主要是执行时间。

于 2011-05-08T13:32:08.513 回答
4

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

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

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

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

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

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

于 2011-05-08T13:21:11.720 回答
2

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


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

于 2011-05-08T13:54:56.063 回答