安卓:运行一个空方法会产生多少开销?

7

我创建了一个类来处理我的调试输出,这样我就不需要在发布之前删除所有的日志输出。

public class Debug {
    public static void debug( String module, String message) {
        if( Release.DEBUG )
            Log.d(module, message);
    }
}

在阅读另一个问题后,我了解到如果常量Release.DEBUG为false,则if语句的内容不会被编译。

我想知道运行这个空方法会产生多少开销?(一旦移除if子句,方法中就没有代码了)它会对我的应用程序产生任何影响吗?显然,在编写移动设备应用程序时,性能是一个重要问题 =P

谢谢

Gary

5个回答

14

在搭载Android 2.3.2的Nexus S上进行的测量:

10^6 iterations of 1000 calls to an empty static void function: 21s  <==> 21ns/call
10^6 iterations of 1000 calls to an empty non-static void function: 65s  <==> 65ns/call

10^6 iterations of 500 calls to an empty static void function: 3.5s  <==> 7ns/call
10^6 iterations of 500 calls to an empty non-static void function: 28s  <==> 56ns/call

10^6 iterations of 100 calls to an empty static void function: 2.4s  <==> 24ns/call
10^6 iterations of 100 calls to an empty non-static void function: 2.9s  <==> 29ns/call

控制:

10^6 iterations of an empty loop: 41ms <==> 41ns/iteration
10^7 iterations of an empty loop: 560ms <==> 56ns/iteration
10^9 iterations of an empty loop: 9300ms <==> 9.3ns/iteration

我已经多次重复测量,没有发现显著的偏差。你可以看到,每次调用的成本会根据工作负载(可能由于JIT编译)而有很大差异,但可以得出3个结论:

  1. dalvik/java在优化未使用代码方面表现较差

  2. 静态函数调用可以优化得比非静态函数调用更好(非静态函数是虚函数,需要在虚拟表中查找)

  3. Nexus S上的成本不大于70ns/调用(即约为70个CPU周期),与一个空for循环迭代(即一个本地变量的递增和一个条件检查)的成本相当

请注意,在您的情况下,字符串参数将始终被评估。如果您进行字符串连接,则会涉及创建中间字符串。这将非常昂贵并涉及大量垃圾收集。例如执行一个函数:

void empty(String string){
}

传入参数如下:

empty("Hello " + 42 + " this is a string " + count );

进行100次这样的调用的10^4次迭代需要10秒。也就是说,每个调用需要10微秒,即比空调用慢了约1000倍。它还会产生大量的GC活动。唯一避免这种情况的方法是手动内联函数,即使用>>if<<语句而不是debug函数调用。这很丑陋,但是使其工作的唯一方法。


是的,如果你没有注释掉,它真的会咬你的StringBuilder。原因是运行时JIT或AOT编译器无法提前确定字符串创建是否会失败,从而影响程序的流程。在其周围加上if很麻烦。预编译器可能是正确的方法,但现在使用Android默认工具没有简单的方法,我想。 - Lassi Kinnunen

2

除非你在一个嵌套很深的循环中调用这个函数,否则我不会太担心它。


2
一个好的编译器会移除整个空方法,从而完全没有开销。我不确定Dalvik编译器是否已经这样做了,但我认为自Froyo引入即时编译器以来,这很可能已经实现了。
另请参阅:内联扩展

获取apktool副本,对您的应用程序进行反编译,并查看它是否已从dalvik字节码中进行了优化。虽然在安装或运行时加载期间可能会进行优化,但这似乎是最明显的阶段。 - Chris Stratton
如果你反编译apk,实际上你看不到art或dalvik的优化,你只能看到java编译器和dex转换(可能还有proguard)所做的优化。你需要从更深处提取缓存的东西。 - Lassi Kinnunen

2

就性能而言,生成传递到调试函数中的消息的开销将更加严重,因为它们很可能会进行内存分配,例如

Debug.debug(mymodule, "My error message" + myerrorcode);

即使消息被删除,这种情况仍会发生。 不幸的是,如果你的目标是提高性能,你真的需要在调用该函数时加上 "if( Release.DEBUG ) ",而不是在函数内部加入。你会在很多安卓代码中看到这一点。


1

这是一个有趣的问题,我喜欢 @misiu_mp 的分析,所以我想用一台运行Android 6.0.1的Nexus 7进行2016年的测试来更新它。以下是测试代码:

public void runSpeedTest() {
    long startTime;
    long[] times = new long[100000];
    long[] staticTimes = new long[100000];
    for (int i = 0; i < times.length; i++) {
        startTime = System.nanoTime();
        for (int j = 0; j < 1000; j++) {
            emptyMethod();
        }
        times[i] = (System.nanoTime() - startTime) / 1000;
        startTime = System.nanoTime();
        for (int j = 0; j < 1000; j++) {
            emptyStaticMethod();
        }
        staticTimes[i] = (System.nanoTime() - startTime) / 1000;
    }
    int timesSum = 0;
    for (int i = 0; i < times.length; i++) { timesSum += times[i]; Log.d("status", "time," + times[i]); sleep(); }
    int timesStaticSum = 0;
    for (int i = 0; i < times.length; i++) { timesStaticSum += staticTimes[i]; Log.d("status", "statictime," + staticTimes[i]); sleep(); }
    sleep();
    Log.d("status", "final speed = " + (timesSum / times.length));
    Log.d("status", "final static speed = " + (timesStaticSum / times.length));
}

private void sleep() {
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

private void emptyMethod() { }
private static void emptyStaticMethod() { }

sleep() 函数被添加以防止 Log.d 缓冲区溢出。

我多次尝试过它,结果与 @misiu_mp 的结果非常一致:

10^5 iterations of 1000 calls to an empty static void function: 29ns/call
10^5 iterations of 1000 calls to an empty non-static void function: 34ns/call

静态方法调用始终比非静态方法调用稍快,但似乎自Android 2.3.2以来,差距已经显著缩小,并且无论是静态方法还是非静态方法,调用空方法仍然需要付出代价。然而,查看时间直方图会发现有趣的事情。大多数调用(无论是静态还是非静态)都在30-40ns之间,仔细观察数据,它们几乎都是精确的30ns。

enter image description here

使用空循环运行相同的代码(注释掉方法调用)会产生平均速度为8ns,但是约有3/4的测量时间为0ns,其余时间恰好为30ns。我不确定如何解释这些数据,但我不确定@misiu_mp的结论是否仍然成立。空静态方法和非静态方法之间的差异微不足道,并且大部分测量值恰好为30ns。话虽如此,似乎仍然存在运行空方法的一些非零成本。

如果您在调用方法中添加一个字符串创建,例如典型的调试消息为"status"+int,我认为您将看不到任何区别(即使静态方法内部未对字符串执行任何操作)。 - Lassi Kinnunen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接