C++11 support on Android with g++ 4.8
Summary
The C++11 support on Android when using g++ 4.8 is pretty good. The important concurrency features that were missing from g++ 4.6 are now available in the Standard Template Library.
Testing libgnustl_shared.so
After examining the LLVM libc++ library on Android, I wanted to check which C++11 features are currently available{target="_blank"} using g++ 4.8 and the GNU Standard Template Library (libstdc++). g++ 4.8 improved support for C++11 significantly (4.6{target="_blank"} vs. 4.8{target="_blank"}), so the question was whether that support also made it into the Android-specific compilers. With the LLVM libc++ library, a lot of very specific platform code such as support for atomic operations was stubbed out, which would cascade down into problems with the mutual exclusion mechanisms and threads.
Based on a review of an existing codebase, the features most interesting to me were contained in the
following headers: atomic, chrono, condition_variable (condition_variable), memory
(shared_ptr, unique_ptr), mutex (lock_guard, mutex, unique_lock), thread (thread).
Also of interest was compiler support for lambdas and auto keyword support.
So let's see what's available, by modifying the android-ndk-r9/samples/test-libstdc++ native
executable to utilize a number of C++11 features to perform the same race condition-prone task.
Namely, the program generates a random number of threads which each attempt to simultaneously
increment some global variable by one, one hundred times.
C++ gives you a few ways to solve the problem, and I've tried each of them here. The logic of the
incrementation isn't exactly identical, and in some cases incredibly inefficient (the conditional_variable case basically has all threads waiting for a single thread to complete, before they can acquire a shared resource mutex and begin executing). In any case, this is more a
proof of library completeness than a proof of algorithmic efficiency.
The test source code looks like this:
// New in C++ 11.
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <random>
#include <thread>
// Regular STL.
#include <list>
#include <string>
// C-compatibility.
#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstddef>
// Constants.
const int INCREMENT = 100;
// Utility functions (std::random)
std::random_device randomDevice;
std::default_random_engine randomEngine(randomDevice());
int diceRoll(int sides = 20)
{
std::uniform_int_distribution<int> distribution(1, sides);
int roll = distribution(randomEngine);
printf(" -- std::random rolled: %d\n", roll);
return roll;
}
// std::atomic
std::atomic<int> atomicInt(0);
void stdAtomicThreadFn(void)
{
// Try creating a bunch of threads, each one interested
// only in incrementing the global atomicInt.
// If it's truly atomic, there shouldn't be any race
// conditions and the final int value should be an
// exact multiple of this thread's loop.
for (int i = 0; i < INCREMENT; ++i)
{
++atomicInt;
}
}
// std::mutex + std::condition_variable + std::unique_lock + lambda tests
int nonAtomicInt = 0;
bool nonAtomicThreadsReady = false;
std::mutex nonAtomicIntMutex;
std::condition_variable nonAtomicIntCv;
void cvMutexThreadFn(void)
{
// Use the mutex and signalling capabilities in the
// Standard Library to safely increment the nonAtomicInt.
// Start the thread but wait on a condition variable
// before starting the nonAtomicInt incrementation.
std::unique_lock<std::mutex> lock(nonAtomicIntMutex);
nonAtomicIntCv.wait(lock, [] { return nonAtomicThreadsReady; });
// This runs after the nonAtomicIntMutex lock is reacquired,
// therefore only a single thread at a time is modifying
// nonAtomicInt.
for (int i = 0; i < INCREMENT; ++i)
{
nonAtomicInt++;
}
}
// std::lock_guard + std::mutex tests
int lockGuardInt = 0;
std::mutex lockGuardIntMutex;
void incrementLockGuardInt(void)
{
std::lock_guard<std::mutex> lock(lockGuardIntMutex);
lockGuardInt++;
}
// std::unique_ptr, std::shared_ptr, and std::make_shared test class.
class PtrTest
{
private:
std::string ptrType;
public:
PtrTest(std::string const& ptrType) : ptrType(ptrType) { printf(" -- PtrTest(): %s\n", ptrType.c_str()); };
~PtrTest() { printf(" -- ~PtrTest(): %s\n", ptrType.c_str()); };
};
void testA(void)
{
printf("std::atomic + std::random + std::thread + auto + ranged-for test\n");
std::list<std::thread> atomicIntThreads;
const int atomicIntThreadCount = diceRoll();
// Create a number of threads, all incrementing atomicInt.
for (int i = 0; i < atomicIntThreadCount; ++i)
{
atomicIntThreads.push_back(std::thread(&stdAtomicThreadFn));
}
// Join those threads.
for (auto & oneThread : atomicIntThreads)
{
oneThread.join();
}
assert(atomicInt == atomicIntThreadCount * INCREMENT);
printf(" -- atomicInt final value: %d\n", atomicInt.load());
}
void testB(void)
{
printf("std::condition_variable + std::mutex + std::unique_lock + std::random + std::thread + lambda test\n");
std::list<std::thread> nonAtomicIntThreads;
const int nonAtomicIntThreadCount = diceRoll();
// Create a number of threads, all incrementing nonAtomicInt.
for (int i = 0; i < nonAtomicIntThreadCount; ++i)
{
nonAtomicIntThreads.push_back(std::thread(&cvMutexThreadFn));
}
// Start those threads.
nonAtomicThreadsReady = true;
nonAtomicIntCv.notify_all();
// Join those threads.
for (auto & oneNonAtomicThread : nonAtomicIntThreads)
{
oneNonAtomicThread.join();
}
assert(nonAtomicInt == nonAtomicIntThreadCount * INCREMENT);
printf(" -- nonAtomicInt final value: %d\n", nonAtomicInt);
}
void testC(void)
{
printf("std::lock_guard + std::mutex + std::random + std::thread + lambda test\n");
std::list<std::thread> lockGuardThreads;
const int lockGuardThreadCount = diceRoll();
// Create a number of threads, all incrementing lockGuardInt.
for (int i = 0; i < lockGuardThreadCount; ++i)
{
lockGuardThreads.push_back(std::thread([] {
for (int j = 0; j < INCREMENT; ++j) incrementLockGuardInt();
}));
}
// Join those threads.
for (auto & oneLockGuardThread : lockGuardThreads)
{
oneLockGuardThread.join();
}
assert(lockGuardInt == lockGuardThreadCount * INCREMENT);
printf(" -- lockGuardInt final value: %d\n", lockGuardInt);
}
void testD(void)
{
printf("std::unique_ptr + std::shared_ptr + std::make_shared tests\n");
std::unique_ptr<PtrTest> uP(new PtrTest("std::unique_ptr"));
std::shared_ptr<PtrTest> sP = std::make_shared<PtrTest>("std::shared_ptr");
}
int main(void)
{
testA();
testB();
testC();
testD();
return 0;
}
I added an Application.mk file to the jni folder. Debugging is activated, so that the assert()
macro isn't disabled.
APP_ABI := armeabi
APP_STL := gnustl_shared
APP_CFLAGS := --std=c++11
APP_OPTIM := debug
NDK_TOOLCHAIN_VERSION := 4.8
Building works fine:
$ ndk-build
Compile++ thumb : test-libstl /sources/cxx-stl/gnu-libstdc++/4.8/libs/armeabi/
Executable : test-libstl
Install : test-libstl => libs/armeabi/test-libstl
Install : libgnustl_shared.so => libs/armeabi/libgnustl_shared.so
$ adb push libs/armeabi/libgnustl_shared.so /data/tmp
366 KB/s (804484 bytes in 2.143s)
$ adb push libs/armeabi/test-libstl /data/tmp
284 KB/s (50536 bytes in 0.173s)
After pushing the test-libstl executable to the device, make sure the /data/tmp folder is added
to the LD_LIBRARY_PATH:
localhost tmp # LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/data/tmp
Then just run the executable, and you should see:
localhost tmp # ./test-libstl
std::atomic + std::random + std::thread + auto + ranged-for test
-- std::random rolled: 11
-- atomicInt final value: 1100
std::condition_variable + std::mutex + std::unique_lock + std::random + std::thread + lambda test
-- std::random rolled: 15
-- nonAtomicInt final value: 1500
std::lock_guard + std::mutex + std::random + std::thread + lambda test
-- std::random rolled: 7
-- lockGuardInt final value: 700
std::unique_ptr + std::shared_ptr + std::make_shared tests
-- PtrTest(): std::unique_ptr
-- PtrTest(): std::shared_ptr
-- ~PtrTest(): std::shared_ptr
-- ~PtrTest(): std::unique_ptr
To run the tests, I used a rooted phone running Android 2.3.7.
In real deployments where you're building a loadable shared-library using the Java Native Interface,
you'll need to follow the instructions from the NDK's CPLUSPLUS-SUPPORT.html file and call
System.loadLibrary on libgnustl_shared.so before loading the eventual shared library form of
your app. (Managing standalone executable lifecycles tends to be a pain in the ass inside of Android apps and services, at least based on the experiences we had at my previous company.)
Overall, I'm pretty positive about the support for concurrency in g++ 4.8 on Android, and am thrilled to see that it may now be possible to write a single modern C++ codebase across all of the major desktop and mobile platforms.
- ← Previous
Mucking around with libc++ on Android - Next →
Ubuntu 13.10 chroot on Android