如何使用 neon 内在函数优化直方图统计?

mao*_*ofu 3 intrinsics neon

我想用 neon 内在函数优化直方图统计代码。但我没有成功。这是 c 代码:

#define NUM (7*1024*1024)
uint8 src_data[NUM];
uint32 histogram_result[256] = {0};
for (int i = 0; i < NUM; i++)
{
    histogram_result[src_data[i]]++;
}
Run Code Online (Sandbox Code Playgroud)

Historam 统计更像是串行处理。用 neon 内在函数很难优化。有人知道如何优化吗?提前谢谢。

sh1*_*sh1 5

您无法直接对存储进行矢量化,但可以对它们进行管道化,并且可以对 32 位平台上的地址计算进行矢量化(在 64 位平台上的程度较小)。

您要做的第一件事(实际上并不需要 NEON 受益)是展开直方图数组,以便您可以立即获得更多数据:

#define NUM (7*1024*1024)
uint8 src_data[NUM];
uint32 histogram_result[256][4] = {{0}};
for (int i = 0; i < NUM; i += 4)
{
    uint32_t *p0 = &histogram_result[src_data[i + 0]][0];
    uint32_t *p1 = &histogram_result[src_data[i + 1]][1];
    uint32_t *p2 = &histogram_result[src_data[i + 2]][2];
    uint32_t *p3 = &histogram_result[src_data[i + 3]][3];
    uint32_t c0 = *p0;
    uint32_t c1 = *p1;
    uint32_t c2 = *p2;
    uint32_t c3 = *p3;
    *p0 = c0 + 1;
    *p1 = c1 + 1;
    *p2 = c2 + 1;
    *p3 = c3 + 1;
}

for (int i = 0; i < 256; i++)
{
    packed_result[i] = histogram_result[i][0]
                     + histogram_result[i][1]
                     + histogram_result[i][2]
                     + histogram_result[i][3];
}
Run Code Online (Sandbox Code Playgroud)

请注意,p0top3永远不能指向相同的地址,因此重新排序它们的读取和写入就可以了。

由此,您可以使用内在函数对p0to的计算进行矢量化p3,并且可以对终结循环进行矢量化。

首先按原样测试它(因为我没有!)。然后,您可以尝试将数组构造为 ,而result[4][256]不是result[256][4],或者使用更小或更大的展开因子。

对此应用一些 NEON 内在函数:

uint32 histogram_result[256 * 4] = {0};
static const uint16_t offsets[] = { 0x000, 0x001, 0x002, 0x003,
                                    0x000, 0x001, 0x002, 0x003 };
uint16x8_t voffs = vld1q_u16(offsets);
for (int i = 0; i < NUM; i += 8) {
    uint8x8_t p = vld1_u8(&src_data[i]);
    uint16x8_t p16 = vshll_n_u8(p, 16);
    p16 = vaddq_u16(p16, voffs);
    uint32_t c0 = histogram_result[vget_lane_u16(p16, 0)];
    uint32_t c1 = histogram_result[vget_lane_u16(p16, 1)];
    uint32_t c2 = histogram_result[vget_lane_u16(p16, 2)];
    uint32_t c3 = histogram_result[vget_lane_u16(p16, 3)];
    histogram_result[vget_lane_u16(p16, 0)] = c0 + 1;
    c0 = histogram_result[vget_lane_u16(p16, 4)];
    histogram_result[vget_lane_u16(p16, 1)] = c1 + 1;
    c1 = histogram_result[vget_lane_u16(p16, 5)];
    histogram_result[vget_lane_u16(p16, 2)] = c2 + 1;
    c2 = histogram_result[vget_lane_u16(p16, 6)];
    histogram_result[vget_lane_u16(p16, 3)] = c3 + 1;
    c3 = histogram_result[vget_lane_u16(p16, 7)];
    histogram_result[vget_lane_u16(p16, 4)] = c0 + 1;
    histogram_result[vget_lane_u16(p16, 5)] = c1 + 1;
    histogram_result[vget_lane_u16(p16, 6)] = c2 + 1;
    histogram_result[vget_lane_u16(p16, 7)] = c3 + 1;
}
Run Code Online (Sandbox Code Playgroud)

当直方图数组展开为 x8 而不是 x4 时,您可能需要使用八个标量累加器而不是四个,但您必须记住,这意味着八个计数寄存器和八个地址寄存器,这比 32 位 ARM 拥有的寄存器更多(因为您不能使用SP和PC)。

不幸的是,由于地址计算掌握在 NEON 内在函数手中,我认为编译器无法安全地推理它如何重新排序读取和写入,因此您必须显式地重新排序它们并希望您正在这样做最好的方法。

  • @PeterCordes,NEON 允许您在一次操作中从向量填充两个标量寄存器,因此这只是四个通道的其中两个操作;但延迟可能是有序核心上的问题。当我实现它时(在我的日常工作中),我从“src_data”读取并在 NEON 中执行所有地址算术,然后将地址传递给标量,然后加载、递增并将所有内容存储在标量中。我认为标量路径可能更适合随机访问并发性,并且不值得仅仅为了增量操作而通过 SIMD。 (2认同)