Android Java for loop uses less CPU and is slower than C++ for loop

Viewed 137

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:

  1. Less instructions per iteration
  2. 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?
0 Answers
Related