I have been exploring efficient ways to do image processing in Android and while comparing a simple point operation like YUV to RGB color space conversion of a 8 mega pixel image (8 million pixels) I got following performance difference between Java & Native simple for-loops while running on a certain Android device (arm64-v8a ABI):
| Approach | Average | Verdict |
|---|---|---|
| Java | 353 ms | - |
| Native standard | 76.4 ms | 4.62x faster |
By simple for-loops I mean both of them were row major for loops (no tiling, no explicit paralellization, no SIMD instructions)
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
// .. yuv to rgb conversion
}
}
If I understand correctly this is due to the optimisations C++ compiler does for the given CPU architecture. On profiling I observed the C++ code to have good CPU usage while the java code to have much less.
| Approach | Avg CPU usage |
|---|---|
| Java | ~12% |
| Native | 70%+ |
This was observed while profiling debug version.
Are there ways to achieve better parallelism (& better throughput) with pure Java code in Android? As in some known APIs or frameworks which allows us to do this easily.
(Please note: the question is strictly about improving performance with Java code and not with alternative frameworks like RenderScript, Vulcan, Halide etc.)
Update 1 - related to Mark Keen's comment
I tried explicitly parallelising the Java code using thread pools & tiling
final int TILES_PER_AXIS = 4;
int tileWidth = width / TILES_PER_AXIS;
int tileHeight = height / TILES_PER_AXIS;
int threadCount = TILES_PER_AXIS * TILES_PER_AXIS;
final ExecutorService executor = Executors.newFixedThreadPool(threadCount);
List<Future<Void>> futures = new ArrayList<>();
int threadCount = TILES_PER_AXIS * TILES_PER_AXIS;
for (int i = 0; i < threadCount; i++) {
int startY = i / TILES_PER_AXIS * tileHeight;
int endY = startY + tileHeight;
int startX = i / TILES_PER_AXIS * tileWidth;
int endX = startX + tileWidth;
Future<Void> future = executor.submit(() -> {
process(startX, endX, startY, endY, ...other data);
return /* Void */ null;
});
futures.add(future);
}
// Wait on all futures
for (Future<?> future : futures) {
future.get(); // do anything you need, e.g. isDone(), ...
}
And the process(..) function only does the task in the given bounds of (startX, startY) to (endX, endY)
With this I did see both higher CPU usage and performance to a certain limit, but still not satisfied with the CPU usage (& performance with respect to the native code).
Pixel 4A, YUV 420 --> Bitmap, 8MP (3264x2448)
| Approach | Threads | CPU usage | Average latency |
|---|---|---|---|
| Direct loops | 1 | ~12% | 353 ms |
| Parallel 2x2 tiles | 4 | ~32% | 173 ms |
| Parallel 4x4 tiles | 16 | ~26% | 129.9 ms |
| Parallel 8x8 tiles | 64 | ~15% | 161.50 ms |
Update 2 - improved performance with direct byte[] access.
One key difference between Java code and native code in this case was the java loops were accessing the data using a readonly ByteBuffer while the native code was accessing the underlying data directly. I suppose for loops with direct pointers access are:
- Less instructions per iteration
- Easier for the compiler to auto-vectorise.
So one way to address this was using fast byte[] copy and doing the conversion using byte[]. This of course not so good hit on java heap. Some performance numbers I saw with this:
| Approach | Average | Verdict |
|---|---|---|
| Java (unoptimised} | 353 ms | |
| Native (unoptimised) | 76.4 ms | 4.62x faster |
Java byte[] + single threaded |
119.5 ms | 2.9x faster |
Java byte[] + multi threaded |
53.8 ms | 6.56x faster |
And CPU usage:
| Approach | Avg CPU usage |
|---|---|
Java ByteBuffer + single threaded |
~12% |
| Native | ~70%+ |
Java byte[] + multi threaded |
~33%+ |
My write-up on this topic: Faster image processing in Android Java using multi threading (with my limited understanding, but still exploring)
Questions:
- Can we do better?
- Is there a easier way to do this way of tiling in Java?