【问题标题】:Android: How much overhead is generated by running an empty method?Android:运行一个空方法会产生多少开销?
【发布时间】:2011-05-19 18:03:37
【问题描述】:

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

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

看了另一个问题,得知如果常量Release.DEBUG为false,if语句的内容是不编译的。

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

谢谢

加里

【问题讨论】:

    标签: java android performance overhead


    【解决方案1】:

    除非您从深度嵌套的循环中调用它,否则我不会担心。

    【讨论】:

      【解决方案2】:

      一个好的编译器会删除整个空方法,根本不会产生任何开销。我不确定 Dalvik 编译器是否已经这样做了,但我怀疑它很可能,至少在带有 Froyo 的即时编译器到来之后。

      另请参阅:Inline expansion

      【讨论】:

      • 获取一份 apktool 的副本,反编译您的应用程序,看看它是否已从 dalvik 字节码中优化。在安装或运行时加载期间可以在稍后阶段对其进行优化,但这似乎是最明显的一个。
      • 如果您反编译 apk,您实际上看不到 art 或 dalvik 优化,您只会看到 java 编译器和 dex 转换器(可能还有 proguard)所做的优化。您需要从内部提取缓存的内容。
      【解决方案3】:

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

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

      即使邮件被分箱,这种情况仍然会发生。 不幸的是,如果您的目标是性能,那么您确实需要在调用此函数周围而不是在函数本身内部使用“if(Release.DEBUG)”,并且您会在很多 android 代码中看到这一点。

      【讨论】:

        【解决方案4】:

        在装有 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 的成本不大于 70ns/调用(即 ~70 个 cpu 周期) 并且与一次空 for 循环迭代的成本相当(即对局部变量进行一次增量和一次条件检查)

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

        void empty(String string){
        }
        

        使用诸如

        之类的参数调用
        empty("Hello " + 42 + " this is a string " + count );
        

        100 次此类调用的 10^4 次迭代需要 10 秒。那是 10us/呼叫,即比空呼叫慢约 1000 倍。它还会产生大量的 GC 活动。避免这种情况的唯一方法是手动内联函数,即使用 >>if

        【讨论】:

        • 是的,如果您没有注释,那真的是字符串生成器会咬您。原因是运行时 jit 或 aot 编译器无法提前确定字符串创建是否会失败,影响程序的流程。如果周围很乱。预编译器将是正确的方法,但我认为现在使用 android 默认工具并没有简单的方法来做到这一点。
        【解决方案5】:

        这是一个有趣的问题,我喜欢@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
        

        静态方法调用总是比非静态方法调用稍快,但似乎 a) 自 Android 2.3.2 以来差距已大大缩小,b) 调用空方法仍然需要成本,静态与否。

        然而,查看时间直方图会发现一些有趣的事情。大多数调用,无论是否静态,都需要 30-40ns,仔细观察数据,它们几乎都是 30ns。

        使用空循环运行相同的代码(注释掉方法调用)会产生 8ns 的平均速度,但是,大约 3/4 的测量时间是 0ns,而其余时间正好是 30ns。

        我不确定如何解释这些数据,但我不确定@misiu_mp 的结论是否仍然成立。空静态和非静态方法之间的差异可以忽略不计,测量的优势正好是 30ns。话虽如此,运行空方法似乎仍有一些非零成本。

        【讨论】:

        • 如果你在调用方法中添加一个字符串创建,就像一个典型的调试消息是“status”+int,我想你根本看不出有什么区别(即使没有对字符串做任何事情在静态方法中)。
        猜你喜欢
        • 2010-10-07
        • 2014-07-15
        • 2020-01-04
        • 2014-02-17
        • 2021-07-17
        • 1970-01-01
        • 1970-01-01
        • 2015-09-12
        • 2017-05-12
        相关资源
        最近更新 更多