【问题标题】:Why is NDK slower then Renderscript on a non parallelizable operation?为什么 NDK 在非并行化操作上比 Renderscript 慢?
【发布时间】:2023-01-20 06:54:31
【问题描述】:

像大多数 RenderScript (RS) 用户一样,我对它的 deprecation 感到惊讶。可以理解,但仍然令人沮丧。

首先介绍一下上下文。

我的算法的两个图像处理块依赖于 RS:canny 和距离变换。

Canny 非常“直接”,足以迁移到 Vulkan,我什至获得了与 Renderscript 相同的结果(有时 Vulkan 速度更快)。

距离变换算法 [Rosenfeld 和 Pfaltz 1966] 是不可并行化的,因此它在 RenderScript 中的当前实现是纯粹串行的,使用了 invoke()。在 RS 代码下方,使用 RS 分配、设置/获取等都是正常的……

因为我需要找到 RS 的替代品,而 Vulkan 不适合非并行操作,所以我认为 NDK 在速度方面应该与 RS 相当。事实上,我认为它会更快,因为您不需要从/复制到 Allocations <-> Java。

实施 NDK C++ 后相等的RS 代码让我惊讶地发现 NDK 慢了 2 到 3 倍。

我一直在想的是为什么会这样。 RenderScript Allocations 是否是内存访问的最佳速度? RenderScript 中是否存在一些隐藏的魔法?

使用 invoke() 和分配的简单 for 循环如何比 NDK C++ 中的相同 for 循环更快?

(在几款 Android 智能手机上测试结果相同——慢 2/3 倍)

更新我

根据solidpixel 的要求添加了一些代码。

内核.rs

#pragma version(1)
#pragma rs java_package_name(distancetransform)

rs_allocation inAlloc;
uint32_t width;
uint32_t height;
uint max_value;

uint __attribute__((kernel)) initialize(uint32_t x, uint32_t y) {

    if(rsGetElementAt_uint(inAlloc,x,y)==1) {
        return 0;
    } else{
        return max_value;
    }
    
}

uint __attribute__((kernel)) clear(uint32_t x, uint32_t y) {
    return 0;
}

//SEQUENCIAL NO MAP X,Y

void first_pass_() {
    
    int i,j;
    
    for (i=1;i<height-1;i++){
        for (j=1;j<width-1;j++){
            uint c00 = rsGetElementAt_uint(inAlloc,j-1,i-1)+4;
            uint c01 = rsGetElementAt_uint(inAlloc,j,i-1)+3;
            uint c02 = rsGetElementAt_uint(inAlloc,j+1,i-1)+4;
            uint c10 = rsGetElementAt_uint(inAlloc,j-1,i)+3;
            uint c11 = rsGetElementAt_uint(inAlloc,j,i);
        
            uint min_a = min(c00,c01);
            uint min_b = min(c02,c10);
            uint min_ab = min(min_a,min_b);
            uint min_sum = min(min_ab,c11);
            
            rsSetElementAt_uint(inAlloc,min_sum,j,i);
        }
    }
}

void second_pass_() {
    
    int i,j;
    
    for (i=height-2;i>0;i--){
        for (j=width-2;j>0;j--){
            uint c00 = rsGetElementAt_uint(inAlloc,j,i);
            uint c01 = rsGetElementAt_uint(inAlloc,j+1,i)+3;
            uint c02 = rsGetElementAt_uint(inAlloc,j-1,i+1)+4;
            uint c10 = rsGetElementAt_uint(inAlloc,j,i+1)+3;
            uint c11 = rsGetElementAt_uint(inAlloc,j+1,i+1)+4;
            
            uint min_a = min(c00,c01);
            uint min_b = min(c02,c10);
            uint min_ab = min(min_a,min_b);
            uint min_sum = min(min_ab,c11);
            
            rsSetElementAt_uint(inAlloc,min_sum,j,i);
        }
    }
}

爪哇*

public void distanceTransform(IntBuffer edgeBuffer) {
        
        long total_0 = System.nanoTime();
        
        edgeBuffer.get(_input);
        edgeBuffer.rewind();
        _allocK.copyFrom(_input);
        _script.forEach_initialize(_allocK);
        
        _script.invoke_first_pass_();
        _script.invoke_second_pass_();
        
        _allocK.copyTo(_result);
        
        _distMapBuffer.put(_result);
        _distMapBuffer.rewind();
        
        long total_1 = System.nanoTime();
        Log.d(TAG,"total call time = "+((total_1-total_0)*0.000001)+"ms");
    }

(*)与问题无关但与完成无关:edgeBuffer 和 distMapBuffer 是 Java NIO 缓冲区,用于有效绑定其他语言。

ndk.cpp文件

extern "C" JNIEXPORT void JNICALL Java_distanceTransform(
        JNIEnv* env, jobject /* this */,jobject edgeMap, jobject distMap) {
    auto* dt = (int32_t*)env->GetDirectBufferAddress(distMap);
    auto* edgemap = (int32_t*)env->GetDirectBufferAddress(edgeMap);

    auto s_init = std::chrono::high_resolution_clock::now();

    int32_t i, j;
    int32_t size = h*w;
    int32_t max_val = w+h;
    for (i = 0; i < size; i++) {
        if (edgemap[i]!=0) {
            dt[i] = 0;
        } else {
            dt[i] = max_val;
        }
    }

    auto e_init = std::chrono::high_resolution_clock::now();
    auto elapsed_init = std::chrono::duration_cast<std::chrono::nanoseconds>(e_init - s_init);
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Time init = %f", elapsed_init.count() * 1e-9);

    auto s_first = std::chrono::high_resolution_clock::now();

    for (i = 1; i < h-1; i++) {
        for (j = 1; j < w-1; j++) {
            int32_t c00 = dt[(i-1)*w+(j-1)]+4;
            int32_t c01 = dt[(i-1)*w+j]+3;
            int32_t c02 = dt[(i-1)*w+(j+1)]+4;
            int32_t c10 = dt[i*w+(j-1)]+3;
            int32_t c11 = dt[i*w+j];

            int32_t min_a = c00<c01?c00:c01;
            int32_t min_b = c02<c10?c02:c10;
            int32_t min_ab = min_a<min_b?min_a:min_b;
            int32_t min_sum = min_ab<c11?min_ab:c11;
            dt[i*w+j] = min_sum;
        }
    }

    auto e_first = std::chrono::high_resolution_clock::now();
    auto elapsed_first = std::chrono::duration_cast<std::chrono::nanoseconds>(e_first - s_first);
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Time first pass = %f", elapsed_first.count() * 1e-9);

    auto s_second = std::chrono::high_resolution_clock::now();

    for (i = h-2; i > 0; i--) {
        for (j = w-2; j > 0; j--) {
            int32_t c00 = dt[i*w+(j+1)]+3;
            int32_t c01 = dt[(i+1)*w+(j-1)]+4;
            int32_t c02 = dt[(i+1)*w+j]+3;
            int32_t c10 = dt[(i+1)*w+(j+1)]+4;
            int32_t c11 = dt[i*w+j];

            int32_t min_a = c00<c01?c00:c01;
            int32_t min_b = c02<c10?c02:c10;
            int32_t min_ab = min_a<min_b?min_a:min_b;
            int32_t min_sum = min_ab<c11?min_ab:c11;
            dt[i*w+j] = min_sum;
        }
    }

    auto e_second = std::chrono::high_resolution_clock::now();
    auto elapsed_second = std::chrono::duration_cast<std::chrono::nanoseconds>(e_second - s_second);
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Time second pass = %f", elapsed_second.count() * 1e-9);
}

【问题讨论】:

  • 需要在您的 NDK 解决方案上发布一些代码。您在问我们“为什么未知代码 A 比未知代码 B 快”,答案可能是任何问题。
  • 感谢@solidpixel 的回答。我添加了代码细分,显示了 RS 和 NDK 实现的相关和必要部分。

标签: android android-ndk vulkan renderscript


【解决方案1】:

从我们的内部错误跟踪器中反映我的评论:

问题是 Android Studio 中的“调试”构建变体是使用 -O0 编译的。如果你更积极地优化,NDK 会更快。

事实证明要改变这个有点棘手。如果您执行set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2"),它会在-O0 之前插入,因此没有任何效果。相反,按照Turn on compiler optimization for Android Studio debug build via Cmake,这样做:target_compile_options(dt-ndk-jni PRIVATE "$&lt;$&lt;CONFIG:DEBUG&gt;:-O2&gt;")。然后,-O2 在 -O0 之后进行并覆盖它。

您可以通过查看 app/.cxx/cmake/debug/arm64-v8a/compile_commands.json 查看传递了哪些标志

这是我在 Pixel 6 pro 上获得的结果,确保手机在运行基准测试时处于唤醒状态,以便一切都在性能核心上运行。

使用-O0:

  • 平均 RS:7.85 +/- 2.402 毫秒
  • 平均 NDK:10.20 +/- 1.476 毫秒

使用-Os:

  • 平均 RS:8.06 +/- 2.339 毫秒
  • 平均 NDK:3.74 +/- 1.399 毫秒

使用-O2:

  • 平均 RS:8.49 +/- 4.359 毫秒
  • 平均 NDK:3.53 +/- 0.508 毫秒

在 -O2 和电话睡着的情况下,我得到:

  • 平均 RS:26.81 +/- 13.839 毫秒
  • 平均 NDK:9.09 +/- 3.646 毫秒

编辑:使用“发布”构建变体也将优化构建,但使用它可能并不总是一个选项。

【讨论】:

  • 谢谢詹姆斯!勾选为回答并且还基于内部错误票证链接!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-14
  • 1970-01-01
  • 2018-03-24
  • 1970-01-01
相关资源
最近更新 更多