像大多数RenderScript(RS)用户一样,我对它的deprecation感到惊讶。可以理解,但仍然令人沮丧。
先说点背景。
我的算法的两个图像处理块依赖于RS:Canny & Distance转换器。
Canny很“直接”地迁移到了Vulkan,我甚至取得了与Renderscript相同的结果(有时Vulkan的速度更快)。
距离变换算法[Rosenfeld和Pfaltz 1966年]是不可并行的,因此它在RenderScript中的当前实现是纯粹串行的,使用invoke()。下面的RS代码使用RS分配、设置/获取等都是正常的。
因为我需要找到RS的替代品,而Vulkan不适合非并行操作,所以我认为NDK的速度应该可以与RS相媲美。实际上,我认为NDK会更快,因为你不需要从/向Allocations Java复制<->。
在实现了NDK C++等效的RS代码后,我惊讶地看到NDK慢了2到3倍。
我一直在想的是为什么会这样。RenderScript的分配对于内存访问来说是最佳的速度吗?RenderScript中是否隐藏了一些魔法?
一个带有invoke()和Allocations的简单for循环怎么可能比NDK C++中的相同for循环更快呢?
(在几款Android智能手机上测试,结果相同-速度慢2/3倍)
更新I
根据solidpixel的要求添加了一些代码。
kernel.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);
}
}
}
java *
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);
}
1条答案
按热度按时间6yt4nkrj1#
反映我的意见,从我们的内部错误跟踪:
问题是Android Studio中的"debug"构建变体是使用-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 "$<$<CONFIG:DEBUG>:-O2>")
。然后,-O2在-O0之后并覆盖它。您可以通过查看app/. cxx/cmake/debug/arm64-v8a/compile_commands. json来查看正在传递哪些标志
以下是我在Pixel 6 Pro上得到的结果,确保手机在运行基准测试时是唤醒的,以便一切都在性能核心上运行。
带-O0:
带-Os:
含-O2: