4

试图对一个巨大的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;
}

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

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

我观察到 2 个问题:

  1. 版本 1 具有相同的性能。
  2. 版本 2 的性能要好30%以上。

  1. 我可以重写它以更好吗?wheremy_array被声明为 uint32_t my_array[BIG_LENGTH];
  2. 有没有一种非 NEON方法可以提高常规 xoring 代码的性能?展开循环并没有带来任何改进。
4

4 回答 4

5

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

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

于 2013-10-03T16:18:17.123 回答
2

众所周知,gcc 上的 neon 内在函数很糟糕。不确定它是否得到了改进,但在 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
于 2013-10-03T15:50:15.653 回答
2

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

硬件限制

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

编译器

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

您是否正在验证编译器生成的目标代码?对于这样一个简单的循环,这将非常容易,并且可以帮助您尝试许多配置选项并检查生成的代码。

调整

您是否在检查使用受限指针是否可以提高性能?

对齐信息呢?(例如,您没有在内部示例中提及,但他们希望大小是 2 或 4 的倍数,当然,使用四元寄存器可以创造 %30 的改进。)

尝试对齐缓存线大小又如何呢?

硬件能力

你知道你的硬件能做什么吗?例如,Cortex-A9 被引入为“乱序投机问题超标量”。您可以利用双重问题能力吗?

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

于 2013-10-04T08:59:51.020 回答
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;
}
于 2013-10-03T18:52:10.903 回答