Android NDK: How to Reduce Binaries Size - The Algolia Blog

When 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 APPCPPFLAGS_ in your Application.mk file and use a compatible STL, for example:

APP_CPPFLAGS += -fexceptions -frtti
APP_STL := stlport_shared

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 Android NDK toolchain):

try {
    ...
} catch (std::exception& e) {
    env->ThrowNew(env->FindClass("java/lang/Exception"), "Error occured");
}

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 Android 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:

std::cerr << .... << std::endl;

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 _androidlog_print method:

#include <android/log.h>
#define APPNAME "MyApp"

__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "The value of 1 + 1 is %d", 1+1);

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

LOCAL_LDLIBS := -llog

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:

JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)

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

LOCAL_CPPFLAGS += -fvisibility=hidden
LOCAL_CFLAGS += -fvisibility=hidden

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:

LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections 
LOCAL_LDFLAGS += -Wl,--gc-sections

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

LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections -fvisibility=hidden
LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections -fvisibility=hidden
LOCAL_CFLAGS += -ffunction-sections -fdata-sections  LOCAL_LDFLAGS += -Wl,--gc-sections

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:

ifneq ($(TARGET_ARCH),mips)
  LOCAL_LDFLAGS += -Wl,--icf=safe
endif

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

ifeq ($(TARGET_ARCH),mips)
  LOCAL_LDFLAGS += -Wl,--gc-sections
else
  LOCAL_LDFLAGS += -Wl,--gc-sections,--icf=safe
endif

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):

--- android-ndk-r8d/toolchains/mipsel-linux-android-4.6/setup.mk
+++ android-ndk-r8d.new/toolchains/mipsel-linux-android-4.6/setup.mk
@@ -41,12 +41,12 @@
 TARGET_C_INCLUDES := 
     $(SYSROOT)/usr/include

-TARGET_mips_release_CFLAGS := -O2 
+TARGET_mips_release_CFLAGS := -Os 
                               -g 
                               -DNDEBUG 
                               -fomit-frame-pointer 
                               -funswitch-loops     
-                              -finline-limit=300
+                              -finline-limit=64

 TARGET_mips_debug_CFLAGS := -O0 
                             -g 
--- android-ndk-r8d/toolchains/x86-4.6/setup.mk
+++ android-ndk-r8d.new/toolchains/x86-4.6/setup.mk
@@ -39,13 +39,13 @@

 TARGET_CFLAGS += -fstack-protector

-TARGET_x86_release_CFLAGS := -O2 
+TARGET_x86_release_CFLAGS := -Os 
                              -g 
                              -DNDEBUG 
                              -fomit-frame-pointer 
                              -fstrict-aliasing    
                              -funswitch-loops     
-                             -finline-limit=300
+                             -finline-limit=64

 # When building for debug, compile everything as x86.
 TARGET_x86_debug_CFLAGS := $(TARGET_x86_release_CFLAGS)

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:

APP_ABI := armeabi x86

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!

Simplicity is the most Complex Feature!

I've been convinced for a long time that simplicity is the most important property of a product. Long-gone are the 90s when a product was admired for its complexity. But I am also convinced that this is the most complex property to achieve and maintain as time passes by.

A good example of an over-complex product is Atlassian JIRA, a bug tracker that also do scrum management and plenty of other things via dozens of plugins. It's basically a toolbox to create the bug tracker adapted to your company.

In my previous job, I faced an uncomfortable situation with JIRA because of its complexity. We used it for bug tracking and scrum management and I tried to upgrade our old version to the latest one. After some long hours to upgrade our setup on a test server, I finally got the latest version working but most of our installed plugins were not available anymore because the authors did not port their plugins to the new plugin API. Of course each plugin was there for a reason and I was in a tricky situation: keep the old version with security issues or upgrade to a new version without our plugins.

But it was far more vicious: There were about 10 versions between our old version and the latest one, and I didn't find any of these versions working with our set of plugins! In the end, we were forced to keep our old version.

Atlassian forgot the most important lesson, even with a toolbox: simplicity! This is probably more expensive for them to keep plugin backward compatibility, but I would prefer for them to not have any plugin rather than breaking compatibility at each release. The final system is too complex to be maintainable and our final decision was to stop paying for JIRA support since we were blocked with an old release. It is even bad for their business.

You should be focused on simplicity for your users even it this results in more complexity for you (like maintaining backward compatibility)! As this post is strongly related to backward compatibility of API, I encourage you to reread this famous post of Jeol Spolsky: How microsoft lost the API war.

Never, Ever, Hinder the Use of your Products!

One of the worst user experiences I have ever had with software was with the Sony PS3. I kind of liked this product; I found the user interface very nice and well organized... but they were much too aggressive about upgrades! They simply blocked features until the upgrade was done!

A few weeks ago I wanted to watch a VOD movie with my wife. I launched the Playstation Store that asked me to upgrade the OS to the latest version. That's 45 minutes before being able to access the Playstation again! But wait! Once the new OS was installed, I tried to launch  the Playstation Store again... This time, it was the Playstation Application that was not up to date !

In total it took me over 1 hour to do upgrades and guess what, at the end it was just too late to watch the movie!

Generally, frequent upgrades are good for your users, and I am sure there are plenty of bug fixes/improvments in the lastest version. But Sony has just made the wrong choice in blocking features until the upgrade is done. This is just plain frustrating for users! On the contrary, Android and iOS propose an upgrade that you can apply when you want. Best of all, they download in the background.

It may sound evident, but it is very important to ensure your users will always be able able to keep control over their products. You should never force them to do something they do not want to, like Sony did with the PS3.

Algolia Search is now in Beta for Microsoft Windows 8 Desktops and Tablets - The Algolia Blog

You may have been expecting Windows Phone 8 as our next platform of choice, but we preferred to go Windows 8 first! With more than 40M sold in a month, let's say it's a slightly more interesting market for now.

Windows 8: Our First Move to the Desktop

Yes, desktop apps are not as dead as many people think, especially now that both Mac OSX and Windows come with their app stores. Sure, Windows is now running on tablets - and we also support them - but it will take time before they surpass desktops.

What's more, the new Windows 8 Metro Modern UI includes a charm bar with search and autocompletion as the number one feature. Microsoft actually provides an interface to ease application integration in the charm search bar... but no library to help you implement it. No problem, here comes Algolia Search for Windows 8!

Awesome Features for Your Apps

The core technology is exactly the same as the one in our iOS and Android versions. That means you'll get exactly the same exceptional performance... well probably even better depending of the hardware you're running on. You'll also get instant visual feedback, typo-tolerance and multiple attributes support! See all the features on our website.

Easy to Integrate in your Language of Choice

Check out the dedicated tutorials! The Algolia Search library has been designed to work directly on the platform. That means you can use it in whatever language you prefer, be it JS or one of the .Net choices (C#, VB, C++). The tutorials are available in both JS and in C#.

Moreover it is fully compatible with Microsoft Search Contracts. It just became child's play to integrate Search in your Windows app!

Hey Microsoft, There are a Couple of Features You Could Improve

Windows 8 is clearly a step in the right direction, but we encountered some problems we didn't expect!

First is the multi-arch support. While it's just straightforward with iOS or Android (out of the JNI part...), using a native lib forces the developer to chose an architecture on Windows! Fortunately, Visual Studio Package Generator handles that correctly and proposes you to select all architectures for your final export. But well... we'd have preferred it plain and simple.

Our second deception is a bit more problematic as it prevents one of our very nice features: typo-tolerant highlighting during autocompletion. It starts with best of intentions from Microsoft. To remove the burden of implementing highlight for autocompletion on developers, Windows 8 handles it directly. The only problem is that it cannot be replaced by our own :( So you can obtain hits even if your query contains typos, but hits containing text different from the query won't be highlighted.

We hope to be able to change this behaviour in the future. In the meantime, we're impatient to get your feedback on this beta! Register for your Windows 8 version of Algolia Search now!

Smart Contacts Demo Hits the App Store!

After going back and forth a few times with Apple, we are really happy to announce the availibility of our Smart Contacts application on the App Store! Even if we built it to demonstrate the amazing features of our search technology SDK, it can be used as an in-place replacement of the traditional iPhone Contacts app. Check it up!

Smart Contacts demo hits the App Store

Outstanding Search Features

It plugs directly into your iPhone contacts and classically allows you to update them or create new ones. What's more interesting is the Search integration! It features:Smart Contacts Screenshot

  • Instant search with autocompletion and visual feedback as you type
  • Search on all contacts attributes (name, company, notes, etc.)
  • Typo tolerance to account for smartphone small keyboards
  • And even search by initials!

iOS API Limitations

There are a few of features we would have loved to integrate that were not possible due to limitations in iOS APIs: group management, in place replacement of contacts in the phone app (that's maybe a lot to ask!), and ranking based on contact popularity.

Free your Imagination!

If you ever wanted to see Algolia Search in action, now is the time. Check it up with your own data and imagine what Algolia can do for the application you develop!

And don't forget your 5 stars ranking ;)

Search