在Android中使用Neon指令优化并行效率

2020-06-14  本文已影响0人  郑海鹏

Neon指令简介
普通运算都是单指令单数据的,例如加法同时只能对两个数做加法运算,得到一个结果。 Neon 指令是单指令多数据的指令(Single Instruction Multiple Data,简称SIMD),可以对一组数据同时执行一个指令,从而大幅提高并行运算的效率。

Neon 指令本身没什么难度,主要是得熟悉 API,我们先看例子,再看API。

一、使用 Neon 指令将图片转为灰度图

代码从 Java 层传入一张 1600 x 1245 的图片,独立执行 20 次,耗时 5284 ms,平均每次 264ms 。
作为对比,使用相同算法,用普通C++实现,耗时 12826ms,平均每次 641ms 。

使用 Neon 的代码:

jintArray deColorUseNeon(JNIEnv *env, jsize size, jint* colors) {
    // 保存结果(去色后的图片)
    auto *p = new unsigned char [size * 4];

    // 像素的头指针
    const auto* colorsHead = (const unsigned char* )colors;

    // 8(每个颜色8-bit) x 8(一次处理8个像素) x 4(ARGB)
    uint8x8x4_t currentPixels;

    unsigned char c38[] = {38, 38, 38, 38, 38, 38, 38, 38};
    uint8x8_t _38 = vld1_u8(c38);
    unsigned char c75[] = {75, 75, 75, 75, 75, 75, 75, 75};
    uint8x8_t _75 = vld1_u8(c75);
    unsigned char c15[] = {15, 15, 15, 15, 15, 15, 15, 15};
    uint8x8_t _15 = vld1_u8(c15);

    for (unsigned int i = 0; i < size * 4; i += 32) {
        // 读取 8 个像素
        currentPixels = vld4_u8(colorsHead + i);

        // 利用公式  (r * 38 + g * 75 + b * 15) / 128 计算灰度值:

        // 乘以每个颜色各自的系数
        uint16x8_t tempR = vmull_u8(currentPixels.val[2], _38);
        uint16x8_t tempG = vmull_u8(currentPixels.val[1], _75);
        uint16x8_t tempB = vmull_u8(currentPixels.val[0], _15);

        // 相加
        uint16x8_t sum = vaddq_u16(tempR, tempG);
        sum = vaddq_u16(sum, tempB);

        // 除以 128,得到灰色通道的值
        uint8x8_t gray = vshrn_n_u16(sum, 7);

        currentPixels.val[2] = gray;
        currentPixels.val[1] = gray;
        currentPixels.val[0] = gray;

        // 保存到结果中
        vst4_u8(p + i, currentPixels);
    }

    jintArray array = env->NewIntArray(size);
    env->SetIntArrayRegion(array, 0, size, (const jint *)p);
    return array;
}

传统方式实现:

jintArray deColor(JNIEnv *env, jsize size, jint* colors) {
    // 保存结果(去色后的图片)
    jint *p = new jint[size];


    unsigned char r, g, b;
    unsigned char temp;
    int* num;
    int alpha;
    for (unsigned int i = 0; i < size; i++) {
        num = colors + i;
        alpha = ((*num) >> 24) & 0xFF;
        r = ((*num) >> 16) & 0xFF;
        g = ((*num) >> 8) & 0xFF;
        b = (*num) & 0xFF;
        // 利用公式  (r * 38 + g * 75 + b * 15) / 128 计算灰度值:
        temp =  (r * 38 + g * 75 + b * 15) >> 7;
        p[i] = (alpha << 24) | (temp << 16) | (temp << 8) | temp;
    }

    jintArray array = env->NewIntArray(size);
    env->SetIntArrayRegion(array, 0, size, p);
    return array;
}

两者的去色效果是相同的。


二、指令集介绍

使用 Neon 指令需要在 C++ 中引入头文件 <arm_neon.h>
这个头文件中定义了支持的数据类型和数据结构,以及各个指令函数。

(1) 数据结构

数据结构有两类: (1) 基本的一维向量;(2) 多维向量。

基本的一维向量:
数据类型的命名规则: [type][size] x [count] _t
例如 int8x8_t 表示 int 类型的向量,单个元素占16 bit,有4个元素,总共占据64 bit。
例如 uint32x4_t 表示 unsigned int 类型的向量,单个元素的大小是32 bit,有4个元素,总共占128 bit。
常见的类型有:

   int8x8_t,    int16x4_t,   int32x2_t,   int64x1_t, 
   float32x2_t, float16x4_t, uint8x8_t,   uint16x4_t,  
   uint32x2_t,  uint64x1_t
   
   int8x16_t,   int16x8_t,   int32x4_t,   int64x2_t,
   float32x4_t, float16x8_t, uint8x16_t,  uint16x8_t,  
   uint32x4_t,  uint64x2_t

多维向量:
多维向量是基本数据类型的数组形式,命名规则: [type][size] x [count] x [lengthOfArray] _t。
例如 uint8x16x4_t 表示 uint8x16_t 的数组,长度为4,常用与ARGB图像处理,它的定义如下:

typedef struct uint8x16x4_t {
    uint8x16_t val[4];
} uint8x16x4_t;

(2) 指令

指令按照作用划分,分为:
(1) 加载(把数据从内存装载到寄存器);
(2) 保存(把数据从寄存器保存到内存);
(3) 加减乘运算(没有除);
(4) 与(&)、或(|)、异或(^) 运算;
(5) 其他。

按照操作的数据大小,分为:
正常指令、窄指令、长指令、饱和指令等。

我们按照作用去了解比较好,因为相同作用的指令命名有规律。

(具体指令比较多,后续每种指令单独附上链接)

上一篇下一篇

猜你喜欢

热点阅读