优化NEON XOR实施

问题描述 投票:4回答:4

试图xor一个巨大的uint32阵列我决定使用NEON协处理器。

我实现了两个c版本:

版本1:

uint32_t xor_array_ver_1(uint32_t *array, int size)
{
    uint32x2_t acc = vmov_n_u32(0);
    uint32_t acc1 = 0;
    for (; size != 0; size -= 2) {
        uint32x2_t vec;
        vec = vld1_u32(array);
        array += 2;
        acc = veor_u32(acc, vec);
    }
    acc1 = vget_lane_u32(acc,0) ^ vget_lane_u32(acc,1);
    return acc1;
}

版本2:

uint32_t xor_array_ver_2(uint32_t *array, int size)
{
    uint32x4_t acc = vmovq_n_u32(0);
    uint32_t acc1 = 0;

    for (; size != 0; size -= 4) {
        uint32x4_t vec;
        vec = vld1q_u32(array);
        array += 4;
        acc = veorq_u32(acc, vec);
    }

    acc1 ^= vgetq_lane_u32(acc,0);
    acc1 ^= vgetq_lane_u32(acc,1);
    acc1 ^= vgetq_lane_u32(acc,2);
    acc1 ^= vgetq_lane_u32(acc,3);

    return acc1;
}

将上述两个版本与传统的xor实现进行比较:

for (i=0; i<arr_size; i++)
        val ^= my_array[i];

我发现了两个问题:

  1. 版本1具有相同的性能。
  2. 版本2的好几率超过30%。

  1. 我可以重写它甚至更好吗?其中my_array被宣布为uint32_t my_array[BIG_LENGTH];
  2. 是否有非NEON方式可以提高常规xoring代码的性能? unrolling the loop没有任何改进。
c optimization arm neon cpu-cache
4个回答
5
投票

很可能这将是内存带宽有限 - 一旦你使可用的DRAM带宽饱和,这对于每个负载只有一个ALU操作应该很容易做到,你将无法从优化中获得任何进一步的好处。

如果可能的话,尝试将您的XOR与相同数据上的另一个操作结合起来 - 这样就可以分摊缓存未命中的成本。


2
投票

没有任何代码片段的长篇答案。

硬件限制

首先,您应该问自己,我期待什么?你想写最快的代码吗?你怎么验证?首先,例如,编写一些关于硬件可以实现的测试。正如人们所说,这将主要是内存带宽有限,但是你需要知道你的内存接口有多快。弄清楚您的平台的L1,L2和RAM容量/性能特征,然后您就会知道对于不同的缓冲区大小,您最多可以期待什么。

编译器

你在使用最新的编译器吗?接下来的问题是,您是否正在使用最适合您的工具?除非你这样说,否则大多数编译器都没有积极地尝试优化你的代码。您是否正在配置它们以获得最佳收益?您是否启用完全优化(gcc:-O3),矢量化(gcc:-ftree-vectorize -ftree-vectorizer-verbose = 1)?您是否为平台设置了正确的配置标志(-mcpu -mfpu)?

你是编译器生成的verifying object code吗?对于这样一个简单的循环,这将非常简单,并帮助您尝试许多配置选项并检查生成的代码。

调整

你在检查使用restricted pointers是否提高了性能?

怎么样alignment information? (例如,你没有在你的内在函数例子中提到,但是他们希望大小是2或4的乘法,当然使用quad寄存器可以创造%30的改进。)

还有什么关于尝试在缓存行大小上对齐?

硬件功能

你知道你的硬件有什么能力吗?例如,Cortex-A9被引入为“无序推测问题超标量”。你能利用双重问题的能力吗?

所以答案介于“它取决于”和“你需要实验”之间。


2
投票

众所周知,gcc上的霓虹内在函数很糟糕。不确定它是否有所改进,但在asm中执行相同的任务应该会给你带来比普通c更好的改进30%。您可能需要首先展开内部循环。将内在函数转换为适当的asm的简单方法是使用与内在函数一起工作的armcc(arm编译器)。

所以,首先尝试展开你的普通c版本(伪代码):

for (i=arr_size; i<arr_size; i -= 4)
{
    val1 ^= my_array[0];
    val2 ^= my_array[1];
    val1 ^= my_array[2];
    val2 ^= my_array[3];
    my_array += 4;
}

用霓虹灯做这样的事情应该会给你更好的结果。最后,你应该切换到neon asm,这很简单(就我个人而言,我发现它比内在函数更容易编写)。

这是NEON asm的建议(未经测试,由您决定如何组装)

//data has to be suitably aligned (it has to be 8 or 16 byte aligned, not sure).
//dataSize in bytes has to be multiple of 64 and has to be at least 128.
//function does xor of uint32_t values and returns the result.
unsigned xor_array_64(const void *data, int dataSize);

xor_array_64:
      vldm r0!,{d0-d7}
      subs r1,r1,#0x40
0:
      pld [r0, #0xC0]
      vldm r0!,{d16-d23}
      veor q0, q0, q8
      veor q1, q1, q9
      veor q2, q2, q10
      veor q3, q3, q11
      subs r1,r1,#0x40
      bge 0b

      veor q0, q0, q1
      veor q2, q2, q3
      veor q0, q0, q2
      veor d0, d0, d1

      vtrn.32 d1, d0
      veor d0, d0, d1

      vmov r0, s0
      bx lr

1
投票

我不是为ARM编写的,我根本不熟悉NEON,但我有以下想法,这取决于ARM NEON是一个流水线架构,我不知道它是不是......

如果Paul R对你的内存带宽已经饱和是正确的,那么这可能几乎没有任何好处,但是如果你稍微重构你的代码怎么办呢.....

uint32_t xor_array_ver_2(uint32_t *array, int size)
{
  // Caveat:  'size' must be a positive multiple of 4, otherwise this
  //          code will loop for a very long time... and almost certainly
  //          segfault (or whatever term your system uses).

  uint32x4_t acc = vmovq_n_u32(0);
  uint32x4_t next_vec = vld1q_u32(array);
  uint32_t acc1 = 0;

  for (size-=4, array+=4; size != 0; size-=4) {
     uint32x4_t vec = next_vec;
     array += 4;
     next_vec = vld1q_u32(array);
     acc = veorq_u32(acc, vec);
  }
  acc = veorq_u32(acc, next_vec);

  acc1 ^= vgetq_lane_u32(acc,0);
  acc1 ^= vgetq_lane_u32(acc,1);
  acc1 ^= vgetq_lane_u32(acc,2);
  acc1 ^= vgetq_lane_u32(acc,3);

  return acc1;
}

....目标是在下一个循环需要之前开始加载下一个向量元素。

您可能会尝试的另一个小麻烦是:

uint32_t xor_array_ver_2(uint32_t *array, int size)
{
  // Caveat:  'size' must be a positive multiple of 4, otherwise this
  //          code will loop for a very long time... and almost certainly
  //          segfault (or whatever term your system uses).

  uint32x4_t acc = vmovq_n_u32(0);
  uint32x4_t next_vec = vld1q_u32(&array[size-4]);
  uint32_t acc1 = 0;

  for (size-=8; size>=0; size-=4) {
     uint32x4_t vec = next_vec;
     next_vec = vld1q_u32(&array[size]);
     acc = veorq_u32(acc, vec);
  }
  acc = veorq_u32(acc, next_vec);

  acc1 ^= vgetq_lane_u32(acc,0);
  acc1 ^= vgetq_lane_u32(acc,1);
  acc1 ^= vgetq_lane_u32(acc,2);
  acc1 ^= vgetq_lane_u32(acc,3);

  return acc1;
}
© www.soinside.com 2019 - 2024. All rights reserved.