A Blog About Programming, Search & User Experience

Android NDK: How to Reduce Binaries Size

ScaleWhen we started Algolia Development for Android, binary size optimization was not one of our main concerns. In fact we even started to develop in JAVA before switching to C/C++ for reasons of performance.

We were reminded of the importance of binary size by Cyril Mottier who informed us that it would be difficult to integrate our lib in AVelov Android Application because its size. AVelov is 638KB and Algolia was 850KB, which would mean that AVelov would more than double in size with Algolia Search embedded.

To address this problem we managed to reduce Algolia binary size from 850KB to 307KB. In this post we share how we did it.

Do not use Exceptions and RTTI

We actually do not use exceptions in our native lib, but for the sake of completeness, I’ll cover this point too.

C++ exceptions and RTTI are disabled by default but you can enable them via APP_CPPFLAGS in your Application.mk file and use a compatible STL, for example:

Whilst using exceptions and RTTI can help you to use existing code, it will obviously increase your binary size. If you have a way to remove them, go for it! Actually, there’s another reason to avoid using C++ exceptions: their support is still far from perfect. For example if was impossible for us to catch a C++ exception and launch a Java exception in JNI. The following code results in a crash (will probably be fixed in a future release of the NDK toolchain):

Do not use iostream

When starting to investigate our library size following Cyril’s feedback, we discovered that Algolia binaries had vastly increased in size since our last release (from 850KB to 1.35MB)! We first suspected the NDK toolchain since we upgraded it and tested different toolchains, but we only observed minor changes.

By dichotomy search in our commits, we discovered that a single line of code was responsible for the inflation:

As incredible as it may sound, using iostream increases a lot the binary size. Our tests shown that it adds a least 300KB per architecture! You must be very careful with iostream and prefer to use __android_log_print method:

Make sure you also link against the logging library, in your Android.mk file:

Use -fvisibility=hidden

An efficient way to reduce binary size is to use the visibility feature of gcc. This feature lets you control which functions will be exported in the symbols table. Hopefully, JNI comes with a JNIEXPORT macro that flags JNI functions as public. You just have to check that all functions used by JNI are prefixed by JNIEXPORT, like this one:

Then you have just to add -fvisibility=hidden for C and C++ files in Android.mk file:

In our case the binaries were down to 809KB (-5%) but remember the gains may be very different for your project. Make your own measures!

Discard Unused Functions with gc-sections

Another interesting approach is to remove unused code in the binary. It can drastically reduce its size if for example part of your code is only used for tests.
To enable this feature, you just have to change the C and C++ compilation flags and the linker flags in Android.mk:

Of course you can combine this feature with the visibility one:


This optim only got us a 1% gain, but once combined with the previous visibility one, we were down to 691KB (-18.7%).

Remove Duplicated Code

You can remove duplicated code with the –icf=safe option of the linker. Be careful, this option will probably remove your code inlining, you must check that this flag does not impact performance.

This option is not yet available on the mips architecture so you need to add an architecture check in Android.mk:

And if you want to combine this option with gc-sections:

We actually only obtained a 0.8% gain in size with this one. All previous optimizations combined, we were now at 687KB (-19.2%).

Change the Default Flags of the Toolchain

If you want to go even further, you can change the default compilation flags of the toolchain. Flags are not identical accross architectures, for example:

  • inline-limit is set to 64 for arm and set to 300 for x86 and mips
  • Optimization flag is set to -Os (optimize for size) for arm and set to -O2 (optimize for performance) for x86 and mips

As arm is used by the large majority of devices, we have applied arm settings for other architectures. Here is the patch we applied on the toolchain (version r8d):

We were good for a 8.5% gain with these new flags. Once combined with previous optimizations, we were now at 613KB (-27.9%).

Limit the Number of Architectures

Our final suggestion is to limit the number of architectures. Supporting armeabi-v7a is mandory for performance if you have a lot of floating point computation, but armeabi will provide a similar result if you do not need a FPU. As for mips processors… well they just are not in use on the market today.

And if binary size is really important to you, you can just limit your support to armeabi and x86 architectures in Application.mk:

Obviously, this optim was the killer one. Dropping two out of four architectures halved the binaries size. Overall we obtained a size of 307KB, a 64% gain from the initial 850KB (not counting the bump at 1.35MB due to iostream).

Conclusion

I hope this post will help you to reduce the size of your native libraries on Android since default flags are far from optimal. Don’t expect to obtain the same size reductions, they will highly depend on your specific usage. And if you know other methods to reduce binary size, please share in the comments!

 

  • Sylvain UTARD

    Not sure we could use any symbols/stacks if it crashes on a production
    (mobile phone actually) environment. So what about stripping your
    libraries ?

    • http://www.algolia.com/ Julien Lemoine

      Good Question, in fact the most common tips you can find on the web is to run strip -s on your librairies. But in fact the NDK toolchains automatically strip all librairies when compiled in release (default), so there is nothing to gain here:)

  • http://www.facebook.com/luke.weber.944 Luke Weber

    About limiting architectures that you support, I think a better approach is to just submit an apk for each architecture that only contains the libs for that specific architecture. In the store, it should download the apk that’s specific to that phone’s hardware based on having a libs/x86, libs/armeabi or libs/armbeabi-v7a directory present in that specific apk.

    Links:
    http://developer.android.com/google/play/publishing/multiple-apks.html – (CPU is a valid target for an apk)
    http://developer.android.com/google/play/filters.html – (Native Platform is an automatic filter for download.)
    https://plus.google.com/u/1/+AndroidDevelopers/posts/cAajFQkW9Fg (Post that they released the feature)

    • http://www.algolia.com/ Julien Lemoine

      Good approach, it would be nice if Google Play could generate these filter automatically for native libraries.

      • http://www.facebook.com/luke.weber.944 Luke Weber

        I also had a similar idea but They cant create new apks for you because you sign the contents so whatever you upload is what’s delivered.

        Also I noticed decent reduction in binary size just using clang in the newest ndks. Also ccflags contains cflags and then for large projects you can put it in you application make file like this.

        https://github.com/lukeweber/webrtc-jingle-client/blob/master/android/voice-client-core/jni/default_final.mk

        • http://www.algolia.com/ Julien Lemoine

          Indeed it can only be done by the platforms-tools and not on Google Play.

          We have tried to use clang on our usecase but clang produced bigger binaries (+20%) compared to gcc with all our flags. There is probably some flags that are not handled or have a different name under clang.

          • http://www.facebook.com/luke.weber.944 Luke Weber

            It didn’t complain about any of the flags so I think they were respected, more or less. For me, I don’t see visibility doing anything, because I’m mostly linking static libraries into my shared object, in which case, visibility isn’t respected on linked statics. They are however cleaned up after striping the lib.

            Anyways my results:

            With flags:
            Clang – 4.5M (28%)
            gcc4.6 – 5.1M (19%)

            Without flags:
            Clang – 6.0M (5%)
            gcc4.6 – 6.3M (Baseline)

  • Philippe Hétroy

    Hi Julien,

    On ARMV7 targets you may want to try -mthumb2 too?

    • http://www.algolia.com/ Julien Lemoine

      -mthumb2 is not recognized by toolchain (tested with gcc 4.6, gcc 4.7 and clang 3.1)

      • Philippe Hétroy

        Sorry, use -mthumb which triggers the use of thumb-2 instruction set on armv7
        Actually setting APP_ABI=armeabi-v7a instead of armeabi should enable that option for you but it would break armv5 compatibility too.

        • http://www.algolia.com/ Julien Lemoine

          You are right, -mthumb is already set by default in release mode for armabi-v7a (in all toolchains).

  • Gilles Vollant

    NDK now uses GCC 4.6. Did you tried option -flto (link time optimizer) ? This must reduce code size and enhance speed, but on my project, I didn’t see improvement

    • http://www.algolia.com/ Julien Lemoine

      I just tested it with our code, I got a nice ICE (Internal Compiler Error)with GCC 4.6.

      With GCC 4.7 I had no ICE but my binaries are bigger (-flto passed as argument to compiler and linker)

      • Gilles Vollant

        code is sometime bigger and faster… also, did you compare gcc and clang?

        • Gilles

          On my mac xcode test, clang flto reduce size