October 21, 2019

    Comparison of ProGuard vs. R8: October 2019 edition

    Android developers often ask us what the difference is between ProGuard and R8. In a previous blog, we looked at ProGuard and an early version of R8. The programs have since evolved, so it's time for an update. Both programs still optimize Java bytecode. They remove unused classes, fields, and methods, and they optimize the method bodies for size and for performance. R8 can optionally convert the code to Dalvik bytecode, while ProGuard is typically combined with Android's standard Dalvik converter D8. The short version of the post is that both ProGuard and R8 have similar features, with some distinct advantages for ProGuard. As the default in the Android Gradle plugin, many new developers may have more exposure to R8. However, app developers seeking extensive support backporting and help with configuration problems will likely find ProGuard a better fit.

    Enabling optimization

    R8 is now the default in the Android Gradle plugin when you enable enable minification in the 'build.gradle' file of your project:

    minifyEnabled true
    shrinkResources true
    proguardFile getDefaultProguardFile('proguard-android-optimize.txt')
    proguardFile 'proguard-rules.pro'

    You can enable ProGuard in favor of R8 by also adapting 'gradle.properties':

    android.enableR8=false
    android.enableR8.libraries=false
    

    Optimizations

    We've applied ProGuard and R8 to our suite of 2200 small optimization tests, to get a feel for their strengths. Looking at the expected optimizations, they still turn out to be similar:

    Optimization ProGuard R8
    Remove unused classes/fields/methods x x
    Inline constants x x
    Propagate constants x x
    Remove unused code x x
    Propagate constant arguments x  
    Propagate constant fields x x
    Remove write-only fields x x
    Make classes/fields/methods final/... x  
    Simplify plain enum types x x
    Simplify basic container classes   x
    Merge interfaces with single implementations x x
    Merge classes x x
    Remove unused parameters x x
    Propagate constant return values x x
    Make methods private x  
    Make methods static x  
    Desynchronize methods x  
    Simplify tail recursion x  
    Inline methods x x
    Outline common code into new methods   x
    Merge code x x
    Backport Java 8 closures x x
    Optimize Kotlin constructs   x
    Optimize use of GSON x  
    Remove logging calls x x
    Remove logging code x /
    Peephole optimizations 520 6

    R8 is more effective in inlining container classes and slightly more aggressive in removing unused classes, fields, and methods. For example, processing ProGuard 6.1.1 itself, R8 achieves a size reduction of about 10%, compared to ProGuard's 8.5%. The differences vary and are small. With Kotlin now being a first-class language for Android, R8 has some Kotlin-specific optimizations that aren't in ProGuard yet. Knowing the importance of Kotlin code to the future of Android development, the ProGuard team is currently developing new features for Kotlin support.

    On the other hand, ProGuard is still stronger in simplifying enum types to primitive integers. It also propagates constant method arguments, which is typically useful for generic libraries that are called with specific settings for the app. ProGuard's multiple optimization passes can then often produce a cascade of optimizations. A first pass can, for example, propagate a constant method argument, so that a next pass can remove the argument and further propagate the value. The effect of multiple passes is notably visible when removing logging code. ProGuard is more effective at removing all traces, including the string operations that compose logging messages.

    ProGuard also applies 520 peephole optimizations, versus the six R8 currently offers. A pattern matching algorithm can identify and replace short instruction sequences, improving the efficiency of the code and opening up opportunities for more optimizations. Especially mathematical operations and string operations can benefit from it, in the sequence of optimization passes.

    Finally, ProGuard has the unique ability to optimize code that uses the GSON library to serialize or deserialize objects to JSON. The library heavily relies on reflection, which is convenient but somewhat inefficient. ProGuard's optimization can replace it by more efficient, direct access.

    Usability

    Java 8 introduced the stream API and the date/time API to help Java developers be more productive. Since older Android devices don't support these APIs, Android developers needed additional support to leverage them. R8 can backport the stream API, and in the early preview of Android Studio 4.0 also the date/time API. ProGuard has already been backporting both APIs for a while now.

    Reflection is a known hurdle for any code analysis and processing. If an app accesses classes, fields or methods through some form of reflection at run-time, removing or renaming them can break the proper functioning of the app. As a developer, you then need to create a ProGuard/R8 configuration with the right '-keep' options. This may require some effort and expertise, which may seem daunting. ProGuard has an edge here, by printing out suggestions at build-time, and with an option '-addconfigurationdebugging' to let the app print out suggestions at run-time. In our experience, this option is invaluable to come to a working configuration as quickly as possible. You can read more details in our blog, ProGuard configuration made easy.

    When debugging the processed app, ReTrace is the standard tool to convert obfuscated stack traces back to readable stack traces. ProGuard continues to be stronger in producing correct debug information for inlined methods, for which a single line in a stack trace from optimized code may need to be expanded to multiple lines that are actually in the source code.

    Performance

    Build performance was a major argument for integrating ProGuard's functionality in R8. Merging different tools avoids the internal overhead of writing and then reading intermediary jar files. The code optimization is also complex and time-consuming. We've again compared processing ProGuard 6.1.1 itself, as Java bytecode. We're only considering the optimization, not the conversion to Dalvik bytecode by D8 or inside R8 itself, which also takes non-trivial time.

    ProGuard (1 pass)         11 seconds
    ProGuard (5 passes)       37 seconds
    R8                        32 seconds


    With a single optimization pass, ProGuard is a lot faster than R8. With the default setting of 5 optimization passes in the Android Gradle plugin, ProGuard may take longer. However, with ProGuard, you can easily reduce the number of passes in your configuration ("-optimizationpasses 1"), if short build times are important.

    Stability

    We have been developing ProGuard for more than 15 years now, and the Android team is actively developing the much younger R8.

    However, ProGuard and R8 both have issues open on their respective bug trackers, with both teams actively working to resolve them. In our experiments, our test suite crashed R8 on 3 tests out of 2200. The processed results of R8 were incorrect for 5 tests. This is not so bad, since our test suite contains obscure constructs that we have collected over the years.

    Obfuscation

    ProGuard and R8 both apply basic name obfuscation: they rename classes, fields, and methods with short, meaningless names. They can also remove debugging attributes. For more advanced app protection, you can of course turn to our software DexGuard. It optimizes and protects the entire Android app, including bytecode, native code, resources, and assets.

    Conclusions

    On the surface, ProGuard and R8 are quite similar. They both use the same configuration, so switching between them is easy. Zooming in closer, there are some differences. R8 is better at inlining container classes, thus avoiding object allocations. ProGuard is better at simplifying enum types and propagating values, creating more opportunities for optimization in subsequent steps. Most importantly in our experience, ProGuard offers more extensive support for backporting and more help with configuration problems. Its option '-addconfigurationdebugging' provides invaluable feedback at run-time about possibly missing configurations. If you want to create a working configuration fast, you should definitely give this feature a go.

    Download ProGuard today to see the difference for yourself.

    Eric Lafortune - Creator of ProGuard

    Get proactive about advanced mobile app protection.

    Download our PDF for a quick comparison of ProGuard & DexGuard >

    Other posts you might be interested in