In my previous post on this topic, A performance comparison between Java and C on the Nexus 5, I compared the performance of an audio low-pass filter in Java and C. The results were clear: The C version outperformed, and by a significant amount. This result brought more attention to the post than I was expecting; some of you were also curious about RenderScript, and I’m pleased to say that Jean-Luc Brouillet, a member of Google’s RenderScript team, got in touch with me and generously volunteered an implementation of the DSP code in RenderScript.
With this new code, I refactored the code into a new benchmark with test audio data, so that I could compare the different implementations and verify their output. I’ll be sharing both the code and the results with you today.
Motivations and intentions
Some of you might be curious about why I am so interested in this subject. 🙂 I normally spend most of my development hours coding for Android, using Java; in fact, my first book, OpenGL ES 2 for Android: A Quick-Start Guide, is a beginner’s guide to OpenGL that focuses on Android and Java.
Normally, when I develop code, the most important questions on my mind are: “Is this easy to maintain?” “Is it correct?” “If I come back and revisit this code a month later, am I going to understand what the heck I was doing?” Since Java is the primary development language on Android, it just makes sense for me to do most of my development there.
So why the recent focus on native development? Here are two big reasons:
- The performance of Java on Android isn’t suitable for everything. For critical performance paths, it can be a big competitive advantage to move that code over to native, so that it completes in less time and uses less battery.
- I’m interested in branching out to other platforms down the road, probably starting with iOS, and I’m curious if it makes sense to share some code between iOS and Android using a common code base in C/C++. It’s important that this code runs without many abstractions in the way, so I’m not very interested in custom/proprietary solutions like Xamarin’s C# or an HTML5-based toolkit.
It’s starting to become clear to me that it can make sense to work with more than one language, and to choose these languages in situations where the benefits outweigh the cost. Trying to work with Android’s UI toolkit from C++ is painful; running a DSP filter from Java and watching it use more battery and take more time than it needs to is just as painful.
Our new test scenario
For this round of benchmarks, we’ll be comparing several different implementations of a low-pass IIR filter, implemented with coefficients generated with mkfilter. We’ll run a test audio file through each implementation, and record the best score for each.
How does the test work?
- First, we load a test audio file into memory.
- We then execute the DSP algorithm over the test audio, benchmarking the elapsed time. The data is processed in chunks to reflect the fact that this is similar to how we would process data coming off of the microphone.
- The results are written to a new audio file in the device’s default storage, under “PerformanceTest/”.
Here are our test implementations:
- Java. This is a straightforward implementation of the algorithm.
- Java (tuned). This is the same as 1, but with all of the functions manually inlined.
- C. This uses the Java Native Interface (JNI) to pass the data back and forth.
- RenderScript. A big thank you to Mr. Brouillet from the RenderScript team for taking the time to contribute this!
The tests were run on a Nexus 5 device running Android 4.4.3. Here are the results:
|Implementation||Execution environment||Compiler||Shorts/second||Relative run time
(lower is better)
|C||Dalvik JNI||gcc 4.6||17,639,081||1.00|
|C||Dalvik JNI||gcc 4.8||16,516,757||1.07|
|RenderScript||Dalvik||RenderScript (API 19)||15,206,495||1.16|
|RenderScript||Dalvik||RenderScript (API 18)||13,234,397||1.33|
|C||Dalvik JNI||clang 3.4||13,208,408||1.34|
|Java (tuned)||Art (Proguard)||7,235,607||2.44|
|Java (tuned)||Dalvik (Proguard)||5,365,814||3.29|
For this test, the C implementation is the king of the hill, with gcc 4.6 giving the best performance. The gcc compiler is followed by RenderScript and clang 3.4, and the two Java implementations are at the back of the pack, with Dalvik giving the worst performance.
The C implementation compiled with gcc gave the best performance out of the entire group. All tests were done with -ffast-math and -O3, using the NDK r9d. Switching between Dalvik and ART had no impact on the C run times.
I’m not sure why there is still a large gap between clang and gcc; would everything on iOS run that much faster if Apple was using gcc? Clang will likely continue to improve and I hope to see this gap closed in the future. I’m also curious about why gcc 4.6 seems to generate better code than 4.8. Perhaps someone familiar with ARM assembly and the compilers would be able to weigh in why?
Even though I’m a newbie at C and I learned about JNI in part by doing these benchmarks, I didn’t find the code overly difficult to write. There’s enough documentation out there that I was able to figure things out, and the algorithm output matches that of the other implementations; however, since C is an unsafe language, I’m not entirely convinced that I haven’t stumbled into undefined behaviour or otherwise done something insane. 🙂
In the previous post, someone asked about RenderScript, so I started working on an implementation. Unfortunately, I had zero experience with RenderScript at the time so I wasn’t able to get it working. Luckily, Jean-Luc Brouillet from the RenderScript team also saw the post and ported over the algorithm for me!
As you can see, the results are very promising: RenderScript offers better performance than clang and almost the same performance as gcc, without requiring use of the NDK or of JNI glue! As with C, switching between Dalvik and ART had no impact on the run times.
RenderScript also offers the possibility to easily parallelize the code and/or run it on the GPU which can potentially give a huge speedup, though unfortunately we weren’t able to take advantage of that here since this particular DSP algorithm is not trivially parallelizable. However, for other algorithms like a simple gain, RenderScript can give a significant boost with small changes to the code, and without having to worry about threading or other such headaches.
In my humble view, the RenderScript implementation does need some more polishing and the documentation needs to be significantly improved, as I doubt I would have gotten it working on my own without help. Here are some of the issues that I ran into with the RenderScript port:
- Not all functions are documented. For example, the algorithm uses rsSetElementAt_short() which I can’t find anywhere except for some obscure C files in the Android source code.
- The allocation functions are missing a way to copy data into an offset of an array. To work around this, I use a scratch buffer and System.arraycopy() to move the data around, and to keep things fair, I changed the other implementations to work in the same way. While this slows them down slightly, I don’t believe it’s an unfair advantage for RenderScript, because in real-world usage, I would expect to process the data coming off the microphone and write that directly into a file, not into an offset of some array.
- The fastest RenderScript implementation only works on Android 4.4 KitKat devices. Going down one version to Android 4.3 changes the RenderScript API which requires me to change the code slightly, slowing things down for both 4.3 and 4.4. RenderScript does offer a “support” mode via the support API which should enable backwards compatibility, but I wasn’t able to get this to work for me for APIs older than 18 (Android 4.3).
So while there are some issues with RenderScript as implemented today, these are all issues that can hopefully be fixed. RenderScript also has the significant advantage of running code on the CPU and GPU in parallel, and doesn’t require JNI glue code. This makes it a serious contender to C, especially if portability to older devices or other platforms is not a big concern.
As with last time, Java fills out the bottom of the pack. The performance is especially terrible with the default Dalvik implementation; in fact, it would be even worse if I hadn’t manually replaced the modulo operator with a bit mask, which I was hoping the compiler could do with the static information available to it, but it doesn’t.
Some people asked about Proguard, so I tried it out with the following config (full details in the test project):
The results were mixed. Switching between Dalvik and ART made much more of a difference, as did manually inlining all of the functions together. The best result with Dalvik was without Proguard, and was 3.11x slower than the best C implementation. The best result with ART was with Proguard, and was 2.44x slower than the best C implementation. If we compare the normal Java version to the best C result, we get a 5.02x slowdown with ART and a 14.45x slowdown with Dalvik.
It does look like the performance of Java will be getting a lot better once ART becomes widely deployed; instead of huge slowdowns, we’ll be seeing between 3x and 5x, which does make a difference. I can already see the improvements when sorting and displaying ListViews in UI code, so this isn’t just something that affects niche code like audio DSP filters.
Desktop results (just for fun)
Just like last time, again, here are some desktop results, just for fun. 🙂 These tests were run on a 2.6 GHz Intel Core i7 running OS X 10.9.3.
|Implementation||Execution environment||Compiler||Shorts/second||Relative speed
(higher is better)
|C||Java SE 1.6u65 JNI||gcc 4.9||129,909,662||7.36|
|C||Java SE 1.6u65 JNI||clang 3.4||96,022,644||5.44|
|Java||Java SE 1.8u5 (+XX:AggressiveOpts)||82,988,332||4.70|
|Java (tuned)||Java SE 1.8u5 (+XX:AggressiveOpts)||79,288,025||4.50|
|Java||Java SE 1.8u5||64,964,399||3.68|
|Java (tuned)||Java SE 1.8u5||64,748,201||3.67|
|Java (tuned)||Java SE 1.6u65||63,965,575||3.63|
|Java||Java SE 1.6u65||53,245,864||3.02|
As on the Nexus 5, the C implementation compiled with gcc dominates; however, I’m very impressed with where Java ended up!
I used the following compilers with optimization flags -march=native -ffast-math -O3:
- Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
- gcc version 4.9.0 20140416 (prerelease) (MacPorts gcc49 4.9-20140416_2)
As on the Nexus 5, gcc’s generated code is much faster than clang’s; perhaps this will change in the future but for now, gcc is still the king. I also find it interesting that the gap between the best run time here and the best run time on the Nexus 5 is similar to the gap between C and ART on the Nexus 5. Not so far apart, they are!
I’m also impressed with the latest Java for OS X. While manually inlining all of the functions together was required for an improvement on Java 1.6, the manually-inlined version was actually slower on Java 1.8. This shows that not only is this sort of code abuse no longer required on the latest Java, but also that the compiler is smarter than we are at optimizing the code.
Adding +XX:AggressiveOpts to Java 1.8 sped things up even more, almost closing the gap with clang! That is very impressive in my eyes, since Java has an old reputation of being a slow language, but in some cases and situations, it can be almost as fast as C if not faster.
The worst Java performance is 2.43x slower than the best C performance, which is about the same relative difference as the best Java performance on Android with ART. Performance differences aren’t always just about language choice; they can also be very dependent on the quality of implementation. At this time, the Google team has made different trade-offs which place ART at around the same relative level of performance, for this specific test case, as Java 1.6. The improved performance of Java 1.8 on the desktop shows that it’s clearly possible to close up the gap on Android in the future.
Explore the code!
The project can be downloaded at GitHub: https://github.com/learnopengles/dsp-perf-test. To compile the code, download or clone the repository and import the projects into Eclipse with File->Import->Existing Projects Into Workspace. If the Android project is missing references, go to its properties, Java Build Path, Projects, and add “JavaPerformanceTest”.
The results are written to “PerformanceTest/” on the device’s default storage, so please double-check that you don’t have anything there before running the tests.
So, what do you think? Does it make sense to drop down into native code? Or are native languages a relic of the past, and there’s no reason to use anything other than modern, safe languages? I would love to hear your feedback.
About the book
Android is booming like never before, with millions of devices shipping every day. In OpenGL ES 2 for Android: A Quick-Start Guide, you’ll learn all about shaders and the OpenGL pipeline, and discover the power of OpenGL ES 2.0, which is much more feature-rich than its predecessor.
It’s never been a better time to learn how to create your own 3D games and live wallpapers. If you can program in Java and you have a creative vision that you’d like to share with the world, then this is the book for you.
14 thoughts on “A performance comparison redux: Java, C, and Renderscript on the Nexus 5”
GCC and Clang are both moderately horrible at generating ARM code. Try using armcc instead- it is much better, but still fairly terrible compared to hand-coded ARM assembly code. The real problem is that ARM instructions have an extremely flexible set of operands and condition codes- even in the world of Thumb2EE and AARCH64, that flexible second operand means that choosing the best sequence of operations is NP-hard. There are something like ten million valid combinations of instruction, condition and operands, and choosing the best ones is difficult.
Instead, compilers tend to generate code sequences from a list of boilerplate templates, and this is not particularly efficient. I recently optimized one function in a piece of firmware my company makes. This function is responsible for transforming a stream of pixels into a sequence of I/O operations. When compiled with gcc 4.6, the function required about 4000 microseconds to run; clang clocked in at about 4100, rvct/armcc managed it in 2900 microseconds, and my hand-written assembly language version ran in 1200 microseconds. (My code was not particularly tight, either- it just avoided saving a bunch of registers it didn’t need to, and didn’t branch as often.)
This is pretty typical for ARM, and it’s not so much of a problem on x86 because their instruction format is much more rigid and doesn’t have this combinatorial explosion of possible code sequences to nearly the same degree.
Thank you for sharing that, I wasn’t aware of that aspect of the ARM architecture. Interesting!
I think this proves that you need to update your ES tutorials using native code.
I do have the air hockey tutorial in native code. 😉
Im reading your book, I am currently on chapter 5. So far is great….
I’ll be testing your c implementation of the hockey tutorial on native..
But what air hockey tutorial, the one of chapter 5 .. ¿?
It would represent the completed air hockey project, IIRC.
As you stated in this post, the book uses Java and GLSurfaceView which requires JNI calls. I also checked native version of the AirHockey in github and that code also uses GLSurfaceView and JNI calls. I’m new at these topics, and really wonder if we can do the same thing without crossing JNI boundaries. Native version crosses JNI boundary for input handling but as far as I know it is possible to handle inputs via native code? Additionally, is it possible to draw directly native window of the Android? If we do that so, wouldn’t performance be better?
(BTW Thanks a lot for book and blog. They are both wonderful.)
Hi Ricardo, yes, you could use NativeActivity instead (https://developer.android.com/reference/android/app/NativeActivity.html). If there are SDKs only accessible from Java, you’ll still need to use JNI to access them. AFAIK though, NativeActivity is implemented behind the scenes using the Java APIs, so there’ll still be JNI involved and the performance would be similar.
“Trying to work with Android’s UI toolkit from C++ is painful;”
Could you please elaborate on that, or reference an article about it?
Just search for posts and tutorials on accessing Java from C++ via JNI and you’ll quickly get an idea of what I mean. 🙂
Pleeeease…for the java desktop version use a proper microbench tool…is simple and warm properly the Jvm avoiding all the typical mistakes to let the JVM be free to drop critical execution paths and so on…http://openjdk.java.net/projects/code-tools/jmh/
I’ve found it very reasonable to share libs between android and ios in C/C++. The pre-compiler macros make it easy too write x-platform C code by: